@ruixinkeji/prism-ui 1.0.0 → 1.0.2

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.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/components/PrismAIAssist/PrismAIAssist.vue +98 -98
  3. package/components/PrismAddressInput/PrismAddressInput.vue +597 -597
  4. package/components/PrismCityCascadeSelect/PrismCityCascadeSelect.vue +793 -793
  5. package/components/PrismCityPicker/PrismCityPicker.vue +1008 -1008
  6. package/components/PrismCitySelect/PrismCitySelect.vue +435 -435
  7. package/components/PrismCode/PrismCode.vue +749 -749
  8. package/components/PrismCodeInput/PrismCodeInput.vue +156 -156
  9. package/components/PrismDateTimePicker/PrismDateTimePicker.vue +953 -953
  10. package/components/PrismDropdown/PrismDropdown.vue +77 -77
  11. package/components/PrismGroupSticky/PrismGroupSticky.vue +352 -352
  12. package/components/PrismIdCardInput/PrismIdCardInput.vue +253 -253
  13. package/components/PrismImagePicker/PrismImagePicker.vue +457 -457
  14. package/components/PrismIndexBar/PrismIndexBar.vue +243 -243
  15. package/components/PrismLicensePlateInput/PrismLicensePlateInput.vue +1100 -1100
  16. package/components/PrismMusicPlayer/PrismMusicPlayer.vue +530 -530
  17. package/components/PrismNavBar/PrismNavBar.vue +199 -199
  18. package/components/PrismSecureInput/PrismSecureInput.vue +360 -360
  19. package/components/PrismSticky/PrismSticky.vue +173 -173
  20. package/components/PrismSwiper/PrismSwiper.vue +338 -338
  21. package/components/PrismSwitch/PrismSwitch.vue +202 -202
  22. package/components/PrismTabBar/PrismTabBar.vue +147 -147
  23. package/components/PrismTabs/PrismTabs.vue +49 -49
  24. package/components/PrismVoiceInput/PrismVoiceInput.vue +529 -529
  25. package/fonts/fa-brands-400.woff2 +0 -0
  26. package/fonts/fa-regular-400.woff2 +0 -0
  27. package/fonts/fa-solid-900.woff2 +0 -0
  28. package/fonts/fa-v4compatibility.woff2 +0 -0
  29. package/fonts/font-awesome.css +913 -0
  30. package/fonts/iconfont.woff2 +0 -0
  31. package/package.json +7 -1
  32. package/store/app.d.ts +21 -0
  33. package/store/app.js +68 -0
  34. package/styles/base.scss +2 -2
@@ -1,529 +1,529 @@
1
- <template>
2
- <view class="prism-voice-input" :class="{ 'dark-mode': appStore.isDarkMode }">
3
- <!-- 模式1: 按钮模式(默认) -->
4
- <view
5
- v-if="mode === 'button'"
6
- class="voice-btn"
7
- :class="{ 'recording': isRecording }"
8
- @touchstart.prevent="startRecording"
9
- @touchend.prevent="stopRecording"
10
- @touchcancel.prevent="cancelRecording"
11
- >
12
- <view class="voice-icon-wrapper">
13
- <text class="fa fa-microphone"></text>
14
- <!-- 录音波纹动画 -->
15
- <view class="voice-waves" v-if="isRecording">
16
- <view class="wave wave-1"></view>
17
- <view class="wave wave-2"></view>
18
- <view class="wave wave-3"></view>
19
- </view>
20
- </view>
21
- </view>
22
-
23
- <!-- 模式2: 弹窗模式 -->
24
- <view class="voice-panel" v-if="mode === 'modal' && visible" @click="handleCancel">
25
- <view class="voice-content" @click.stop>
26
- <view class="voice-title">{{ title }}</view>
27
- <view class="voice-wave" :class="{ 'recording': isRecording }">
28
- <view class="wave-bar" v-for="i in 5" :key="i"></view>
29
- </view>
30
- <view class="voice-time">{{ formatTime(recordingTime) }}</view>
31
- <view class="voice-tip">{{ isRecording ? '松开结束录音' : '按住开始录音' }}</view>
32
- <view class="voice-actions">
33
- <view class="voice-cancel-btn" @click="handleCancel">
34
- <text class="fa fa-times"></text>
35
- <text>取消</text>
36
- </view>
37
- <view
38
- class="voice-record-btn"
39
- :class="{ 'recording': isRecording }"
40
- @touchstart.prevent="startRecording"
41
- @touchend.prevent="stopRecording"
42
- @mousedown.prevent="startRecording"
43
- @mouseup.prevent="stopRecording"
44
- >
45
- <text class="fa fa-microphone"></text>
46
- </view>
47
- <view class="voice-confirm-btn" :class="{ 'disabled': !hasRecorded }" @click="handleConfirm">
48
- <text class="fa fa-check"></text>
49
- <text>完成</text>
50
- </view>
51
- </view>
52
- </view>
53
- </view>
54
-
55
- <!-- 按钮模式的录音弹窗提示 -->
56
- <view class="voice-modal" v-if="mode === 'button' && isRecording" @click.stop>
57
- <view class="modal-content">
58
- <view class="voice-animation">
59
- <view class="voice-bar" v-for="i in 5" :key="i" :style="{ animationDelay: `${i * 0.1}s` }"></view>
60
- </view>
61
- <text class="voice-text">{{ cancelHint ? '松开取消' : '正在录音...' }}</text>
62
- <text class="voice-tip-time">{{ recordingTime }}s</text>
63
- </view>
64
- </view>
65
- </view>
66
- </template>
67
-
68
- <script setup>
69
- import { ref, watch, onUnmounted } from 'vue';
70
- import { useAppStore } from '@/store/app';
71
-
72
- const props = defineProps({
73
- // 模式:button(按钮)或 modal(弹窗)
74
- mode: {
75
- type: String,
76
- default: 'button',
77
- validator: (val) => ['button', 'modal'].includes(val)
78
- },
79
- // 弹窗模式的可见性
80
- visible: {
81
- type: Boolean,
82
- default: false
83
- },
84
- // 弹窗标题
85
- title: {
86
- type: String,
87
- default: '语音输入'
88
- },
89
- // 最长录音时间(秒)
90
- maxDuration: {
91
- type: Number,
92
- default: 60
93
- }
94
- });
95
-
96
- const emit = defineEmits(['update:visible', 'start', 'end', 'cancel', 'result', 'confirm']);
97
-
98
- const appStore = useAppStore();
99
- const isRecording = ref(false);
100
- const cancelHint = ref(false);
101
- const recordingTime = ref(0);
102
- const hasRecorded = ref(false);
103
-
104
- let recordingTimer = null;
105
- let recorderManager = null;
106
-
107
- // 格式化时间
108
- function formatTime(seconds) {
109
- const mins = Math.floor(seconds / 60);
110
- const secs = seconds % 60;
111
- return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
112
- }
113
-
114
- function startRecording() {
115
- isRecording.value = true;
116
- cancelHint.value = false;
117
- recordingTime.value = 0;
118
- hasRecorded.value = false;
119
-
120
- emit('start');
121
-
122
- // 开始计时
123
- recordingTimer = setInterval(() => {
124
- recordingTime.value++;
125
- if (recordingTime.value >= props.maxDuration) {
126
- stopRecording();
127
- }
128
- }, 1000);
129
-
130
- // 使用 uni-app 录音管理器
131
- // #ifdef APP-PLUS || MP-WEIXIN
132
- recorderManager = uni.getRecorderManager();
133
- recorderManager.onStop((res) => {
134
- if (!cancelHint.value) {
135
- emit('result', {
136
- tempFilePath: res.tempFilePath,
137
- duration: recordingTime.value
138
- });
139
- }
140
- });
141
- recorderManager.start({
142
- duration: props.maxDuration * 1000,
143
- format: 'mp3'
144
- });
145
- // #endif
146
- }
147
-
148
- function stopRecording() {
149
- if (!isRecording.value) return;
150
-
151
- isRecording.value = false;
152
- hasRecorded.value = recordingTime.value > 0;
153
- clearInterval(recordingTimer);
154
-
155
- emit('end', { duration: recordingTime.value });
156
-
157
- // #ifdef APP-PLUS || MP-WEIXIN
158
- if (recorderManager) {
159
- recorderManager.stop();
160
- }
161
- // #endif
162
- }
163
-
164
- function cancelRecording() {
165
- if (!isRecording.value && props.mode === 'button') return;
166
-
167
- cancelHint.value = true;
168
- isRecording.value = false;
169
- clearInterval(recordingTimer);
170
-
171
- emit('cancel');
172
-
173
- // #ifdef APP-PLUS || MP-WEIXIN
174
- if (recorderManager) {
175
- recorderManager.stop();
176
- }
177
- // #endif
178
- }
179
-
180
- // 弹窗模式 - 取消
181
- function handleCancel() {
182
- if (recordingTimer) {
183
- clearInterval(recordingTimer);
184
- recordingTimer = null;
185
- }
186
- isRecording.value = false;
187
- emit('cancel');
188
- closePanel();
189
- }
190
-
191
- // 弹窗模式 - 确认
192
- function handleConfirm() {
193
- if (!hasRecorded.value) return;
194
-
195
- uni.showLoading({ title: '识别中...' });
196
-
197
- setTimeout(() => {
198
- uni.hideLoading();
199
- emit('confirm', {
200
- duration: recordingTime.value
201
- });
202
- closePanel();
203
- uni.showToast({ title: '识别成功', icon: 'success' });
204
- }, 1500);
205
- }
206
-
207
- // 关闭弹窗
208
- function closePanel() {
209
- emit('update:visible', false);
210
- recordingTime.value = 0;
211
- hasRecorded.value = false;
212
- }
213
-
214
- // 监听 visible 变化,重置状态
215
- watch(() => props.visible, (val) => {
216
- if (val) {
217
- isRecording.value = false;
218
- recordingTime.value = 0;
219
- hasRecorded.value = false;
220
- }
221
- });
222
-
223
- onUnmounted(() => {
224
- if (recordingTimer) {
225
- clearInterval(recordingTimer);
226
- }
227
- });
228
- </script>
229
-
230
- <style lang="scss">
231
- .prism-voice-input {
232
- display: inline-flex;
233
- align-items: center;
234
- justify-content: center;
235
-
236
- // ============ 按钮模式 ============
237
- .voice-btn {
238
- width: 80rpx;
239
- height: 80rpx;
240
- border-radius: 50%;
241
- background: var(--prism-primary-color, #3478F6);
242
- display: flex;
243
- align-items: center;
244
- justify-content: center;
245
- transition: all 0.3s ease;
246
- position: relative;
247
-
248
- &:active, &.recording {
249
- transform: scale(1.1);
250
- box-shadow: 0 8rpx 24rpx rgba(52, 120, 246, 0.4);
251
- }
252
-
253
- .voice-icon-wrapper {
254
- position: relative;
255
- display: flex;
256
- align-items: center;
257
- justify-content: center;
258
-
259
- .fa {
260
- font-size: 36rpx;
261
- color: #FFFFFF;
262
- z-index: 2;
263
- }
264
- }
265
-
266
- .voice-waves {
267
- position: absolute;
268
- width: 100%;
269
- height: 100%;
270
- display: flex;
271
- align-items: center;
272
- justify-content: center;
273
-
274
- .wave {
275
- position: absolute;
276
- width: 100%;
277
- height: 100%;
278
- border-radius: 50%;
279
- border: 2rpx solid rgba(255, 255, 255, 0.5);
280
- animation: waveAnim 1.5s infinite ease-out;
281
- }
282
-
283
- .wave-1 { animation-delay: 0s; }
284
- .wave-2 { animation-delay: 0.5s; }
285
- .wave-3 { animation-delay: 1s; }
286
- }
287
- }
288
-
289
- .voice-modal {
290
- position: fixed;
291
- top: 0;
292
- left: 0;
293
- right: 0;
294
- bottom: 0;
295
- background: rgba(0, 0, 0, 0.6);
296
- display: flex;
297
- align-items: center;
298
- justify-content: center;
299
- z-index: 9999;
300
-
301
- .modal-content {
302
- width: 300rpx;
303
- height: 300rpx;
304
- background: var(--prism-bg-color-card, #FFFFFF);
305
- border-radius: 24rpx;
306
- display: flex;
307
- flex-direction: column;
308
- align-items: center;
309
- justify-content: center;
310
- gap: 24rpx;
311
- }
312
-
313
- .voice-animation {
314
- display: flex;
315
- align-items: center;
316
- justify-content: center;
317
- gap: 8rpx;
318
- height: 80rpx;
319
-
320
- .voice-bar {
321
- width: 8rpx;
322
- height: 40rpx;
323
- background: var(--prism-primary-color, #3478F6);
324
- border-radius: 4rpx;
325
- animation: voiceBarAnim 0.8s infinite ease-in-out alternate;
326
- }
327
- }
328
-
329
- .voice-text {
330
- font-size: 28rpx;
331
- color: var(--prism-text-primary, #1D2129);
332
- }
333
-
334
- .voice-tip-time {
335
- font-size: 48rpx;
336
- font-weight: 600;
337
- color: var(--prism-primary-color, #3478F6);
338
- }
339
- }
340
-
341
- @keyframes waveAnim {
342
- 0% {
343
- transform: scale(1);
344
- opacity: 1;
345
- }
346
- 100% {
347
- transform: scale(2);
348
- opacity: 0;
349
- }
350
- }
351
-
352
- @keyframes voiceBarAnim {
353
- 0% { height: 20rpx; }
354
- 100% { height: 60rpx; }
355
- }
356
-
357
- // ============ 弹窗模式 ============
358
- .voice-panel {
359
- position: fixed;
360
- top: 0;
361
- left: 0;
362
- right: 0;
363
- bottom: 0;
364
- background: var(--prism-mask-bg, rgba(0, 0, 0, 0.5));
365
- display: flex;
366
- align-items: center;
367
- justify-content: center;
368
- z-index: 9999;
369
- }
370
-
371
- .voice-content {
372
- width: 560rpx;
373
- background: var(--prism-bg-color-card, #FFFFFF);
374
- border-radius: 24rpx;
375
- padding: 40rpx;
376
- display: flex;
377
- flex-direction: column;
378
- align-items: center;
379
- }
380
-
381
- .voice-title {
382
- font-size: 32rpx;
383
- font-weight: 600;
384
- color: var(--prism-text-primary, #1D2129);
385
- margin-bottom: 40rpx;
386
- }
387
-
388
- .voice-wave {
389
- display: flex;
390
- align-items: center;
391
- justify-content: center;
392
- gap: 12rpx;
393
- height: 100rpx;
394
- margin-bottom: 24rpx;
395
-
396
- .wave-bar {
397
- width: 8rpx;
398
- height: 20rpx;
399
- background: var(--prism-text-placeholder, #C9CDD4);
400
- border-radius: 4rpx;
401
- transition: all 0.3s;
402
- }
403
-
404
- &.recording .wave-bar {
405
- background: var(--prism-primary-color, #3478F6);
406
- animation: prism-voice-wave 0.5s ease-in-out infinite alternate;
407
- }
408
-
409
- &.recording .wave-bar:nth-child(1) { animation-delay: 0s; }
410
- &.recording .wave-bar:nth-child(2) { animation-delay: 0.1s; }
411
- &.recording .wave-bar:nth-child(3) { animation-delay: 0.2s; }
412
- &.recording .wave-bar:nth-child(4) { animation-delay: 0.3s; }
413
- &.recording .wave-bar:nth-child(5) { animation-delay: 0.4s; }
414
- }
415
-
416
- @keyframes prism-voice-wave {
417
- 0% { height: 20rpx; }
418
- 100% { height: 80rpx; }
419
- }
420
-
421
- .voice-time {
422
- font-size: 48rpx;
423
- font-weight: 600;
424
- color: var(--prism-text-primary, #1D2129);
425
- margin-bottom: 16rpx;
426
- }
427
-
428
- .voice-tip {
429
- font-size: 26rpx;
430
- color: var(--prism-text-secondary, #86909C);
431
- margin-bottom: 40rpx;
432
- }
433
-
434
- .voice-actions {
435
- display: flex;
436
- align-items: center;
437
- justify-content: space-between;
438
- width: 100%;
439
- }
440
-
441
- .voice-cancel-btn,
442
- .voice-confirm-btn {
443
- display: flex;
444
- flex-direction: column;
445
- align-items: center;
446
- gap: 8rpx;
447
- color: var(--prism-text-secondary, #86909C);
448
-
449
- .fa {
450
- width: 64rpx;
451
- height: 64rpx;
452
- border-radius: 50%;
453
- display: flex;
454
- align-items: center;
455
- justify-content: center;
456
- font-size: 28rpx;
457
- }
458
-
459
- text:last-child {
460
- font-size: 24rpx;
461
- }
462
-
463
- &:active {
464
- opacity: 0.7;
465
- }
466
- }
467
-
468
- .voice-cancel-btn .fa {
469
- color: var(--prism-danger-color, #F53F3F);
470
- background: rgba(245, 63, 63, 0.15);
471
- }
472
-
473
- .voice-confirm-btn {
474
- color: var(--prism-success-color, #00B42A);
475
-
476
- .fa {
477
- background: rgba(0, 180, 42, 0.15);
478
- color: #00B42A;
479
- }
480
-
481
- &.disabled {
482
- opacity: 0.4;
483
- pointer-events: none;
484
- }
485
- }
486
-
487
- .voice-record-btn {
488
- width: 120rpx;
489
- height: 120rpx;
490
- background: var(--prism-primary-color, #3478F6);
491
- border-radius: 50%;
492
- display: flex;
493
- align-items: center;
494
- justify-content: center;
495
- transition: all 0.2s ease;
496
-
497
- .fa {
498
- font-size: 48rpx;
499
- color: #FFFFFF;
500
- }
501
-
502
- &:active,
503
- &.recording {
504
- transform: scale(1.1);
505
- background: var(--prism-primary-color-active, #2563EB);
506
- box-shadow: 0 0 0 16rpx rgba(52, 120, 246, 0.2);
507
- }
508
- }
509
- }
510
-
511
- // 深色模式
512
- .dark-mode.prism-voice-input {
513
- .voice-modal .modal-content,
514
- .voice-content {
515
- background: var(--prism-bg-color-card, #1A1A1A);
516
- }
517
-
518
- .voice-modal .voice-text,
519
- .voice-title,
520
- .voice-time {
521
- color: var(--prism-text-primary, #E5E6EB);
522
- }
523
-
524
- .voice-cancel-btn .fa,
525
- .voice-confirm-btn .fa {
526
- background: rgba(255, 255, 255, 0.1);
527
- }
528
- }
529
- </style>
1
+ <template>
2
+ <view class="prism-voice-input" :class="{ 'dark-mode': appStore.isDarkMode }">
3
+ <!-- 模式1: 按钮模式(默认) -->
4
+ <view
5
+ v-if="mode === 'button'"
6
+ class="voice-btn"
7
+ :class="{ 'recording': isRecording }"
8
+ @touchstart.prevent="startRecording"
9
+ @touchend.prevent="stopRecording"
10
+ @touchcancel.prevent="cancelRecording"
11
+ >
12
+ <view class="voice-icon-wrapper">
13
+ <text class="fa fa-microphone"></text>
14
+ <!-- 录音波纹动画 -->
15
+ <view class="voice-waves" v-if="isRecording">
16
+ <view class="wave wave-1"></view>
17
+ <view class="wave wave-2"></view>
18
+ <view class="wave wave-3"></view>
19
+ </view>
20
+ </view>
21
+ </view>
22
+
23
+ <!-- 模式2: 弹窗模式 -->
24
+ <view class="voice-panel" v-if="mode === 'modal' && visible" @click="handleCancel">
25
+ <view class="voice-content" @click.stop>
26
+ <view class="voice-title">{{ title }}</view>
27
+ <view class="voice-wave" :class="{ 'recording': isRecording }">
28
+ <view class="wave-bar" v-for="i in 5" :key="i"></view>
29
+ </view>
30
+ <view class="voice-time">{{ formatTime(recordingTime) }}</view>
31
+ <view class="voice-tip">{{ isRecording ? '松开结束录音' : '按住开始录音' }}</view>
32
+ <view class="voice-actions">
33
+ <view class="voice-cancel-btn" @click="handleCancel">
34
+ <text class="fa fa-times"></text>
35
+ <text>取消</text>
36
+ </view>
37
+ <view
38
+ class="voice-record-btn"
39
+ :class="{ 'recording': isRecording }"
40
+ @touchstart.prevent="startRecording"
41
+ @touchend.prevent="stopRecording"
42
+ @mousedown.prevent="startRecording"
43
+ @mouseup.prevent="stopRecording"
44
+ >
45
+ <text class="fa fa-microphone"></text>
46
+ </view>
47
+ <view class="voice-confirm-btn" :class="{ 'disabled': !hasRecorded }" @click="handleConfirm">
48
+ <text class="fa fa-check"></text>
49
+ <text>完成</text>
50
+ </view>
51
+ </view>
52
+ </view>
53
+ </view>
54
+
55
+ <!-- 按钮模式的录音弹窗提示 -->
56
+ <view class="voice-modal" v-if="mode === 'button' && isRecording" @click.stop>
57
+ <view class="modal-content">
58
+ <view class="voice-animation">
59
+ <view class="voice-bar" v-for="i in 5" :key="i" :style="{ animationDelay: `${i * 0.1}s` }"></view>
60
+ </view>
61
+ <text class="voice-text">{{ cancelHint ? '松开取消' : '正在录音...' }}</text>
62
+ <text class="voice-tip-time">{{ recordingTime }}s</text>
63
+ </view>
64
+ </view>
65
+ </view>
66
+ </template>
67
+
68
+ <script setup>
69
+ import { ref, watch, onUnmounted } from 'vue';
70
+ import { useAppStore } from '@/store/app';
71
+
72
+ const props = defineProps({
73
+ // 模式:button(按钮)或 modal(弹窗)
74
+ mode: {
75
+ type: String,
76
+ default: 'button',
77
+ validator: (val) => ['button', 'modal'].includes(val)
78
+ },
79
+ // 弹窗模式的可见性
80
+ visible: {
81
+ type: Boolean,
82
+ default: false
83
+ },
84
+ // 弹窗标题
85
+ title: {
86
+ type: String,
87
+ default: '语音输入'
88
+ },
89
+ // 最长录音时间(秒)
90
+ maxDuration: {
91
+ type: Number,
92
+ default: 60
93
+ }
94
+ });
95
+
96
+ const emit = defineEmits(['update:visible', 'start', 'end', 'cancel', 'result', 'confirm']);
97
+
98
+ const appStore = useAppStore();
99
+ const isRecording = ref(false);
100
+ const cancelHint = ref(false);
101
+ const recordingTime = ref(0);
102
+ const hasRecorded = ref(false);
103
+
104
+ let recordingTimer = null;
105
+ let recorderManager = null;
106
+
107
+ // 格式化时间
108
+ function formatTime(seconds) {
109
+ const mins = Math.floor(seconds / 60);
110
+ const secs = seconds % 60;
111
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
112
+ }
113
+
114
+ function startRecording() {
115
+ isRecording.value = true;
116
+ cancelHint.value = false;
117
+ recordingTime.value = 0;
118
+ hasRecorded.value = false;
119
+
120
+ emit('start');
121
+
122
+ // 开始计时
123
+ recordingTimer = setInterval(() => {
124
+ recordingTime.value++;
125
+ if (recordingTime.value >= props.maxDuration) {
126
+ stopRecording();
127
+ }
128
+ }, 1000);
129
+
130
+ // 使用 uni-app 录音管理器
131
+ // #ifdef APP-PLUS || MP-WEIXIN
132
+ recorderManager = uni.getRecorderManager();
133
+ recorderManager.onStop((res) => {
134
+ if (!cancelHint.value) {
135
+ emit('result', {
136
+ tempFilePath: res.tempFilePath,
137
+ duration: recordingTime.value
138
+ });
139
+ }
140
+ });
141
+ recorderManager.start({
142
+ duration: props.maxDuration * 1000,
143
+ format: 'mp3'
144
+ });
145
+ // #endif
146
+ }
147
+
148
+ function stopRecording() {
149
+ if (!isRecording.value) return;
150
+
151
+ isRecording.value = false;
152
+ hasRecorded.value = recordingTime.value > 0;
153
+ clearInterval(recordingTimer);
154
+
155
+ emit('end', { duration: recordingTime.value });
156
+
157
+ // #ifdef APP-PLUS || MP-WEIXIN
158
+ if (recorderManager) {
159
+ recorderManager.stop();
160
+ }
161
+ // #endif
162
+ }
163
+
164
+ function cancelRecording() {
165
+ if (!isRecording.value && props.mode === 'button') return;
166
+
167
+ cancelHint.value = true;
168
+ isRecording.value = false;
169
+ clearInterval(recordingTimer);
170
+
171
+ emit('cancel');
172
+
173
+ // #ifdef APP-PLUS || MP-WEIXIN
174
+ if (recorderManager) {
175
+ recorderManager.stop();
176
+ }
177
+ // #endif
178
+ }
179
+
180
+ // 弹窗模式 - 取消
181
+ function handleCancel() {
182
+ if (recordingTimer) {
183
+ clearInterval(recordingTimer);
184
+ recordingTimer = null;
185
+ }
186
+ isRecording.value = false;
187
+ emit('cancel');
188
+ closePanel();
189
+ }
190
+
191
+ // 弹窗模式 - 确认
192
+ function handleConfirm() {
193
+ if (!hasRecorded.value) return;
194
+
195
+ uni.showLoading({ title: '识别中...' });
196
+
197
+ setTimeout(() => {
198
+ uni.hideLoading();
199
+ emit('confirm', {
200
+ duration: recordingTime.value
201
+ });
202
+ closePanel();
203
+ uni.showToast({ title: '识别成功', icon: 'success' });
204
+ }, 1500);
205
+ }
206
+
207
+ // 关闭弹窗
208
+ function closePanel() {
209
+ emit('update:visible', false);
210
+ recordingTime.value = 0;
211
+ hasRecorded.value = false;
212
+ }
213
+
214
+ // 监听 visible 变化,重置状态
215
+ watch(() => props.visible, (val) => {
216
+ if (val) {
217
+ isRecording.value = false;
218
+ recordingTime.value = 0;
219
+ hasRecorded.value = false;
220
+ }
221
+ });
222
+
223
+ onUnmounted(() => {
224
+ if (recordingTimer) {
225
+ clearInterval(recordingTimer);
226
+ }
227
+ });
228
+ </script>
229
+
230
+ <style lang="scss">
231
+ .prism-voice-input {
232
+ display: inline-flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+
236
+ // ============ 按钮模式 ============
237
+ .voice-btn {
238
+ width: 80rpx;
239
+ height: 80rpx;
240
+ border-radius: 50%;
241
+ background: var(--prism-primary-color, #3478F6);
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: center;
245
+ transition: all 0.3s ease;
246
+ position: relative;
247
+
248
+ &:active, &.recording {
249
+ transform: scale(1.1);
250
+ box-shadow: 0 8rpx 24rpx rgba(52, 120, 246, 0.4);
251
+ }
252
+
253
+ .voice-icon-wrapper {
254
+ position: relative;
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+
259
+ .fa {
260
+ font-size: 36rpx;
261
+ color: #FFFFFF;
262
+ z-index: 2;
263
+ }
264
+ }
265
+
266
+ .voice-waves {
267
+ position: absolute;
268
+ width: 100%;
269
+ height: 100%;
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+
274
+ .wave {
275
+ position: absolute;
276
+ width: 100%;
277
+ height: 100%;
278
+ border-radius: 50%;
279
+ border: 2rpx solid rgba(255, 255, 255, 0.5);
280
+ animation: waveAnim 1.5s infinite ease-out;
281
+ }
282
+
283
+ .wave-1 { animation-delay: 0s; }
284
+ .wave-2 { animation-delay: 0.5s; }
285
+ .wave-3 { animation-delay: 1s; }
286
+ }
287
+ }
288
+
289
+ .voice-modal {
290
+ position: fixed;
291
+ top: 0;
292
+ left: 0;
293
+ right: 0;
294
+ bottom: 0;
295
+ background: rgba(0, 0, 0, 0.6);
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: center;
299
+ z-index: 9999;
300
+
301
+ .modal-content {
302
+ width: 300rpx;
303
+ height: 300rpx;
304
+ background: var(--prism-bg-color-card, #FFFFFF);
305
+ border-radius: 24rpx;
306
+ display: flex;
307
+ flex-direction: column;
308
+ align-items: center;
309
+ justify-content: center;
310
+ gap: 24rpx;
311
+ }
312
+
313
+ .voice-animation {
314
+ display: flex;
315
+ align-items: center;
316
+ justify-content: center;
317
+ gap: 8rpx;
318
+ height: 80rpx;
319
+
320
+ .voice-bar {
321
+ width: 8rpx;
322
+ height: 40rpx;
323
+ background: var(--prism-primary-color, #3478F6);
324
+ border-radius: 4rpx;
325
+ animation: voiceBarAnim 0.8s infinite ease-in-out alternate;
326
+ }
327
+ }
328
+
329
+ .voice-text {
330
+ font-size: 28rpx;
331
+ color: var(--prism-text-primary, #1D2129);
332
+ }
333
+
334
+ .voice-tip-time {
335
+ font-size: 48rpx;
336
+ font-weight: 600;
337
+ color: var(--prism-primary-color, #3478F6);
338
+ }
339
+ }
340
+
341
+ @keyframes waveAnim {
342
+ 0% {
343
+ transform: scale(1);
344
+ opacity: 1;
345
+ }
346
+ 100% {
347
+ transform: scale(2);
348
+ opacity: 0;
349
+ }
350
+ }
351
+
352
+ @keyframes voiceBarAnim {
353
+ 0% { height: 20rpx; }
354
+ 100% { height: 60rpx; }
355
+ }
356
+
357
+ // ============ 弹窗模式 ============
358
+ .voice-panel {
359
+ position: fixed;
360
+ top: 0;
361
+ left: 0;
362
+ right: 0;
363
+ bottom: 0;
364
+ background: var(--prism-mask-bg, rgba(0, 0, 0, 0.5));
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ z-index: 9999;
369
+ }
370
+
371
+ .voice-content {
372
+ width: 560rpx;
373
+ background: var(--prism-bg-color-card, #FFFFFF);
374
+ border-radius: 24rpx;
375
+ padding: 40rpx;
376
+ display: flex;
377
+ flex-direction: column;
378
+ align-items: center;
379
+ }
380
+
381
+ .voice-title {
382
+ font-size: 32rpx;
383
+ font-weight: 600;
384
+ color: var(--prism-text-primary, #1D2129);
385
+ margin-bottom: 40rpx;
386
+ }
387
+
388
+ .voice-wave {
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+ gap: 12rpx;
393
+ height: 100rpx;
394
+ margin-bottom: 24rpx;
395
+
396
+ .wave-bar {
397
+ width: 8rpx;
398
+ height: 20rpx;
399
+ background: var(--prism-text-placeholder, #C9CDD4);
400
+ border-radius: 4rpx;
401
+ transition: all 0.3s;
402
+ }
403
+
404
+ &.recording .wave-bar {
405
+ background: var(--prism-primary-color, #3478F6);
406
+ animation: prism-voice-wave 0.5s ease-in-out infinite alternate;
407
+ }
408
+
409
+ &.recording .wave-bar:nth-child(1) { animation-delay: 0s; }
410
+ &.recording .wave-bar:nth-child(2) { animation-delay: 0.1s; }
411
+ &.recording .wave-bar:nth-child(3) { animation-delay: 0.2s; }
412
+ &.recording .wave-bar:nth-child(4) { animation-delay: 0.3s; }
413
+ &.recording .wave-bar:nth-child(5) { animation-delay: 0.4s; }
414
+ }
415
+
416
+ @keyframes prism-voice-wave {
417
+ 0% { height: 20rpx; }
418
+ 100% { height: 80rpx; }
419
+ }
420
+
421
+ .voice-time {
422
+ font-size: 48rpx;
423
+ font-weight: 600;
424
+ color: var(--prism-text-primary, #1D2129);
425
+ margin-bottom: 16rpx;
426
+ }
427
+
428
+ .voice-tip {
429
+ font-size: 26rpx;
430
+ color: var(--prism-text-secondary, #86909C);
431
+ margin-bottom: 40rpx;
432
+ }
433
+
434
+ .voice-actions {
435
+ display: flex;
436
+ align-items: center;
437
+ justify-content: space-between;
438
+ width: 100%;
439
+ }
440
+
441
+ .voice-cancel-btn,
442
+ .voice-confirm-btn {
443
+ display: flex;
444
+ flex-direction: column;
445
+ align-items: center;
446
+ gap: 8rpx;
447
+ color: var(--prism-text-secondary, #86909C);
448
+
449
+ .fa {
450
+ width: 64rpx;
451
+ height: 64rpx;
452
+ border-radius: 50%;
453
+ display: flex;
454
+ align-items: center;
455
+ justify-content: center;
456
+ font-size: 28rpx;
457
+ }
458
+
459
+ text:last-child {
460
+ font-size: 24rpx;
461
+ }
462
+
463
+ &:active {
464
+ opacity: 0.7;
465
+ }
466
+ }
467
+
468
+ .voice-cancel-btn .fa {
469
+ color: var(--prism-danger-color, #F53F3F);
470
+ background: rgba(245, 63, 63, 0.15);
471
+ }
472
+
473
+ .voice-confirm-btn {
474
+ color: var(--prism-success-color, #00B42A);
475
+
476
+ .fa {
477
+ background: rgba(0, 180, 42, 0.15);
478
+ color: #00B42A;
479
+ }
480
+
481
+ &.disabled {
482
+ opacity: 0.4;
483
+ pointer-events: none;
484
+ }
485
+ }
486
+
487
+ .voice-record-btn {
488
+ width: 120rpx;
489
+ height: 120rpx;
490
+ background: var(--prism-primary-color, #3478F6);
491
+ border-radius: 50%;
492
+ display: flex;
493
+ align-items: center;
494
+ justify-content: center;
495
+ transition: all 0.2s ease;
496
+
497
+ .fa {
498
+ font-size: 48rpx;
499
+ color: #FFFFFF;
500
+ }
501
+
502
+ &:active,
503
+ &.recording {
504
+ transform: scale(1.1);
505
+ background: var(--prism-primary-color-active, #2563EB);
506
+ box-shadow: 0 0 0 16rpx rgba(52, 120, 246, 0.2);
507
+ }
508
+ }
509
+ }
510
+
511
+ // 深色模式
512
+ .dark-mode.prism-voice-input {
513
+ .voice-modal .modal-content,
514
+ .voice-content {
515
+ background: var(--prism-bg-color-card, #1A1A1A);
516
+ }
517
+
518
+ .voice-modal .voice-text,
519
+ .voice-title,
520
+ .voice-time {
521
+ color: var(--prism-text-primary, #E5E6EB);
522
+ }
523
+
524
+ .voice-cancel-btn .fa,
525
+ .voice-confirm-btn .fa {
526
+ background: rgba(255, 255, 255, 0.1);
527
+ }
528
+ }
529
+ </style>