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.
Files changed (43) hide show
  1. package/components/im-arrow-bar/im-arrow-bar.vue +59 -0
  2. package/components/im-bar-group/im-bar-group.vue +17 -0
  3. package/components/im-btn-bar/im-btn-bar.vue +60 -0
  4. package/components/im-chat-at-box/im-chat-at-box.vue +189 -0
  5. package/components/im-chat-group-readed/im-chat-group-readed.vue +172 -0
  6. package/components/im-chat-item/im-chat-item.vue +215 -0
  7. package/components/im-chat-message-item/im-chat-message-item.vue +559 -0
  8. package/components/im-chat-record/im-chat-record.vue +303 -0
  9. package/components/im-file-upload/im-file-upload.vue +300 -0
  10. package/components/im-friend-item/im-friend-item.vue +74 -0
  11. package/components/im-group-item/im-group-item.vue +60 -0
  12. package/components/im-group-member-selector/im-group-member-selector.vue +199 -0
  13. package/components/im-group-rtc-join/im-group-rtc-join.vue +112 -0
  14. package/components/im-head-image/im-head-image.vue +122 -0
  15. package/components/im-image-upload/im-image-upload.vue +90 -0
  16. package/components/im-loading/im-loading.vue +63 -0
  17. package/components/im-long-press-menu/im-long-press-menu.vue +137 -0
  18. package/components/im-nav-bar/im-nav-bar.vue +99 -0
  19. package/components/im-switch-bar/im-switch-bar.vue +60 -0
  20. package/components/im-virtual-scroller/im-virtual-scroller.vue +52 -0
  21. package/index.js +13 -3
  22. package/package.json +10 -3
  23. package/plugins/pinia.js +19 -0
  24. package/theme.scss +158 -0
  25. package/types/components/arrow-bar.d.ts +14 -0
  26. package/types/components/bar-group.d.ts +14 -0
  27. package/types/components/btn-bar.d.ts +16 -0
  28. package/types/components/chat-at-box.d.ts +22 -0
  29. package/types/components/chat-group-readed.d.ts +30 -0
  30. package/types/components/chat-item.d.ts +21 -0
  31. package/types/components/chat-message-item.d.ts +28 -0
  32. package/types/components/chat-record.d.ts +14 -0
  33. package/types/components/chat-upload.d.ts +58 -0
  34. package/types/components/friend-item.d.ts +19 -0
  35. package/types/components/group-item.d.ts +18 -0
  36. package/types/components/group-member-selector.d.ts +31 -0
  37. package/types/components/group-rtc-join.d.ts +31 -0
  38. package/types/components/head-image.d.ts +18 -0
  39. package/types/components/im-loading.d.ts +20 -0
  40. package/types/components/long-press-menu.d.ts +23 -0
  41. package/types/components/sample.d.ts +1 -3
  42. package/types/components/switch-bar.d.ts +19 -0
  43. 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>