im-ui-mobile 0.0.43 → 0.0.44

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 (42) 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 +8 -2
  23. package/plugins/pinia.js +19 -0
  24. package/types/components/arrow-bar.d.ts +14 -0
  25. package/types/components/bar-group.d.ts +14 -0
  26. package/types/components/btn-bar.d.ts +16 -0
  27. package/types/components/chat-at-box.d.ts +22 -0
  28. package/types/components/chat-group-readed.d.ts +30 -0
  29. package/types/components/chat-item.d.ts +21 -0
  30. package/types/components/chat-message-item.d.ts +28 -0
  31. package/types/components/chat-record.d.ts +14 -0
  32. package/types/components/chat-upload.d.ts +58 -0
  33. package/types/components/friend-item.d.ts +19 -0
  34. package/types/components/group-item.d.ts +18 -0
  35. package/types/components/group-member-selector.d.ts +31 -0
  36. package/types/components/group-rtc-join.d.ts +31 -0
  37. package/types/components/head-image.d.ts +18 -0
  38. package/types/components/im-loading.d.ts +20 -0
  39. package/types/components/long-press-menu.d.ts +23 -0
  40. package/types/components/sample.d.ts +1 -3
  41. package/types/components/switch-bar.d.ts +19 -0
  42. package/types/components/virtual-scroller.d.ts +20 -0
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <view class="group-item" @click="showGroupInfo()">
3
+ <head-image :name="group.showGroupName" :url="group.headImageThumb" size="small"></head-image>
4
+ <view class="group-name">
5
+ <view>{{ group.showGroupName }}</view>
6
+ </view>
7
+ </view>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ interface Props {
12
+ group?: any;
13
+ }
14
+
15
+ const props = defineProps<Props>();
16
+
17
+ interface Emits {
18
+ (e: 'click', targetId: number): void
19
+ }
20
+
21
+ const emit = defineEmits<Emits>()
22
+
23
+ const showGroupInfo = () => {
24
+ // TODO:组件化
25
+ // uni.navigateTo({
26
+ // url: "/pages-contacts/pages/group/info?id=" + props.group?.id
27
+ // });
28
+
29
+ emit('click', props.group?.id)
30
+ };
31
+ </script>
32
+
33
+ <style scope lang="scss">
34
+ .group-item {
35
+ height: 90rpx;
36
+ display: flex;
37
+ margin-bottom: 2rpx;
38
+ position: relative;
39
+ padding: 18rpx 20rpx;
40
+ align-items: center;
41
+ background-color: white;
42
+ white-space: nowrap;
43
+
44
+ &:hover {
45
+ background-color: $im-bg-active;
46
+ }
47
+
48
+ &.active {
49
+ background-color: $im-bg-active;
50
+ }
51
+
52
+ .group-name {
53
+ font-size: $im-font-size;
54
+ padding-left: 20rpx;
55
+ text-align: left;
56
+ white-space: nowrap;
57
+ overflow: hidden;
58
+ }
59
+ }
60
+ </style>
@@ -0,0 +1,199 @@
1
+ <template>
2
+ <u-popup ref="popup" mode="bottom">
3
+ <view class="group-member-selector">
4
+ <view class="top-bar">
5
+ <view class="top-tip">选择成员</view>
6
+ <button class="top-btn" size="mini" @click="onClean()">清空 </button>
7
+ <button class="top-btn" size="mini" @click="onOk()">确定({{ checkedIds.length }})
8
+ </button>
9
+ </view>
10
+ <scroll-view v-show="checkedIds.length > 0" scroll-x="true" scroll-left="120">
11
+ <view class="checked-users">
12
+ <view v-for="m in checkedMembers" class="user-item" :key="m.userId">
13
+ <head-image :name="m.showNickName" :url="m.headImage" :size="60"></head-image>
14
+ </view>
15
+ </view>
16
+ </scroll-view>
17
+ <view class="search-bar">
18
+ <u-search v-model="searchText" :show-action="false" placeholder="搜索"></u-search>
19
+ </view>
20
+ <view class="member-items">
21
+ <virtual-scroller :items="showMembers">
22
+ <template v-slot="{ item }">
23
+ <view class="member-item" @click="onSwitchChecked(item)">
24
+ <head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
25
+ :size="90"></head-image>
26
+ <view class="member-name">{{ item.showNickName }}
27
+ </view>
28
+ <view class="member-checked">
29
+ <radio :checked="item.checked" :disabled="item.locked"
30
+ @click.stop="onSwitchChecked(item)" />
31
+ </view>
32
+ </view>
33
+ </template>
34
+ </virtual-scroller>
35
+ </view>
36
+ </view>
37
+ </u-popup>
38
+ </template>
39
+
40
+
41
+ <script setup lang="ts">
42
+ interface Props {
43
+ group?: any;
44
+ members?: any[];
45
+ maxSize?: number;
46
+ }
47
+
48
+ interface Emits {
49
+ (e: 'complete', userIds: number[]): void;
50
+ }
51
+
52
+ interface Member {
53
+ userId: number;
54
+ checked?: boolean;
55
+ locked?: boolean;
56
+ hide?: boolean;
57
+ quit?: boolean;
58
+ showNickName: string;
59
+ }
60
+
61
+ const props = withDefaults(defineProps<Props>(), {
62
+ members: () => [],
63
+ maxSize: 50
64
+ });
65
+
66
+ const emit = defineEmits<Emits>();
67
+
68
+ const searchText = ref("");
69
+ const popup = ref() // 组件引用
70
+
71
+ const init = (checkedIds: number[], lockedIds: number[], hideIds: number[]) => {
72
+ props.members.forEach((m: Member) => {
73
+ m.checked = checkedIds.includes(m.userId);
74
+ m.locked = lockedIds.includes(m.userId);
75
+ m.hide = hideIds.includes(m.userId);
76
+ });
77
+ };
78
+
79
+ const open = () => {
80
+ popup.value.open();
81
+ };
82
+
83
+ const onSwitchChecked = (m: Member) => {
84
+ if (!m.locked) {
85
+ m.checked = !m.checked;
86
+ }
87
+ // 达到选择上限
88
+ if (props.maxSize > 0 && checkedIds.value.length > props.maxSize) {
89
+ m.checked = false;
90
+ uni.showToast({
91
+ title: `最多选择${props.maxSize}位用户`,
92
+ icon: "none"
93
+ });
94
+ }
95
+ };
96
+
97
+ const onClean = () => {
98
+ props.members.forEach((m: Member) => {
99
+ if (!m.locked && m.checked) {
100
+ m.checked = false;
101
+ }
102
+ });
103
+ };
104
+
105
+ const onOk = () => {
106
+ // this.$refs.popup.close();
107
+ emit("complete", checkedIds.value);
108
+ };
109
+
110
+ const checkedIds = computed(() => {
111
+ return checkedMembers.value.map(m => m.userId);
112
+ });
113
+
114
+ const checkedMembers = computed(() => {
115
+ return props.members.filter((m: Member) => !m.quit && !m.hide && m.checked);
116
+ });
117
+
118
+ const showMembers = computed(() => {
119
+ return props.members.filter((m: Member) =>
120
+ !m.quit && !m.hide && m.showNickName.includes(searchText.value)
121
+ );
122
+ });
123
+
124
+ defineExpose({
125
+ init,
126
+ open
127
+ })
128
+ </script>
129
+
130
+
131
+ <style lang="scss" scoped>
132
+ .group-member-selector {
133
+ position: relative;
134
+ display: flex;
135
+ flex-direction: column;
136
+ background-color: white;
137
+ padding: 10rpx;
138
+ border-radius: 15rpx 15rpx 0 0;
139
+ overflow: hidden;
140
+
141
+ .top-bar {
142
+ display: flex;
143
+ align-items: center;
144
+ height: 70rpx;
145
+ padding: 10rpx 30rpx;
146
+
147
+ .top-tip {
148
+ flex: 1;
149
+ }
150
+
151
+ .top-btn {
152
+ margin-left: 10rpx;
153
+ }
154
+ }
155
+
156
+ .checked-users {
157
+ display: flex;
158
+ align-items: center;
159
+ height: 90rpx;
160
+ padding: 0 30rpx;
161
+
162
+ .user-item {
163
+ padding: 3rpx;
164
+ }
165
+ }
166
+
167
+ .member-items {
168
+ position: relative;
169
+ flex: 1;
170
+ overflow: hidden;
171
+
172
+ .member-item {
173
+ height: 120rpx;
174
+ display: flex;
175
+ position: relative;
176
+ padding: 0 30rpx;
177
+ align-items: center;
178
+ background-color: white;
179
+ white-space: nowrap;
180
+
181
+ .member-name {
182
+ display: flex;
183
+ align-items: center;
184
+ flex: 1;
185
+ padding-left: 20rpx;
186
+ font-size: 30rpx;
187
+ font-weight: 600;
188
+ line-height: 60rpx;
189
+ white-space: nowrap;
190
+ overflow: hidden;
191
+
192
+ .uni-tag {
193
+ margin-left: 5rpx;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ </style>
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <u-popup ref="popup" mode="center">
3
+ <u-modal :show="true" :zoom="false" :async-close="false" title="是否加入通话?" confirm-text="加入" @confirm="onOk">
4
+ <div class="group-rtc-join">
5
+ <div class="host-info">
6
+ <div>发起人</div>
7
+ <head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image>
8
+ </div>
9
+ <div class="user-info">
10
+ <div>{{ rtcInfo.userInfos.length + '人正在通话中' }}</div>
11
+ <scroll-view scroll-x="true" scroll-left="120">
12
+ <view class="user-list">
13
+ <view v-for="user in rtcInfo.userInfos" class="user-item" :key="user.id">
14
+ <head-image :name="user.nickName" :url="user.headImage" :size="80"></head-image>
15
+ </view>
16
+ </view>
17
+ </scroll-view>
18
+ </div>
19
+ </div>
20
+ </u-modal>
21
+ </u-popup>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { ref } from 'vue';
26
+ import { UserInfo } from '../../libs';
27
+
28
+ interface Props {
29
+ groupId?: number;
30
+ }
31
+
32
+ interface RtcInfo {
33
+ userInfos: Array<{
34
+ id: number;
35
+ nickName: string;
36
+ headImage: string;
37
+ isCamera?: boolean;
38
+ isMicroPhone?: boolean;
39
+ }>,
40
+ host: any
41
+ }
42
+
43
+ const props = defineProps<Props>();
44
+
45
+ const rtcInfo = ref<RtcInfo>({ userInfos: [], host: {} });
46
+ const userInfo = ref<UserInfo>()
47
+
48
+ interface Emits {
49
+ (e: 'ok', groupId: number, inviterId: number, userInfos: string): void
50
+ }
51
+ const emit = defineEmits<Emits>()
52
+
53
+ const open = (userInfo1: UserInfo, rtcInfoData: RtcInfo) => {
54
+ userInfo.value = userInfo1;
55
+ rtcInfo.value = rtcInfoData;
56
+ };
57
+
58
+ const onOk = () => {
59
+ const users = [...rtcInfo.value.userInfos];
60
+ const mine = userInfo.value as UserInfo // useUserStore().userInfo;
61
+
62
+ // 加入自己的信息
63
+ if (!users.find((user) => user.id === mine.id)) {
64
+ users.push({
65
+ id: mine.id,
66
+ nickName: mine.nickName,
67
+ headImage: String(mine.headImageThumb),
68
+ isCamera: false,
69
+ isMicroPhone: true
70
+ });
71
+ }
72
+
73
+ const userInfos = encodeURIComponent(JSON.stringify(users));
74
+
75
+ // TODO:组件化
76
+ // uni.navigateTo({
77
+ // url: `/pages-chat/pages/chat/chat-group-video?groupId=${props.groupId}&isHost=false&inviterId=${mine.id}&userInfos=${userInfos}`
78
+ // });
79
+
80
+ emit('ok', Number(props.groupId), mine.id, userInfos)
81
+ };
82
+
83
+ defineExpose({
84
+ open
85
+ })
86
+ </script>
87
+
88
+ <style lang="scss" scoped>
89
+ .group-rtc-join {
90
+ width: 100%;
91
+
92
+ .host-info {
93
+ font-size: 16px;
94
+ padding: 10px;
95
+ }
96
+
97
+ .user-info {
98
+ font-size: 16px;
99
+ padding: 10px;
100
+ }
101
+
102
+ .user-list {
103
+ display: flex;
104
+ align-items: center;
105
+ height: 90rpx;
106
+
107
+ .user-item {
108
+ padding: 3rpx;
109
+ }
110
+ }
111
+ }
112
+ </style>
@@ -0,0 +1,122 @@
1
+ <template>
2
+ <view class="head-image none-pointer-events" @click="showUserInfo" :title="name">
3
+ <image v-if="url" class="avatar-image" :src="url" :style="avatarImageStyle" lazy-load="true"
4
+ mode="aspectFill" />
5
+ <view v-else class="avatar-text" :style="avatarTextStyle">
6
+ {{ name?.substring(0, 1).toUpperCase() }}
7
+ </view>
8
+ <view v-if="online" class="online" title="用户当前在线" />
9
+ </view>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed } from 'vue';
14
+
15
+ interface Props {
16
+ id?: number;
17
+ size?: number | string;
18
+ url?: string;
19
+ name?: string;
20
+ radius?: string;
21
+ online?: boolean;
22
+ }
23
+
24
+ const props = withDefaults(defineProps<Props>(), {
25
+ size: 'default',
26
+ name: '',
27
+ radius: "50%",
28
+ online: false
29
+ });
30
+
31
+ interface Emits {
32
+ (e: 'click', id: number): void
33
+ }
34
+ const emit = defineEmits<Emits>()
35
+
36
+ const colors = [
37
+ "#5daa31", "#c7515a", "#e03697", "#85029b",
38
+ "#c9b455", "#326eb6"
39
+ ];
40
+
41
+ const showUserInfo = () => {
42
+ if (props.id && props.id > 0) {
43
+ // TODO:组件化
44
+ // uni.navigateTo({ url: "/pages/common/user-info?id=" + props.id });
45
+ emit('click', props.id)
46
+ }
47
+ };
48
+
49
+ const _size = computed(() => {
50
+ if (typeof props.size === 'number') {
51
+ return props.size;
52
+ } else if (typeof props.size === 'string') {
53
+ const sizeMap: Record<string, number> = {
54
+ 'default': 96,
55
+ 'small': 84,
56
+ 'smaller': 72,
57
+ 'mini': 60,
58
+ 'minier': 48,
59
+ 'lage': 108,
60
+ 'lager': 120,
61
+ };
62
+ return sizeMap[props.size] || 96;
63
+ }
64
+ return 96;
65
+ });
66
+
67
+ const avatarImageStyle = computed(() => {
68
+ return `width:${_size.value}rpx;height:${_size.value}rpx;`;
69
+ });
70
+
71
+ const avatarTextStyle = computed(() => {
72
+ return `width: ${_size.value}rpx;
73
+ height:${_size.value}rpx;
74
+ background: linear-gradient(145deg,#ffffff20 25%,#00000060),${textColor.value};
75
+ font-size:${_size.value * 0.45}rpx;
76
+ border-radius: ${props.radius};`;
77
+ });
78
+
79
+ const textColor = computed(() => {
80
+ if (!props.name) {
81
+ return '#fff';
82
+ }
83
+ let hash = 0;
84
+ for (let i = 0; i < props.name.length; i++) {
85
+ hash += props.name.charCodeAt(i);
86
+ }
87
+ return colors[hash % colors.length];
88
+ });
89
+ </script>
90
+
91
+ <style scoped lang="scss">
92
+ .head-image {
93
+ position: relative;
94
+ cursor: pointer;
95
+
96
+ .avatar-image {
97
+ position: relative;
98
+ overflow: hidden;
99
+ border-radius: 50%;
100
+ vertical-align: bottom;
101
+ }
102
+
103
+ .avatar-text {
104
+ color: white;
105
+ border-radius: 50%;
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ }
110
+
111
+ .online {
112
+ position: absolute;
113
+ right: -10%;
114
+ bottom: 0;
115
+ width: 24rpx;
116
+ height: 24rpx;
117
+ background: limegreen;
118
+ border-radius: 50%;
119
+ border: 6rpx solid white;
120
+ }
121
+ }
122
+ </style>
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <view @click="selectAndUpload()">
3
+ <slot></slot>
4
+ </view>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ interface Props {
9
+ maxCount?: number;
10
+ maxSize?: number;
11
+ sourceType?: string;
12
+ isPermanent?: boolean;
13
+ thumbSize?: number;
14
+ onBefore?: (file: any) => boolean;
15
+ onSuccess?: (file: any, data: any) => void;
16
+ onError?: (file: any, error: any) => void;
17
+ uploadUrl: string;
18
+ token?: string;
19
+ }
20
+
21
+ interface UploadResponse {
22
+ code: number;
23
+ message: string;
24
+ data?: any;
25
+ }
26
+
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ maxCount: 1,
29
+ maxSize: 5 * 1024 * 1024,
30
+ sourceType: 'album',
31
+ isPermanent: false,
32
+ thumbSize: 50
33
+ });
34
+
35
+ const uploadHeaders = ref({
36
+ accessToken: uni.getStorageSync('loginInfo').accessToken
37
+ });
38
+
39
+ const selectAndUpload = () => {
40
+ uni.chooseImage({
41
+ count: props.maxCount,
42
+ sourceType: [props.sourceType],
43
+ sizeType: ['original'],
44
+ success: (res: any) => {
45
+ res.tempFiles.forEach((file: any) => {
46
+ if (!props.onBefore || props.onBefore(file)) {
47
+ // 调用上传图片的接口
48
+ uploadImage(file);
49
+ }
50
+ });
51
+ }
52
+ });
53
+ };
54
+
55
+ const uploadImage = (file: any) => {
56
+ const uploadUrl = props.uploadUrl + `?isPermanent=${props.isPermanent}&thumbSize=${props.thumbSize}`;
57
+
58
+ uni.uploadFile({
59
+ url: uploadUrl, //env.BaseApiUrl + action,
60
+ header: {
61
+ accessToken: props.token // getToken()
62
+ },
63
+ filePath: file.path,
64
+ name: 'file',
65
+ success: (res) => {
66
+ const data: UploadResponse = JSON.parse(res.data);
67
+ if (data.code !== 200) {
68
+ uni.showToast({
69
+ icon: "none",
70
+ title: data.message,
71
+ });
72
+ props.onError?.(file, data);
73
+ } else {
74
+ props.onSuccess?.(file, data);
75
+ }
76
+ },
77
+ fail: (err) => {
78
+ console.log(err);
79
+ props.onError?.(file, err);
80
+ }
81
+ });
82
+ };
83
+
84
+ // 暴露方法给父组件
85
+ // defineExpose({
86
+ // selectAndUpload
87
+ // });
88
+ </script>
89
+
90
+ <style></style>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <view class="loading" :style="loadingStyle">
3
+ <view class="rotate iconfont icon-loading" :style="icontStyle"></view>
4
+ <slot></slot>
5
+ </view>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ interface Props {
10
+ size?: number;
11
+ iconColor?: string;
12
+ mask?: boolean;
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ size: 100,
17
+ iconColor: '',
18
+ mask: true
19
+ });
20
+
21
+ const icontStyle = computed(() => {
22
+ let style = `font-size:${props.size}rpx;`;
23
+ if (props.iconColor) {
24
+ style += `color: ${props.iconColor};`;
25
+ } else if (props.mask) {
26
+ style += 'color: #eee;';
27
+ }
28
+ return style;
29
+ });
30
+
31
+ const loadingStyle = computed(() => {
32
+ return props.mask ? "background: rgba(0, 0, 0, 0.3);" : "";
33
+ });
34
+ </script>
35
+
36
+ <style lang="scss" scoped>
37
+ .loading {
38
+ width: 100%;
39
+ height: 100%;
40
+ position: absolute;
41
+ left: 0;
42
+ top: 0;
43
+ z-index: 10000;
44
+ display: flex;
45
+ justify-content: center;
46
+ align-items: center;
47
+ }
48
+
49
+ .rotate {
50
+ animation: rotate 2s ease-in-out infinite;
51
+
52
+ }
53
+
54
+ @keyframes rotate {
55
+ from {
56
+ transform: rotate(0deg)
57
+ }
58
+
59
+ to {
60
+ transform: rotate(360deg)
61
+ }
62
+ }
63
+ </style>