im-ui-mobile 0.0.43 → 0.0.45
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/components/im-arrow-bar/im-arrow-bar.vue +59 -0
- package/components/im-bar-group/im-bar-group.vue +17 -0
- package/components/im-btn-bar/im-btn-bar.vue +60 -0
- package/components/im-chat-at-box/im-chat-at-box.vue +189 -0
- package/components/im-chat-group-readed/im-chat-group-readed.vue +172 -0
- package/components/im-chat-item/im-chat-item.vue +215 -0
- package/components/im-chat-message-item/im-chat-message-item.vue +559 -0
- package/components/im-chat-record/im-chat-record.vue +303 -0
- package/components/im-file-upload/im-file-upload.vue +300 -0
- package/components/im-friend-item/im-friend-item.vue +74 -0
- package/components/im-group-item/im-group-item.vue +60 -0
- package/components/im-group-member-selector/im-group-member-selector.vue +199 -0
- package/components/im-group-rtc-join/im-group-rtc-join.vue +112 -0
- package/components/im-head-image/im-head-image.vue +122 -0
- package/components/im-image-upload/im-image-upload.vue +90 -0
- package/components/im-loading/im-loading.vue +63 -0
- package/components/im-long-press-menu/im-long-press-menu.vue +137 -0
- package/components/im-nav-bar/im-nav-bar.vue +99 -0
- package/components/im-switch-bar/im-switch-bar.vue +60 -0
- package/components/im-virtual-scroller/im-virtual-scroller.vue +52 -0
- package/index.js +13 -3
- package/package.json +10 -3
- package/plugins/pinia.js +19 -0
- package/theme.scss +158 -0
- package/types/components/arrow-bar.d.ts +14 -0
- package/types/components/bar-group.d.ts +14 -0
- package/types/components/btn-bar.d.ts +16 -0
- package/types/components/chat-at-box.d.ts +22 -0
- package/types/components/chat-group-readed.d.ts +30 -0
- package/types/components/chat-item.d.ts +21 -0
- package/types/components/chat-message-item.d.ts +28 -0
- package/types/components/chat-record.d.ts +14 -0
- package/types/components/chat-upload.d.ts +58 -0
- package/types/components/friend-item.d.ts +19 -0
- package/types/components/group-item.d.ts +18 -0
- package/types/components/group-member-selector.d.ts +31 -0
- package/types/components/group-rtc-join.d.ts +31 -0
- package/types/components/head-image.d.ts +18 -0
- package/types/components/im-loading.d.ts +20 -0
- package/types/components/long-press-menu.d.ts +23 -0
- package/types/components/sample.d.ts +1 -3
- package/types/components/switch-bar.d.ts +19 -0
- package/types/components/virtual-scroller.d.ts +20 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view class="chat-record">
|
|
3
|
+
<view class="chat-record-bar" :class="{ recording: recording }" id="chat-record-bar" @click.stop=""
|
|
4
|
+
@touchstart.prevent="onStartRecord" @touchmove.prevent="onTouchMove" @touchend.prevent="onEndRecord">
|
|
5
|
+
{{ recording ? '正在录音' : '长按 说话' }}
|
|
6
|
+
</view>
|
|
7
|
+
<view v-if="recording" class="chat-record-window" :style="recordWindowStyle">
|
|
8
|
+
<view class="rc-wave">
|
|
9
|
+
<text class="note" style="--d: 0"></text>
|
|
10
|
+
<text class="note" style="--d: 1"></text>
|
|
11
|
+
<text class="note" style="--d: 2"></text>
|
|
12
|
+
<text class="note" style="--d: 3"></text>
|
|
13
|
+
<text class="note" style="--d: 4"></text>
|
|
14
|
+
<text class="note" style="--d: 5"></text>
|
|
15
|
+
<text class="note" style="--d: 6"></text>
|
|
16
|
+
</view>
|
|
17
|
+
<view class="rc-tip">{{ recordTip }}</view>
|
|
18
|
+
<view class="cancel-btn" @click="onCancel">
|
|
19
|
+
<u-icon name="close" :color="moveToCancel ? 'red' : 'black'" :size="moveToCancel ? 45 : 40"></u-icon>
|
|
20
|
+
</view>
|
|
21
|
+
<view class="opt-tip" :class="moveToCancel ? 'red' : 'black'">{{ moveToCancel ? '松手取消' : '松手发送,上划取消' }}
|
|
22
|
+
</view>
|
|
23
|
+
</view>
|
|
24
|
+
|
|
25
|
+
</view>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
import recorderApp from '../../utils/recorderApp';
|
|
30
|
+
import recorderH5 from '../../utils/recorderH5';
|
|
31
|
+
|
|
32
|
+
const recording = ref(false);
|
|
33
|
+
const moveToCancel = ref(false);
|
|
34
|
+
const recordBarTop = ref(0);
|
|
35
|
+
const druation = ref(0);
|
|
36
|
+
const rcTimer = ref<number | null>(null);
|
|
37
|
+
|
|
38
|
+
const getRecorder = () => {
|
|
39
|
+
// #ifdef H5
|
|
40
|
+
return recorderH5
|
|
41
|
+
// #endif
|
|
42
|
+
|
|
43
|
+
// #ifndef H5
|
|
44
|
+
return recorderApp
|
|
45
|
+
// #endif
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let rc = getRecorder()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
interface Emits {
|
|
52
|
+
(e: 'send', data: any): void;
|
|
53
|
+
}
|
|
54
|
+
const emit = defineEmits<Emits>();
|
|
55
|
+
|
|
56
|
+
const onTouchMove = (e: any) => {
|
|
57
|
+
const moveY = e.touches[0].clientY;
|
|
58
|
+
moveToCancel.value = moveY < recordBarTop.value - 40;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const onCancel = () => {
|
|
62
|
+
if (recording.value) {
|
|
63
|
+
moveToCancel.value = true;
|
|
64
|
+
onEndRecord();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const onStartRecord = async () => {
|
|
69
|
+
/* 用户第一次使用语音会唤醒录音权限请求,此时会导致@touchend失效,
|
|
70
|
+
一直处于录音状态,这里允许用户再次点击发送语音并结束录音 */
|
|
71
|
+
if (recording.value) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log("开始录音");
|
|
75
|
+
moveToCancel.value = false;
|
|
76
|
+
// await initRecordBar();
|
|
77
|
+
|
|
78
|
+
// if (!rc.checkIsEnable()) {
|
|
79
|
+
// return;
|
|
80
|
+
// }
|
|
81
|
+
|
|
82
|
+
rc.start().then(() => {
|
|
83
|
+
recording.value = true;
|
|
84
|
+
console.log("开始录音成功");
|
|
85
|
+
// 开始计时
|
|
86
|
+
startTimer();
|
|
87
|
+
}).catch((e) => {
|
|
88
|
+
console.log("录音失败" + JSON.stringify(e));
|
|
89
|
+
uni.showToast({
|
|
90
|
+
title: "录音失败",
|
|
91
|
+
icon: "none"
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const onEndRecord = () => {
|
|
97
|
+
if (!recording.value) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
recording.value = false;
|
|
101
|
+
// 停止计时
|
|
102
|
+
stopTimer();
|
|
103
|
+
// 停止录音
|
|
104
|
+
rc.close();
|
|
105
|
+
// 触屏位置是否移动到了取消区域
|
|
106
|
+
if (moveToCancel.value) {
|
|
107
|
+
console.log("录音取消");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// 大于1秒才发送
|
|
111
|
+
if (druation.value <= 1) {
|
|
112
|
+
uni.showToast({
|
|
113
|
+
title: "说话时间太短",
|
|
114
|
+
icon: 'none'
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
rc.upload().then((data) => {
|
|
120
|
+
emit("send", data);
|
|
121
|
+
}).catch((e) => {
|
|
122
|
+
uni.showToast({
|
|
123
|
+
title: e,
|
|
124
|
+
icon: 'none'
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const startTimer = () => {
|
|
130
|
+
druation.value = 0;
|
|
131
|
+
stopTimer();
|
|
132
|
+
rcTimer.value = Number(setInterval(() => {
|
|
133
|
+
druation.value++;
|
|
134
|
+
// 大于60s,直接结束
|
|
135
|
+
if (druation.value >= 60) {
|
|
136
|
+
onEndRecord();
|
|
137
|
+
}
|
|
138
|
+
}, 1000));
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const stopTimer = () => {
|
|
142
|
+
if (rcTimer.value) {
|
|
143
|
+
clearInterval(rcTimer.value);
|
|
144
|
+
rcTimer.value = null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// const initRecordBar = () => {
|
|
149
|
+
// const query = uni.createSelectorQuery().in(getCurrentInstance());
|
|
150
|
+
// query.select('#chat-record-bar').boundingClientRect((rect: any) => {
|
|
151
|
+
// // 顶部高度位置
|
|
152
|
+
// recordBarTop.value = rect.top;
|
|
153
|
+
// }).exec();
|
|
154
|
+
// };
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 初始化录音条位置信息
|
|
158
|
+
* 兼容微信小程序和H5环境
|
|
159
|
+
*/
|
|
160
|
+
const initRecordBar = (): Promise<{ top: number; height: number; width: number }> => {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
// 获取组件实例
|
|
163
|
+
const instance = getCurrentInstance()
|
|
164
|
+
if (!instance) {
|
|
165
|
+
reject(new Error('无法获取组件实例'))
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 创建选择器查询
|
|
170
|
+
const query = uni.createSelectorQuery().in(instance)
|
|
171
|
+
|
|
172
|
+
query.select('#chat-record-bar').boundingClientRect((rect: any) => {
|
|
173
|
+
if (rect) {
|
|
174
|
+
const positionInfo = {
|
|
175
|
+
top: rect.top || 0,
|
|
176
|
+
height: rect.height || 0,
|
|
177
|
+
width: rect.width || 0,
|
|
178
|
+
left: rect.left || 0,
|
|
179
|
+
right: rect.right || 0,
|
|
180
|
+
bottom: rect.bottom || 0
|
|
181
|
+
}
|
|
182
|
+
resolve(positionInfo)
|
|
183
|
+
} else {
|
|
184
|
+
reject(new Error('无法获取录音条位置信息'))
|
|
185
|
+
}
|
|
186
|
+
}).exec()
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const recordWindowStyle = computed(() => {
|
|
191
|
+
const windowHeight = uni.getWindowInfo().windowHeight;
|
|
192
|
+
const bottom = windowHeight - recordBarTop.value + 12;
|
|
193
|
+
return `bottom:${bottom}px;`;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const recordTip = computed(() => {
|
|
197
|
+
if (druation.value > 50) {
|
|
198
|
+
return `${60 - druation.value}s后将停止录音`;
|
|
199
|
+
}
|
|
200
|
+
return `录音时长:${druation.value}s`;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
onUnmounted(() => {
|
|
204
|
+
stopTimer();
|
|
205
|
+
recording.value = false;
|
|
206
|
+
});
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<style lang="scss" scoped>
|
|
210
|
+
.chat-record {
|
|
211
|
+
.rc-wave {
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: flex-end;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
position: relative;
|
|
216
|
+
height: 80rpx;
|
|
217
|
+
|
|
218
|
+
.note {
|
|
219
|
+
background: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
|
220
|
+
width: 4px;
|
|
221
|
+
height: 50%;
|
|
222
|
+
border-radius: 5rpx;
|
|
223
|
+
margin-right: 4px;
|
|
224
|
+
animation: loading 0.5s infinite linear;
|
|
225
|
+
animation-delay: calc(0.1s * var(--d));
|
|
226
|
+
|
|
227
|
+
@keyframes loading {
|
|
228
|
+
0% {
|
|
229
|
+
background-image: linear-gradient(to right, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
|
230
|
+
height: 20%;
|
|
231
|
+
border-radius: 5rpx;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
50% {
|
|
235
|
+
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
|
236
|
+
height: 80%;
|
|
237
|
+
border-radius: 5rpx;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
100% {
|
|
241
|
+
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
|
242
|
+
height: 20%;
|
|
243
|
+
border-radius: 5rpx;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.chat-record-bar {
|
|
250
|
+
padding: 10rpx;
|
|
251
|
+
margin: 10rpx;
|
|
252
|
+
border-radius: 10rpx;
|
|
253
|
+
text-align: center;
|
|
254
|
+
box-shadow: $im-box-shadow;
|
|
255
|
+
|
|
256
|
+
&.recording {
|
|
257
|
+
background-color: $im-color-primary;
|
|
258
|
+
color: #fff;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.chat-record-window {
|
|
263
|
+
position: fixed;
|
|
264
|
+
left: 0;
|
|
265
|
+
right: 0;
|
|
266
|
+
height: 360rpx;
|
|
267
|
+
background-color: rgba(255, 255, 255, 0.95);
|
|
268
|
+
padding: 30rpx;
|
|
269
|
+
|
|
270
|
+
.icon-microphone {
|
|
271
|
+
text-align: center;
|
|
272
|
+
font-size: 80rpx;
|
|
273
|
+
padding: 10rpx;
|
|
274
|
+
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.rc-tip {
|
|
278
|
+
text-align: center;
|
|
279
|
+
font-size: $im-font-size-small;
|
|
280
|
+
color: $im-text-color-light;
|
|
281
|
+
margin-top: 20rpx;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.cancel-btn {
|
|
285
|
+
text-align: center;
|
|
286
|
+
margin-top: 40rpx;
|
|
287
|
+
height: 80rpx;
|
|
288
|
+
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.opt-tip {
|
|
292
|
+
text-align: center;
|
|
293
|
+
font-size: 30rpx;
|
|
294
|
+
padding: 20rpx;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.red {
|
|
298
|
+
color: $im-color-danger !important;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
</style>
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view v-if="visible" class="u-upload-wrapper">
|
|
3
|
+
<u-upload ref="uploadRef" :max-count="props.maxCount" :max-size="props.maxSize" :size-type="props.sizeType"
|
|
4
|
+
:source-type="props.sourceType" :deletable="props.deletable" :preview-full-image="props.previewFullImage"
|
|
5
|
+
:multiple="props.multiple" :disabled="props.disabled" :custom-btn="props.customBtn"
|
|
6
|
+
:show-progress="props.showProgress" :upload-text="props.uploadText" :width="props.width"
|
|
7
|
+
:height="props.height" @after-read="onAfterRead" @delete="onDelete" @oversize="onOversize">
|
|
8
|
+
<!-- 自定义上传按钮 -->
|
|
9
|
+
<slot></slot>
|
|
10
|
+
</u-upload>
|
|
11
|
+
</view>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
// 定义类型
|
|
16
|
+
interface UploadFile {
|
|
17
|
+
url: string
|
|
18
|
+
name?: string
|
|
19
|
+
size?: number
|
|
20
|
+
type?: string
|
|
21
|
+
progress?: number
|
|
22
|
+
status?: 'uploading' | 'success' | 'error'
|
|
23
|
+
response?: any
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface AfterReadFile {
|
|
27
|
+
name: string
|
|
28
|
+
size: number
|
|
29
|
+
thumb: string
|
|
30
|
+
type: string
|
|
31
|
+
url: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface UploadProps {
|
|
35
|
+
// 文件列表
|
|
36
|
+
modelValue?: UploadFile[]
|
|
37
|
+
// 最大上传数量
|
|
38
|
+
maxCount?: number
|
|
39
|
+
// 单个文件大小限制(字节)
|
|
40
|
+
maxSize?: number
|
|
41
|
+
// 文件大小选择 ['original', 'compressed']
|
|
42
|
+
sizeType?: string[]
|
|
43
|
+
// 文件来源 ['album', 'camera']
|
|
44
|
+
sourceType?: string[]
|
|
45
|
+
// 是否显示删除按钮
|
|
46
|
+
deletable?: boolean
|
|
47
|
+
// 是否预览完整图片
|
|
48
|
+
previewFullImage?: boolean
|
|
49
|
+
// 是否多选
|
|
50
|
+
multiple?: boolean
|
|
51
|
+
// 是否禁用
|
|
52
|
+
disabled?: boolean
|
|
53
|
+
// 是否使用自定义按钮
|
|
54
|
+
customBtn?: boolean
|
|
55
|
+
// 是否显示进度条
|
|
56
|
+
showProgress?: boolean
|
|
57
|
+
// 上传文字提示
|
|
58
|
+
uploadText?: string
|
|
59
|
+
// 图片宽度
|
|
60
|
+
width?: number | string
|
|
61
|
+
// 图片高度
|
|
62
|
+
height?: number | string
|
|
63
|
+
// 上传地址
|
|
64
|
+
action?: string
|
|
65
|
+
// 上传字段名
|
|
66
|
+
name?: string
|
|
67
|
+
// 请求头
|
|
68
|
+
headers?: Record<string, string>
|
|
69
|
+
// 附加数据
|
|
70
|
+
data?: Record<string, any>
|
|
71
|
+
// 是否自动上传
|
|
72
|
+
autoUpload?: boolean,
|
|
73
|
+
// 令牌
|
|
74
|
+
token?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface UploadEmits {
|
|
78
|
+
(e: 'update:modelValue', files: UploadFile[]): void
|
|
79
|
+
(e: 'change', files: UploadFile[]): void
|
|
80
|
+
(e: 'before', file: UploadFile): void
|
|
81
|
+
(e: 'success', file: UploadFile, response: any): void
|
|
82
|
+
(e: 'fail', file: UploadFile, error: any): void
|
|
83
|
+
(e: 'progress', file: UploadFile, progress: number): void
|
|
84
|
+
(e: 'delete', file: UploadFile, index: number): void
|
|
85
|
+
(e: 'oversize', file: UploadFile): void
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Props 和 Emits
|
|
89
|
+
const props = withDefaults(defineProps<UploadProps>(), {
|
|
90
|
+
modelValue: () => [],
|
|
91
|
+
maxCount: 9,
|
|
92
|
+
maxSize: 10 * 1024 * 1024, // 10MB
|
|
93
|
+
sizeType: () => ['original', 'compressed'],
|
|
94
|
+
sourceType: () => ['album', 'camera'],
|
|
95
|
+
deletable: true,
|
|
96
|
+
previewFullImage: true,
|
|
97
|
+
multiple: true,
|
|
98
|
+
disabled: false,
|
|
99
|
+
customBtn: false,
|
|
100
|
+
showProgress: true,
|
|
101
|
+
uploadText: '',
|
|
102
|
+
width: 50,
|
|
103
|
+
height: 50,
|
|
104
|
+
action: '/file/upload',
|
|
105
|
+
name: 'file',
|
|
106
|
+
headers: () => ({
|
|
107
|
+
'Authorization': `Bearer xxx`,
|
|
108
|
+
'AccessToken': 'xxx',
|
|
109
|
+
'Content-Type': 'multipart/form-data'
|
|
110
|
+
}),
|
|
111
|
+
data: () => ({}),
|
|
112
|
+
autoUpload: true,
|
|
113
|
+
token: ''
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const emit = defineEmits<UploadEmits>()
|
|
117
|
+
|
|
118
|
+
// 响应式数据
|
|
119
|
+
const uploadRef = ref()
|
|
120
|
+
const fileList = ref<UploadFile[]>([])
|
|
121
|
+
const uploadingFiles = reactive(new Map<string, UploadFile>())
|
|
122
|
+
const fileMap = ref(new Map<string, any>());
|
|
123
|
+
const visible = ref(true)
|
|
124
|
+
|
|
125
|
+
// 监听 modelValue 变化
|
|
126
|
+
watch(() => props.modelValue, (newVal) => {
|
|
127
|
+
fileList.value = [...newVal]
|
|
128
|
+
}, { immediate: true, deep: true })
|
|
129
|
+
|
|
130
|
+
// 文件选择后读取
|
|
131
|
+
const onAfterRead = async ({ file }: any) => {
|
|
132
|
+
const files: AfterReadFile[] = file
|
|
133
|
+
|
|
134
|
+
for (const item of files) {
|
|
135
|
+
// before
|
|
136
|
+
if (!fileMap.value.has(item.name)) {
|
|
137
|
+
emit('before', item)
|
|
138
|
+
fileMap.value.set(item.name, item);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const uploadFile: UploadFile = {
|
|
142
|
+
url: item.url,
|
|
143
|
+
name: item.name,
|
|
144
|
+
size: item.size,
|
|
145
|
+
type: item.type,
|
|
146
|
+
progress: 0,
|
|
147
|
+
status: 'uploading'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const fileId = generateFileId()
|
|
151
|
+
uploadingFiles.set(fileId, uploadFile)
|
|
152
|
+
|
|
153
|
+
// 添加到文件列表
|
|
154
|
+
fileList.value.push(uploadFile)
|
|
155
|
+
emitFileChange()
|
|
156
|
+
|
|
157
|
+
if (props.autoUpload) {
|
|
158
|
+
await uploadFileToServer(item, uploadFile, fileId)
|
|
159
|
+
} else {
|
|
160
|
+
uploadFile.status = 'success'
|
|
161
|
+
uploadingFiles.delete(fileId)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 上传文件到服务器
|
|
167
|
+
const uploadFileToServer = (file: AfterReadFile, uploadFile: UploadFile, fileId: string): Promise<void> => {
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
if (!props.action) {
|
|
170
|
+
// 如果没有上传地址,直接标记为成功
|
|
171
|
+
uploadFile.status = 'success'
|
|
172
|
+
uploadFile.response = { message: 'No upload action provided' }
|
|
173
|
+
uploadingFiles.delete(fileId)
|
|
174
|
+
emit('success', file, uploadFile)
|
|
175
|
+
resolve()
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const headers = {
|
|
180
|
+
'Authorization': `Bearer ${props.token}`,
|
|
181
|
+
'AccessToken': props.token,
|
|
182
|
+
'Content-Type': 'multipart/form-data'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
uni.uploadFile({
|
|
186
|
+
url: props.action,
|
|
187
|
+
filePath: file.url,
|
|
188
|
+
name: props.name,
|
|
189
|
+
header: headers,
|
|
190
|
+
// formData: props.data,
|
|
191
|
+
success: (res) => {
|
|
192
|
+
uploadFile.response = res
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
uploadFile.status = 'success'
|
|
196
|
+
uploadFile.progress = 100
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
fileList.value.splice(fileList.value.length - 1, 1, {
|
|
200
|
+
...file,
|
|
201
|
+
...res,
|
|
202
|
+
status: 'success'
|
|
203
|
+
})
|
|
204
|
+
emit('success', file, JSON.parse(res.data).data)
|
|
205
|
+
} catch (error) {
|
|
206
|
+
uploadFile.status = 'error'
|
|
207
|
+
console.error(error)
|
|
208
|
+
emit('fail', file, error)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
uploadingFiles.delete(fileId)
|
|
212
|
+
resolve()
|
|
213
|
+
},
|
|
214
|
+
fail: (error) => {
|
|
215
|
+
uploadFile.status = 'error'
|
|
216
|
+
console.error(error)
|
|
217
|
+
uploadingFiles.delete(fileId)
|
|
218
|
+
emit('fail', file, error)
|
|
219
|
+
resolve()
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 删除文件
|
|
226
|
+
const onDelete = (event: any) => {
|
|
227
|
+
const { index } = event
|
|
228
|
+
const deletedFile = fileList.value[index]
|
|
229
|
+
|
|
230
|
+
fileList.value.splice(index, 1)
|
|
231
|
+
emitFileChange()
|
|
232
|
+
emit('delete', deletedFile, index)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 文件大小超出限制
|
|
236
|
+
const onOversize = (event: any) => {
|
|
237
|
+
const file = event
|
|
238
|
+
emit('oversize', file)
|
|
239
|
+
uni.showToast({
|
|
240
|
+
title: `文件大小不能超过 ${props.maxSize / 1024 / 1024}MB`,
|
|
241
|
+
icon: 'none'
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 生成文件ID
|
|
246
|
+
const generateFileId = (): string => {
|
|
247
|
+
return Date.now() + '-' + Math.random().toString(36).substr(2, 9)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 触发文件变化事件
|
|
251
|
+
const emitFileChange = () => {
|
|
252
|
+
emit('update:modelValue', fileList.value)
|
|
253
|
+
emit('change', fileList.value)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 手动上传
|
|
257
|
+
const submit = (): Promise<UploadFile[]> => {
|
|
258
|
+
return new Promise((resolve) => {
|
|
259
|
+
const pendingFiles = fileList.value.filter(file => file.status !== 'success')
|
|
260
|
+
|
|
261
|
+
if (pendingFiles.length === 0) {
|
|
262
|
+
resolve(fileList.value)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 这里可以实现批量上传逻辑
|
|
267
|
+
Promise.all(pendingFiles.map(file => {
|
|
268
|
+
// 重新上传逻辑
|
|
269
|
+
return Promise.resolve(file)
|
|
270
|
+
})).then(() => {
|
|
271
|
+
resolve(fileList.value)
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 清空文件列表
|
|
277
|
+
const clearFiles = () => {
|
|
278
|
+
fileList.value = []
|
|
279
|
+
uploadingFiles.clear()
|
|
280
|
+
emitFileChange()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 获取文件列表
|
|
284
|
+
const getFiles = (): UploadFile[] => {
|
|
285
|
+
return fileList.value
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 隐藏
|
|
289
|
+
const hide = () => {
|
|
290
|
+
visible.value = false
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 暴露方法给父组件
|
|
294
|
+
defineExpose({
|
|
295
|
+
submit,
|
|
296
|
+
clearFiles,
|
|
297
|
+
getFiles,
|
|
298
|
+
hide
|
|
299
|
+
})
|
|
300
|
+
</script>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view class="friend-item" @click="showFriendInfo()">
|
|
3
|
+
<head-image :name="friend.nickName" :online="friend.online" :url="friend.headImage" size="small"></head-image>
|
|
4
|
+
<view class="friend-info">
|
|
5
|
+
<view class="friend-name">{{ friend.nickName }}</view>
|
|
6
|
+
<view class="friend-online">
|
|
7
|
+
<image v-show="friend.onlineWeb" class="online" src="/static/image/online_web.png" title="电脑设备在线" />
|
|
8
|
+
<image v-show="friend.onlineApp" class="online" src="/static/image/online_app.png" title="移动设备在线" />
|
|
9
|
+
</view>
|
|
10
|
+
<slot></slot>
|
|
11
|
+
</view>
|
|
12
|
+
</view>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
interface Props {
|
|
17
|
+
friend?: any;
|
|
18
|
+
detail?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
22
|
+
detail: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const showFriendInfo = () => {
|
|
26
|
+
if (props.detail) {
|
|
27
|
+
uni.navigateTo({
|
|
28
|
+
url: "/pages/common/user-info?id=" + props.friend.id
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style scope lang="scss">
|
|
35
|
+
.friend-item {
|
|
36
|
+
height: 90rpx;
|
|
37
|
+
display: flex;
|
|
38
|
+
margin-bottom: 1rpx;
|
|
39
|
+
position: relative;
|
|
40
|
+
padding: 10rpx;
|
|
41
|
+
padding-left: 20rpx;
|
|
42
|
+
align-items: center;
|
|
43
|
+
background-color: white;
|
|
44
|
+
white-space: nowrap;
|
|
45
|
+
|
|
46
|
+
&:hover {
|
|
47
|
+
background-color: $im-bg;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.friend-info {
|
|
51
|
+
flex: 1;
|
|
52
|
+
display: flex;
|
|
53
|
+
padding-left: 20rpx;
|
|
54
|
+
text-align: left;
|
|
55
|
+
|
|
56
|
+
.friend-name {
|
|
57
|
+
flex: 1;
|
|
58
|
+
font-size: $im-font-size;
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.friend-online {
|
|
64
|
+
margin-top: 4rpx;
|
|
65
|
+
|
|
66
|
+
.online {
|
|
67
|
+
padding-right: 4rpx;
|
|
68
|
+
width: 24rpx;
|
|
69
|
+
height: 24rpx;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</style>
|