xydata-tools 1.0.41 → 1.0.42

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/README.md CHANGED
@@ -1,310 +1,106 @@
1
- # 鑫义科技前端开发者工具包
2
-
3
- ## 内网文档:[http://172.16.1.254:8090/display/UX/xydata-tools](http://172.16.1.254:8090/display/UX/xydata-tools)
4
-
5
- ## 功能
6
-
7
- - `aaa` 模版(有独立的登录页面,不支持统一登陆):SecurityLayout、系统管理
8
- - `sso` 模版(没有独立的登录页面,支持统一登陆):SecurityLayout、系统管理
9
- - `both` 模版(有独立的登录页面,支持统一登陆): SecurityLayout、一般配合 `sso` 模板的系统管理使用
10
- - 数据字典页面
11
- - 组织机构页面
12
- - 附件上传组件
13
- - 附件下载组件
14
-
15
- ---
16
-
17
- ## 使用说明
18
-
19
- ### 全局注册 Service
20
-
21
- 在项目中注册请求服务,可放置在 `global.jsx` 或 `app.js` 中:
22
-
23
- ```javascript
24
- import { registerService } from "xydata-tools";
25
-
26
- registerService("替换你的项目服务");
27
- ```
28
-
29
- ---
30
-
31
- ### 数据字典使用方法
32
-
33
- ```javascript
34
- import React from "react";
35
- import { DictionaryFront } from "xydata-tools";
36
-
37
- const Demo = () => {
38
- return <DictionaryFront />;
39
- };
40
-
41
- export default Demo;
42
- ```
43
-
44
- ---
45
-
46
- ### 组织机构模块
47
-
48
- ```javascript
49
- import React from "react";
50
- import { OrganizationFront } from "xydata-tools";
51
-
52
- const Demo = () => {
53
- return <OrganizationFront />;
54
- };
55
-
56
- export default Demo;
57
- ```
58
-
59
- ---
60
-
61
- ### 附件上传组件
62
-
63
- 基于 `ProformUploadButton` 二次封装的上传组件,支持以下功能:
64
-
65
- 1. 支持单独使用或在表单内使用。
66
- 2. 支持上传图片、文件,可根据 `listType` 进行多种上传风格切换。
67
- 3. 内置文件大小、格式校验。
68
- 4. 内置常用请求头参数。
69
- 5. 内置图片预览功能(`listType` 为 `"picture"` 或 `"picture-card"` 时可用)。
70
- 6. 兼容所有原生组件功能。
71
-
72
- #### 示例代码
73
-
74
- ```javascript
75
- import React from "react";
76
- import { UploadFile } from "xydata-tools";
77
- import { Form } from "antd";
78
-
79
- const Demo = () => {
80
- const [form] = Form.useForm();
81
-
82
- // 单独使用
83
- <UploadFile
84
- title="导入模板"
85
- listType="text"
86
- accept=".xlsx,.xls"
87
- icon={<ImportOutlined />}
88
- buttonOptions={{ type: "primary" }}
89
- showUploadList={false}
90
- onChange={({ fileList }) => {
91
- // 通过监听状态为done获取最终的上传结果
92
- if (fileList.every((item) => item.status === "done")) {
93
- console.log(fileList );
94
- }
95
- }}
96
- checkFile={{
97
- suffixs: ["xlsx", "xls"],
98
- maxSize: 1024,
99
- }}
100
- multiple={true}
101
- />;
102
-
103
- // 在表单内使用
104
- <Form form={form}>
105
- <UploadFile form={form} name="file" label="图片上传" listType="picture-card" />
106
- </Form>
107
-
108
- // 在 FormList 内使用
109
- <ProFormList {...yourProps}>
110
- {(field) => {
111
- return <UploadFile key={field.key} name={[field.name, "cover"]} />;
112
- }}
113
- </ProFormList>;
114
- };
115
-
116
- export default Demo;
117
- ```
118
-
119
- #### 参数说明
120
-
121
- | 参数名 | 类型 | 描述 | 默认值 |
122
- | ----------------- | -------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------- |
123
- | `form` | `Antd.Form.Instance` | 表单实例 | 无 |
124
- | `label` | `string` | 表单标签 | 无 |
125
- | `name` | `string` | 表单字段 | 无 |
126
- | `rules` | `Array[Object]` | 表单校验规则 | 无 |
127
- | `accept` | `string` | 接受上传的文件类型 | 无 |
128
- | `action` | `string` | 上传的地址 | `/api/${注册的服务}/file/upload/v3?type=openFile` |
129
- | `max` | `string` | 最大上传数量,超过最大数量就会隐藏上传按钮 | 无 |
130
- | `multiple` | `boolean` | 是否支持多选文件,开启后按住 `Ctrl` 可选择多个文件 | `false` |
131
- | `icon` | `ReactNode` | `Button` 或 `Dragger` 的图标 | 无 |
132
- | `title` | `ReactNode` | `Button` 或 `Dragger` 的标题 | 无 |
133
- | `description` | `ReactNode` | 当 `listType` 为 `dragger` 时的描述 | 无 |
134
- | `listType` | `string` | 上传列表的内建样式,支持 `text`、`picture`、`picture-card`、`dragger` | `text` |
135
- | `showUploadList` | `Boolean/Object` | 是否展示文件列表,可设为对象,用于单独设定 `showPreviewIcon` 等配置 | `true` |
136
- | `readonly` | `boolean` | 是否只读 | `false` |
137
- | `disabled` | `boolean` | 是否禁用 | `false` |
138
- | `checkFile` | `Object` | 文件校验配置,包括 `suffixs`(限制的文件格式)和 `maxSize`(文件大小限制,单位 KB) | 无 |
139
- | `onChange` | `Function` | 上传文件改变时的回调,上传中、完成、失败都会调用 | 无 |
140
- | `formItemOptions` | `Object` | 表单项配置,覆盖以上设置 | 无 |
141
- | `uploadOptions` | `Object` | 上传配置,覆盖以上设置 | 无 |
142
- | `buttonOptions` | `Object` | 上传按钮的配置,`listType` 为 `dragger` 时无效 | 默认配置了 `loading` |
143
-
144
- ---
145
-
146
- ### 附件下载组件
147
-
148
- 内置了下载功能的按钮组件,支持以下功能:
149
-
150
- 1. 支持格式为链接或流的文件下载。
151
- 2. 兼容原生组件功能。
152
-
153
- #### 示例代码
154
-
155
- ```javascript
156
- import React from "react";
157
- import { DownloadFile } from "xydata-tools";
158
-
159
- const Demo = () => {
160
- return (
161
- <DownloadFile
162
- shape="round"
163
- type="link"
164
- queryOption={{
165
- url: "/api/tb/exportTbExcel",
166
- params: { peopleType: 0 },
167
- customField: {
168
- url: "exportFileUrl",
169
- fileName: "exportFileName",
170
- },
171
- }}
172
- />
173
- );
174
- };
175
-
176
- export default Demo;
177
- ```
178
-
179
- #### 参数说明
180
-
181
- | 参数名 | 类型 | 描述 | 默认值 |
182
- | -------------------------- | -------------------- | ----------------------------------------------------------------- | -------- |
183
- | `text` | `String / ReactNode` | 按钮内容 | 下载按钮 |
184
- | `queryOption` | `Object` | 下载设置 | 无 |
185
- | `queryOption.url` | `String` | 接口地址 | 无 |
186
- | `queryOption.headers` | `Object` | 请求头 | 无 |
187
- | `queryOption.method` | `String` | 请求方法,支持 `GET` 或 `POST` | 无 |
188
- | `queryOption.fileName` | `String` | 文件名称 | 无 |
189
- | `queryOption.params` | `Object` | 请求参数 | 无 |
190
- | `queryOption.customField` | `Object` | 自定义返回接口返回的文件名和地址(适用于返回 JSON 的接口) | 无 |
191
- | `queryOption.onError` | `Function` | 接口异常回调 | 无 |
192
- | `queryOption.downloadType` | `String` | 下载类型,`pure` 表示传入的是下载地址(OSS 链接),而不是下载接口 | 无 |
193
- | 其他 | `-` | 兼容 Antd.Button 所有原生参数 | 无 |
194
-
195
-
196
- #### 配套的 提交/回显 方法
197
-
198
- ```javascript
199
-
200
- import { getUploadFileData, setUploadFileData } from "xydata-tools";
201
-
202
- // 获取表单数据
203
- const getFieldsValue= async () => {
204
- const fieldsValue = await form.validateFields();
205
- const { cover } = fieldsValue;
206
- const data = await getUploadFileData(cover);
207
- };
208
-
209
- // 回显表单数据
210
- const renderFieldsValue = async () => {
211
- const cover = [
212
- {
213
- name: "image.png",
214
- url: "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png",
215
- fileName:'xxx'
216
- },
217
- {
218
- url: "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png",
219
- },
220
- ];
221
- const data = setUploadFileData(cover);
222
- form.setFieldsValue({
223
- cover: data,
224
- });
225
- };
226
- ```
227
- ---
228
-
229
- ### `sso` 模版的系统管理模块
230
-
231
- #### 用户管理
232
-
233
- ```javascript
234
- import React from "react";
235
- import { Sso } from "xydata-tools";
236
- import { connect } from "umi";
237
-
238
- const UserPage = (props) => {
239
- const { currentUser } = props;
240
- return <Sso.User currentUser={currentUser} />;
241
- };
242
-
243
- export default connect(({ user }) => ({
244
- currentUser: user.currentUser,
245
- }))(UserPage);
246
- ```
247
-
248
- #### 权限管理
249
-
250
- ```javascript
251
- import React from "react";
252
- import { Sso } from "xydata-tools";
253
-
254
- const PermPage = () => {
255
- return <Sso.Permission />;
256
- };
257
-
258
- export default PermPage;
259
- ```
260
-
261
- #### 角色管理
262
-
263
- ```javascript
264
- <Sso.Role currentUser={currentUser} />
265
- ```
266
-
267
- #### 设置权限
268
-
269
- ```javascript
270
- <Sso.PermRole currentUser={currentUser} />
271
- ```
272
-
273
- ---
274
-
275
- ### 本地开发
276
-
277
- 1. 在项目根目录执行:
278
-
279
- ```bash
280
- yarn dev
281
- ```
282
-
283
- 2. 在 example 目录下执行:
284
-
285
- ```bash
286
- yarn start
287
- ```
288
-
289
- 3. example 是一个由 `umi` 搭建的后台项目,可直接引用 `xydata-tools` 内的组件进行预览。
290
-
291
- 4. 新组件可在 `/xydata-tools/src/components` 下进行开发更新。
292
-
293
- 5. 在 src 内的修改操作将会被实时编译到 dist 文件夹中。
294
-
295
- 6. 打包发布:
296
-
297
- ```bash
298
- npm run prepublishOnly
299
- npm config set registry https://registry.npmjs.org
300
- npm config set proxy false
301
- npm publish -d
302
- ```
303
-
304
- 7. 发布完成后,还原 npm 源(淘宝源):
305
-
306
- ```bash
307
- npm config set registry https://registry.npmmirror.com
308
- ```
309
-
310
- ---
1
+ # 鑫义科技前端开发者工具包
2
+
3
+ 内网文档:[http://172.16.1.254:8090/display/UX/xydata-tools](http://172.16.1.254:8090/display/UX/xydata-tools)
4
+
5
+ # xydata-tools 文档索引
6
+
7
+ ## 📚 文档目录
8
+
9
+ #### Web 端组件
10
+
11
+ - [附件上传组件](./组件使用文档/附件上传组件.md)
12
+
13
+ - 支持图片、文件上传
14
+ - 内置文件大小、格式校验
15
+ - 支持预览功能
16
+
17
+ - [附件下载组件](./组件使用文档/附件下载组件.md)
18
+
19
+ - 支持链接和流式文件下载
20
+ - 兼容原生按钮功能
21
+
22
+ - [数据字典组件](./组件使用文档/数据字典.md)
23
+
24
+ - 完整的数据字典管理功能
25
+ - 支持分类管理
26
+
27
+ - [组织机构组件](./组件使用文档/组织机构.md)
28
+
29
+ - 树形结构展示
30
+ - 人员管理功能
31
+
32
+ #### uni-app 组件
33
+
34
+ - [uni-app 文件上传组件](./uni-app组件使用文档.md)
35
+ - **PicPicker** - 图片选择器组件
36
+ - **VideoPicker** - 视频选择器组件
37
+
38
+ ### 模版使用文档
39
+
40
+ - [AAA 模版](./模版使用文档/AAA模版.md)
41
+
42
+ - 适用场景:有独立登录页面,不支持统一登录
43
+ - 包含:SecurityLayout、系统管理模块
44
+
45
+ - [SSO 模版](./模版使用文档/SSO模版.md)
46
+
47
+ - 适用场景:没有独立登录页面,支持统一登录
48
+ - 包含:SecurityLayout、系统管理模块
49
+
50
+ - [Both 模版](./模版使用文档/Both模版.md)
51
+
52
+ - 适用场景:同时支持独立登录和统一登录
53
+ - 包含:SecurityLayout
54
+
55
+ ---
56
+
57
+ ## 本地开发
58
+
59
+ react
60
+
61
+ 1. 在项目根目录执行:
62
+
63
+ ```bash
64
+ yarn dev
65
+ ```
66
+
67
+ 2. 在 example 目录下执行:
68
+
69
+ ```bash
70
+ yarn start
71
+ ```
72
+
73
+ 3. example 是一个由 `umi` 搭建的后台项目,可直接引用 `xydata-tools` 内的组件进行预览。
74
+ 4. 新组件可在 `/xydata-tools/src/components` 下进行开发更新。
75
+ 5. src 内的修改操作将会被实时编译到 dist 文件夹中。
76
+
77
+ uniapp
78
+
79
+ 1. 在项目根目录执行:
80
+
81
+ ```bash
82
+ yarn dev
83
+ ```
84
+
85
+ 2. 在 example-uniapp 下运行
86
+ ```bash
87
+ yarn link xydata-tools
88
+ ```
89
+ 3. 将 example-uniapp 添加到 H-Builder 中运行
90
+
91
+ 打包发布:
92
+
93
+ ```bash
94
+ npm run prepublishOnly
95
+ npm config set registry https://registry.npmjs.org
96
+ npm config set proxy false
97
+ npm publish -d
98
+ ```
99
+
100
+ 发布完成后,还原 npm 源(淘宝源):
101
+
102
+ ```bash
103
+ npm config set registry https://registry.npmmirror.com
104
+ ```
105
+
106
+ ---
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,452 @@
1
+ <template>
2
+ <view class="pic-picker-box-wrapper">
3
+ <view class="pic-picker-box" v-if="!readonly">
4
+ <view class="upload-view">
5
+ <view class="item flex-center" v-for="(item, index) in imageUrls" :key="index">
6
+ <image @click.stop="previewImage(imageUrls, index)" style="border-radius: 24rpx;" :src="item"
7
+ mode="aspectFill" class="avatar"></image>
8
+ <view class="clear-btn" @click="removeImage(index)">
9
+ <text class="clear-icon">×</text>
10
+ </view>
11
+ </view>
12
+
13
+ <!-- 自定义上传按钮插槽,可完全替换容器 -->
14
+ <slot name="upload-button" :select="selectAndUploadImage" :limit="limit" :count="fileList.length">
15
+ <!-- 默认上传按钮 -->
16
+ <view class="item flex-center selection upload-trigger"
17
+ :class="{ 'no-margin': imageUrls.length === 0 }" v-if="fileList.length < limit"
18
+ @click="selectAndUploadImage">
19
+ <image :src="defaultAddImg" style="width: 64rpx;height: 64rpx;"></image>
20
+ <view class="upload-text">上传图片</view>
21
+ </view>
22
+ </slot>
23
+ </view>
24
+ </view>
25
+
26
+ <block v-else-if="fileList.length">
27
+ <view class="grid-imgbox" :class="'grid-imgbox' + fileList.length" v-if="fileList.length > 1">
28
+ <view v-for="(file, i) in fileList" :key="i">
29
+ <image :src="file.url" mode="aspectFill" @click.stop="previewImage(imageUrls, i)" :lazy-load="true">
30
+ </image>
31
+ </view>
32
+ </view>
33
+
34
+ <view class="grid-imgbox grid-imgbox1" v-if="fileList.length === 1">
35
+ <image :src="fileList[0].url" mode="aspectFill" @click.stop="previewImage(imageUrls, 0)"></image>
36
+ </view>
37
+ </block>
38
+ </view>
39
+ </template>
40
+
41
+ <script>
42
+ import Compressor from 'compressorjs';
43
+
44
+ export default {
45
+ name: 'PicPicker',
46
+ props: {
47
+ /** 上传地址 */
48
+ uploadUrl: {
49
+ type: String,
50
+ default: ''
51
+ },
52
+ /** 只读模式 */
53
+ readonly: {
54
+ type: Boolean,
55
+ default: false
56
+ },
57
+ /** 文件列表 */
58
+ files: {
59
+ type: Array,
60
+ default: () => []
61
+ },
62
+ /** 最大上传数量限制 */
63
+ limit: {
64
+ type: Number,
65
+ default: 9
66
+ },
67
+ /** 上传按钮的默认图标 */
68
+ defaultAddImg: {
69
+ type: String,
70
+ default: require('../../assets/defaultAddPicBg.png')
71
+ },
72
+ },
73
+ data() {
74
+ return {
75
+ fileList: [],
76
+ }
77
+ },
78
+ computed: {
79
+ /** 图片预览URL列表 */
80
+ imageUrls() {
81
+ return this.fileList.map(file => file.url)
82
+ }
83
+ },
84
+ watch: {
85
+ files(val) {
86
+ this.fileList = val
87
+ }
88
+ },
89
+ mounted() {
90
+ this.fileList = this.files
91
+ console.log(this.fileList)
92
+ },
93
+ methods: {
94
+ /**
95
+ * 预览图片
96
+ */
97
+ previewImage(urls, current) {
98
+ uni.previewImage({
99
+ current,
100
+ urls,
101
+ indicator: "number"
102
+ })
103
+ },
104
+
105
+ /**
106
+ * 选择并上传图片
107
+ */
108
+ selectAndUploadImage() {
109
+ if (this.fileList.length >= this.limit) {
110
+ return
111
+ }
112
+
113
+ const remainCount = this.limit - this.fileList.length
114
+
115
+ uni.chooseImage({
116
+ count: remainCount,
117
+ sizeType: ["original", "compressed"],
118
+ sourceType: ["album"],
119
+ // #ifndef MP-WEIXIN
120
+ success: (chooseImageRes) => {
121
+ if (chooseImageRes.tempFiles.length + this.fileList.length > this.limit) {
122
+ uni.showToast({
123
+ icon: 'none',
124
+ title: `上传最大限制${this.limit}张`
125
+ })
126
+ return
127
+ }
128
+
129
+ uni.showLoading({
130
+ title: '上传中',
131
+ mask: true
132
+ })
133
+
134
+ chooseImageRes.tempFilePaths.forEach((tempPath, index) => {
135
+ const tempFile = chooseImageRes.tempFiles[index]
136
+
137
+ new Compressor(tempFile, {
138
+ quality: 0.8,
139
+ success: (compressedFile) => {
140
+ this.uploadFile(this.uploadUrl, compressedFile)
141
+ },
142
+ error: (err) => {
143
+ uni.hideLoading()
144
+ uni.showToast({
145
+ icon: 'none',
146
+ title: '图片压缩失败'
147
+ })
148
+ },
149
+ })
150
+ })
151
+ },
152
+ // #endif
153
+
154
+ // #ifdef MP-WEIXIN
155
+ success: (chooseImageRes) => {
156
+ if (chooseImageRes.tempFiles.length + this.fileList.length > this.limit) {
157
+ uni.showToast({
158
+ icon: 'none',
159
+ title: `上传最大限制${this.limit}张`
160
+ })
161
+ return
162
+ }
163
+
164
+ uni.showLoading({
165
+ title: '上传中',
166
+ mask: true
167
+ })
168
+
169
+ chooseImageRes.tempFilePaths.forEach((tempPath, index) => {
170
+ const tempFile = chooseImageRes.tempFiles[index]
171
+
172
+ // JPG 格式进行压缩
173
+ if (tempPath.indexOf('.jpg') !== -1) {
174
+ uni.compressImage({
175
+ src: tempFile.path,
176
+ quality: 100,
177
+ success: (res) => {
178
+ this.uploadFileByPath(this.uploadUrl, res.tempFilePath, tempPath)
179
+ }
180
+ })
181
+ } else {
182
+ this.uploadFileByPath(this.uploadUrl, tempPath, tempPath)
183
+ }
184
+ })
185
+ },
186
+ // #endif
187
+ })
188
+ },
189
+
190
+ /**
191
+ * 上传文件(非微信小程序,使用 File 对象)
192
+ */
193
+ uploadFile(url, file) {
194
+ uni.uploadFile({
195
+ url,
196
+ file,
197
+ name: "file",
198
+ success: (uploadFileRes) => {
199
+ const result = JSON.parse(uploadFileRes.data)
200
+
201
+ const fileInfo = {
202
+ fileName: result.data.fileName,
203
+ name: result.data.originFileName,
204
+ url: result.data.pathUrl
205
+ }
206
+ this.fileList.push(fileInfo)
207
+ },
208
+ fail: () => {
209
+ uni.showToast({
210
+ icon: 'none',
211
+ title: '上传失败'
212
+ })
213
+ },
214
+ complete: () => {
215
+ uni.hideLoading()
216
+ this.emitChange()
217
+ }
218
+ })
219
+ },
220
+
221
+ /**
222
+ * 上传文件(微信小程序,使用文件路径)
223
+ */
224
+ uploadFileByPath(url, filePath, previewPath) {
225
+ uni.uploadFile({
226
+ url,
227
+ filePath,
228
+ name: "file",
229
+ success: (uploadFileRes) => {
230
+ const result = JSON.parse(uploadFileRes.data)
231
+
232
+ const fileInfo = {
233
+ fileName: result.data.fileName,
234
+ name: result.data.originFileName,
235
+ url: result.data.pathUrl
236
+ }
237
+ this.fileList.push(fileInfo)
238
+ },
239
+ fail: () => {
240
+ uni.showToast({
241
+ icon: 'none',
242
+ title: '上传失败'
243
+ })
244
+ },
245
+ complete: () => {
246
+ uni.hideLoading()
247
+ this.emitChange()
248
+ }
249
+ })
250
+ },
251
+
252
+ /**
253
+ * 删除图片
254
+ */
255
+ removeImage(index) {
256
+ this.fileList = this.fileList.filter((_, i) => i !== index)
257
+ this.emitChange()
258
+ },
259
+
260
+ /**
261
+ * 触发变更事件
262
+ */
263
+ emitChange() {
264
+ this.$emit('change', this.fileList)
265
+ }
266
+ }
267
+ }
268
+ </script>
269
+
270
+ <style lang="scss">
271
+ .pic-picker-box-wrapper {
272
+ width: 100%;
273
+ }
274
+
275
+ .flex-align-center {
276
+ display: flex;
277
+ align-items: center;
278
+ justify-content: center;
279
+ }
280
+
281
+ .flex-center {
282
+ display: flex;
283
+ justify-content: center;
284
+ }
285
+
286
+ .selection {
287
+ background: #FBFBFB;
288
+ border-radius: 16rpx 16rpx 16rpx 16rpx;
289
+ border: 2rpx dashed #DDDDDD !important;
290
+ cursor: pointer;
291
+ transition: all 0.3s;
292
+
293
+ &:active {
294
+ opacity: 0.7;
295
+ }
296
+ }
297
+
298
+ .upload-trigger {
299
+ display: flex;
300
+ flex-direction: column;
301
+ align-items: center;
302
+ justify-content: center;
303
+ width: 100%;
304
+ height: 100%;
305
+
306
+ .upload-text {
307
+ color: #999;
308
+ font-size: 24rpx;
309
+ margin-top: 8rpx;
310
+ }
311
+ }
312
+
313
+ .grid-imgbox {
314
+ display: grid;
315
+ width: calc(100% + 12rpx);
316
+ margin-right: -12rpx;
317
+ margin-bottom: -12rpx;
318
+ --wadth: 200rpx;
319
+
320
+ view {
321
+ width: 100%;
322
+ height: 100%;
323
+ padding-right: 12rpx;
324
+ padding-bottom: 12rpx;
325
+ box-sizing: border-box;
326
+ overflow: hidden;
327
+ }
328
+
329
+ image {
330
+ width: 100%;
331
+ height: 100%;
332
+ border-radius: 12rpx;
333
+ display: block;
334
+ }
335
+ }
336
+
337
+ .grid-imgbox1 {
338
+ grid-template-columns: 100%;
339
+ grid-template-rows: 500rpx;
340
+ }
341
+
342
+ .grid-imgbox2 {
343
+ grid-template-columns: 50% 50%;
344
+ grid-template-rows: 320rpx;
345
+ }
346
+
347
+ .grid-imgbox3 {
348
+ grid-template-columns: 1fr 1fr 1fr;
349
+ grid-template-rows: var(--wadth);
350
+ }
351
+
352
+ .grid-imgbox4 {
353
+ grid-template-columns: 50% 50%;
354
+ grid-template-rows: 280rpx 280rpx;
355
+ }
356
+
357
+ .grid-imgbox5 {
358
+ grid-template-columns: 1fr 1fr 1fr;
359
+ grid-template-rows: repeat(2, var(--wadth));
360
+ }
361
+
362
+ .grid-imgbox6 {
363
+ grid-template-columns: 1fr 1fr 1fr;
364
+ grid-template-rows: repeat(2, var(--wadth));
365
+ }
366
+
367
+ .grid-imgbox7 {
368
+ grid-template-columns: 1fr 1fr 1fr;
369
+ grid-template-rows: repeat(3, var(--wadth));
370
+ }
371
+
372
+ .grid-imgbox8 {
373
+ grid-template-columns: 1fr 1fr 1fr;
374
+ grid-template-rows: repeat(3, var(--wadth));
375
+ }
376
+
377
+ .grid-imgbox9 {
378
+ grid-template-columns: 1fr 1fr 1fr;
379
+ grid-template-rows: repeat(3, var(--wadth));
380
+ }
381
+
382
+ .pic-picker-box {
383
+ .upload-view {
384
+ padding: 8rpx 0;
385
+ display: flex;
386
+ flex-wrap: wrap;
387
+
388
+ ::v-deep.file-picker__box {
389
+ width: 25% !important;
390
+ padding-top: 25% !important;
391
+ }
392
+
393
+ .item {
394
+ width: 160rpx;
395
+ height: 160rpx;
396
+ margin-right: 20rpx;
397
+ margin-bottom: 20rpx;
398
+ position: relative;
399
+ border: 1px solid #DDDDDD;
400
+ box-shadow: 0 0 10rpx 0 rgba(0, 0, 0, 0.1);
401
+ border-radius: 12rpx;
402
+
403
+ &.no-margin {
404
+ margin: 0;
405
+ }
406
+
407
+ .img {
408
+ width: 160rpx;
409
+ height: 160rpx;
410
+ }
411
+
412
+ .avatar {
413
+ width: 100%;
414
+ height: 100%;
415
+ }
416
+
417
+ .clear-btn {
418
+ position: absolute;
419
+ right: -12rpx;
420
+ top: -12rpx;
421
+ width: 40rpx;
422
+ height: 40rpx;
423
+ background-color: rgba(0, 0, 0, 0.6);
424
+ border-radius: 50%;
425
+ display: flex;
426
+ align-items: center;
427
+ justify-content: center;
428
+ z-index: 2;
429
+
430
+ .clear-icon {
431
+ color: #ffffff;
432
+ font-size: 28rpx;
433
+ line-height: 0.1;
434
+ font-weight: 300;
435
+ }
436
+ }
437
+ }
438
+ }
439
+
440
+ .tips {
441
+ padding-bottom: 12rpx;
442
+ font-size: 26rpx;
443
+ color: #999999;
444
+ }
445
+ }
446
+
447
+
448
+ .face {
449
+ width: 480rpx;
450
+ height: 480rpx;
451
+ }
452
+ </style>
@@ -0,0 +1,462 @@
1
+ <template>
2
+ <view class="video-picker-wrapper">
3
+ <view class="video-picker-box" v-if="!readonly">
4
+ <view class="upload-view">
5
+ <!-- 视频预览列表 -->
6
+ <view class="item video-item flex-center" v-for="(video, index) in fileList" :key="index">
7
+ <video :id="'videoPlayer' + index" class="avatar" :src="video.url"
8
+ :controls="activeVideoIndex === index" :loop="true"
9
+ :show-center-play-btn="false" :muted="activeVideoIndex !== index"
10
+ @click.stop="handleVideoPlay(index)"
11
+ @fullscreenchange="handleFullscreenChange"></video>
12
+
13
+ <!-- 播放按钮 -->
14
+ <view class="play-btn" v-if="activeVideoIndex !== index" @click.stop="handleVideoPlay(index)">
15
+ <image class="play-icon" src="../../assets/videoPlay.png"></image>
16
+ </view>
17
+
18
+ <!-- 删除按钮 -->
19
+ <view class="clear-btn" @click="removeFile(index)">
20
+ <text class="clear-icon">×</text>
21
+ </view>
22
+ </view>
23
+
24
+ <!-- 上传中状态 -->
25
+ <view class="item flex-center upload-trigger uploading" v-if="uploading">
26
+ <image class="loading-icon" src="../../assets/loading.png" />
27
+ <view class="upload-text" style="font-size: 22rpx;">上传中 {{ uploadProgress }}%</view>
28
+ <view class="progress-bar">
29
+ <view class="progress-bar-inner" :style="{ width: uploadProgress + '%' }"></view>
30
+ </view>
31
+ </view>
32
+
33
+ <!-- 自定义上传按钮插槽 -->
34
+ <slot name="upload-button" :select="selectAndUploadVideo" :count="fileList.length">
35
+ <!-- 默认上传按钮 -->
36
+ <view class="item flex-center selection upload-trigger"
37
+ :class="{ 'no-margin': fileList.length === 0 }"
38
+ v-if="fileList.length < limit && !uploading"
39
+ @click="selectAndUploadVideo">
40
+ <image :src="defaultUploadIcon" style="width: 64rpx;height: 64rpx;"></image>
41
+ <view class="upload-text">上传视频</view>
42
+ </view>
43
+ </slot>
44
+ </view>
45
+ </view>
46
+
47
+ <!-- 只读模式 -->
48
+ <view class="video-readonly" v-else-if="fileList.length">
49
+ <view class="video-list">
50
+ <view class="video-item-readonly" v-for="(video, index) in fileList" :key="index">
51
+ <video class="video-player" :src="video.url" controls></video>
52
+ </view>
53
+ </view>
54
+ </view>
55
+ </view>
56
+ </template>
57
+
58
+ <script>
59
+ export default {
60
+ name: 'VideoPicker',
61
+ props: {
62
+ /** 上传地址 */
63
+ uploadUrl: {
64
+ type: String,
65
+ default: ''
66
+ },
67
+ /** 只读模式 */
68
+ readonly: {
69
+ type: Boolean,
70
+ default: false
71
+ },
72
+ /** 视频文件列表 */
73
+ files: {
74
+ type: Array,
75
+ default: () => []
76
+ },
77
+ /** 最大上传数量 */
78
+ limit: {
79
+ type: Number,
80
+ default: 9
81
+ },
82
+ /** 上传按钮的默认图标 */
83
+ defaultUploadIcon: {
84
+ type: String,
85
+ default: require('../../assets/uploadVideoBg.png')
86
+ },
87
+ },
88
+ data() {
89
+ return {
90
+ fileList: [],
91
+ uploading: false,
92
+ uploadProgress: 0,
93
+ activeVideoIndex: -1, // 当前播放的视频索引,-1 表示没有播放
94
+ videoContext: null,
95
+ }
96
+ },
97
+ watch: {
98
+ files(val) {
99
+ this.fileList = val
100
+ }
101
+ },
102
+ mounted() {
103
+ this.fileList = this.files
104
+ },
105
+ methods: {
106
+ /**
107
+ * 处理视频播放
108
+ * @param {number} index - 视频索引
109
+ */
110
+ handleVideoPlay(index) {
111
+ if (this.activeVideoIndex !== index) {
112
+ this.videoContext = uni.createVideoContext('videoPlayer' + index, this)
113
+ this.videoContext.requestFullScreen()
114
+ this.videoContext.play()
115
+ this.activeVideoIndex = index
116
+ }
117
+ },
118
+
119
+ /**
120
+ * 处理全屏变化
121
+ */
122
+ handleFullscreenChange(e) {
123
+ if (!e.detail.fullScreen) {
124
+ this.activeVideoIndex = -1
125
+ if (this.videoContext) {
126
+ this.videoContext.pause()
127
+ }
128
+ }
129
+ },
130
+
131
+ /**
132
+ * 选择并上传视频
133
+ */
134
+ selectAndUploadVideo() {
135
+ if (this.fileList.length >= this.limit || this.uploading) {
136
+ if (this.fileList.length >= this.limit) {
137
+ uni.showToast({
138
+ title: `最多上传${this.limit}个视频`,
139
+ icon: 'none'
140
+ })
141
+ }
142
+ return
143
+ }
144
+
145
+ uni.chooseVideo({
146
+ sourceType: ['camera', 'album'],
147
+ success: (res) => {
148
+ this.uploading = true
149
+ this.uploadProgress = 0
150
+
151
+ // 直接上传视频,不获取封面(让服务端处理或用户手动选择)
152
+ this.uploadVideo(res.tempFilePath)
153
+ }
154
+ })
155
+ },
156
+
157
+ /**
158
+ * 上传视频
159
+ * @param {string} filePath - 视频文件路径
160
+ */
161
+ uploadVideo(filePath) {
162
+ const uploadTask = uni.uploadFile({
163
+ url: this.uploadUrl,
164
+ filePath,
165
+ name: "file",
166
+ success: (uploadFileRes) => {
167
+ const result = JSON.parse(uploadFileRes.data)
168
+ console.log('视频上传成功:', result)
169
+
170
+ // 添加视频到列表
171
+ const fileInfo = {
172
+ fileName: result.data.fileName,
173
+ name: result.data.originFileName || 'video',
174
+ url: result.data.pathUrl
175
+ }
176
+
177
+ this.fileList.push(fileInfo)
178
+ this.uploadProgress = 100
179
+ this.uploading = false
180
+ this.emitChange()
181
+ },
182
+ fail: (err) => {
183
+ console.error('视频上传失败:', err)
184
+ uni.showToast({
185
+ icon: 'none',
186
+ title: '视频上传失败'
187
+ })
188
+ this.uploading = false
189
+ }
190
+ })
191
+
192
+ // 监听上传进度
193
+ uploadTask.onProgressUpdate((res) => {
194
+ this.uploadProgress = res.progress
195
+ this.$emit('progress', {
196
+ ...res,
197
+ stage: 'video'
198
+ })
199
+ })
200
+ },
201
+
202
+ /**
203
+ * 删除视频
204
+ * @param {number} index - 视频索引
205
+ */
206
+ removeFile(index) {
207
+ this.fileList.splice(index, 1)
208
+
209
+ // 如果删除的是当前播放的视频,重置播放状态
210
+ if (this.activeVideoIndex === index) {
211
+ this.activeVideoIndex = -1
212
+ if (this.videoContext) {
213
+ this.videoContext.pause()
214
+ }
215
+ } else if (this.activeVideoIndex > index) {
216
+ // 如果删除的视频在当前播放视频之前,索引需要减1
217
+ this.activeVideoIndex--
218
+ }
219
+
220
+ this.emitChange()
221
+ },
222
+
223
+ /**
224
+ * 触发变更事件
225
+ */
226
+ emitChange() {
227
+ this.$emit('change', this.fileList)
228
+ }
229
+ }
230
+ }
231
+ </script>
232
+
233
+ <style lang="scss">
234
+ .video-picker-wrapper {
235
+ width: 100%;
236
+ }
237
+
238
+ .flex-align-center {
239
+ display: flex;
240
+ align-items: center;
241
+ }
242
+
243
+ .flex-center {
244
+ display: flex;
245
+ justify-content: center;
246
+ }
247
+
248
+ .selection {
249
+ background: #FBFBFB;
250
+ border-radius: 16rpx 16rpx 16rpx 16rpx;
251
+ border: 2rpx dashed #DDDDDD !important;
252
+ cursor: pointer;
253
+ transition: all 0.3s;
254
+
255
+ &:active {
256
+ opacity: 0.7;
257
+ }
258
+ }
259
+
260
+ .upload-trigger {
261
+ display: flex;
262
+ flex-direction: column;
263
+ align-items: center;
264
+ justify-content: center;
265
+ width: 100%;
266
+ height: 100%;
267
+
268
+ .upload-text {
269
+ color: #999;
270
+ font-size: 24rpx;
271
+ margin-top: 8rpx;
272
+ }
273
+ }
274
+
275
+ @keyframes rotate {
276
+ 0% {
277
+ transform: rotate(0deg);
278
+ }
279
+
280
+ 100% {
281
+ transform: rotate(360deg);
282
+ }
283
+ }
284
+
285
+ .video-picker-box {
286
+ .upload-view {
287
+ padding: 8rpx 0;
288
+ display: flex;
289
+ flex-wrap: wrap;
290
+ min-height: 208rpx;
291
+
292
+ .item {
293
+ width: 160rpx;
294
+ height: 160rpx;
295
+ margin-right: 20rpx;
296
+ margin-bottom: 20rpx;
297
+ position: relative;
298
+ border: 1px solid #DDDDDD;
299
+ box-shadow: 0 0 10rpx 0 rgba(0, 0, 0, 0.1);
300
+ border-radius: 12rpx;
301
+
302
+ &.no-margin {
303
+ margin: 0;
304
+ }
305
+
306
+ &.video-item {
307
+ width: 160rpx;
308
+ height: 160rpx;
309
+ }
310
+
311
+ .avatar {
312
+ width: 100%;
313
+ height: 100%;
314
+ border-radius: 12rpx;
315
+ }
316
+
317
+ .play-btn {
318
+ position: absolute;
319
+ top: 50%;
320
+ left: 50%;
321
+ transform: translate(-50%, -50%);
322
+ width: 60rpx;
323
+ height: 60rpx;
324
+ display: flex;
325
+ align-items: center;
326
+ justify-content: center;
327
+ z-index: 2;
328
+
329
+ .play-icon {
330
+ width: 100%;
331
+ height: 100%;
332
+ opacity: 0.9;
333
+ }
334
+ }
335
+
336
+ .clear-btn {
337
+ position: absolute;
338
+ right: -12rpx;
339
+ top: -12rpx;
340
+ width: 40rpx;
341
+ height: 40rpx;
342
+ background-color: rgba(0, 0, 0, 0.6);
343
+ border-radius: 50%;
344
+ display: flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ z-index: 2;
348
+
349
+ .clear-icon {
350
+ color: #ffffff;
351
+ font-size: 28rpx;
352
+ line-height: 0.1;
353
+ font-weight: 300;
354
+ }
355
+ }
356
+
357
+ .upload-progress {
358
+ position: absolute;
359
+ top: 0;
360
+ left: 0;
361
+ right: 0;
362
+ bottom: 0;
363
+ background-color: rgba(0, 0, 0, 0.5);
364
+ border-radius: 12rpx;
365
+ z-index: 3;
366
+
367
+ .progress-mask {
368
+ width: 100%;
369
+ height: 100%;
370
+ }
371
+
372
+ .loading-icon {
373
+ font-size: 60rpx;
374
+ color: #ffffff;
375
+ animation: rotate 0.8s linear infinite;
376
+ }
377
+
378
+ .progress-text {
379
+ color: #ffffff;
380
+ font-size: 24rpx;
381
+ margin-top: 16rpx;
382
+ }
383
+
384
+ .progress-bar {
385
+ width: 120rpx;
386
+ height: 8rpx;
387
+ background-color: rgba(255, 255, 255, 0.3);
388
+ border-radius: 4rpx;
389
+ margin-top: 16rpx;
390
+ overflow: hidden;
391
+
392
+ .progress-bar-inner {
393
+ height: 100%;
394
+ background-color: #1890ff;
395
+ border-radius: 4rpx;
396
+ transition: width 0.3s;
397
+ }
398
+ }
399
+ }
400
+ }
401
+
402
+ .uploading {
403
+ border: 2rpx dashed #1890ff;
404
+ background-color: rgba(24, 144, 255, 0.05);
405
+
406
+ .loading-icon {
407
+ height: 48rpx;
408
+ width: 48rpx;
409
+ color: #1890ff;
410
+ animation: rotate 2s linear infinite;
411
+ }
412
+
413
+ .upload-text {
414
+ color: #1890ff;
415
+ font-size: 24rpx;
416
+ }
417
+
418
+ .progress-bar {
419
+ width: 140rpx;
420
+ height: 12rpx;
421
+ background-color: #e6f7ff;
422
+ border-radius: 6rpx;
423
+ margin-top: 16rpx;
424
+ overflow: hidden;
425
+
426
+ .progress-bar-inner {
427
+ height: 100%;
428
+ background-color: #1890ff;
429
+ border-radius: 6rpx;
430
+ transition: width 0.3s;
431
+ }
432
+ }
433
+ }
434
+ }
435
+ }
436
+
437
+ .video-readonly {
438
+ width: 100%;
439
+
440
+ .video-list {
441
+ display: flex;
442
+ flex-wrap: wrap;
443
+ gap: 20rpx;
444
+
445
+ .video-item-readonly {
446
+ width: 100%;
447
+
448
+ .video-player {
449
+ width: 100%;
450
+ height: 400rpx;
451
+ border-radius: 12rpx;
452
+ }
453
+ }
454
+ }
455
+ }
456
+
457
+ .tips {
458
+ padding-bottom: 12rpx;
459
+ font-size: 26rpx;
460
+ color: #999999;
461
+ }
462
+ </style>
@@ -0,0 +1 @@
1
+ export {};
package/dist/uniapp.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * uni-app 专用组件导出
3
+ * 使用方式: import { PicPicker, VideoPicker } from 'xydata-tools/dist/uniapp'
4
+ *
5
+ * @module uniapp
6
+ */
7
+
8
+ /**
9
+ * PicPicker - uni-app 图片选择上传组件
10
+ */
11
+ export { default as PicPicker } from "./components/uni-app/PicPicker.vue";
12
+
13
+ /**
14
+ * VideoPicker - uni-app 视频选择上传组件
15
+ */
16
+ export { default as VideoPicker } from "./components/uni-app/VideoPicker.vue";
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "xydata-tools",
3
- "version": "1.0.41",
3
+ "version": "1.0.42",
4
4
  "description": "xydata tools",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./uniapp": "./dist/uniapp.js"
10
+ },
7
11
  "scripts": {
8
12
  "dev": "yarn link && father dev",
9
13
  "build": "father build",
@@ -28,8 +32,12 @@
28
32
  "@babel/core": "7.24.9",
29
33
  "@babel/preset-env": "7.24.8",
30
34
  "@babel/preset-react": "7.24.7",
35
+ "@vitejs/plugin-vue2": "2.3.1",
31
36
  "father": "4.1.5",
32
- "terser": "5.31.3"
37
+ "terser": "5.31.3",
38
+ "vue": "2.7.16",
39
+ "vue-loader": "15.11.1",
40
+ "vue-template-compiler": "2.7.16"
33
41
  },
34
42
  "dependencies": {
35
43
  "@ant-design/icons": "4.5.0",
@@ -41,6 +49,7 @@
41
49
  "querystring": "0.2.1",
42
50
  "stringify": "5.2.0",
43
51
  "styled-components": "4.4.1",
44
- "umi-request": "1.0.8"
52
+ "umi-request": "1.0.8",
53
+ "compressorjs": "1.2.1"
45
54
  }
46
55
  }