imeik-bizui 2.0.3 → 2.0.5

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.
@@ -1,452 +0,0 @@
1
- <template>
2
- <!-- 录屏组件容器 -->
3
- <div class="record-screen">
4
- <!-- 预览列表区域 -->
5
- <div class="preview-list">
6
- <!-- 遍历已录制视频列表,显示预览项 -->
7
- <div v-for="(recordingUrl, index) in recordings" :key="index" class="preview-item">
8
- <!-- 如果有视频URL则显示视频预览 -->
9
- <img
10
- v-if="recordingUrl"
11
- :src="`${recordingUrl}?x-oss-process=video/snapshot,t_1000,f_jpg,w_1140,h_640`"
12
- class="video-preview"
13
- />
14
- <!-- 加载中状态显示loading -->
15
- <ImLoading v-else />
16
- <!-- 播放按钮 -->
17
- <div v-if="recordingUrl" class="play-btn" @click="playVideo(recordingUrl)">
18
- <img src="https://udstatic.imeik.com/pcUploads/1732257019241/play-icon.png" alt="播放" />
19
- </div>
20
- <!-- 预览项操作按钮区域 -->
21
- <div class="preview-actions">
22
- <!-- 删除按钮 -->
23
- <span class="close-btn" @click="removeRecording(index)">
24
- <img src="https://udstatic.imeik.com/pcUploads/1732247355378/delete-icon.png" alt="删除" />
25
- </span>
26
- </div>
27
- </div>
28
- <!-- 录制按钮和上传按钮的容器 -->
29
- <div v-if="recordings.length < maxRecordings" class="action-buttons">
30
- <!-- 添加 v-if 判断是否支持录屏 -->
31
- <div v-if="isScreenRecordingSupported" class="record-btn" @click="startRecording">
32
- <img class="icon-record" src="https://udstatic.imeik.com/pcUploads/1732247683538/record-icon.png" alt="录屏" />
33
- <span class="text">开始录制</span>
34
- </div>
35
- <div class="upload-btn" @click="triggerFileUpload">
36
- <img class="icon-upload" src="https://udstatic.imeik.com/pcUploads/1732929953519/plus-icon.png" alt="上传" />
37
- <span class="text">上传视频</span>
38
- <input
39
- ref="fileInput"
40
- type="file"
41
- accept="video/*"
42
- style="display: none"
43
- @change="handleFileUpload"
44
- />
45
- </div>
46
- </div>
47
- </div>
48
- <!-- tips -->
49
- <div class="tips">{{ tips }}</div>
50
- <!-- 错误提示信息 -->
51
- <div v-if="error" class="error-tip">{{ error }}</div>
52
- <!-- 视频播放弹窗 -->
53
- <ImDialog
54
- title="视频播放"
55
- :visible.sync="dialogVisible"
56
- width="900px"
57
- height="auto"
58
- append-to-body
59
- hide-footer
60
- >
61
- <!-- 视频播放器 -->
62
- <video
63
- v-if="currentVideoUrl"
64
- :src="currentVideoUrl"
65
- controls
66
- autoplay
67
- style="width: 100%; max-height: 70vh;"
68
- >
69
- </video>
70
- </ImDialog>
71
- </div>
72
- </template>
73
-
74
- <script>
75
- import { fnUploadRequest } from './upload'
76
- export default {
77
- name: 'RecordScreen',
78
- // 组件属性定义
79
- props: {
80
- value: {
81
- type: Array,
82
- default: () => [] // 默认空数组
83
- },
84
- maxRecordings: {
85
- type: Number,
86
- default: 3 // 默认最大录制数量
87
- },
88
- tips: {
89
- type: String,
90
- default: '请上传视频文件,每个文件不超过500M,最多上传3个视频'
91
- }
92
- },
93
- // 组件内部数据
94
- data() {
95
- return {
96
- recordings: [], // 录制视频列表
97
- isRecording: false, // 是否正在录制
98
- mediaRecorder: null, // 媒体录制器实例
99
- recordedChunks: [], // 录制的视频数据块
100
- error: '', // 错误信息
101
- dialogVisible: false, // 视频播放弹窗显示状态
102
- currentVideoUrl: '' // 当前播放的视频URL
103
- }
104
- },
105
- computed: {
106
- // 添加计算属性检查是否支持录屏
107
- isScreenRecordingSupported() {
108
- return !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia)
109
- }
110
- },
111
- // 监听属性变化
112
- watch: {
113
- value: {
114
- handler(newVal) {
115
- this.setMyValue(newVal)
116
- },
117
- immediate: true // 组件创建时立即执行一次
118
- }
119
- },
120
- methods: {
121
- // 设置组件内部的recordings值
122
- setMyValue(newVal) {
123
- try {
124
- this.recordings = JSON.parse(JSON.stringify(newVal))
125
- } catch (err) {
126
- this.recordings = []
127
- }
128
- },
129
-
130
- // 开始录制
131
- async startRecording() {
132
- if (this.isRecording) {
133
- return
134
- }
135
- this.error = ''
136
-
137
- try {
138
- // 获取屏幕共享流
139
- const stream = await navigator.mediaDevices.getDisplayMedia({
140
- video: {
141
- cursor: 'always', // 显示鼠标指针
142
- displaySurface: 'window' // 可选择窗口
143
- },
144
- audio: false // 不录制音频
145
- })
146
-
147
- // 创建MediaRecorder实例
148
- this.mediaRecorder = new MediaRecorder(stream)
149
- this.recordedChunks = []
150
-
151
- // 监听数据可用事件
152
- this.mediaRecorder.ondataavailable = (event) => {
153
- if (event.data.size > 0) {
154
- this.recordedChunks.push(event.data)
155
- }
156
- }
157
-
158
- // 通知父组件开始录制
159
- this.mediaRecorder.onstart = () => {
160
- this.$emit('startRecord')
161
- this.isRecording = true
162
- }
163
-
164
- // 监听录制停止事件
165
- this.mediaRecorder.onstop = () => {
166
- this.isRecording = false
167
- // 将录制的数据块合并为blob
168
- const blob = new Blob(this.recordedChunks, { type: 'video/webm' })
169
- const fileName = '录屏-' + new Date().getTime() + '.webm'
170
- const file = new File([blob], fileName, { type: 'video/webm' })
171
-
172
- this.recordings.push('') // 添加空占位
173
- // 上传文件
174
- fnUploadRequest({
175
- file,
176
- onSuccess: (res) => {
177
- // 更新视频URL
178
- this.$set(this.recordings, this.recordings.length - 1, res.url)
179
- // 通知父组件值变化
180
- this.onUpdate()
181
- // 停止所有媒体轨道
182
- stream.getTracks().forEach((track) => track.stop())
183
- },
184
- onError: (err) => {
185
- this.error = err.message || '截屏失败,请重试'
186
- stream.getTracks().forEach((track) => track.stop())
187
- this.recordings.pop()
188
- }
189
- })
190
- }
191
-
192
- // 检查录制数量限制
193
- if (this.recordings.length >= this.maxRecordings) {
194
- throw new Error(`最多只能上传${this.maxRecordings}个录屏`)
195
- } else {
196
- // 开始录制
197
- this.mediaRecorder.start()
198
- }
199
- } catch (err) {
200
- this.$emit('endRecord')
201
- if (err.message === 'Permission denied') {
202
- this.error = '操作权限被阻止'
203
- } else {
204
- this.error = err.message || '录屏失败,请重试'
205
- }
206
- this.isRecording = false
207
- }
208
- },
209
-
210
- // 停止录制
211
- stopRecording() {
212
- if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
213
- this.mediaRecorder.stop()
214
- }
215
- this.$emit('endRecord') // 通知父组件录制结束
216
- },
217
-
218
- // 删除指定索引的录制视频
219
- removeRecording(index) {
220
- URL.revokeObjectURL(this.recordings[index].url) // 释放URL对象
221
- this.recordings.splice(index, 1)
222
- this.onUpdate() // 通知父组件值变化
223
- },
224
-
225
- // 更新父组件的值
226
- onUpdate() {
227
- console.log('this.recordings', this.recordings)
228
- this.$emit('input', this.recordings)
229
- },
230
-
231
- // 播放视频
232
- playVideo(videoUrl) {
233
- this.currentVideoUrl = videoUrl
234
- this.dialogVisible = true
235
- },
236
-
237
- // 关闭视频播放弹窗
238
- handleClose() {
239
- this.dialogVisible = false
240
- this.currentVideoUrl = ''
241
- },
242
-
243
- // 添加新方法
244
- triggerFileUpload() {
245
- this.$refs.fileInput.click()
246
- },
247
-
248
- handleFileUpload(event) {
249
- const file = event.target.files[0]
250
- if (!file) return
251
-
252
- // 检查文件类型
253
- if (!file.type.startsWith('video/')) {
254
- this.error = '请上传视频文件'
255
- return
256
- }
257
-
258
- // 检查文件大小
259
- const maxSize = 500 * 1024 * 1024
260
- if (file.size > maxSize) {
261
- this.error = '视频文件大小不能超过500M'
262
- return
263
- }
264
-
265
- this.recordings.push('') // 添加空占位
266
-
267
- // 使用现有的上传方法
268
- fnUploadRequest({
269
- file,
270
- onSuccess: (res) => {
271
- this.$set(this.recordings, this.recordings.length - 1, res.url)
272
- this.onUpdate()
273
- },
274
- onError: (err) => {
275
- this.error = err.message || '上传失败,请重试'
276
- this.recordings.pop() // 移除空占位
277
- }
278
- })
279
-
280
- // 清空input以允许重复上传相同文件
281
- event.target.value = ''
282
- }
283
- }
284
- }
285
- </script>
286
-
287
- <style scoped lang="scss">
288
- .record-screen {
289
- // 预览列表样式
290
- .preview-list {
291
- display: flex;
292
- flex-wrap: wrap;
293
- gap: 10px;
294
- margin-top: 10px;
295
-
296
- // 预览项样式
297
- .preview-item {
298
- position: relative;
299
- width: 228px;
300
- height: 128px;
301
- border-radius: 5px;
302
- border: 1px solid #d9d9d9;
303
- text-align: center;
304
- line-height: 128px;
305
-
306
- // 视频预览样式
307
- .video-preview {
308
- position: relative;
309
- z-index: 1;
310
- width: 100%;
311
- height: 100%;
312
- object-fit: cover;
313
- }
314
-
315
- // 预览项遮罩层
316
- &::after {
317
- content: '';
318
- position: absolute;
319
- top: 0;
320
- left: 0;
321
- width: 100%;
322
- height: 100%;
323
- background-color: rgba(0, 0, 0, 0.3);
324
- z-index: 1;
325
- border-radius: 5px;
326
- }
327
-
328
- // 播放按钮样式
329
- .play-btn {
330
- position: absolute;
331
- z-index: 3;
332
- top: 50%;
333
- left: 50%;
334
- transform: translate(-50%, -50%);
335
- width: 45px;
336
- height: 45px;
337
- cursor: pointer;
338
- background: rgba(0, 0, 0, 0.2);
339
- border-radius: 50%;
340
- display: flex;
341
- align-items: center;
342
- justify-content: center;
343
-
344
- img {
345
- width: 100%;
346
- height: 100%;
347
- }
348
-
349
- &:hover {
350
- background: rgba(0, 0, 0, 0.5);
351
- }
352
- }
353
-
354
- // 预览项操作按钮样式
355
- .preview-actions {
356
- position: absolute;
357
- z-index: 3;
358
- top: -8px;
359
- right: -8px;
360
-
361
- // 删除按钮样式
362
- .close-btn {
363
- display: flex;
364
- align-items: center;
365
- justify-content: center;
366
- width: 16px;
367
- height: 16px;
368
- cursor: pointer;
369
- padding: 0;
370
-
371
- img {
372
- width: 16px;
373
- height: 16px;
374
- }
375
-
376
- &:hover {
377
- opacity: 0.8;
378
- }
379
- }
380
- }
381
- }
382
-
383
- // 录制按钮和上传按钮的容器
384
- .action-buttons {
385
- display: flex;
386
- gap: 10px;
387
-
388
- .record-btn,
389
- .upload-btn {
390
- display: flex;
391
- flex-direction: column;
392
- gap: 13px;
393
- align-items: center;
394
- justify-content: center;
395
- width: 128px;
396
- height: 128px;
397
- border: 1px dashed #d9d9d9;
398
- border-radius: 3px;
399
- cursor: pointer;
400
-
401
- &:hover {
402
- border-color: #40a9ff;
403
- }
404
-
405
- .icon-record {
406
- width: 26px;
407
- height: 20px;
408
- }
409
-
410
- .icon-upload {
411
- width: 26px;
412
- height: 26px;
413
- }
414
-
415
- .text {
416
- font-size: 12px;
417
- color: #999999;
418
- line-height: 14px;
419
- }
420
- }
421
- }
422
- }
423
-
424
- // tips
425
- .tips {
426
- margin-top: 10px;
427
- font-size: 12px;
428
- color: #999999;
429
- line-height: 12px;
430
- }
431
-
432
- // 错误提示样式
433
- .error-tip {
434
- margin-top: 8px;
435
- color: #ff4d4f;
436
- font-size: 12px;
437
- }
438
- }
439
-
440
- // 录制中动画
441
- @keyframes pulse {
442
- 0% {
443
- border-color: #ff4d4f;
444
- }
445
- 50% {
446
- border-color: #ffccc7;
447
- }
448
- 100% {
449
- border-color: #ff4d4f;
450
- }
451
- }
452
- </style>
@@ -1,52 +0,0 @@
1
- import { findAliToken } from '../../../api/user.js'
2
- import Vue from 'vue'
3
- const vue = new Vue()
4
- const OSS = require('ali-oss')
5
-
6
- const VUE_APP_CDN_DOMAIN = 'https://udstatic.imeik.com'
7
-
8
- export function beforeUpload(file, maxSize = 20) {
9
- if (file.size > maxSize * 1024 * 1024) {
10
- vue.$message.error(`上传文件[${file.name}]大小不能超过 ${maxSize}MB!`)
11
- return false
12
- }
13
- return true
14
- }
15
-
16
- export async function fnUploadRequest(options) {
17
- try {
18
- const res = await findAliToken()
19
- const resData = res.data
20
- const client = new OSS({
21
- accessKeyId: resData.accessKeyId, // 查看你自己的阿里云KEY
22
- accessKeySecret: resData.accessKeySecret, // 查看自己的阿里云KEYSECRET
23
- stsToken: resData.securityToken,
24
- bucket: resData.bucket, // 你的 OSS bucket 名称
25
- region: 'oss-cn-beijing', // bucket 所在地址,我的是 华北2 北京
26
- endpoint: 'oss-cn-beijing.aliyuncs.com', // 自己的域名
27
- timeout: 600000
28
- // cname: 'udstatic.imeik.com'
29
- })
30
- const file = options.file // 拿到 file
31
- const fileName = file.name // .substr(0, file.name.lastIndexOf('.'))
32
- if (encodeURIComponent(fileName).length > 300) {
33
- vue.$message.warning('文件名称长度太长可能导致错误,请重新命名!')
34
- }
35
- const date = new Date().getTime()
36
- const fileNames = `${date}/${fileName}` // 拼接文件名,保证唯一,这里使用时间戳+原文件名
37
- // 上传文件,这里是上传到OSS的 uploads文件夹下
38
- client.put('pcUploads/' + fileNames, file).then(res => {
39
- if (res.res.statusCode === 200) {
40
- res.url = res.url.replace('http://imeikud.oss-cn-beijing.aliyuncs.com', VUE_APP_CDN_DOMAIN)
41
- res.url = res.url.replace('https://imeikud.oss-cn-beijing.aliyuncs.com', VUE_APP_CDN_DOMAIN)
42
- const nameArr = res.name.split('/')
43
- res.name = nameArr[nameArr.length - 1]
44
- options.onSuccess(res)
45
- } else {
46
- options.onError('上传失败')
47
- }
48
- })
49
- } catch (e) {
50
- options.onError('上传失败')
51
- }
52
- }