提现功能需要添加

This commit is contained in:
bobobobo
2026-01-04 23:35:06 +08:00
parent a4ae562396
commit 42eba945e8
58 changed files with 4825 additions and 1015 deletions

19
App.vue
View File

@@ -1,15 +1,20 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { useTokenStore } from './stores/token'
import { reLaunch } from './utils/router'
import { useAuthUser } from './composables/useAuthUser'
import { useUserStore } from './stores/user'
import { TUIChatKit } from './TUIKit';
TUIChatKit.init();
const { token } = useAuthUser()
const { loginTencentIM } = useUserStore()
/** 静默登录逻辑 */
const silentLogin = async () => {
console.log(token.value, '==')
if (token.value) {
reLaunch('/pages/news-list/news-list')
loginTencentIM()
reLaunch('/TUIKit/components/TUIConversation/index')
return
}
@@ -34,4 +39,14 @@
<style lang="scss">
/*每个页面公共css */
@import './styles/global.scss';
/* common css for page */
// uni-page-body,
// html,
// body,
// page {
// width: 100% !important;
// height: 100% !important;
// overflow: hidden;
// }
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="chat" :style="{marginBottom: keywordHight + 'px'}">
<div class="chat" :style="{ marginBottom: keywordHight + 'px' }">
<div :class="['tui-chat', !isPC && 'tui-chat-h5']">
<div
v-if="!currentConversationID"
@@ -20,7 +20,10 @@
<Forward @toggleMultipleSelectMode="toggleMultipleSelectMode" />
<MessageList
ref="messageListRef"
:class="['tui-chat-message-list', !isPC && 'tui-chat-h5-message-list']"
:class="[
'tui-chat-message-list',
!isPC && 'tui-chat-h5-message-list'
]"
:isGroup="isGroup"
:groupID="groupID"
:isNotInGroup="isNotInGroup"
@@ -33,7 +36,7 @@
v-if="isNotInGroup"
:class="{
'tui-chat-leave-group': true,
'tui-chat-leave-group-mobile': isMobile,
'tui-chat-leave-group-mobile': isMobile
}"
>
{{ leaveGroupReasonText }}
@@ -63,7 +66,7 @@
'tui-chat-message-input',
!isPC && 'tui-chat-h5-message-input',
isUniFrameWork && 'tui-chat-uni-message-input',
isWeChat && 'tui-chat-wx-message-input',
isWeChat && 'tui-chat-wx-message-input'
]"
:enableAt="featureConfig.InputMention"
:isMuted="false"
@@ -78,232 +81,275 @@
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed } from '../../adapter-vue';
import TUIChatEngine, {
TUITranslateService,
TUIConversationService,
TUIStore,
StoreName,
IMessageModel,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine-lite';
import TUICore, { TUIConstants, ExtensionInfo } from '@tencentcloud/tui-core-lite';
import ChatHeader from './chat-header/index.vue';
import MessageList from './message-list/index.vue';
import MessageInput from './message-input/index.vue';
import MultipleSelectPanel from './mulitple-select-panel/index.vue';
import Forward from './forward/index.vue';
import MessageInputToolbar from './message-input-toolbar/index.vue';
import { isPC, isWeChat, isUniFrameWork, isMobile, isApp } from '../../utils/env';
import { ToolbarDisplayType } from '../../interface';
import TUIChatConfig from './config';
import {
ref,
onMounted,
onUnmounted,
computed
} from '../../adapter-vue'
import TUIChatEngine, {
TUITranslateService,
TUIConversationService,
TUIStore,
StoreName,
IMessageModel,
IConversationModel
} from '@tencentcloud/chat-uikit-engine-lite'
import TUICore, {
TUIConstants,
ExtensionInfo
} from '@tencentcloud/tui-core-lite'
import ChatHeader from './chat-header/index.vue'
import MessageList from './message-list/index.vue'
import MessageInput from './message-input/index.vue'
import MultipleSelectPanel from './mulitple-select-panel/index.vue'
import Forward from './forward/index.vue'
import MessageInputToolbar from './message-input-toolbar/index.vue'
import {
isPC,
isWeChat,
isUniFrameWork,
isMobile,
isApp
} from '../../utils/env'
import { ToolbarDisplayType } from '../../interface'
import TUIChatConfig from './config'
// @Start uniapp use Chat only
import { onLoad, onUnload } from '@dcloudio/uni-app';
import { initChat, logout } from './entry-chat-only.ts';
// @Start uniapp use Chat only
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { initChat, logout } from './entry-chat-only.ts'
onLoad((options) => {
initChat(options);
});
onLoad(options => {
initChat(options)
})
onUnload(() => {
// Whether logout is decided by yourself when the page is unloaded. The default is false.
logout(false).then(() => {
// Handle success result from promise.then when you set true.
}).catch(() => {
// handle error
});
});
// @End uniapp use Chat only
onUnload(() => {
// Whether logout is decided by yourself when the page is unloaded. The default is false.
logout(false)
.then(() => {
// Handle success result from promise.then when you set true.
})
.catch(() => {
// handle error
})
})
// @End uniapp use Chat only
const emits = defineEmits(['closeChat']);
const emits = defineEmits(['closeChat'])
const groupID = ref(undefined);
const isGroup = ref(false);
const isNotInGroup = ref(false);
const notInGroupReason = ref<number>();
const currentConversationID = ref();
const isMultipleSelectMode = ref(false);
const inputToolbarDisplayType = ref<ToolbarDisplayType>('none');
const messageInputRef = ref();
const messageListRef = ref<InstanceType<typeof MessageList>>();
const headerExtensionList = ref<ExtensionInfo[]>([]);
const featureConfig = TUIChatConfig.getFeatureConfig();
const keywordHight = ref(0);
const groupID = ref(undefined)
const isGroup = ref(false)
const isNotInGroup = ref(false)
const notInGroupReason = ref<number>()
const currentConversationID = ref()
const isMultipleSelectMode = ref(false)
const inputToolbarDisplayType = ref<ToolbarDisplayType>('none')
const messageInputRef = ref()
const messageListRef = ref<InstanceType<typeof MessageList>>()
const headerExtensionList = ref<ExtensionInfo[]>([])
const featureConfig = TUIChatConfig.getFeatureConfig()
const keywordHight = ref(0)
const systemInfo = uni.getSystemInfoSync();
const screenHeight = systemInfo.screenHeight;
const systemInfo = uni.getSystemInfoSync()
const screenHeight = systemInfo.screenHeight
const windowResizeCallback = (res) => {
const value = screenHeight - res.size.windowHeight;
if (value > 0 && inputToolbarDisplayType.value !== 'dialog') {
inputToolbarDisplayType.value = 'none';
const windowResizeCallback = res => {
const value = screenHeight - res.size.windowHeight
if (value > 0 && inputToolbarDisplayType.value !== 'dialog') {
inputToolbarDisplayType.value = 'none'
}
uni.$emit('scroll-to-bottom')
keywordHight.value = value
}
uni.$emit('scroll-to-bottom');
keywordHight.value = value;
};
uni.onWindowResize(windowResizeCallback);
uni.onWindowResize(windowResizeCallback)
onMounted(() => {
TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate,
});
});
onMounted(() => {
TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate
})
})
onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate,
});
reset();
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate
})
reset()
})
const isInputToolbarShow = computed<boolean>(() => {
return isUniFrameWork ? inputToolbarDisplayType.value !== 'none' : true;
});
const isInputToolbarShow = computed<boolean>(() => {
return isUniFrameWork
? inputToolbarDisplayType.value !== 'none'
: true
})
const leaveGroupReasonText = computed<string>(() => {
let text = '';
switch (notInGroupReason.value) {
case 4:
text = TUITranslateService.t('TUIChat.您已被管理员移出群聊');
break;
case 5:
text = TUITranslateService.t('TUIChat.该群聊已被解散');
break;
case 8:
text = TUITranslateService.t('TUIChat.您已退出该群聊');
break;
default:
text = TUITranslateService.t('TUIChat.您已退出该群聊');
break;
const leaveGroupReasonText = computed<string>(() => {
let text = ''
switch (notInGroupReason.value) {
case 4:
text = TUITranslateService.t('TUIChat.您已被管理员移出群聊')
break
case 5:
text = TUITranslateService.t('TUIChat.该群聊已被解散')
break
case 8:
text = TUITranslateService.t('TUIChat.您已退出该群聊')
break
default:
text = TUITranslateService.t('TUIChat.您已退出该群聊')
break
}
return text
})
const reset = () => {
TUIConversationService.switchConversation('')
}
return text;
});
const reset = () => {
TUIConversationService.switchConversation('');
};
const closeChat = (conversationID: string) => {
emits('closeChat', conversationID)
reset()
}
const closeChat = (conversationID: string) => {
emits('closeChat', conversationID);
reset();
};
const insertEmoji = (emojiObj: object) => {
messageInputRef.value?.insertEmoji(emojiObj)
}
const insertEmoji = (emojiObj: object) => {
messageInputRef.value?.insertEmoji(emojiObj);
};
const handleEditor = (message: IMessageModel, type: string) => {
if (!message || !type) return
switch (type) {
case 'reference':
// todo
break
case 'reply':
// todo
break
case 'reedit':
if (message?.payload?.text) {
messageInputRef?.value?.reEdit(message?.payload?.text)
}
break
default:
break
}
}
const handleEditor = (message: IMessageModel, type: string) => {
if (!message || !type) return;
switch (type) {
case 'reference':
// todo
break;
case 'reply':
// todo
break;
case 'reedit':
if (message?.payload?.text) {
messageInputRef?.value?.reEdit(message?.payload?.text);
const handleGroup = () => {
headerExtensionList.value[0].listener.onClicked({
groupID: groupID.value
})
}
function changeToolbarDisplayType(type: ToolbarDisplayType) {
setTimeout(() => {
inputToolbarDisplayType.value =
inputToolbarDisplayType.value === type ? 'none' : type
if (inputToolbarDisplayType.value !== 'none' && isUniFrameWork) {
uni.$emit('scroll-to-bottom')
}
break;
default:
break;
}
};
const handleGroup = () => {
headerExtensionList.value[0].listener.onClicked({ groupID: groupID.value });
};
function changeToolbarDisplayType(type: ToolbarDisplayType) {
setTimeout(() => {
inputToolbarDisplayType.value = inputToolbarDisplayType.value === type ? 'none' : type;
if (inputToolbarDisplayType.value !== 'none' && isUniFrameWork) {
uni.$emit('scroll-to-bottom');
}
}, 100)
}
function scrollToLatestMessage() {
messageListRef.value?.scrollToLatestMessage();
}
function toggleMultipleSelectMode(visible?: boolean) {
isMultipleSelectMode.value = visible === undefined ? !isMultipleSelectMode.value : visible;
}
function mergeForwardMessage() {
messageListRef.value?.mergeForwardMessage();
}
function oneByOneForwardMessage() {
messageListRef.value?.oneByOneForwardMessage();
}
function updateUIUserNotInGroup(conversation: IConversationModel) {
if (conversation?.operationType > 0) {
headerExtensionList.value = [];
isNotInGroup.value = true;
/**
* 4 - be removed from the group
* 5 - group is dismissed
* 8 - quit group
*/
notInGroupReason.value = conversation?.operationType;
} else {
isNotInGroup.value = false;
notInGroupReason.value = undefined;
}
}
function onCurrentConversationUpdate(conversation: IConversationModel) {
updateUIUserNotInGroup(conversation);
// return when currentConversation is null
if (!conversation) {
return;
}
// return when currentConversationID.value is the same as conversation.conversationID.
if (currentConversationID.value === conversation?.conversationID) {
return;
}, 100)
}
isGroup.value = false;
let conversationType = TUIChatEngine.TYPES.CONV_C2C;
const conversationID = conversation.conversationID;
if (conversationID.startsWith(TUIChatEngine.TYPES.CONV_GROUP)) {
conversationType = TUIChatEngine.TYPES.CONV_GROUP;
isGroup.value = true;
groupID.value = conversationID.replace(TUIChatEngine.TYPES.CONV_GROUP, '');
function scrollToLatestMessage() {
messageListRef.value?.scrollToLatestMessage()
}
headerExtensionList.value = [];
isMultipleSelectMode.value = false;
// Initialize chatType
TUIChatConfig.setChatType(conversationType);
// While converstaion change success, notify callkit and roomkit、or other components.
TUICore.notifyEvent(TUIConstants.TUIChat.EVENT.CHAT_STATE_CHANGED, TUIConstants.TUIChat.EVENT_SUB_KEY.CHAT_OPENED, { groupID: groupID.value });
// The TUICustomerServicePlugin plugin determines if the current conversation is a customer service conversation, then sets chatType and activates the conversation.
TUICore.callService({
serviceName: TUIConstants.TUICustomerServicePlugin.SERVICE.NAME,
method: TUIConstants.TUICustomerServicePlugin.SERVICE.METHOD.ACTIVE_CONVERSATION,
params: { conversationID: conversationID },
});
// When open chat in room, close main chat ui and reset theme.
if (TUIChatConfig.getChatType() === TUIConstants.TUIChat.TYPE.ROOM) {
if (TUIChatConfig.getFeatureConfig(TUIConstants.TUIChat.FEATURE.InputVoice) === true) {
TUIChatConfig.setTheme('light');
currentConversationID.value = '';
return;
function toggleMultipleSelectMode(visible?: boolean) {
isMultipleSelectMode.value =
visible === undefined ? !isMultipleSelectMode.value : visible
}
function mergeForwardMessage() {
messageListRef.value?.mergeForwardMessage()
}
function oneByOneForwardMessage() {
messageListRef.value?.oneByOneForwardMessage()
}
function updateUIUserNotInGroup(conversation: IConversationModel) {
if (conversation?.operationType > 0) {
headerExtensionList.value = []
isNotInGroup.value = true
/**
* 4 - be removed from the group
* 5 - group is dismissed
* 8 - quit group
*/
notInGroupReason.value = conversation?.operationType
} else {
isNotInGroup.value = false
notInGroupReason.value = undefined
}
}
// Get chat header extensions
if (TUIChatConfig.getChatType() === TUIConstants.TUIChat.TYPE.GROUP) {
headerExtensionList.value = TUICore.getExtensionList(TUIConstants.TUIChat.EXTENSION.CHAT_HEADER.EXT_ID);
function onCurrentConversationUpdate(conversation: IConversationModel) {
updateUIUserNotInGroup(conversation)
// return when currentConversation is null
if (!conversation) {
return
}
// return when currentConversationID.value is the same as conversation.conversationID.
if (currentConversationID.value === conversation?.conversationID) {
return
}
isGroup.value = false
let conversationType = TUIChatEngine.TYPES.CONV_C2C
const conversationID = conversation.conversationID
if (conversationID.startsWith(TUIChatEngine.TYPES.CONV_GROUP)) {
conversationType = TUIChatEngine.TYPES.CONV_GROUP
isGroup.value = true
groupID.value = conversationID.replace(
TUIChatEngine.TYPES.CONV_GROUP,
''
)
}
headerExtensionList.value = []
isMultipleSelectMode.value = false
// Initialize chatType
TUIChatConfig.setChatType(conversationType)
// While converstaion change success, notify callkit and roomkit、or other components.
TUICore.notifyEvent(
TUIConstants.TUIChat.EVENT.CHAT_STATE_CHANGED,
TUIConstants.TUIChat.EVENT_SUB_KEY.CHAT_OPENED,
{ groupID: groupID.value }
)
// The TUICustomerServicePlugin plugin determines if the current conversation is a customer service conversation, then sets chatType and activates the conversation.
TUICore.callService({
serviceName: TUIConstants.TUICustomerServicePlugin.SERVICE.NAME,
method:
TUIConstants.TUICustomerServicePlugin.SERVICE.METHOD
.ACTIVE_CONVERSATION,
params: { conversationID: conversationID }
})
// When open chat in room, close main chat ui and reset theme.
if (TUIChatConfig.getChatType() === TUIConstants.TUIChat.TYPE.ROOM) {
if (
TUIChatConfig.getFeatureConfig(
TUIConstants.TUIChat.FEATURE.InputVoice
) === true
) {
TUIChatConfig.setTheme('light')
currentConversationID.value = ''
return
}
}
// Get chat header extensions
if (TUIChatConfig.getChatType() === TUIConstants.TUIChat.TYPE.GROUP) {
headerExtensionList.value = TUICore.getExtensionList(
TUIConstants.TUIChat.EXTENSION.CHAT_HEADER.EXT_ID
)
}
TUIStore.update(
StoreName.CUSTOM,
'activeConversation',
conversationID
)
currentConversationID.value = conversationID
}
TUIStore.update(StoreName.CUSTOM, 'activeConversation', conversationID);
currentConversationID.value = conversationID;
}
</script>
<style scoped lang="scss" src="./style/index.scss"></style>
<style scoped lang="scss" src="./style/index.scss">
</style>

View File

@@ -1,3 +1,12 @@
uni-page-body,
html,
body,
page {
width: 100% !important;
height: 100% !important;
overflow: hidden;
}
@import '../../../assets/styles/common';
@import './web';
@import './h5';

View File

@@ -26,64 +26,84 @@
</template>
<script setup lang="ts">
import { deepCopy } from '../../../TUIChat/utils/utils';
import ContactListItem from '../contact-list-item/index.vue';
import { sortByFirstChar } from '../../utils/sortByFirstChar';
import { onMounted, onUnmounted, ref } from '../../../../adapter-vue';
import { Friend, StoreName, TUIStore } from '@tencentcloud/chat-uikit-engine-lite';
import { FriendListData } from '../../../../interface';
import { deepCopy } from '../../../TUIChat/utils/utils'
import ContactListItem from '../contact-list-item/index.vue'
import { sortByFirstChar } from '../../utils/sortByFirstChar'
import { onMounted, onUnmounted, ref } from '../../../../adapter-vue'
import {
Friend,
StoreName,
TUIStore
} from '@tencentcloud/chat-uikit-engine-lite'
import { FriendListData } from '../../../../interface'
const emits = defineEmits(['enterConversation']);
const emits = defineEmits(['enterConversation'])
const friendListData = ref<FriendListData>({
key: 'friendList',
title: '我的好友',
map: {},
});
const friendListData = ref<FriendListData>({
key: 'friendList',
title: '我的好友',
map: {}
})
function onFriendListUpdated(friendList: Friend[]) {
const { groupedList } = sortByFirstChar(
friendList,
(friend: Friend) => friend.remark || friend.profile?.nick || friend.userID || '');
friendListData.value.map = groupedList;
}
function onFriendListUpdated(friendList: Friend[]) {
const { groupedList } = sortByFirstChar(
friendList,
(friend: Friend) =>
friend.remark || friend.profile?.nick || friend.userID || ''
)
onMounted(() => {
TUIStore.watch(StoreName.FRIEND, {
friendList: onFriendListUpdated,
});
});
console.log(groupedList, '====222==')
friendListData.value.map = {
A: [
{
profile: {
nick: '测试第三方',
avatar:
'https://jckj-1309258891.cos.ap-chengdu.myqcloud.com/common/19101767159307246_.png'
}
}
],
...groupedList
}
}
onUnmounted(() => {
TUIStore.unwatch(StoreName.FRIEND, {
friendList: onFriendListUpdated,
});
});
onMounted(() => {
TUIStore.watch(StoreName.FRIEND, {
friendList: onFriendListUpdated
})
})
const enterConversation = (item: any) => {
emits('enterConversation', item);
};
onUnmounted(() => {
TUIStore.unwatch(StoreName.FRIEND, {
friendList: onFriendListUpdated
})
})
const enterConversation = (item: any) => {
emits('enterConversation', item)
}
</script>
<style scoped lang="scss">
.friend-list {
ul, li {
list-style: none;
padding: 0;
.friend-list {
ul,
li {
list-style: none;
padding: 0;
}
}
}
.friend-group-title {
padding: 8px 16px;
background-color: #f8f9fa;
font-size: 14px;
font-weight: 500;
color: #666;
line-height: 20px;
}
.friend-group-title {
padding: 8px 16px;
background-color: #f8f9fa;
font-size: 14px;
font-weight: 500;
color: #666;
line-height: 20px;
}
.friend-item {
margin: 0 15px;
padding: 5px 0;
}
.friend-item {
margin: 0 15px;
padding: 5px 0;
}
</style>

View File

@@ -28,7 +28,13 @@
</span>
</div>
<div class="tui-contact-list-item-header-right">
<div>{{ TUITranslateService.t(`TUIContact.${contactListObj.title}`) }}</div>
<div>
{{
TUITranslateService.t(
`TUIContact.${contactListObj.title}`
)
}}
</div>
<Icon
:file="currentContactListKey === key ? downSVG : rightSVG"
size="20px"
@@ -41,7 +47,8 @@
</div>
<template v-else>
<li
v-for="contactListItem in contactListMap[currentContactListKey].list"
v-for="contactListItem in contactListMap[currentContactListKey]
.list"
:key="contactListItem.renderKey"
class="tui-contact-list-item-main-item"
:class="['selected']"
@@ -55,19 +62,13 @@
</template>
</div>
<ul
v-else-if="contactSearchingStatus"
class="tui-contact-list"
>
<ul v-else-if="contactSearchingStatus" class="tui-contact-list">
<li
v-for="(item, key) in contactSearchResult"
:key="key"
class="tui-contact-list-item"
>
<div
v-if="item.list[0]"
class="tui-contact-search-list"
>
<div v-if="item.list[0]" class="tui-contact-search-list">
<div class="tui-contact-search-list-title">
{{ TUITranslateService.t(`TUIContact.${item.label}`) }}
</div>
@@ -89,288 +90,369 @@
v-if="isContactSearchNoResult"
class="tui-contact-search-list-default"
>
{{ TUITranslateService.t("TUIContact.无搜索结果") }}
{{ TUITranslateService.t('TUIContact.无搜索结果') }}
</div>
</ul>
</template>
<script setup lang="ts">
import {
TUITranslateService,
TUIStore,
StoreName,
TUIFriendService,
TUIUserService,
} from '@tencentcloud/chat-uikit-engine-lite';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core-lite';
import { ref, computed, onMounted, onUnmounted, provide } from '../../../adapter-vue';
import { isPC } from '../../../utils/env';
import { deepCopy } from '../../TUIChat/utils/utils';
import {
TUITranslateService,
TUIStore,
StoreName,
TUIFriendService,
TUIUserService
} from '@tencentcloud/chat-uikit-engine-lite'
import TUICore, { TUIConstants } from '@tencentcloud/tui-core-lite'
import {
ref,
computed,
onMounted,
onUnmounted,
provide
} from '../../../adapter-vue'
import { isPC } from '../../../utils/env'
import { deepCopy } from '../../TUIChat/utils/utils'
import Icon from '../../common/Icon.vue';
import ContactListItem from './contact-list-item/index.vue';
import FriendList from './components/FriendList.vue';
import Icon from '../../common/Icon.vue'
import ContactListItem from './contact-list-item/index.vue'
import FriendList from './components/FriendList.vue'
import downSVG from '../../../assets/icon/down-icon.svg';
import rightSVG from '../../../assets/icon/right-icon.svg';
import newContactsSVG from '../../../assets/icon/new-contacts.svg';
import groupSVG from '../../../assets/icon/groups.svg';
import blackListSVG from '../../../assets/icon/black-list.svg';
import downSVG from '../../../assets/icon/down-icon.svg'
import rightSVG from '../../../assets/icon/right-icon.svg'
import newContactsSVG from '../../../assets/icon/new-contacts.svg'
import groupSVG from '../../../assets/icon/groups.svg'
import blackListSVG from '../../../assets/icon/black-list.svg'
import type {
IContactList,
IContactSearchResult,
IBlackListUserItem,
IUserStatus,
IUserStatusMap,
IContactInfoType,
} from '../../../interface';
import type {
IGroupModel,
Friend,
FriendApplication } from '@tencentcloud/chat-uikit-engine-lite';
import type {
IContactList,
IContactSearchResult,
IBlackListUserItem,
IUserStatus,
IUserStatusMap,
IContactInfoType
} from '../../../interface'
import type {
IGroupModel,
Friend,
FriendApplication
} from '@tencentcloud/chat-uikit-engine-lite'
const currentContactListKey = ref<keyof IContactList>('');
const currentContactInfo = ref<IContactInfoType>({} as IContactInfoType);
const contactListMap = ref<IContactList>({
friendApplicationList: {
icon: newContactsSVG,
key: 'friendApplicationList',
title: '新的联系人',
list: [] as FriendApplication[],
unreadCount: 0,
},
groupList: {
icon: groupSVG,
key: 'groupList',
title: '我的群聊',
list: [] as IGroupModel[],
},
blackList: {
icon: blackListSVG,
key: 'blackList',
title: '黑名单',
list: [] as IBlackListUserItem[],
},
});
const currentContactListKey = ref<keyof IContactList>('')
const currentContactInfo = ref<IContactInfoType>({} as IContactInfoType)
const contactListMap = ref<IContactList>({
friendApplicationList: {
icon: newContactsSVG,
key: 'friendApplicationList',
title: '新的联系人',
list: [] as FriendApplication[],
unreadCount: 0
},
groupList: {
icon: groupSVG,
key: 'groupList',
title: '我的群聊',
list: [] as IGroupModel[]
},
blackList: {
icon: blackListSVG,
key: 'blackList',
title: '黑名单',
list: [] as IBlackListUserItem[]
}
})
const contactSearchingStatus = ref<boolean>(false);
const contactSearchResult = ref<IContactSearchResult>();
const displayOnlineStatus = ref<boolean>(false);
const userOnlineStatusMap = ref<IUserStatusMap>();
const contactSearchingStatus = ref<boolean>(false)
const contactSearchResult = ref<IContactSearchResult>()
const displayOnlineStatus = ref<boolean>(false)
const userOnlineStatusMap = ref<IUserStatusMap>()
const isContactSearchNoResult = computed((): boolean => (
!contactSearchResult?.value?.user?.list[0]
&& !contactSearchResult?.value?.group?.list[0]
));
const isContactSearchNoResult = computed(
(): boolean =>
!contactSearchResult?.value?.user?.list[0] &&
!contactSearchResult?.value?.group?.list[0]
)
onMounted(() => {
TUIStore.watch(StoreName.APP, {
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated,
});
onMounted(() => {
TUIStore.watch(StoreName.APP, {
enabledCustomerServicePlugin:
onCustomerServiceCommercialPluginUpdated
})
TUIStore.watch(StoreName.GRP, {
groupList: onGroupListUpdated,
});
TUIStore.watch(StoreName.GRP, {
groupList: onGroupListUpdated
})
TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated,
});
TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated
})
TUIStore.watch(StoreName.FRIEND, {
friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated,
});
TUIStore.watch(StoreName.FRIEND, {
friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated
})
TUIStore.watch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
});
});
TUIStore.watch(StoreName.CUSTOM, {
currentContactSearchingStatus:
onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated
})
})
onUnmounted(() => {
TUIStore.unwatch(StoreName.APP, {
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated,
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.APP, {
enabledCustomerServicePlugin:
onCustomerServiceCommercialPluginUpdated
})
TUIStore.unwatch(StoreName.GRP, {
groupList: onGroupListUpdated,
});
TUIStore.unwatch(StoreName.GRP, {
groupList: onGroupListUpdated
})
TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated,
});
TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated
})
TUIStore.unwatch(StoreName.FRIEND, {
friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated,
});
TUIStore.unwatch(StoreName.FRIEND, {
friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated
})
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
});
});
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactSearchingStatus:
onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated
})
})
function toggleCurrentContactList(key: keyof IContactList) {
if (currentContactListKey.value === key) {
currentContactListKey.value = '';
currentContactInfo.value = {} as IContactInfoType;
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', '');
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', {} as IContactInfoType);
} else {
currentContactListKey.value = key;
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', key);
if (key === 'friendApplicationList') {
TUIFriendService.setFriendApplicationRead();
function toggleCurrentContactList(key: keyof IContactList) {
if (currentContactListKey.value === key) {
currentContactListKey.value = ''
currentContactInfo.value = {} as IContactInfoType
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', '')
TUIStore.update(
StoreName.CUSTOM,
'currentContactInfo',
{} as IContactInfoType
)
} else {
currentContactListKey.value = key
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', key)
console.log(111)
if (key === 'friendApplicationList') {
TUIFriendService.setFriendApplicationRead()
}
}
}
}
function selectItem(item: any) {
currentContactInfo.value = item;
// For a result in the search list, before viewing the contactInfo details,
// it is necessary to update the data for the "already in the group list/already in the friend list" situation to obtain more detailed information
if (contactSearchingStatus.value) {
let targetListItem;
if ((currentContactInfo.value as Friend)?.userID) {
targetListItem = contactListMap.value?.friendList?.list?.find(
(item: IContactInfoType) => (item as Friend)?.userID === (currentContactInfo.value as Friend)?.userID,
);
} else if ((currentContactInfo.value as IGroupModel)?.groupID) {
targetListItem = contactListMap.value?.groupList?.list?.find(
(item: IContactInfoType) => (item as IGroupModel)?.groupID === (currentContactInfo.value as IGroupModel)?.groupID,
);
function selectItem(item: any) {
currentContactInfo.value = item
// For a result in the search list, before viewing the contactInfo details,
// it is necessary to update the data for the "already in the group list/already in the friend list" situation to obtain more detailed information
if (contactSearchingStatus.value) {
let targetListItem
if ((currentContactInfo.value as Friend)?.userID) {
targetListItem = contactListMap.value?.friendList?.list?.find(
(item: IContactInfoType) =>
(item as Friend)?.userID ===
(currentContactInfo.value as Friend)?.userID
)
} else if ((currentContactInfo.value as IGroupModel)?.groupID) {
targetListItem = contactListMap.value?.groupList?.list?.find(
(item: IContactInfoType) =>
(item as IGroupModel)?.groupID ===
(currentContactInfo.value as IGroupModel)?.groupID
)
}
if (targetListItem) {
currentContactInfo.value = targetListItem
}
}
if (targetListItem) {
currentContactInfo.value = targetListItem;
TUIStore.update(
StoreName.CUSTOM,
'currentContactInfo',
currentContactInfo.value
)
}
const selectFriend = (item: any) => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactListKey',
'friendList'
)
selectItem(item)
}
function onDisplayOnlineStatusUpdated(status: boolean) {
displayOnlineStatus.value = status
}
function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
if (list?.size > 0) {
userOnlineStatusMap.value = Object.fromEntries(list?.entries())
}
}
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', currentContactInfo.value);
}
const selectFriend = (item: any) => {
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', 'friendList');
selectItem(item);
};
function onCustomerServiceCommercialPluginUpdated(isEnabled: boolean) {
if (!isEnabled) {
return
}
function onDisplayOnlineStatusUpdated(status: boolean) {
displayOnlineStatus.value = status;
}
// After the customer purchases the customer service plug-in,
// the engine updates the enabledCustomerServicePlugin to true through the commercial capability bit.
const contactListExtensionID =
TUIConstants.TUIContact.EXTENSION.CONTACT_LIST.EXT_ID
const tuiContactExtensionList = TUICore.getExtensionList(
contactListExtensionID
)
function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
if (list?.size > 0) {
userOnlineStatusMap.value = Object.fromEntries(list?.entries());
}
}
const customerData = tuiContactExtensionList.find(
(extension: any) => {
const { name, accountList = [] } = extension.data || {}
return name === 'customer' && accountList.length > 0
}
)
function onCustomerServiceCommercialPluginUpdated(isEnabled: boolean) {
if (!isEnabled) {
return;
if (customerData) {
const { data, text } = customerData
const { accountList } = (data || {}) as { accountList: string[] }
TUIUserService.getUserProfile({ userIDList: accountList })
.then(res => {
if (res.data.length > 0) {
const customerList = {
title: text,
list: res.data.map((item: any, index: number) => ({
...item,
renderKey: generateRenderKey('customerList', item, index),
infoKeyList: [],
btnKeyList: ['enterC2CConversation']
})),
key: 'customerList'
}
contactListMap.value = {
...contactListMap.value,
customerList
}
}
})
.catch(() => {})
}
}
// After the customer purchases the customer service plug-in,
// the engine updates the enabledCustomerServicePlugin to true through the commercial capability bit.
const contactListExtensionID = TUIConstants.TUIContact.EXTENSION.CONTACT_LIST.EXT_ID;
const tuiContactExtensionList = TUICore.getExtensionList(contactListExtensionID);
const customerData = tuiContactExtensionList.find((extension: any) => {
const { name, accountList = [] } = extension.data || {};
return name === 'customer' && accountList.length > 0;
});
if (customerData) {
const { data, text } = customerData;
const { accountList } = (data || {}) as { accountList: string[] };
TUIUserService.getUserProfile({ userIDList: accountList })
.then((res) => {
if (res.data.length > 0) {
const customerList = {
title: text,
list: res.data.map((item: any, index: number) => ({
...item,
renderKey: generateRenderKey('customerList', item, index),
infoKeyList: [],
btnKeyList: ['enterC2CConversation'],
})),
key: 'customerList',
};
contactListMap.value = { ...contactListMap.value, customerList };
}
})
.catch(() => { });
function onGroupListUpdated(groupList: IGroupModel[]) {
updateContactListMap('groupList', groupList)
}
}
function onGroupListUpdated(groupList: IGroupModel[]) {
updateContactListMap('groupList', groupList);
}
function onUserBlacklistUpdated(userBlacklist: IBlackListUserItem[]) {
updateContactListMap('blackList', userBlacklist)
}
function onUserBlacklistUpdated(userBlacklist: IBlackListUserItem[]) {
updateContactListMap('blackList', userBlacklist);
}
function onFriendApplicationUnreadCountUpdated(friendApplicationUnreadCount: number) {
contactListMap.value.friendApplicationList.unreadCount = friendApplicationUnreadCount;
}
function onFriendApplicationListUpdated(friendApplicationList: FriendApplication[]) {
updateContactListMap('friendApplicationList', friendApplicationList);
}
function updateContactListMap(key: keyof IContactList, list: IContactInfoType[]) {
contactListMap.value[key].list = list;
contactListMap.value[key].list.map((item: IContactInfoType, index: number) => item.renderKey = generateRenderKey(key, item, index));
updateCurrentContactInfoFromList(contactListMap.value[key].list, key);
}
function updateCurrentContactInfoFromList(list: IContactInfoType[], type: keyof IContactList) {
if (
!(currentContactInfo.value as Friend)?.userID
&& !(currentContactInfo.value as IGroupModel)?.groupID
function onFriendApplicationUnreadCountUpdated(
friendApplicationUnreadCount: number
) {
return;
contactListMap.value.friendApplicationList.unreadCount =
friendApplicationUnreadCount
}
if (type === currentContactListKey.value || contactSearchingStatus.value) {
currentContactInfo.value = list?.find(
(item: any) =>
(item?.groupID && item?.groupID === (currentContactInfo.value as IGroupModel)?.groupID) || (item?.userID && item?.userID === (currentContactInfo.value as Friend)?.userID),
) || {} as IContactInfoType;
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', currentContactInfo.value);
function onFriendApplicationListUpdated(
friendApplicationList: FriendApplication[]
) {
updateContactListMap('friendApplicationList', friendApplicationList)
}
}
function generateRenderKey(contactListMapKey: keyof IContactList, contactInfo: IContactInfoType, index: number) {
return `${contactListMapKey}-${(contactInfo as Friend).userID || (contactInfo as IGroupModel).groupID || (`index${index}`)}`;
}
function updateContactListMap(
key: keyof IContactList,
list: IContactInfoType[]
) {
contactListMap.value[key].list = list
contactListMap.value[key].list.map(
(item: IContactInfoType, index: number) =>
(item.renderKey = generateRenderKey(key, item, index))
)
updateCurrentContactInfoFromList(contactListMap.value[key].list, key)
}
function onCurrentContactSearchResultUpdated(searchResult: IContactSearchResult) {
contactSearchResult.value = searchResult;
}
function updateCurrentContactInfoFromList(
list: IContactInfoType[],
type: keyof IContactList
) {
if (
!(currentContactInfo.value as Friend)?.userID &&
!(currentContactInfo.value as IGroupModel)?.groupID
) {
return
}
if (
type === currentContactListKey.value ||
contactSearchingStatus.value
) {
currentContactInfo.value =
list?.find(
(item: any) =>
(item?.groupID &&
item?.groupID ===
(currentContactInfo.value as IGroupModel)?.groupID) ||
(item?.userID &&
item?.userID ===
(currentContactInfo.value as Friend)?.userID)
) || ({} as IContactInfoType)
TUIStore.update(
StoreName.CUSTOM,
'currentContactInfo',
currentContactInfo.value
)
}
}
function onCurrentContactSearchingStatusUpdated(searchingStatus: boolean) {
contactSearchingStatus.value = searchingStatus;
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', {} as IContactInfoType);
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', '');
}
function generateRenderKey(
contactListMapKey: keyof IContactList,
contactInfo: IContactInfoType,
index: number
) {
return `${contactListMapKey}-${
(contactInfo as Friend).userID ||
(contactInfo as IGroupModel).groupID ||
`index${index}`
}`
}
function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) {
currentContactInfo.value = contactInfo;
}
function onCurrentContactSearchResultUpdated(
searchResult: IContactSearchResult
) {
contactSearchResult.value = searchResult
}
function onCurrentContactListKeyUpdated(contactListKey: string) {
currentContactListKey.value = contactListKey;
}
function onCurrentContactSearchingStatusUpdated(
searchingStatus: boolean
) {
contactSearchingStatus.value = searchingStatus
TUIStore.update(
StoreName.CUSTOM,
'currentContactInfo',
{} as IContactInfoType
)
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', '')
}
provide('userOnlineStatusMap', userOnlineStatusMap);
function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) {
currentContactInfo.value = contactInfo
}
function onCurrentContactListKeyUpdated(contactListKey: string) {
currentContactListKey.value = contactListKey
}
provide('userOnlineStatusMap', userOnlineStatusMap)
</script>
<style lang="scss" scoped src="./style/index.scss"></style>

View File

@@ -4,19 +4,24 @@
v-else-if="isShowContactList"
:class="['tui-contact', !isPC && 'tui-contact-h5']"
>
<Navigation :title="currentContactKey ? contactInfoTitle : TUITranslateService.t('TUIChat.腾讯云 IM')">
<Navigation
:title="
currentContactKey
? contactInfoTitle
: TUITranslateService.t('TUIChat.腾讯云 IM')
"
>
<template #left>
<div v-show="currentContactKey" @click="resetContactType">
<Icon
:file="backSVG"
/>
<Icon :file="backSVG" />
</div>
</template>
<template #right>
<div v-show="!isShowContactSearch && !currentContactKey" @click="openContactSearch">
<Icon
:file="addCircle"
/>
<div
v-show="!isShowContactSearch && !currentContactKey"
@click="openContactSearch"
>
<Icon :file="addCircle" />
</div>
</template>
</Navigation>
@@ -28,173 +33,204 @@
</div>
<div
v-else
:class="['tui-contact-left', !isPC && 'tui-contact-h5-left']">
<ContactSearch
v-if="isShowContactSearch"
:class="['tui-contact-left', !isPC && 'tui-contact-h5-left']"
>
<ContactSearch v-if="isShowContactSearch" />
<ContactList
:class="[
'tui-contact-left-list',
!isPC && 'tui-contact-h5-left-list'
]"
/>
<ContactList :class="['tui-contact-left-list', !isPC && 'tui-contact-h5-left-list']" />
</div>
</div>
</template>
<script lang="ts" setup>
import { TUIStore, StoreName, TUITranslateService } from '@tencentcloud/chat-uikit-engine-lite';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { onMounted, onUnmounted, ref, watchEffect } from '../../adapter-vue';
import { isPC, isUniFrameWork } from '../../utils/env';
import Navigation from '../common/Navigation/index.vue';
import Icon from '../common/Icon.vue';
import {
TUIStore,
StoreName,
TUITranslateService
} from '@tencentcloud/chat-uikit-engine-lite'
import { TUIGlobal } from '@tencentcloud/universal-api'
import {
onMounted,
onUnmounted,
ref,
watchEffect
} from '../../adapter-vue'
import { isPC, isUniFrameWork } from '../../utils/env'
import Navigation from '../common/Navigation/index.vue'
import Icon from '../common/Icon.vue'
import SelectFriend from './select-friend/index.vue';
import ContactSearch from './contact-search/index.vue';
import ContactList from './contact-list/index.vue';
import ContactInfo from './contact-info/index.vue';
import SelectFriend from './select-friend/index.vue'
import ContactSearch from './contact-search/index.vue'
import ContactList from './contact-list/index.vue'
import ContactInfo from './contact-info/index.vue'
import addCircle from '../../assets/icon/add-circle.svg';
import backSVG from '../../assets/icon/back.svg';
import { CONTACT_INFO_TITLE } from '../../constant';
import addCircle from '../../assets/icon/add-circle.svg'
import backSVG from '../../assets/icon/back.svg'
import { CONTACT_INFO_TITLE } from '../../constant'
const emits = defineEmits(['switchConversation']);
const emits = defineEmits(['switchConversation'])
const props = defineProps({
// web/h5 single page application display format, uniapp please ignore
displayType: {
type: String,
default: 'contactList', // "contactList" / "selectFriend"
require: false,
},
});
const displayTypeRef = ref<string>(props.displayType || 'contactList');
const isShowSelectFriend = ref(false);
const isShowContactList = ref(true);
const isShowContactInfo = ref(true);
const isShowContactSearch = ref(false);
const currentContactKey = ref('');
const contactInfoTitle = ref<string>('');
watchEffect(() => {
isShowContactList.value = props?.displayType !== 'selectFriend';
});
const switchConversation = (data: any) => {
isUniFrameWork
&& TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index',
});
emits('switchConversation', data);
};
const openContactSearch = () => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchingStatus',
true,
);
};
const resetContactType = () => {
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', '');
};
const onCurrentContactSearchingStatusUpdated = (data: boolean) => {
isShowContactSearch.value = data;
};
const onIsShowSelectFriendComponentUpdated = (data: any) => {
if (!isUniFrameWork && props?.displayType === 'selectFriend') {
isShowSelectFriend.value = data;
isShowContactList.value = false;
return;
}
if (data) {
isShowSelectFriend.value = true;
if (isUniFrameWork) {
displayTypeRef.value = 'selectFriend';
TUIGlobal?.hideTabBar();
const props = defineProps({
// web/h5 single page application display format, uniapp please ignore
displayType: {
type: String,
default: 'contactList', // "contactList" / "selectFriend"
require: false
}
} else {
isShowSelectFriend.value = false;
if (isUniFrameWork) {
displayTypeRef.value = props.displayType;
TUIGlobal?.showTabBar()?.catch(() => { /* ignore */ });
})
const displayTypeRef = ref<string>(props.displayType || 'contactList')
const isShowSelectFriend = ref(false)
const isShowContactList = ref(true)
const isShowContactInfo = ref(true)
const isShowContactSearch = ref(false)
const currentContactKey = ref('')
const contactInfoTitle = ref<string>('')
watchEffect(() => {
isShowContactList.value = props?.displayType !== 'selectFriend'
})
const switchConversation = (data: any) => {
isUniFrameWork &&
TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index'
})
emits('switchConversation', data)
}
const openContactSearch = () => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchingStatus',
true
)
}
const resetContactType = () => {
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', '')
}
const onCurrentContactSearchingStatusUpdated = (data: boolean) => {
isShowContactSearch.value = data
}
const onIsShowSelectFriendComponentUpdated = (data: any) => {
if (!isUniFrameWork && props?.displayType === 'selectFriend') {
isShowSelectFriend.value = data
isShowContactList.value = false
return
}
if (data) {
isShowSelectFriend.value = true
if (isUniFrameWork) {
displayTypeRef.value = 'selectFriend'
TUIGlobal?.hideTabBar()
}
} else {
isShowSelectFriend.value = false
if (isUniFrameWork) {
displayTypeRef.value = props.displayType
TUIGlobal?.showTabBar()?.catch(() => {
/* ignore */
})
}
}
}
};
const onCurrentContactInfoUpdated = (contactInfo: any) => {
isShowContactInfo.value = isPC || (contactInfo && typeof contactInfo === 'object' && Object.keys(contactInfo)?.length > 0);
};
const onCurrentContactInfoUpdated = (contactInfo: any) => {
isShowContactInfo.value =
isPC ||
(contactInfo &&
typeof contactInfo === 'object' &&
Object.keys(contactInfo)?.length > 0)
}
const onCurrentContactListKeyUpdated = (key: string) => {
currentContactKey.value = key;
contactInfoTitle.value = TUITranslateService.t(`TUIContact.${CONTACT_INFO_TITLE[key]}`);
};
const onCurrentContactListKeyUpdated = (key: string) => {
currentContactKey.value = key
contactInfoTitle.value = TUITranslateService.t(
`TUIContact.${CONTACT_INFO_TITLE[key]}`
)
}
onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
isShowSelectFriendComponent: onIsShowSelectFriendComponentUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
isShowSelectFriendComponent: onIsShowSelectFriendComponentUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
});
});
onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, {
currentContactSearchingStatus:
onCurrentContactSearchingStatusUpdated,
isShowSelectFriendComponent: onIsShowSelectFriendComponentUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
currentContactListKey: onCurrentContactListKeyUpdated
})
})
onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactSearchingStatus:
onCurrentContactSearchingStatusUpdated,
isShowSelectFriendComponent: onIsShowSelectFriendComponentUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
currentContactListKey: onCurrentContactListKeyUpdated
})
})
</script>
<style lang="scss" scoped>
@import "../../assets/styles/common";
@import '../../assets/styles/common';
.tui-contact {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
overflow: hidden;
display: flex;
flex-direction: column;
uni-page-body,
html,
body,
page {
width: 100% !important;
height: 100% !important;
overflow: hidden;
}
&-left {
min-width: 285px;
flex: 0 0 24%;
.tui-contact {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
overflow: hidden;
display: flex;
flex-direction: column;
}
&-right {
border-left: 1px solid #f4f5f9;
flex: 1;
overflow: hidden;
}
}
&-left {
min-width: 285px;
flex: 0 0 24%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.tui-contact-h5 {
position: relative;
&-left,
&-right {
width: 100%;
height: 100%;
flex: 1;
}
&-right {
position: absolute;
z-index: 100;
}
&-left {
&-list {
overflow-y: auto;
&-right {
border-left: 1px solid #f4f5f9;
flex: 1;
overflow: hidden;
}
}
.tui-contact-h5 {
position: relative;
&-left,
&-right {
width: 100%;
height: 100%;
flex: 1;
}
&-right {
position: absolute;
z-index: 100;
}
&-left {
&-list {
overflow-y: auto;
}
}
}
}
</style>

View File

@@ -18,174 +18,195 @@
</div>
</template>
<script lang="ts" setup>
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine-lite';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref } from '../../adapter-vue';
import TUISearch from '../TUISearch/index.vue';
import ConversationList from './conversation-list/index.vue';
import ConversationHeader from './conversation-header/index.vue';
import ConversationNetwork from './conversation-network/index.vue';
import { onHide } from '@dcloudio/uni-app';
// #ifdef MP-WEIXIN
// uniapp packaged mini-programs are integrated by default, and the default initialization entry file is imported here
// TUIChatKit init needs to be encapsulated because uni vue2 will report an error when compiling H5 directly through conditional compilation
import './entry.ts';
// #endif
import {
TUIStore,
StoreName
} from '@tencentcloud/chat-uikit-engine-lite'
import { TUIGlobal } from '@tencentcloud/universal-api'
import { ref } from '../../adapter-vue'
import TUISearch from '../TUISearch/index.vue'
import ConversationList from './conversation-list/index.vue'
import ConversationHeader from './conversation-header/index.vue'
import ConversationNetwork from './conversation-network/index.vue'
import { onHide } from '@dcloudio/uni-app'
// #ifdef MP-WEIXIN
// uniapp packaged mini-programs are integrated by default, and the default initialization entry file is imported here
// TUIChatKit init needs to be encapsulated because uni vue2 will report an error when compiling H5 directly through conditional compilation
import './entry.ts'
// #endif
const emits = defineEmits(['handleSwitchConversation']);
const emits = defineEmits(['handleSwitchConversation'])
const totalUnreadCount = ref(0);
const headerRef = ref<typeof ConversationHeader>();
const conversationListDomRef = ref<typeof ConversationList>();
const touchX = ref<number>(0);
const touchY = ref<number>(0);
const isShowConversationHeader = ref<boolean>(true);
const totalUnreadCount = ref(0)
const headerRef = ref<typeof ConversationHeader>()
const conversationListDomRef = ref<typeof ConversationList>()
const touchX = ref<number>(0)
const touchY = ref<number>(0)
const isShowConversationHeader = ref<boolean>(true)
const getTabBarConfig = () => {
try {
// 方法1: 从 getApp() 全局数据中获取
const app = getApp();
if (app?.globalData?.tabBar) {
return app.globalData.tabBar;
}
// 方法2: 从 pages.json 配置中读取(如果可访问)
// @ts-ignore
if (typeof __uniConfig !== 'undefined' && __uniConfig.tabBar) {
// @ts-ignore
return __uniConfig.tabBar;
}
// 方法3: 尝试调用 uni.getTabBar() 检测是否存在 tabbar
const getTabBarConfig = () => {
try {
const tabBar = uni.getTabBar && uni.getTabBar();
if (tabBar) {
return { list: tabBar};
// 方法1: 从 getApp() 全局数据中获取
const app = getApp()
if (app?.globalData?.tabBar) {
return app.globalData.tabBar
}
} catch (e) {
return null;
}
return null;
} catch (error) {
return null;
}
};
const getTabBarIndex = () => {
try {
const pages = getCurrentPages();
if (!pages || pages.length === 0) return;
const currentPage = pages[pages.length - 1];
const currentRoute = currentPage.route;
const isTUIConversationPage = currentRoute && (
currentRoute.includes('TUIKit/components/TUIConversation/index') ||
currentRoute.includes('TUIConversation')
);
if (!isTUIConversationPage) {
return;
}
const tabBarConfig = getTabBarConfig();
if (!tabBarConfig) {
return;
}
let tabBarIndex = -1;
if (tabBarConfig.list && Array.isArray(tabBarConfig.list)) {
tabBarIndex = tabBarConfig.list.findIndex((item: any) => {
const pagePath = item.pagePath || '';
return pagePath.includes('TUIConversation') ||
pagePath.includes('TUIKit/components/TUIConversation/index');
});
}
return tabBarIndex;
} catch (error) {
return -1;
}
};
// 方法2: 从 pages.json 配置中读取(如果可访问)
// @ts-ignore
if (typeof __uniConfig !== 'undefined' && __uniConfig.tabBar) {
// @ts-ignore
return __uniConfig.tabBar
}
TUIStore.watch(StoreName.CONV, {
totalUnreadCount: (count: number) => {
totalUnreadCount.value = count;
const tabBarIndex = getTabBarIndex() ?? -1;
if (tabBarIndex >= 0) {
if (count > 0) {
uni.setTabBarBadge({
index: tabBarIndex,
text: count > 99 ? '99+' : count.toString(),
});
} else {
uni.removeTabBarBadge({
index: tabBarIndex,
});
// 方法3: 尝试调用 uni.getTabBar() 检测是否存在 tabbar
try {
const tabBar = uni.getTabBar && uni.getTabBar()
if (tabBar) {
return { list: tabBar }
}
} catch (e) {
return null
}
return null
} catch (error) {
return null
}
}
const getTabBarIndex = () => {
try {
const pages = getCurrentPages()
if (!pages || pages.length === 0) return
const currentPage = pages[pages.length - 1]
const currentRoute = currentPage.route
const isTUIConversationPage =
currentRoute &&
(currentRoute.includes(
'TUIKit/components/TUIConversation/index'
) ||
currentRoute.includes('TUIConversation'))
if (!isTUIConversationPage) {
return
}
const tabBarConfig = getTabBarConfig()
if (!tabBarConfig) {
return
}
let tabBarIndex = -1
if (tabBarConfig.list && Array.isArray(tabBarConfig.list)) {
tabBarIndex = tabBarConfig.list.findIndex((item: any) => {
const pagePath = item.pagePath || ''
return (
pagePath.includes('TUIConversation') ||
pagePath.includes('TUIKit/components/TUIConversation/index')
)
})
}
return tabBarIndex
} catch (error) {
return -1
}
}
TUIStore.watch(StoreName.CONV, {
totalUnreadCount: (count: number) => {
totalUnreadCount.value = count
const tabBarIndex = getTabBarIndex() ?? -1
if (tabBarIndex >= 0) {
if (count > 0) {
uni.setTabBarBadge({
index: tabBarIndex,
text: count > 99 ? '99+' : count.toString()
})
} else {
uni.removeTabBarBadge({
index: tabBarIndex
})
}
}
}
},
});
})
TUIStore.watch(StoreName.CUSTOM, {
isShowConversationHeader: (showStatus: boolean) => {
isShowConversationHeader.value = showStatus !== false;
},
});
TUIStore.watch(StoreName.CUSTOM, {
isShowConversationHeader: (showStatus: boolean) => {
isShowConversationHeader.value = showStatus !== false
}
})
const handleSwitchConversation = (conversationID: string) => {
TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index',
});
emits('handleSwitchConversation', conversationID);
};
const closeChildren = () => {
headerRef?.value?.closeChildren();
conversationListDomRef?.value?.closeChildren();
};
const handleClickConv = () => {
closeChildren();
};
onHide(closeChildren);
const handleTouchStart = (e: any) => {
touchX.value = e.changedTouches[0].clientX;
touchY.value = e.changedTouches[0].clientY;
};
const handleTouchEnd = (e: any) => {
const x = e.changedTouches[0].clientX;
const y = e.changedTouches[0].clientY;
let turn = '';
if (x - touchX.value > 50 && Math.abs(y - touchY.value) < 50) {
// Swipe right
turn = 'right';
} else if (x - touchX.value < -50 && Math.abs(y - touchY.value) < 50) {
// Swipe left
turn = 'left';
const handleSwitchConversation = (conversationID: string) => {
TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index'
})
emits('handleSwitchConversation', conversationID)
}
if (y - touchY.value > 50 && Math.abs(x - touchX.value) < 50) {
// Swipe down
turn = 'down';
} else if (y - touchY.value < -50 && Math.abs(x - touchX.value) < 50) {
// Swipe up
turn = 'up';
}
// Operate according to the direction
if (turn === 'down' || turn === 'up') {
closeChildren();
}
};
const getPassingRef = (ref) => {
ref.value = conversationListDomRef.value;
};
const closeChildren = () => {
headerRef?.value?.closeChildren()
conversationListDomRef?.value?.closeChildren()
}
const handleClickConv = () => {
closeChildren()
}
onHide(closeChildren)
const handleTouchStart = (e: any) => {
touchX.value = e.changedTouches[0].clientX
touchY.value = e.changedTouches[0].clientY
}
const handleTouchEnd = (e: any) => {
const x = e.changedTouches[0].clientX
const y = e.changedTouches[0].clientY
let turn = ''
if (x - touchX.value > 50 && Math.abs(y - touchY.value) < 50) {
// Swipe right
turn = 'right'
} else if (
x - touchX.value < -50 &&
Math.abs(y - touchY.value) < 50
) {
// Swipe left
turn = 'left'
}
if (y - touchY.value > 50 && Math.abs(x - touchX.value) < 50) {
// Swipe down
turn = 'down'
} else if (
y - touchY.value < -50 &&
Math.abs(x - touchX.value) < 50
) {
// Swipe up
turn = 'up'
}
// Operate according to the direction
if (turn === 'down' || turn === 'up') {
closeChildren()
}
}
const getPassingRef = ref => {
ref.value = conversationListDomRef.value
}
</script>
<style lang="scss" scoped src="./style/index.scss"></style>
<style lang="scss" scoped src="./style/index.scss">
uni-page-body,
html,
body,
page {
width: 100% !important;
height: 100% !important;
overflow: hidden;
}
</style>

View File

@@ -34,6 +34,15 @@ export const getUserData = () => {
})
}
/** 修改用户信息 */
export const updateUserData = data => {
return http({
url: '/api/user/edit',
method: 'put',
data
})
}
/** 获取用户地址列表 */
export const getUserAddress = (data, loading = true) => {
return http({
@@ -128,3 +137,4 @@ export const getTencentUserSig = () => {
method: 'get'
})
}

View File

@@ -50,3 +50,94 @@ export const getUserPayPwd = () => {
method: 'get'
})
}
/**
* 添加/修改支付密码
* @param {*} data
* @param {*} method post 添加 put 修改
* @returns
*/
export const updateUserPayPwd = (data, method = 'post') => {
return http({
url: '/api/service/userPassword',
method,
data
})
}
/**
* 添加/修改银行卡
* @param {*} data
* @param {*} method post 添加 put 修改
* @returns
*/
export const addUserPayPwd = (data, method = 'post') => {
return http({
url: '/api/service/userCard',
method,
data
})
}
/** 删除银行卡 */
export const deleteUserPayPwd = id => {
return http({
url: `/api/service/userCard/${id}`,
method: 'delete'
})
}
/** 获取银行卡列表 */
export const getUserBankList = () => {
return http({
url: '/api/service/userCard/list',
method: 'get'
})
}
/** 获取银行卡详情 */
export const getUserBankDetail = id => {
return http({
url: `/api/service/userCard/details/${id}`,
method: 'get'
})
}
/**
* 添加/修改用户第三方支付账户
* @param {*} data 提交数据
* @param {*} method post 添加 put 修改
* @returns
*/
export const addUserThirdPay = (data, method = 'post') => {
return http({
url: '/api/service/userPayment',
method,
data
})
}
/** 获取用户第三方支付详情 */
export const getUserThirdPay = id => {
return http({
url: `/api/service/userPayment/${id}`,
method: 'get'
})
}
/** 获取身份证信息 */
export const getUserIdCard = () => {
return http({
url: '/api/service/userVerification/details',
method: 'get'
})
}
/** 添加身份证 */
export const addUserIdCard = data => {
return http({
url: '/api/service/userVerification',
method: 'post',
data
})
}

View File

@@ -28,6 +28,12 @@
default: () => []
})
/** 图片回显列表 */
const fileLists = defineModel('list', {
type: Array,
default: () => []
})
/** 是否为身份证状态 */
const isIdCard = computed(() => props.isFront || props.isBack)
@@ -61,25 +67,31 @@
}
const onSelect = async e => {
e.tempFiles.forEach(v => {
uploadFile(v.path)
})
return
const upData = e.tempFiles.map(v => v.path)[0]
imageValue.value = await uploadSingleFile(upData, {
url: '/api/common/admin/upload/up/single'
})
if (props.limit === '1') {
const upData = e.tempFiles.map(v => v.path)[0]
imageValue.value = await uploadSingleFile(upData, {
url: '/api/common/admin/upload/up/single'
})
} else {
e.tempFiles.forEach(v => {
uploadFile(v.path)
})
}
}
const onDelete = e => {
imageValue.value.splice(e.index, 1)
if (props.limit === '1') {
imageValue.value = ''
} else {
imageValue.value.splice(e.index, 1)
}
}
</script>
<template>
<view :class="{ 'file_card-box': isIdCard }" class="cb-file-picker">
<uni-file-picker
v-model="fileLists"
:file-mediatype="props.type"
:image-styles="imageStyles"
:limit="props.limit"

63
package-lock.json generated
View File

@@ -9,7 +9,11 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@tencentcloud/chat-uikit-uniapp": "^3.2.0",
"@tencentcloud/chat-uikit-engine-lite": "1.0.3",
"@tencentcloud/chat-uikit-uniapp": "3.1.0",
"@tencentcloud/tui-core-lite": "1.0.0",
"@tencentcloud/universal-api": "^2.4.0",
"dayjs": "^1.11.10",
"unplugin-vue2-script-setup": "^0.11.4"
}
},
@@ -654,18 +658,18 @@
}
},
"node_modules/@tencentcloud/chat-uikit-engine-lite": {
"version": "1.0.4",
"resolved": "https://repo.huaweicloud.com/repository/npm/@tencentcloud/chat-uikit-engine-lite/-/chat-uikit-engine-lite-1.0.4.tgz",
"integrity": "sha512-YfDuXDkUlz7V1RA6iAEO2PLBI/efz60TRwAVy/Vhlu7oqZOMlphra72J30N3n5WhxbsFfcfTShoqW3OwcatUQg==",
"version": "1.0.3",
"resolved": "https://repo.huaweicloud.com/repository/npm/@tencentcloud/chat-uikit-engine-lite/-/chat-uikit-engine-lite-1.0.3.tgz",
"integrity": "sha512-Zh539tplI+BY1uljuXVoOCzR1k/UuBhX2bx9fZw0yRDKf3KGWWAjnkDzl3erpgYpNGXX6qPd/PhPhLWbEB5ouw==",
"license": "ISC",
"dependencies": {
"@tencentcloud/lite-chat": "^1.6.3"
"@tencentcloud/lite-chat": "^1.5.0"
}
},
"node_modules/@tencentcloud/chat-uikit-uniapp": {
"version": "3.2.0",
"resolved": "https://repo.huaweicloud.com/repository/npm/@tencentcloud/chat-uikit-uniapp/-/chat-uikit-uniapp-3.2.0.tgz",
"integrity": "sha512-5qUVRcsndwhWLAidgFMTk/JAhnkl2HrHux9jFJrk5iXZzXPdv6Wn9G2MA2bZcMQrWirYuTd7sAH/umcDYZsk3Q==",
"version": "3.1.0",
"resolved": "https://repo.huaweicloud.com/repository/npm/@tencentcloud/chat-uikit-uniapp/-/chat-uikit-uniapp-3.1.0.tgz",
"integrity": "sha512-cKQBaFGscSlwBK0fjK7feMY675c3K35uoUbntkiFZo2o8UwW2w5Cmp/76HbaQ0DY/wxG/fMTbfU9RQCfVe3SjQ==",
"license": "ISC",
"dependencies": {
"@tencentcloud/chat-uikit-engine-lite": "~1.0.3",
@@ -680,9 +684,9 @@
"unplugin-vue2-script-setup": "^0.11.3"
},
"peerDependencies": {
"@trtc/calls-uikit-vue": "~4.4.0",
"@trtc/calls-uikit-vue2.6": "~4.4.0",
"@trtc/calls-uikit-wx": "~4.2.0"
"@trtc/calls-uikit-vue": "4.2.2",
"@trtc/calls-uikit-vue2.6": "4.2.2",
"@trtc/calls-uikit-wx": "4.2.2"
}
},
"node_modules/@tencentcloud/lite-chat": {
@@ -735,13 +739,13 @@
"license": "ISC"
},
"node_modules/@trtc/call-engine-lite-js": {
"version": "3.5.0",
"resolved": "https://repo.huaweicloud.com/repository/npm/@trtc/call-engine-lite-js/-/call-engine-lite-js-3.5.0.tgz",
"integrity": "sha512-XSKDFBOcs6kITZnbLdECg889bbhTbdmHdg/EQRmGJx/2KakEe/uubfSDydOPBO47UgV2QZ9dGiRVAX5SCY2g6A==",
"version": "3.4.8",
"resolved": "https://repo.huaweicloud.com/repository/npm/@trtc/call-engine-lite-js/-/call-engine-lite-js-3.4.8.tgz",
"integrity": "sha512-VU+2VMhuNUfJg3F8TZ992d6iB33Rjc/xhbez0OkzYyejpaOtU1mExdU25On4xjeAx7XIZ6n80vfU5uEBsPusyw==",
"license": "ISC",
"peer": true,
"dependencies": {
"@tencentcloud/lite-chat": "^1.6.3",
"@tencentcloud/lite-chat": "^1.5.0",
"core-js": "^3.8.3",
"eventemitter3": "^4.0.7",
"rtc-detect": "^0.0.5",
@@ -762,41 +766,40 @@
}
},
"node_modules/@trtc/calls-uikit-vue": {
"version": "4.4.2",
"resolved": "https://repo.huaweicloud.com/repository/npm/@trtc/calls-uikit-vue/-/calls-uikit-vue-4.4.2.tgz",
"integrity": "sha512-a5oaNSO4qN0FSsDb65Kl9BEghDdWV0l5RdjMgaWEjrqNVVCuz7p5s7/6AYaB78dpdsIC9U9FXTVbM2+u79r4zw==",
"version": "4.2.2",
"resolved": "https://repo.huaweicloud.com/repository/npm/@trtc/calls-uikit-vue/-/calls-uikit-vue-4.2.2.tgz",
"integrity": "sha512-xAHBo2RXb/PDCcd07sdftgE2HtiW3QC3hXY8g5yWfTkJ3WIPfDi5OsG9lti0KgzF9ZnfMf4i08ptAoC3k72iHQ==",
"license": "ISC",
"peer": true,
"dependencies": {
"@tencentcloud/lite-chat": "^1.6.3",
"@tencentcloud/lite-chat": "^1.5.0",
"@tencentcloud/tui-core-lite": "1.0.0",
"@trtc/call-engine-lite-js": "~3.5.0"
"@trtc/call-engine-lite-js": "~3.4.0"
}
},
"node_modules/@trtc/calls-uikit-vue2.6": {
"version": "4.4.2",
"resolved": "https://repo.huaweicloud.com/repository/npm/@trtc/calls-uikit-vue2.6/-/calls-uikit-vue2.6-4.4.2.tgz",
"integrity": "sha512-KDHFXQVFMAPCpb0aRrOHAh3LPA4tLlr/LhTMDxyUJbJN6CGhYRIlbDyQ0BHl/z7yHYNkTwXaHkahpCJCmnaPNQ==",
"version": "4.2.2",
"resolved": "https://repo.huaweicloud.com/repository/npm/@trtc/calls-uikit-vue2.6/-/calls-uikit-vue2.6-4.2.2.tgz",
"integrity": "sha512-OjB3R4/vrQiK5uF3FG6UlmUOR+3CvbZ8YMwm6MI0KfXyg27EJOq315JdQKJgX5ZU7HzUstfrMtxYTbGfUJKAcA==",
"license": "ISC",
"peer": true,
"dependencies": {
"@tencentcloud/lite-chat": "^1.6.3",
"@tencentcloud/lite-chat": "^1.5.0",
"@tencentcloud/tui-core-lite": "1.0.0",
"@trtc/call-engine-lite-js": "~3.5.0",
"@trtc/call-engine-lite-wx": "~3.4.7",
"@trtc/call-engine-lite-js": "~3.4.0",
"@vue/composition-api": "^1.7.2"
}
},
"node_modules/@trtc/calls-uikit-wx": {
"version": "4.2.4",
"resolved": "https://repo.huaweicloud.com/repository/npm/@trtc/calls-uikit-wx/-/calls-uikit-wx-4.2.4.tgz",
"integrity": "sha512-PuqIfEqUrLfwCxip1EjOj2HAObzZBvzN3Gpx8ULvWdPu0hqVZIPobo3aTtfGKUCdJPmNq+/iLVQ3MwneXhtR5A==",
"version": "4.2.2",
"resolved": "https://repo.huaweicloud.com/repository/npm/@trtc/calls-uikit-wx/-/calls-uikit-wx-4.2.2.tgz",
"integrity": "sha512-pW0l3UhiekcTZlLDVPBnsvYyIhrjU0Mod9joL+KlO8gXnK/qeBauAyAz8AaipsveJV4FYUh6AZJkDNN+DzAgig==",
"license": "ISC",
"peer": true,
"dependencies": {
"@tencentcloud/trtc-component-wx": "^1.0.5",
"@tencentcloud/tui-core-lite": "1.0.0",
"@trtc/call-engine-lite-wx": "~3.4.7",
"@trtc/call-engine-lite-wx": "~3.4.3",
"aegis-mp-sdk": "latest"
}
},

View File

@@ -10,7 +10,11 @@
"author": "",
"license": "ISC",
"dependencies": {
"@tencentcloud/chat-uikit-uniapp": "^3.2.0",
"unplugin-vue2-script-setup": "^0.11.4"
"@tencentcloud/chat-uikit-engine-lite": "1.0.3",
"@tencentcloud/chat-uikit-uniapp": "3.1.0",
"@tencentcloud/tui-core-lite": "1.0.0",
"@tencentcloud/universal-api": "^2.4.0",
"unplugin-vue2-script-setup": "^0.11.4",
"dayjs": "^1.11.10"
}
}

View File

@@ -14,6 +14,54 @@
"navigationStyle": "custom"
}
},
{
"path": "TUIKit/components/TUIConversation/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "TUIKit/components/TUIChat/index",
"style": {
"navigationStyle": "custom",
"app-plus": {
"softinputMode": "adjustResize"
}
}
},
{
"path": "TUIKit/components/TUIContact/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "TUIKit/components/TUIChat/video-play",
"style": {
"navigationBarTitleText": "腾讯云 IM",
"navigationBarBackgroundColor": "#EBF0F6"
}
},
{
"path": "TUIKit/components/TUIChat/web-view",
"style": {
"navigationBarTitleText": "腾讯云 IM",
"navigationBarBackgroundColor": "#EBF0F6"
}
},
{
"path": "TUIKit/components/TUIGroup/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "TUIKit/components/TUISearch/index",
"style": {
"navigationBarTitleText": "聊天记录",
"navigationBarBackgroundColor": "#EBF0F6"
}
},
{
"path": "pages/login/protocol",
"style": {
@@ -76,8 +124,8 @@
{
"path": "pages/my-index/personal-center/index",
"style": {
"navigationBarTitleText": "个人中心",
"navigationBarBackgroundColor": "#ffffff"
"navigationStyle": "custom",
"navigationBarTitleText": "个人中心"
}
},
{
@@ -262,13 +310,13 @@
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/news-list/news-list",
"pagePath": "TUIKit/components/TUIConversation/index",
"iconPath": "static/images/tabBar/news.png",
"selectedIconPath": "static/images/tabBar/newsHL.png",
"text": "消息"
},
{
"pagePath": "pages/contacts/contacts",
"pagePath": "TUIKit/components/TUIContact/index",
"iconPath": "static/images/tabBar/contacts.png",
"selectedIconPath": "static/images/tabBar/contactsHL.png",
"text": "通讯录"
@@ -287,4 +335,4 @@
}
]
}
}
}

View File

@@ -94,7 +94,7 @@
<view class="swipe-box">
<view
v-if="item.defaultAddress == 0"
class="btn-box"
class="public-uni-swipe-action-right"
@click="onDefault(item)"
>
<uni-icons
@@ -104,7 +104,10 @@
></uni-icons>
<text class="iocn-name">设为默认</text>
</view>
<view class="btn-box" @click="onDelete(item)">
<view
class="public-uni-swipe-action-right"
@click="onDelete(item)"
>
<uni-icons
type="trash"
size="18"
@@ -121,6 +124,8 @@
</template>
<style lang="scss" scoped>
@import '@/styles/global.scss';
.top-right-name {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
@@ -181,28 +186,6 @@
}
.swipe-box {
display: flex;
.btn-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgb(47, 194, 17);
width: 130rpx;
.iocn-name {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: #fff;
text-align: left;
font-style: normal;
text-transform: none;
}
// 最后一个
&:last-child {
background: rgb(206, 59, 22);
}
}
}
}
</style>

View File

@@ -28,7 +28,7 @@
})
setToken(res.token)
await fetchUserInfo()
reLaunch('/pages/news-list/news-list')
reLaunch('/TUIKit/components/TUIConversation/index')
}
const onRegister = () => {

View File

@@ -53,6 +53,9 @@
:placeholder-style="placeholderStyle"
:placeholder="props.placeholder"
/>
<view v-else class="right-box">
<slot name="right"></slot>
</view>
</view>
<view v-if="hasDefaultSlot" class="bottom-slot">
<slot></slot>
@@ -74,6 +77,10 @@
width: 100%;
display: flex;
justify-content: space-between;
.right-box {
display: flex;
align-items: center;
}
}
.bottom-slot {
margin-top: 26rpx;

View File

@@ -0,0 +1,81 @@
<script setup>
import { ref, watch } from 'vue'
const show = defineModel({
type: Boolean,
default: false
})
const name = defineModel('name', {
type: [String, Number],
default: ''
})
const props = defineProps({
title: {
type: String,
default: ''
},
/** 是否为性别选项 */
isSex: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['confirm'])
const inputDialog = ref(null)
watch(
() => show.value,
v => {
if (v) {
// 'bottom'
inputDialog.value.open()
}
}
)
const close = () => {
show.value = false
}
const dialogInputConfirm = () => {
close()
emits('confirm')
}
</script>
<template>
<uni-popup ref="inputDialog" type="dialog">
<uni-popup-dialog
v-model="name"
mode="input"
:title="props.title"
:placeholder="`请输入${props.title}`"
@close="close"
@confirm="dialogInputConfirm"
>
<uni-data-checkbox
v-if="props.isSex"
v-model="name"
:localdata="[
{
text: '男',
value: '0'
},
{
text: '女',
value: '1'
},
{
text: '未知',
value: '2'
}
]"
></uni-data-checkbox>
</uni-popup-dialog>
</uni-popup>
</template>
<style lang="scss" scoped></style>

View File

@@ -52,10 +52,12 @@
>
<view class="left-box">
<image
src="https://wx1.sinaimg.cn/mw690/92eeb099gy1i29hl0ne80j21jk2bcash.jpg"
mode="scaleToFill"
v-if="userInfo?.avatar"
:src="userInfo?.avatar"
mode="aspectFill"
class="avatar"
></image>
<uni-icons v-else type="contact-filled" size="70"></uni-icons>
<view class="nickname">
<text class="name">{{ userInfo?.userName || '' }}</text>
<text class="name">ID:{{ userInfo?.userId || '' }}</text>

View File

@@ -1,27 +1,84 @@
<script setup>
import { reactive } from 'vue'
import { useAuthUser } from '@/composables/useAuthUser'
import PopupBox from '../components/popup-box.vue'
import { chooseImage } from '@/utils/media.js'
import { uploadSingleFile } from '@/utils/uploadFile'
import { useUserStore } from '@/stores/user'
const { updateUserInfo } = useUserStore()
const itemList = [
{ title: '我的二维码', key: '1', value: '' },
{ title: 'ID', key: '2', value: 'userId' },
{ title: '昵称', key: '3', value: 'userName' },
{ title: '性别', key: '4', value: '' },
{ title: '性别', key: '4', value: 'sex' },
{ title: '手机号码', key: '5', value: 'mobile' },
{ title: '个性签名', key: '6', value: '' }
{ title: '个性签名', key: '6', value: 'perSignature' }
]
/** 可修改的 key */
const MODIFY_KEY = ['3', '4', '6']
const { userInfo } = useAuthUser()
const popupData = reactive({
show: false,
title: '修改信息',
name: '',
key: '',
value: ''
})
const formData = reactive({
avatar: '',
userName: '',
sex: '',
perSignature: ''
})
const upInfo = (key, value) => {
if (MODIFY_KEY.includes(key)) {
const titleData = {
3: '昵称',
4: '性别',
6: '个性签名'
}[key]
popupData.value = value
popupData.key = key
popupData.title = titleData
popupData.name = userInfo.value[value]
popupData.show = true
}
}
const editAvatar = async () => {
const paths = await chooseImage({ count: 1 })
const url = await uploadSingleFile(paths[0], {
url: '/api/common/admin/upload/up/single'
})
formData.avatar = url
updateUserInfo({ avatar: url })
}
const onConfirm = () => {
if (MODIFY_KEY.includes(popupData.key)) {
if (popupData.name === userInfo.value[popupData.value]) return
if (popupData.name === '') return
updateUserInfo({ [popupData.value]: popupData.name })
}
}
</script>
<template>
<nav-bar isTopBg isPlaceholder title="个人中心"></nav-bar>
<view class="personal-center">
<view class="public-card">
<view class="public-card" @click="editAvatar">
<view class="left-img">
<image
src="https://p4.itc.cn/images01/20220619/46660ed163164c14be90e605a73ee5e8.jpeg"
v-if="userInfo.avatar"
:src="userInfo.avatar"
mode="aspectFill"
class="avatar"
></image>
<uni-icons v-else type="contact-filled" size="60"></uni-icons>
</view>
<view class="right-box">
<text class="value">换头像</text>
@@ -33,20 +90,37 @@
v-for="(item, index) in itemList"
:key="index"
class="public-card"
@click="upInfo(item.key, item.value)"
>
<view class="left-box">
<text>{{ item.title }}</text>
<text v-if="item.key === '6'" class="text">
这个人很懒什么也没有
{{ userInfo[item.value] || '这个人很懒什么也没有' }}
</text>
</view>
<view class="right-box">
<text v-if="!['1', '6'].includes(item.key)" class="value">
{{ item.value ? userInfo[item.value] : '' }}
</text>
<view v-if="!['1', '6'].includes(item.key)" class="value">
<text v-if="item.key === '4'">
{{
userInfo[item.value] === '2'
? '未设置'
: userInfo[item.value] === '0'
? ''
: ''
}}
</text>
<text v-else>{{ item.value ? userInfo[item.value] : '' }}</text>
</view>
<uni-icons type="right" size="16" color="#999999"></uni-icons>
</view>
</view>
<popup-box
v-model="popupData.show"
v-model:name="popupData.name"
:isSex="popupData.key === '4'"
:title="popupData.title"
@confirm="onConfirm"
/>
</view>
</template>

View File

@@ -59,7 +59,7 @@ page {
}
}
.right-box {
align-items: flex-end;
align-items: center;
.value {
font-weight: 600;
font-size: 28rpx;

View File

@@ -1,14 +1,30 @@
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { reactive } from 'vue'
import { useUI } from '@/utils/use-ui'
import { navigateBack } from '@/utils/router'
import {
addUserThirdPay,
addUserPayPwd,
getUserThirdPay,
getUserBankDetail
} from '@/api/my-index'
import CardInput from '../../components/card-input.vue'
/** 是否第三方进入 */
const IS_THIRD_PAY = ['101', '102']
const { showToast } = useUI()
const stateData = reactive({
title: '',
state: '0'
state: '0',
loading: true
})
const formData = reactive({
// 修改id
id: '',
// 银行卡名称
name: '',
// 开户行名称
@@ -18,34 +34,131 @@
// 微信/支付宝号
codeName: '',
// 图片链接
img: ''
img: '',
// 银行卡类型
cardType: '',
// 图片回显列表
imgList: []
})
const onAdd = () => {
console.log(formData)
const data = {
bankName: formData.name,
cardNumber: formData.cardNum,
bankName: formData.khName
const onAdd = async () => {
let data = {}
const name = formData.id ? '修改' : '添加'
const method = formData.id ? 'put' : 'post'
if (IS_THIRD_PAY.includes(stateData.state)) {
if (!formData.codeName) {
showToast('请输入账号')
return
}
if (!formData.img) {
showToast('请上传收款码')
return
}
data = {
paymentType: stateData.state === '101' ? 2 : 1,
qrCodeUrl: formData.img,
accountNumber: formData.codeName,
paymentId: formData.id
}
await addUserThirdPay(data, method)
} else {
if (!formData.name) {
showToast('请输入银行名称')
return
}
if (!formData.khName) {
showToast('请输入持卡人姓名')
return
}
if (!formData.cardNum) {
showToast('请输入银行卡号')
return
}
if (!formData.cardType) {
showToast('请选择银行卡类型')
return
}
data = {
bankName: formData.name,
cardHolder: formData.khName,
cardNumber: formData.cardNum,
cardType: formData.cardType,
cardId: formData.id
}
await addUserPayPwd(data, method)
}
await showToast(`${name}成功`, 'success')
navigateBack()
}
/** 获取详情 */
const getDetail = async () => {
stateData.loading = true
const res = await getUserThirdPay(
stateData.state === '101' ? '2' : '1'
)
let titltData = ''
if (res?.data) {
const { paymentId, accountNumber, qrCodeUrl } = res.data
formData.codeName = accountNumber
formData.img = qrCodeUrl
formData.id = paymentId
formData.imgList = [
{
url: qrCodeUrl
}
]
titltData = {
101: '修改支付宝账户',
102: '修改微信账户'
}[stateData.state]
} else {
titltData = {
101: '添加支付宝账户',
102: '添加微信账户'
}[stateData.state]
}
stateData.title = titltData
stateData.loading = false
}
/** 获取对应银行卡详情 */
const getBankDetail = async () => {
stateData.loading = true
const res = await getUserBankDetail(stateData.state)
const { cardId, bankName, cardHolder, cardNumber, cardType } =
res.data
formData.id = cardId
formData.name = bankName
formData.khName = cardHolder
formData.cardNum = cardNumber
formData.cardType = cardType
stateData.title = '修改银行卡'
stateData.loading = false
}
onLoad(e => {
const titltData = {
0: '添加银行卡',
101: '添加支付宝账户',
102: '添加微信账户'
}[e.key]
stateData.title = titltData
stateData.state = e.key
if (IS_THIRD_PAY.includes(e.key)) {
getDetail()
} else {
if (e.key === '0') {
stateData.title = '添加银行卡'
stateData.loading = false
} else {
getBankDetail()
}
}
})
</script>
<template>
<view>
<view v-if="!stateData.loading">
<nav-bar isTopBg isPlaceholder :title="stateData.title">
<template #right>
<text class="public-navbar__right-btn" @click="onAdd">添加</text>
<text class="public-navbar__right-btn" @click="onAdd">
{{ formData.id ? '修改' : '添加' }}
</text>
</template>
</nav-bar>
@@ -54,8 +167,26 @@
class="card-details"
>
<CardInput v-model="formData.name" title="银行名称"></CardInput>
<CardInput v-model="formData.khName" title="开户行"></CardInput>
<CardInput v-model="formData.khName" title="持卡人姓名"></CardInput>
<CardInput v-model="formData.cardNum" title="银行卡号"></CardInput>
<CardInput :isInput="false" title="银行卡类型">
<template #right>
<uni-data-checkbox
v-model="formData.cardType"
:localdata="[
{
text: '借记卡',
value: 1
},
{
text: '信用卡',
value: 2
}
]"
class="card-checkbox"
></uni-data-checkbox>
</template>
</CardInput>
</view>
<view v-else class="card-details">
@@ -65,8 +196,16 @@
stateData.state === '101' ? '支付宝账号' : '微信账号'
}`"
></CardInput>
<CardInput :is-input="false" title="收款码">
<cb-file-picker v-model="formData.img"></cb-file-picker>
<CardInput
:is-input="false"
:title="`${
stateData.state === '101' ? '支付宝' : '微信'
}收款码(必传)`"
>
<cb-file-picker
v-model="formData.img"
v-model:list="formData.imgList"
></cb-file-picker>
</CardInput>
</view>
</view>
@@ -77,4 +216,10 @@
.card-details {
padding: 32rpx 24rpx;
}
.card-checkbox {
:deep(.uni-label-pointer) {
margin: 0 !important;
margin-left: 60rpx !important;
}
}
</style>

View File

@@ -1,56 +1,107 @@
<script setup>
import { ref } from 'vue'
import { navigateTo } from '@/utils/router'
import { onShow } from '@dcloudio/uni-app'
import { getUserBankList, deleteUserPayPwd } from '@/api/my-index'
import { useUI } from '@/utils/use-ui'
const itemList = ref([
{
title: '支付宝',
key: '101',
icon: '/static/images/my-index/zfb.png'
},
{ title: '微信', key: '102', icon: '/static/images/my-index/wx.png' }
])
/** 是否第三方进入 */
const IS_THIRD_PAY = ['0', '101', '102']
const itemList = ref([])
const { showToast, showDialog } = useUI()
const onAddCard = key => {
navigateTo('/pages/my-index/wallet/bank-card/card-details', { key })
}
const getList = async type => {
itemList.value = []
const res = await getUserBankList()
itemList.value = [
{
bankName: '支付宝',
key: '101',
icon: '/static/images/my-index/zfb.png'
},
{
bankName: '微信',
key: '102',
icon: '/static/images/my-index/wx.png'
},
...res.data
]
if (type === 1) {
showToast('删除成功', 'success')
}
}
const onDelete = async id => {
const res = await showDialog('提示', '确定要删除吗?')
if (!res) return
await deleteUserPayPwd(id)
await getList(1)
}
onShow(() => {
getList()
})
</script>
<template>
<view class="bank-card">
<view
v-for="(item, index) in itemList"
:key="index"
class="public-card"
@click="onAddCard(item.key)"
>
<view class="left-img">
<image
:src="
item.icon
? item.icon
: 'https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2024%2F1227%2F842e0e65j00sp543n001fd000i700iim.jpg&thumbnail=660x2147483647&quality=80&type=jpg'
"
mode="aspectFill"
class="card"
></image>
<text>{{ item.title }}</text>
</view>
<view class="right-box">
<uni-icons type="right" size="16" color="#999999"></uni-icons>
</view>
</view>
<uni-swipe-action>
<uni-swipe-action-item
v-for="(item, index) in itemList"
:key="index"
:disabled="!!item.key"
class="card-box"
>
<view
class="public-card"
@click="onAddCard(item.key || item.cardId)"
>
<view class="left-img">
<image
v-if="item.icon"
:src="item.icon"
mode="aspectFill"
class="card"
></image>
<text>{{ item.bankName }}</text>
</view>
<view class="right-box">
<text v-if="item.cardNumber">{{ item.cardNumber }}</text>
<uni-icons type="right" size="16" color="#999999"></uni-icons>
</view>
</view>
<template v-slot:right>
<view
class="public-uni-swipe-action-right"
@click="onDelete(item.cardId)"
>
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
<text class="iocn-name">删除</text>
</view>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
<!-- 底部按钮 -->
<bottom-view>
<bottom-view v-if="itemList.length > 0">
<cb-button @click="onAddCard('0')">+添加银行卡</cb-button>
</bottom-view>
</view>
</template>
<style lang="scss" scoped>
@import '@/styles/global.scss';
@import '../../styles/index.scss';
.bank-card {
padding: 38rpx 24rpx;
}
.card-box + .card-box {
margin-top: 16rpx;
}
</style>

View File

@@ -1,12 +1,17 @@
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { reactive } from 'vue'
import CardInput from '../components/card-input.vue'
import { useUI } from '@/utils/use-ui'
import { navigateBack } from '@/utils/router'
import { validateTransactionPassword } from '@/utils/validate'
import { updateUserPayPwd, getUserPayPwd } from '@/api/my-index'
const { showToast } = useUI()
const formData = reactive({
id: '',
state: '0',
// 旧密码
password: '',
// 新密码
@@ -14,41 +19,72 @@
// 确认密码
confirmPassword: ''
})
const onEdit = () => {
const passwordValue = validateTransactionPassword(
formData.password,
'旧密码'
)
if (!passwordValue.valid) {
showToast(passwordValue.message)
return
const onEdit = async () => {
let data = {}
if (formData.state === '1') {
const passwordValue = validateTransactionPassword(
formData.password,
'旧密码'
)
if (!passwordValue.valid) {
showToast(passwordValue.message)
return
}
const newPasswordValue = validateTransactionPassword(
formData.newPassword,
'新的交易密码'
)
if (!newPasswordValue.valid) {
showToast(newPasswordValue.message)
return
}
const confirmPasswordValue = validateTransactionPassword(
formData.confirmPassword,
'确认交易密码'
)
if (!confirmPasswordValue.valid) {
showToast(confirmPasswordValue.message)
return
}
if (formData.newPassword !== formData.confirmPassword) {
showToast('两次密码不一致')
return
}
data = {
id: formData.id,
oldPassword: formData.password,
password: formData.confirmPassword
}
} else {
if (!formData.password) {
showToast('请输入设置交易密码')
return
}
data = {
password: formData.password
}
}
const newPasswordValue = validateTransactionPassword(
formData.newPassword,
'新的交易密码'
)
if (!newPasswordValue.valid) {
showToast(newPasswordValue.message)
return
}
const confirmPasswordValue = validateTransactionPassword(
formData.confirmPassword,
'确认交易密码'
)
if (!confirmPasswordValue.valid) {
showToast(confirmPasswordValue.message)
return
}
if (formData.newPassword !== formData.confirmPassword) {
showToast('两次密码不一致')
return
}
console.log('修改密码:', formData)
await updateUserPayPwd(data, formData.id ? 'put' : 'post')
await showToast(`${formData.id ? '修改' : '添加'}成功`, 'success')
navigateBack()
}
const getData = async () => {
const res = await getUserPayPwd()
formData.id = res.data.id
}
onLoad(e => {
formData.state = e.type
if (e.type === '1') {
getData()
}
})
</script>
<template>
@@ -59,7 +95,7 @@
</template>
</nav-bar>
<view class="input-box">
<view v-if="formData.state === '1'" class="input-box">
<CardInput
v-model="formData.password"
title="旧密码"
@@ -76,6 +112,13 @@
type="password"
></CardInput>
</view>
<view v-else class="input-box">
<CardInput
v-model="formData.password"
title="设置交易密码"
type="password"
></CardInput>
</view>
</view>
</template>

View File

@@ -1,26 +1,38 @@
<script setup>
import { ref } from 'vue'
import { navigateTo } from '@/utils/router'
import { onShow } from '@dcloudio/uni-app'
import { getUserPayPwd } from '@/api/my-index'
import { useAuthUser } from '@/composables/useAuthUser'
const itemList = [
{
title: '提现卡',
key: '1',
url: '/pages/my-index/wallet/bank-card/index'
},
{ title: '交易记录', key: '2', url: '' },
{
title: '修改支付密码',
key: '3',
url: '/pages/my-index/wallet/edit-password'
},
{ title: '实名认证', key: '4', url: '/pages/my-index/wallet/real-id' }
]
const { userInfo } = useAuthUser()
const itemList = ref([])
const getData = async () => {
itemList.value = []
const res = await getUserPayPwd()
console.log(res, '===')
const isSetPayPwd = res?.data ? '修改支付密码' : '设置支付密码'
itemList.value = [
{
title: '提现卡',
key: '1',
url: '/pages/my-index/wallet/bank-card/index'
},
{ title: '交易记录', key: '2', url: '' },
{
title: isSetPayPwd,
key: '3',
isType: !!res?.data,
url: '/pages/my-index/wallet/edit-password'
},
{
title: '实名认证',
key: '4',
url: '/pages/my-index/wallet/real-id'
}
]
}
onShow(() => {
@@ -34,7 +46,7 @@
<view class="top-card">
<view class="left-box">
<text>我的资产</text>
<text>1222</text>
<text>{{ userInfo?.totalPoints }}</text>
</view>
<view class="right-box">
<button>充值</button>
@@ -46,7 +58,13 @@
v-for="(item, index) in itemList"
:key="index"
class="public-card"
@click="item.url && navigateTo(item.url)"
@click="
item.url &&
navigateTo(
item.url,
item.key === '3' ? { type: item.isType ? 1 : 0 } : null
)
"
>
<view class="left-box">
<text>{{ item.title }}</text>

View File

@@ -1,44 +1,121 @@
<script setup>
import { reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref, reactive } from 'vue'
import CardInput from '../components/card-input.vue'
import { useUI } from '@/utils/use-ui'
import { navigateBack } from '@/utils/router'
import { getUserIdCard, addUserIdCard } from '@/api/my-index'
import { validateIdCard } from '@/utils/validate'
const { showToast } = useUI()
const loading = ref(true)
const formData = reactive({
// 修改id
id: '',
// 正面
front: '',
// 背面
back: '',
// 手机号
phone: '',
// 姓名
realName: '',
// 身份证号码
idCard: ''
idCard: '',
frontList: [],
backList: []
})
const getData = async () => {
loading.value = true
const res = await getUserIdCard()
if (res?.data) {
formData.id = res.data.id
formData.front = res.data.idCardFrontUrl
formData.back = res.data.idCardBackUrl
formData.realName = res.data.realName
formData.idCard = res.data.idCardNumber
formData.frontList = [{ url: res.data.idCardFrontUrl }]
formData.backList = [{ url: res.data.idCardBackUrl }]
}
loading.value = false
}
const onAddCode = async () => {
if (!formData.front) {
showToast('请上传身份证人像面')
return
}
if (!formData.back) {
showToast('请上传身份证国徽面')
return
}
if (!formData.realName) {
showToast('请输入姓名')
return
}
const codeData = validateIdCard(formData.idCard)
if (!codeData.valid) {
showToast(codeData.message)
return
}
const data = {
idCardFrontUrl: formData.front,
idCardBackUrl: formData.back,
realName: formData.realName,
idCardNumber: formData.idCard
}
await addUserIdCard(data)
await showToast(`添加成功`, 'success')
navigateBack()
}
onLoad(() => {
getData()
})
</script>
<template>
<view class="real-id">
<view v-if="!loading" class="real-id">
<!-- 说明 -->
<text class="top-text">*为保证您的账户安全,请先完成实名认证</text>
<CardInput :is-input="false" title="收款码">
<view class="qrcode-box">
<cb-file-picker v-model="formData.front" isFront></cb-file-picker>
<cb-file-picker v-model="formData.back" isBack></cb-file-picker>
<cb-file-picker
v-model="formData.front"
v-model:list="formData.frontList"
isFront
></cb-file-picker>
<cb-file-picker
v-model="formData.back"
v-model:list="formData.backList"
isBack
></cb-file-picker>
</view>
</CardInput>
<CardInput
v-model="formData.phone"
title="手机号"
type="tel"
placeholder="请输入手机号"
v-model="formData.realName"
title="姓名"
placeholder="请输入姓名"
></CardInput>
<CardInput
v-model="formData.idCard"
type="text"
title="身份证号"
placeholder="请输入身份证号"
></CardInput>
<!-- 底部按钮 -->
<bottom-view>
<cb-button @click="onAddCode">
确认{{ formData.id ? '修改' : '添加' }}
</cb-button>
</bottom-view>
</view>
</template>

View File

@@ -2,9 +2,6 @@
import { onLoad } from '@dcloudio/uni-app'
import { useAuthUser } from '@/composables/useAuthUser'
import { TUIConversation } from '@/TUIKit'
const { tencentUserSig } = useAuthUser()
onLoad(() => {
console.log(tencentUserSig.value, '===222===')
@@ -17,8 +14,7 @@
<template #right>右侧按钮</template>
</nav-bar>
<cb-search placeholder="搜索"></cb-search> -->
<TUIConversation></TUIConversation>
222
</view>
<!-- -->
</template>

View File

@@ -9,7 +9,7 @@ import {
removeSig
} from '@/utils/storage'
import { useTokenStore } from './token'
import { getUserData, userLogout } from '@/api'
import { getUserData, userLogout, updateUserData } from '@/api'
import { ref } from 'vue'
import { useUI } from '@/utils/use-ui'
import { reLaunch } from '@/utils/router'
@@ -62,6 +62,7 @@ export const useUserStore = defineStore('user', () => {
* 登录腾讯 IM
*/
const loginTencentIM = async () => {
console.log(11111111111111111111111)
await TUILogin.login({
SDKAppID: tencentUserSig.value.sdkappID,
userID: tencentUserSig.value.userId,
@@ -90,16 +91,17 @@ export const useUserStore = defineStore('user', () => {
/** 刷新用户信息(如用户信息被修改) */
const refreshUserInfo = async () => {
const res = await getUserData()
await setUserInfo(res.data)
await setUserInfoData(res.data)
userInfo.value = res.data
}
/**
* 更新部分用户信息(例如昵称、头像)
*/
const updateUserInfo = partialData => {
const updateUserInfo = async partialData => {
if (!userInfo.value) return
userInfo.value = { ...userInfo.value, ...partialData }
setUserInfoData(userInfo.value)
await updateUserData(partialData)
await refreshUserInfo()
}
return {
@@ -107,6 +109,7 @@ export const useUserStore = defineStore('user', () => {
tencentUserSig,
refreshUserInfo,
fetchUserInfo,
loginTencentIM,
setUserInfo,
clearUserInfo,
updateUserInfo

View File

@@ -15,3 +15,25 @@ page {
padding: 12rpx 36rpx;
border-radius: 8rpx;
}
/** uni-swipe-action 滑动右边样式 */
.public-uni-swipe-action-right {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgb(47, 194, 17);
width: 130rpx;
.iocn-name {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: #fff;
text-align: left;
font-style: normal;
text-transform: none;
}
&:last-child {
background: rgb(206, 59, 22);
}
}

View File

@@ -0,0 +1,29 @@
## 1.2.02025-03-31
- 适配出海手机支持 FCM 推送。
## 1.1.02024-12-11
- 大幅减小插件包体积,优化产品体验。
- 兼容 HBuilderX 4.36 的 Breaking changes。如果您需要 vivo/荣耀 的厂商推送,请参考 [文档](https://cloud.tencent.com/document/product/269/103522),正确配置 `manifestPlaceholders.json``mcn-services.json`
## 1.0.02024-11-29
- 优化和 [TencentCloud-TUICallKit 插件](https://ext.dcloud.net.cn/plugin?id=9035) 融合时的产品体验。
- 新增点击通知栏事件 NOTIFICATION_CLICKED支持获取推送扩展信息。
- 在线通道支持自定义铃音功能。
## 0.5.12024-11-07
- 优化和 [@tencentcloud/chat-uikit-uniapp](https://cloud.tencent.com/document/product/269/64507) 融合时的产品体验。
- 优化和 [TencentCloud-TUICallKit 插件](https://ext.dcloud.net.cn/plugin?id=9035) 融合时的产品体验。
- 新增接口 disablePostNotificationInForeground此接口可实现应用在前台时开/关通知栏通知(默认开)。
- 新增接口 createNotificationChannel支持 FCM/OPPO 自定义铃音。
## 0.4.02024-10-17
- 支持与 [TencentCloud-TUICallKit 插件](https://ext.dcloud.net.cn/plugin?id=9035) 融合打包。
## 0.3.02024-10-12
- 新增接口 addPushListener/removePushListener支持获取在线推送消息支持推送消息撤回通知。
## 0.2.02024-09-18
- 支持 FCM
- 支持 hihonor
## 0.1.02024-09-10
- 使用 uts 开发基于腾讯云推送服务Push支持 iOS 和 Android 推送,同时适配各大厂商推送。

View File

View File

@@ -0,0 +1,90 @@
{
"name": "@tencentcloud/uni-app-push",
"id": "TencentCloud-Push",
"main": "index.js",
"displayName": "【官方】uni-app 腾讯云推送服务Push",
"version": "1.2.0",
"description": "使用 uts 开发基于腾讯云推送服务Push支持 iOS 和 Android 推送,同时适配各大厂商推送。",
"license": "ISC",
"keywords": [
"腾讯云",
"Push",
"推送",
"Android/iOS",
"谷歌FCM"
],
"repository": "",
"engines": {
"HBuilderX": "^3.6.8"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "腾讯云即时通信IM隐私保护指引: https://web.sdk.qcloud.com/document/Tencent-IM-Privacy-Protection-Guidelines.html\n移动推送隐私保护指引: https://privacy.qq.com/document/preview/8565a4a2d26e480187ed86b0cc81d727",
"permissions": "本地存储空间"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-android": "y",
"app-ios": "y",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@@ -0,0 +1,299 @@
# TencentCloud-Push
## 简介
使用 uts 开发基于腾讯云推送服务Push支持 iOS 和 Android 推送,同时适配各大厂商推送。
腾讯云推送服务Push提供一站式 App 推送解决方案,助您轻松提升用户留存和互动活跃度,支持与腾讯云即时通信 IM SDK、实时音视频 TRTC SDK、音视频通话 SDK、直播 SDK等音视频终端产品协同集成在不同场景联合使用提升业务整体功能体验。
<img src="https://qcloudimg.tencent-cloud.cn/image/document/60d714484e54b284cfa440adcc885349.png" width="618" height="456">
<img src="https://qcloudimg.tencent-cloud.cn/image/document/864c391ecf6f2724d26e368e4f09e466.png" width="618" height="444">
<img src="https://qcloudimg.tencent-cloud.cn/image/document/6af60f4b20dd46323e8f901a161a80a9.png" width="618" height="454">
#### 数据可视化,辅助运营策略
<img src="https://qcloudimg.tencent-cloud.cn/image/document/6c422f64900053c38a6bf66fe1103b3f.png" width="618" height="334">
#### 支持推送消息全链路问题排查
<img src="https://qcloudimg.tencent-cloud.cn/image/document/156d43ed48971f9bf865ad0c4e2342e3.png" width="618" height="443">
#### 六地服务部署,严守数据安全
提供了中国、东南亚(新加坡、印尼雅加达)、东北亚(韩国首尔)、欧洲(德国法兰克福)以及北美(美国硅谷)数据存储中心供选择,每个数据中心均支持全球接入。如果您的应用在境外上线且用户主要在境外,您可以根据消息传输需求及合规要求,选择适合您业务的境外数据中心,保障您的数据安全。
<img src="https://qcloudimg.tencent-cloud.cn/image/document/2ffc1a103a42d9c01cfb819cd92bbd1d.png" widht="618" height="308">
## 快速跑通
### 步骤1创建应用
进入 [控制台](https://console.cloud.tencent.com/im) ,单击创建应用,填写应用名称,选择数据中心,单击确定,完成应用创建。
![](https://qcloudimg.tencent-cloud.cn/image/document/e2761226f7d2bbdfb0a301192316c7d3.png)
### 步骤2开通推送服务 Push
进入 [推送服务 Push](https://console.cloud.tencent.com/im/push-plugin-push-identifier),单击立即购买或免费试用 。每个应用可免费试用一次有效期7天
![](https://qcloudimg.tencent-cloud.cn/image/document/a7e1f3847c91a807ec9be3a586f1290f.png)
### 步骤3下载腾讯云推送服务Push并复制 Push SDK 到您的项目中
1. 下载腾讯云推送服务Push
```
npm install @tencentcloud/uni-app-push
```
2. 复制 Push SDK 到您的项目中。
【macOS 端】
``` bash
mkdir -p ./uni_modules/TencentCloud-Push && rsync -av ./node_modules/@tencentcloud/uni-app-push/ ./uni_modules/TencentCloud-Push
```
【Window 端】
``` bash
xcopy .\node_modules\@tencentcloud\uni-app-push .\uni_modules\TencentCloud-Push /i /e
```
### 步骤4在 App.vue 中引入并注册腾讯云推送服务Push
将 SDKAppID 和 appKey 替换为您在IM 控制台 - 推送服务 Push - 接入设置页面 获取的应用的信息。如图所示:
![](https://sdk-web-1252463788.cos.ap-hongkong.myqcloud.com/im/assets/push/push.png)
```ts
// 集成 TencentCloud-Push
import * as Push from '@/uni_modules/TencentCloud-Push';
const SDKAppID = 0; // 您的 SDKAppID
const appKey = ''; // 客户端密钥
Push.registerPush(SDKAppID, appKey, (data) => {
console.log('registerPush ok', data);
Push.getRegistrationID((registrationID) => {
console.log('getRegistrationID ok', registrationID);
});
}, (errCode, errMsg) => {
console.error('registerPush failed', errCode, errMsg);
}
);
// 监听通知栏点击事件,获取推送扩展信息
Push.addPushListener(Push.EVENT.NOTIFICATION_CLICKED, (res) => {
// res 为推送扩展信息
console.log('notification clicked', res);
});
// 监听在线推送
Push.addPushListener(Push.EVENT.MESSAGE_RECEIVED, (res) => {
// res 为消息内容
console.log('message received', res);
});
// 监听在线推送被撤回
Push.addPushListener(Push.EVENT.MESSAGE_REVOKED, (res) => {
// res 为被撤回的消息 ID
console.log('message revoked', res);
});
```
### <span id="step5">步骤5测试推送测试前请务必打开手机通知权限允许应用通知。</span>
单击 HBuilderX 的 【运行 > 运行到手机或模拟器 > 制作自定义调试基座】,使用云端证书制作 Android 或 iOS 自定义调试基座。
![](https://qcloudimg.tencent-cloud.cn/image/document/742b7c05364e8ff9a16d5d5601aa038b.png)
自定义调试基座打好后,安装到手机运行。
[登录](https://console.cloud.tencent.com/im/push-plugin-push-check) 控制台,使用测试工具进行在线推送测试。
![](https://sdk-web-1252463788.cos.ap-hongkong.myqcloud.com/im/assets/push/test-online-push.png)
## 厂商推送配置
> - 请注意HBuilderX 4.36 发布了不向下兼容的更新,如果您使用的是 HBuilderX 4.36 或者更高版本,且需要 vivo/荣耀 的厂商推送,
请升级推送版本到 1.1.0 或更高版本,并参考文档正确配置 `manifestPlaceholders.json` 和 `mcs-services.json`。
> - 请在 `nativeResources` 目录下进行推送配置。若项目根目录尚未创建该文件夹,请新建一个名为 `nativeResources` 的文件夹。
> - 离线推送厂商配置完成后,需要打包自定义基座。参考:[[快速跑通]>[步骤5测试推送测试前请务必打开手机通知权限允许应用通知。]](#user-content-step5)
### 【Android】
1. 新建 nativeResources/android/assets 目录。
2. 在 [推送服务 Push > 接入设置 > 一键式快速配置](https://console.cloud.tencent.com/im/push-plugin-push-identifier) 下载 `timpush-configs.json` 文件,配置到 nativeResources/android/assets 目录下。
3. For 华为:
配置 `agconnect-services.json` (此文件获取详见 [厂商配置 > uniapp > 华为 > 步骤4获取应用信息](https://cloud.tencent.com/document/product/269/103769))到 nativeResources/android 目录下。
4. For Google FCM
4.1. 编辑 uni_modules/TencentCloud-Push/utssdk/app-android/config.json 的 `project.plugins`,添加 `"com.google.gms.google-services"`,如下:
```
{
...
"project": {
"plugins": [
...
"com.google.gms.google-services"
]
}
}
```
4.2. 配置 `google-services.json` 文件到 nativeResources/android/ 目录。
5. For 荣耀:
5.1. 编辑 uni_modules/TencentCloud-Push/utssdk/app-android/config.json 的 `dependencies`,添加 `"com.tencent.timpush:honor:8.3.6498"`,如下:
```
{
...
"dependencies": [
...
"com.tencent.timpush:honor:8.3.6498"
]
}
```
5.2. 配置 `mcs-services.json` 文件到 nativeResources/android 目录下。
5.3. 配置 `appID` 到 nativeResources/android/manifestPlaceholders.json 中的 `"HONOR_APPID"`,如下:
```
{
"HONOR_APPID": ""
}
```
6. For vivo
6.1. 编辑 uni_modules/TencentCloud-Push/utssdk/app-android/config.json 的 `dependencies`,添加 `"com.tencent.timpush:vivo:8.3.6498"`,如下:
```
{
...
"dependencies": [
...
"com.tencent.timpush:vivo:8.3.6498"
]
}
```
6.2. 配置 `appID` 和 `appKey` 到 nativeResources/android/manifestPlaceholders.json 中的 `VIVO_APPKEY` 和 `VIVO_APPID`,如下:
```
{
"VIVO_APPKEY": "",
"VIVO_APPID": "",
}
```
### 【iOS】
1. 新建 nativeResources/ios/Resources 目录。
2. 在 nativeResources/ios/Resources 中**新建 timpush-configs.json 文件**。
3. 并将在 [IM控制台 > 推送服务 Push > 接入设置](https://console.cloud.tencent.com/im/push-plugin-push-identifier) 获取的证书ID补充到 timpush-configs.json 文件中。
```
{
"businessID":"xxx"
}
```
## 接口
| API | 描述|
|----|---|
| registerPush | 注册推送服务 (必须在 App 用户同意了隐私政策,并且允许为 App 用户提供推送服务后,再调用该接口使用推送服务)。<br>首次注册成功后TencentCloud-Push SDK 生成该设备的标识 - RegistrationID。<br> 业务侧可以把这个 RegistrationID 保存到业务服务器。业务侧根据 RegistrationID 向设备推送消息或者通知。|
| unRegisterPush | 反注册关闭推送服务。|
| setRegistrationID | 设置注册推送服务使用的推送 ID 标识,即 RegistrationID。<br/>如果业务侧期望业务账号 ID 和推送 ID 一致,方便使用,可使用此接口,此时需注意,此接口需在 registerPush注册推送服务之前调用。|
| getRegistrationID | 注册推送服务成功后,获取推送 ID 标识,即 RegistrationID。|
| getNotificationExtInfo | 获取推送扩展信息。|
| getNotificationExtInfo | 收到离线推送时,点击通知栏拉起 app调用此接口可获取推送扩展信息。|
| addPushListener | 添加 Push 监听器。|
| removePushListener | 移除 Push 监听器。|
| disablePostNotificationInForeground | 应用在前台时,开/关通知栏通知。|
| createNotificationChannel | 创建客户端通知 channel。|
```ts
registerPush(SDKAppID: number, appKey: string, onSuccess: (data: string) => void, onError?: (errCode: number, errMsg: string) => void);
```
|属性|类型|必填|说明|
|----|---|----|----|
|SDKAppID|number|是|推送Push应用 ID|
|appKey|string|是|推送Push应用客户端密钥|
|onSuccess|function|是|接口调用成功的回调函数|
|onError|function|否|接口调用失败的回调函数|
```ts
unRegisterPush(onSuccess: () => void, onError?: (errCode: number, errMsg: string) => void): void;
```
|属性|类型|必填|说明|
|----|---|----|----|
|onSuccess|function|是|接口调用成功的回调函数|
|onError|function|否|接口调用失败的回调函数|
```ts
setRegistrationID(registrationID: string, onSuccess: () => void): void;
```
|属性|类型|必填|说明|
|----|---|----|----|
|registrationID|string|是|设备的推送标识 ID卸载重装会改变。|
|onSuccess|function|是|接口调用成功的回调函数|
```ts
getRegistrationID(onSuccess: (registrationID: string) => void): void;
```
|属性|类型|必填|说明|
|----|---|----|----|
|onSuccess|function|是|接口调用成功的回调函数|
```ts
getNotificationExtInfo(onSuccess: (extInfo: string) => void): void;
```
|属性|类型|必填|说明|
|----|---|----|----|
|onSuccess|function|是|接口调用成功的回调函数|
```ts
addPushListener(eventName: string, listener: (data: any) => void);
```
|属性|类型|必填|说明|
|----|---|----|----|
|eventName|string|是|推送事件类型|
|listener|function|是|推送事件处理方法|
```ts
removePushListener(eventName: string, listener?: (data: any) => void);
```
|属性|类型|必填|说明|
|----|---|----|----|
|eventName|string|是|推送事件类型|
|listener|function|否|推送事件处理方法|
```ts
disablePostNotificationInForeground(disable: boolean);
```
|属性|类型|必填|说明|
|----|---|----|----|
|disable|boolean|是|应用在前台时,开/关通知栏通知,默认开<br/> - true: 应用在前台时,关闭通知栏通知。<br/> - false: 应用在前台时,开启通知栏通知。|
```ts
createNotificationChannel(options: any, listener: (data: any) => void);
```
|属性|类型|必填|说明|
|----|---|----|----|
|options.channelID|string|是|自定义 channel 的 ID|
|options.channelName|string|是|自定义 channel 的名称|
|options.channelDesc|string|否|自定义 channel 的描述|
|options.channelSound|string|否|自定义 channel 的铃音,音频文件名,不带后缀,音频文件需要放到 xxx/nativeResources/android/res/raw 中。<br/> 例如:<br/> `options.channelSound = private_ring`,即设置 `xxx/nativeResources/android/res/raw/private_ring.mp3` 为自定义铃音|
|listener|function|是|接口调用成功的回调函数|

View File

@@ -0,0 +1,285 @@
# TencentCloud-Push
## 简介
使用 uts 开发基于腾讯云推送服务Push支持 iOS 和 Android 推送,同时适配各大厂商推送。
腾讯云推送服务Push提供一站式 App 推送解决方案,助您轻松提升用户留存和互动活跃度,支持与腾讯云即时通信 IM SDK、实时音视频 TRTC SDK、音视频通话 SDK、直播 SDK等音视频终端产品协同集成在不同场景联合使用提升业务整体功能体验。
<img src="https://qcloudimg.tencent-cloud.cn/image/document/60d714484e54b284cfa440adcc885349.png" width="618" height="456">
<img src="https://qcloudimg.tencent-cloud.cn/image/document/864c391ecf6f2724d26e368e4f09e466.png" width="618" height="444">
<img src="https://qcloudimg.tencent-cloud.cn/image/document/6af60f4b20dd46323e8f901a161a80a9.png" width="618" height="454">
#### 数据可视化,辅助运营策略
<img src="https://qcloudimg.tencent-cloud.cn/image/document/6c422f64900053c38a6bf66fe1103b3f.png" width="618" height="334">
#### 支持推送消息全链路问题排查
<img src="https://qcloudimg.tencent-cloud.cn/image/document/156d43ed48971f9bf865ad0c4e2342e3.png" width="618" height="443">
#### 六地服务部署,严守数据安全
提供了中国、东南亚(新加坡、印尼雅加达)、东北亚(韩国首尔)、欧洲(德国法兰克福)以及北美(美国硅谷)数据存储中心供选择,每个数据中心均支持全球接入。如果您的应用在境外上线且用户主要在境外,您可以根据消息传输需求及合规要求,选择适合您业务的境外数据中心,保障您的数据安全。
<img src="https://qcloudimg.tencent-cloud.cn/image/document/2ffc1a103a42d9c01cfb819cd92bbd1d.png" widht="618" height="308">
## 快速跑通
### 步骤1创建应用
进入 [控制台](https://console.cloud.tencent.com/im) ,单击创建应用,填写应用名称,选择数据中心,单击确定,完成应用创建。
![](https://qcloudimg.tencent-cloud.cn/image/document/e2761226f7d2bbdfb0a301192316c7d3.png)
### 步骤2开通推送服务 Push
进入 [推送服务 Push](https://console.cloud.tencent.com/im/push-plugin-push-identifier),单击立即购买或免费试用 。每个应用可免费试用一次有效期7天
![](https://qcloudimg.tencent-cloud.cn/image/document/a7e1f3847c91a807ec9be3a586f1290f.png)
### 步骤3将 [uni-app 腾讯云推送服务Push](https://ext.dcloud.net.cn/plugin?id=20169)插件导入 HbuilderX 中的工程。如图所示:
![](https://qcloudimg.tencent-cloud.cn/image/document/ab8061fea2bf6659f571c2c11aa0d8f4.png)
![](https://qcloudimg.tencent-cloud.cn/image/document/13a3e33564e6ab79d3e609b36e8ba0d5.png)
![](https://qcloudimg.tencent-cloud.cn/image/document/3c7060f9db637c826009926c5f34a1b8.png)
### 步骤4在 App.vue 中引入并注册腾讯云推送服务Push
将 SDKAppID 和 appKey 替换为您在IM 控制台 - 推送服务 Push - 接入设置页面 获取的应用的信息。如图所示:
![](https://sdk-web-1252463788.cos.ap-hongkong.myqcloud.com/im/assets/push/push.png)
```ts
// 集成 TencentCloud-Push
import * as Push from '@/uni_modules/TencentCloud-Push';
const SDKAppID = 0; // 您的 SDKAppID
const appKey = ''; // 客户端密钥
Push.registerPush(SDKAppID, appKey, (data) => {
console.log('registerPush ok', data);
Push.getRegistrationID((registrationID) => {
console.log('getRegistrationID ok', registrationID);
});
}, (errCode, errMsg) => {
console.error('registerPush failed', errCode, errMsg);
}
);
// 监听通知栏点击事件,获取推送扩展信息
Push.addPushListener(Push.EVENT.NOTIFICATION_CLICKED, (res) => {
// res 为推送扩展信息
console.log('notification clicked', res);
});
// 监听在线推送
Push.addPushListener(Push.EVENT.MESSAGE_RECEIVED, (res) => {
// res 为消息内容
console.log('message received', res);
});
// 监听在线推送被撤回
Push.addPushListener(Push.EVENT.MESSAGE_REVOKED, (res) => {
// res 为被撤回的消息 ID
console.log('message revoked', res);
});
```
### <span id="step5">步骤5测试推送测试前请务必打开手机通知权限允许应用通知。</span>
单击 HBuilderX 的 【运行 > 运行到手机或模拟器 > 制作自定义调试基座】,使用云端证书制作 Android 或 iOS 自定义调试基座。
![](https://qcloudimg.tencent-cloud.cn/image/document/742b7c05364e8ff9a16d5d5601aa038b.png)
自定义调试基座打好后,安装到手机运行。
[登录](https://console.cloud.tencent.com/im/push-plugin-push-check) 控制台,使用测试工具进行在线推送测试。
![](https://sdk-web-1252463788.cos.ap-hongkong.myqcloud.com/im/assets/push/test-online-push.png)
## 厂商推送配置
> - 请注意HBuilderX 4.36 发布了不向下兼容的更新,如果您使用的是 HBuilderX 4.36 或者更高版本,且需要 vivo/荣耀 的厂商推送,
请升级推送版本到 1.1.0 或更高版本,并参考文档正确配置 `manifestPlaceholders.json``mcs-services.json`
> - 请在 `nativeResources` 目录下进行推送配置。若项目根目录尚未创建该文件夹,请新建一个名为 `nativeResources` 的文件夹。
> - 厂商推送配置完成后,需要打包自定义基座。参考:[[快速跑通]>[步骤5测试推送测试前请务必打开手机通知权限允许应用通知。]](#step5)
#### 【Android】
1. 新建 nativeResources/android/assets 目录。
2. 在 [推送服务 Push > 接入设置 > 一键式快速配置](https://console.cloud.tencent.com/im/push-plugin-push-identifier) 下载 `timpush-configs.json` 文件,配置到 nativeResources/android/assets 目录下。
3. For 华为:
配置 `agconnect-services.json` (此文件获取详见 [厂商配置 > uniapp > 华为 > 步骤4获取应用信息](https://cloud.tencent.com/document/product/269/103769))到 nativeResources/android 目录下。
4. For Google FCM
4.1. 编辑 uni_modules/TencentCloud-Push/utssdk/app-android/config.json 的 `project.plugins`,添加 `"com.google.gms.google-services"`,如下:
```
{
...
"project": {
"plugins": [
...
"com.google.gms.google-services"
]
}
}
```
4.2. 配置 `google-services.json` 文件到 nativeResources/android/ 目录。
5. For 荣耀:
5.1. 编辑 uni_modules/TencentCloud-Push/utssdk/app-android/config.json 的 `dependencies`,添加 `"com.tencent.timpush:honor:8.3.6498"`,如下:
```
{
...
"dependencies": [
...
"com.tencent.timpush:honor:8.3.6498"
]
}
```
5.2. 配置 `mcs-services.json` 文件到 nativeResources/android 目录下。
5.3. 配置 `appID` 到 nativeResources/android/manifestPlaceholders.json 中的 `"HONOR_APPID"`,如下:
```
{
"HONOR_APPID": ""
}
```
6. For vivo
6.1. 编辑 uni_modules/TencentCloud-Push/utssdk/app-android/config.json 的 `dependencies`,添加 `"com.tencent.timpush:vivo:8.3.6498"`,如下:
```
{
...
"dependencies": [
...
"com.tencent.timpush:vivo:8.3.6498"
]
}
```
6.2. 配置 `appID` 和 `appKey` 到 nativeResources/android/manifestPlaceholders.json 中的 `VIVO_APPKEY` 和 `VIVO_APPID`,如下:
```
{
"VIVO_APPKEY": "",
"VIVO_APPID": "",
}
```
#### 【iOS】
1. 新建 nativeResources/ios/Resources 目录。
2. 在 nativeResources/ios/Resources 目录下新建 `timpush-configs.json` 文件。
3. 将在 [IM控制台 > 推送服务 Push > 接入设置](https://console.cloud.tencent.com/im/push-plugin-push-identifier) 获取的证书ID补充到 `timpush-configs.json` 文件中。
```
{
"businessID":"xxx"
}
```
## 接口
| API | 描述|
|----|---|
| registerPush | 注册推送服务 (必须在 App 用户同意了隐私政策,并且允许为 App 用户提供推送服务后,再调用该接口使用推送服务)。<br>首次注册成功后TencentCloud-Push SDK 生成该设备的标识 - RegistrationID。<br> 业务侧可以把这个 RegistrationID 保存到业务服务器。业务侧根据 RegistrationID 向设备推送消息或者通知。|
| unRegisterPush | 反注册关闭推送服务。|
| setRegistrationID | 设置注册推送服务使用的推送 ID 标识,即 RegistrationID。<br/>如果业务侧期望业务账号 ID 和推送 ID 一致,方便使用,可使用此接口,此时需注意,此接口需在 registerPush注册推送服务之前调用。|
| getRegistrationID | 注册推送服务成功后,获取推送 ID 标识,即 RegistrationID。|
| getNotificationExtInfo | 收到离线推送时,点击通知栏拉起 app调用此接口可获取推送扩展信息。|
| addPushListener | 添加 Push 监听器。|
| removePushListener | 移除 Push 监听器。|
| disablePostNotificationInForeground | 应用在前台时,开/关通知栏通知(默认开)。|
| createNotificationChannel | 创建客户端通知 channel。|
```ts
registerPush(SDKAppID: number, appKey: string, onSuccess: (data: string) => void, onError?: (errCode: number, errMsg: string) => void);
```
|属性|类型|必填|说明|
|----|---|----|----|
|SDKAppID|number|是|推送Push应用 ID|
|appKey|string|是|推送Push应用客户端密钥|
|onSuccess|function|是|接口调用成功的回调函数|
|onError|function|否|接口调用失败的回调函数|
```ts
unRegisterPush(onSuccess: () => void, onError?: (errCode: number, errMsg: string) => void): void;
```
|属性|类型|必填|说明|
|----|---|----|----|
|onSuccess|function|是|接口调用成功的回调函数|
|onError|function|否|接口调用失败的回调函数|
```ts
setRegistrationID(registrationID: string, onSuccess: () => void): void;
```
|属性|类型|必填|说明|
|----|---|----|----|
|registrationID|string|是|设备的推送标识 ID卸载重装会改变。|
|onSuccess|function|是|接口调用成功的回调函数|
```ts
getRegistrationID(onSuccess: (registrationID: string) => void): void;
```
|属性|类型|必填|说明|
|----|---|----|----|
|onSuccess|function|是|接口调用成功的回调函数|
```ts
getNotificationExtInfo(onSuccess: (extInfo: string) => void): void;
```
|属性|类型|必填|说明|
|----|---|----|----|
|onSuccess|function|是|接口调用成功的回调函数|
```ts
addPushListener(eventName: string, listener: (data: any) => void);
```
|属性|类型|必填|说明|
|----|---|----|----|
|eventName|string|是|推送事件类型|
|listener|function|是|推送事件处理方法|
```ts
removePushListener(eventName: string, listener?: (data: any) => void);
```
|属性|类型|必填|说明|
|----|---|----|----|
|eventName|string|是|推送事件类型|
|listener|function|否|推送事件处理方法|
```ts
disablePostNotificationInForeground(disable: boolean);
```
|属性|类型|必填|说明|
|----|---|----|----|
|disable|boolean|是|应用在前台时,开/关通知栏通知,默认开<br/> - true: 应用在前台时,关闭通知栏通知。<br/> - false: 应用在前台时,开启通知栏通知。|
```ts
createNotificationChannel(options: any, listener: (data: any) => void);
```
|属性|类型|必填|说明|
|----|---|----|----|
|options.channelID|string|是|自定义 channel 的 ID|
|options.channelName|string|是|自定义 channel 的名称|
|options.channelDesc|string|否|自定义 channel 的描述|
|options.channelSound|string|否|自定义 channel 的铃音,音频文件名,不带后缀,音频文件需要放到 xxx/nativeResources/android/res/raw 中。<br/> 例如:<br/> `options.channelSound = private_ring`,即设置 `xxx/nativeResources/android/res/raw/private_ring.mp3` 为自定义铃音|
|listener|function|是|接口调用成功的回调函数|

View File

@@ -0,0 +1,27 @@
{
"minSdkVersion": "21",
"dependencies": [
"com.google.android.material:material:1.3.0",
"com.google.code.gson:gson:2.9.1",
"commons-codec:commons-codec:1.15",
"com.github.bumptech.glide:glide:4.12.0",
"com.tencent.timpush:timpush:8.5.6864",
"com.tencent.liteav.tuikit:tuicore:8.5.6864",
"com.tencent.timpush:huawei:8.5.6864",
"com.tencent.timpush:xiaomi:8.5.6864",
"com.tencent.timpush:oppo:8.5.6864",
"com.tencent.timpush:meizu:8.5.6864",
"com.tencent.timpush:fcm:8.5.6864"
],
"project": {
"plugins": [
"com.huawei.agconnect",
"com.hihonor.mcs.asplugin"
],
"dependencies": [
"com.huawei.agconnect:agcp:1.9.1.301",
"com.google.gms:google-services:4.3.15",
"com.hihonor.mcs:asplugin:2.0.1.300"
]
}
}

View File

@@ -0,0 +1,152 @@
import { UTSAndroid } from 'io.dcloud.uts';
import Context from 'android.content.Context';
import TIMPushManager from 'com.tencent.qcloud.tim.push.TIMPushManager';
import TIMPushConfig from 'com.tencent.qcloud.tim.push.config.TIMPushConfig';
import { PushCallbackOptions } from './push-callback-options.uts';
import { PushListenerOptions } from './push-listener-options.uts';
import PushCallback from './push-callback.uts';
import PushListener from './push-listener.uts';
const context: Context | null = UTSAndroid.getAppContext();
console.warn('Push | package.name:', context?.getPackageName());
TIMPushConfig.getInstance().setRunningPlatform(2);
const Push = TIMPushManager.getInstance();
export class EVENT {
static MESSAGE_RECEIVED: string = 'message_received'
static MESSAGE_REVOKED: string = 'message_revoked'
static NOTIFICATION_CLICKED: string = 'notification_clicked'
}
let disableNotification = false;
export function disablePostNotificationInForeground(disable: boolean): void {
console.log('Push | disablePostNotificationInForeground', disable);
disableNotification = disable;
Push.disablePostNotificationInForeground(disableNotification);
}
export function registerPush(SDKAppID: number, appKey: string, onSuccess: (data: string) => void, onError?: (errCode: number, errMsg: string) => void): void {
if (SDKAppID == 0) {
onError?.(9010001, 'Invalid SDKAppID');
} else if (appKey == '') {
onError?.(9010002, 'Invalid appKey');
}
const pushCbOptions: PushCallbackOptions = {
apiName: 'registerPush',
success: (res?: any) => {
Push.disablePostNotificationInForeground(disableNotification);
// 强转下类型,避免类型推断错误
let token: string = res as string;
onSuccess(token);
},
fail: (errCode: number, errMsg: string) => {
onError?.(errCode, errMsg);
}
};
// 注意!!! 这里不要写成 new PushCallback({ api, success, fail }),否则会因类型推断不一致导致编译错误
Push.registerPush(context, SDKAppID.toInt(), appKey, new PushCallback(pushCbOptions));
}
export function setRegistrationID(registrationID: string, onSuccess: () => void): void {
const pushCbOptions: PushCallbackOptions = {
apiName: 'setRegistrationID',
success: (res?: any) => {
onSuccess();
},
fail: (errCode: number, errMsg: string) => {
// 空实现
}
};
// 注意!!! 这里不要写成 new PushCallback({ api, success, fail }),否则会因类型推断不一致导致编译错误
Push.setRegistrationID(registrationID, new PushCallback(pushCbOptions));
}
export function getRegistrationID(onSuccess: (registrationID: string) => void): void {
const pushCbOptions: PushCallbackOptions = {
apiName: 'getRegistrationID',
success: (res?: any) => {
// 强转下类型,避免类型推断错误
let registrationID: string = res as string;
onSuccess(registrationID);
},
fail: (errCode: number, errMsg: string) => {
// 空实现
}
};
// 注意!!! 这里不要写成 new PushCallback({ api, success, fail }),否则会因类型推断不一致导致编译错误
Push.getRegistrationID(new PushCallback(pushCbOptions));
}
export function unRegisterPush(onSuccess: () => void, onError?: (errCode: number, errMsg: string) => void): void {
const pushCbOptions: PushCallbackOptions = {
apiName: 'unRegisterPush',
success: (res?: any) => {
onSuccess();
},
fail: (errCode: number, errMsg: string) => {
// 空实现
},
};
// 注意!!! 这里不要写成 new PushCallback({ api, success, fail }),否则会因类型推断不一致导致编译错误
Push.unRegisterPush(new PushCallback(pushCbOptions));
}
export function createNotificationChannel(options: any, onSuccess: (extInfo: string) => void): void {
const pushCbOptions: PushCallbackOptions = {
apiName: 'createNotificationChannel',
success: (res?: any) => {
let ret: string = res as string;
onSuccess(ret);
},
fail: (errCode: number, errMsg: string) => {
// 空实现
},
};
Push.callExperimentalAPI('createNotificationChannel', JSON.stringify(options), new PushCallback(pushCbOptions));
}
export function getNotificationExtInfo(onSuccess: (extInfo: string) => void): void {
const pushCbOptions: PushCallbackOptions = {
apiName: 'getNotificationExtInfo',
success: (res?: any) => {
let ret: string = res as string;
onSuccess(ret);
},
fail: (errCode: number, errMsg: string) => {
// 空实现
},
};
Push.callExperimentalAPI('getNotificationExtInfo', null, new PushCallback(pushCbOptions));
}
const listenerMap = new Map<string, Array<(res: any) => void>>();
const pushListenerOptions: PushListenerOptions = {
listener: (eventName: string, data: any) => {
listenerMap.get(eventName)?.forEach(item => {
item(data);
});
},
};
const pushListener = new PushListener(pushListenerOptions);
@UTSJS.keepAlive
export function addPushListener(eventName: string, listener: (res: any) => void): void {
if(listenerMap.size === 0) {
Push.addPushListener(pushListener);
}
const listeners:Array<(res: any) => void> = [listener];
listenerMap.get(eventName)?.forEach(item => {
listeners.push(item);
})
listenerMap.set(eventName, listeners);
}
export function removePushListener(eventName: string, listener?: (res: any) => void): void {
listenerMap.delete(eventName);
if(listenerMap.size === 0) {
Push.removePushListener(pushListener);
}
}

View File

@@ -0,0 +1,5 @@
export type PushCallbackOptions = {
apiName: string
success: (res?: any) => void
fail: (errCode: number, errMsg: string) => void
}

View File

@@ -0,0 +1,28 @@
import TIMPushCallback from 'com.tencent.qcloud.tim.push.TIMPushCallback';
import { PushCallbackOptions } from './push-callback-options.uts';
const LOG_PREFIX: string = 'Push |';
export default class PushCallback implements TIMPushCallback<any> {
private apiName: string;
private success: (data?: any) => void;
private fail: (errCode: number, errMsg: string) => void;
constructor(options: PushCallbackOptions) {
this.apiName = options.apiName;
this.success = options.success;
this.fail = options.fail;
}
override onSuccess(data?: any) {
console.log(`${LOG_PREFIX} ${this.apiName} ok, data:`, data);
if (data == null) {
this.success?.('');
} else {
this.success?.(data);
}
}
override onError(errCode: Int, errMsg: string, data?: any) {
this.fail?.(errCode as number, errMsg);
}
}

View File

@@ -0,0 +1,3 @@
export type PushListenerOptions = {
listener: (eventType: string, data: any) => void
}

View File

@@ -0,0 +1,25 @@
import TIMPushListener from 'com.tencent.qcloud.tim.push.TIMPushListener';
import TIMPushMessage from 'com.tencent.qcloud.tim.push.TIMPushMessage';
import { PushListenerOptions } from './push-listener-options.uts';
const LOG_PREFIX: string = 'Push | PushListener';
export default class PushListener implements TIMPushListener {
private listener: (eventType: string, data: any) => void;
constructor(options: PushListenerOptions) {
this.listener = options.listener;
console.log(`${LOG_PREFIX} ok`);
}
override onRecvPushMessage(message: TIMPushMessage) {
this.listener('message_received', { data: message });
}
override onRevokePushMessage(messageID: string) {
this.listener('message_revoked', { data: messageID });
}
override onNotificationClicked(ext: string) {
this.listener('notification_clicked', { data: ext });
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>

View File

@@ -0,0 +1,11 @@
{
"deploymentTarget": "9.0",
"dependencies-pods": [
{
"name": "TXIMSDK_Plus_iOS_XCFramework",
"version": "8.5.6864"
}, {
"name": "TIMPush",
"version": "8.5.6864"
}]
}

View File

@@ -0,0 +1,125 @@
import { TIMPushManager } from "TIMPush"
import { NSObject } from "DCloudUTSFoundation"
import PushListener from './push-listener.uts'
import { PushListenerOptions } from './push-listener-options.uts'
const LOG_PREFIX = 'Push |';
export class EVENT {
static MESSAGE_RECEIVED: string = 'message_received'
static MESSAGE_REVOKED: string = 'message_revoked'
static NOTIFICATION_CLICKED: string = 'notification_clicked'
}
function setRunningPlatform(): void {
console.log(LOG_PREFIX, 'setRunningPlatform');
const param = new NSString("{\"runningPlatform\":2}");
TIMPushManager.callExperimentalAPI('setPushConfig', param = param, succ = (ext?: NSObject): void => {
let platform: string = ext as string;
console.log(LOG_PREFIX, 'setRunningPlatform ok. platform:', platform);
}, fail = (code?: Int32 ,desc?:String): void => {
console.log(LOG_PREFIX, `setRunningPlatform fail. code: ${code}, desc: ${desc}`);
}
);
}
let disableNotification = false;
export function disablePostNotificationInForeground(_disable: boolean): void {
console.log(LOG_PREFIX, 'disablePostNotificationInForeground', _disable);
disableNotification = _disable;
TIMPushManager.disablePostNotificationInForeground(disable = disableNotification);
}
export function registerPush(SDKAppID: number, appKey: string, onSuccess: (data: string) => void, onError?: (errCode: number, errMsg: string) => void): void {
if (SDKAppID == 0) {
onError?.(9010001, 'Invalid SDKAppID');
} else if (appKey == '') {
onError?.(9010002, 'Invalid appKey');
}
setRunningPlatform();
TIMPushManager.registerPush(SDKAppID.toInt32(), appKey = appKey, succ = (deviceToken?: Data): void => {
TIMPushManager.disablePostNotificationInForeground(disable = disableNotification);
console.log('devicetoken ->', deviceToken, deviceToken?.count);
onSuccess('');
}, fail = (code?: Int32 ,desc?:String): void => {
onError?.(code as number, desc as string);
}
);
}
export function unRegisterPush(onSuccess: () => void, onError: (errCode: number, errMsg: string) => void): void {
TIMPushManager.unRegisterPush((): void => {
onSuccess();
}, fail = (code?: Int32 ,desc?:String): void => {
onError(code as number, desc as string);
}
);
}
export function setRegistrationID(registrationID: string, onSuccess: () => void): void {
console.log(LOG_PREFIX, 'setRegistrationID', `registrationID:${registrationID}`);
TIMPushManager.setRegistrationID(registrationID, callback = (): void => {
console.log(LOG_PREFIX, 'setRegistrationID ok');
onSuccess();
});
}
export function getRegistrationID(onSuccess: (registrationID: string) => void): void {
TIMPushManager.getRegistrationID((value ?: string): void => {
// 这里需要转一下,否则会有问题
let ret: string = value as string;
onSuccess(ret);
});
}
export function createNotificationChannel(options: any, onSuccess: (data: string) => void): void {
// 空实现
}
// 注意!!!这里的 extInfo 不能写成 ext否则会跟内部的 ext?:NSObject 有冲突;也不能写成 extension否则会导致编译错误
export function getNotificationExtInfo(onSuccess: (extInfo: string) => void): void {
console.log(LOG_PREFIX, 'getNotificationExtInfo');
TIMPushManager.callExperimentalAPI('getNotificationExtInfo', param = {}, succ = (ext?: NSObject): void => {
let str: string = ext as string;
console.log(LOG_PREFIX, 'getNotificationExtInfo ok. ext:', str);
onSuccess(str);
}, fail = (code?: Int32 ,desc?:String): void => {
// 空实现
}
);
}
const listenerMap = new Map<string, Array<(res: any) => void>>();
const pushListenerOptions: PushListenerOptions = {
listener: (eventName: string, data: any) => {
listenerMap.get(eventName)?.forEach(item => {
item(data);
});
},
};
const pushListener = new PushListener(pushListenerOptions);
@UTSJS.keepAlive
export function addPushListener(eventName: string, _listener: (res: any) => void): void {
console.log(LOG_PREFIX, 'addPushListener', eventName);
if(listenerMap.size === 0) {
TIMPushManager.addPushListener(listener = pushListener);
}
const listeners:Array<(res: any) => void> = [_listener];
listenerMap.get(eventName)?.forEach(item => {
listeners.push(item);
})
listenerMap.set(eventName, listeners);
}
export function removePushListener(eventName: string, _listener?: (res: any) => void): void {
console.log(LOG_PREFIX, 'removePushListener', eventName);
listenerMap.delete(eventName);
if(listenerMap.size === 0) {
TIMPushManager.removePushListener(listener = pushListener);
}
}

View File

@@ -0,0 +1,3 @@
export type PushListenerOptions = {
listener: (eventType: string, data: any) => void
}

View File

@@ -0,0 +1,24 @@
import { TIMPushListener, TIMPushMessage} from "TIMPush"
import { PushListenerOptions } from './push-listener-options.uts';
const LOG_PREFIX: string = 'Push | PushListener';
export default class PushListener implements TIMPushListener {
private listener: (eventType: string, data: any) => void;
constructor(options: PushListenerOptions) {
this.listener = options.listener;
console.log(`${LOG_PREFIX} ok`);
}
onRecvPushMessage(message: TIMPushMessage) {
this.listener('message_received', { data: message });
}
onRevokePushMessage(messageID: string) {
this.listener('message_revoked', { data: messageID });
}
onNotificationClicked(ext: string) {
this.listener('notification_clicked', { data: ext });
}
}

View File

@@ -0,0 +1,11 @@
interface Push {
setRegistrationID(registrationID: string, onSuccess: () => void): void,
registerPush(SDKAppID: number, appKey: string, onSuccess: (data: string) => void, onError?: (errCode: number, errMsg: string) => void): void,
getRegistrationID(onSuccess: (registrationID: string) => void): void,
unRegisterPush(onSuccess: () => void, onError?: (errCode: number, errMsg: string) => void): void,
getNotificationExtInfo(onSuccess: (extInfo: string) => void): void
addPushListener(eventName: string, listener: (res: any) => void): void
removePushListener(eventName: string, listener?: (res: any) => void): void
disablePostNotificationInForeground(disable: boolean): void
createNotificationChannel(options: any, onSuccess: (data: string) => void): void
}

View File

@@ -0,0 +1,17 @@
## 1.0.22024-09-21
- 新增 clearAble属性
## 1.0.12021-11-23
- 优化 label、label-width 属性
## 1.0.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-combox](https://uniapp.dcloud.io/component/uniui/uni-combox)
## 0.1.02021-07-30
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 0.0.62021-05-12
- 新增 组件示例地址
## 0.0.52021-04-21
- 优化 添加依赖 uni-icons, 导入后自动下载依赖
## 0.0.42021-02-05
- 优化 组件引用关系通过uni_modules引用组件
## 0.0.32021-02-04
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,284 @@
<template>
<view class="uni-combox" :class="border ? '' : 'uni-combox__no-border'">
<view v-if="label" class="uni-combox__label" :style="labelStyle">
<text>{{label}}</text>
</view>
<view class="uni-combox__input-box">
<input class="uni-combox__input" type="text" :placeholder="placeholder" placeholder-class="uni-combox__input-plac"
v-model="inputVal" @input="onInput" @focus="onFocus" @blur="onBlur" />
<uni-icons v-if="!inputVal || !clearAble" :type="showSelector? 'top' : 'bottom'" size="14" color="#999" @click="toggleSelector">
</uni-icons>
<uni-icons v-if="inputVal && clearAble" type="clear" size="24" color="#999" @click="clean">
</uni-icons>
</view>
<view class="uni-combox__selector" v-if="showSelector">
<view class="uni-popper__arrow"></view>
<scroll-view scroll-y="true" class="uni-combox__selector-scroll">
<view class="uni-combox__selector-empty" v-if="filterCandidatesLength === 0">
<text>{{emptyTips}}</text>
</view>
<view class="uni-combox__selector-item" v-for="(item,index) in filterCandidates" :key="index"
@click="onSelectorClick(index)">
<text>{{item}}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
/**
* Combox 组合输入框
* @description 组合输入框一般用于既可以输入也可以选择的场景
* @tutorial https://ext.dcloud.net.cn/plugin?id=1261
* @property {String} label 左侧文字
* @property {String} labelWidth 左侧内容宽度
* @property {String} placeholder 输入框占位符
* @property {Array} candidates 候选项列表
* @property {String} emptyTips 筛选结果为空时显示的文字
* @property {String} value 组合框的值
*/
export default {
name: 'uniCombox',
emits: ['input', 'update:modelValue'],
props: {
clearAble: {
type: Boolean,
default: false
},
border: {
type: Boolean,
default: true
},
label: {
type: String,
default: ''
},
labelWidth: {
type: String,
default: 'auto'
},
placeholder: {
type: String,
default: ''
},
candidates: {
type: Array,
default () {
return []
}
},
emptyTips: {
type: String,
default: '无匹配项'
},
// #ifndef VUE3
value: {
type: [String, Number],
default: ''
},
// #endif
// #ifdef VUE3
modelValue: {
type: [String, Number],
default: ''
},
// #endif
},
data() {
return {
showSelector: false,
inputVal: ''
}
},
computed: {
labelStyle() {
if (this.labelWidth === 'auto') {
return ""
}
return `width: ${this.labelWidth}`
},
filterCandidates() {
return this.candidates.filter((item) => {
return item.toString().indexOf(this.inputVal) > -1
})
},
filterCandidatesLength() {
return this.filterCandidates.length
}
},
watch: {
// #ifndef VUE3
value: {
handler(newVal) {
this.inputVal = newVal
},
immediate: true
},
// #endif
// #ifdef VUE3
modelValue: {
handler(newVal) {
this.inputVal = newVal
},
immediate: true
},
// #endif
},
methods: {
toggleSelector() {
this.showSelector = !this.showSelector
},
onFocus() {
this.showSelector = true
},
onBlur() {
setTimeout(() => {
this.showSelector = false
}, 153)
},
onSelectorClick(index) {
this.inputVal = this.filterCandidates[index]
this.showSelector = false
this.$emit('input', this.inputVal)
this.$emit('update:modelValue', this.inputVal)
},
onInput() {
setTimeout(() => {
this.$emit('input', this.inputVal)
this.$emit('update:modelValue', this.inputVal)
})
},
clean() {
this.inputVal = ''
this.onInput()
}
}
}
</script>
<style lang="scss" scoped>
.uni-combox {
font-size: 14px;
border: 1px solid #DCDFE6;
border-radius: 4px;
padding: 6px 10px;
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
// height: 40px;
flex-direction: row;
align-items: center;
// border-bottom: solid 1px #DDDDDD;
}
.uni-combox__label {
font-size: 16px;
line-height: 22px;
padding-right: 10px;
color: #999999;
}
.uni-combox__input-box {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
align-items: center;
}
.uni-combox__input {
flex: 1;
font-size: 14px;
height: 22px;
line-height: 22px;
}
.uni-combox__input-plac {
font-size: 14px;
color: #999;
}
.uni-combox__selector {
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
position: absolute;
top: calc(100% + 12px);
left: 0;
width: 100%;
background-color: #FFFFFF;
border: 1px solid #EBEEF5;
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 2;
padding: 4px 0;
}
.uni-combox__selector-scroll {
/* #ifndef APP-NVUE */
max-height: 200px;
box-sizing: border-box;
/* #endif */
}
.uni-combox__selector-empty,
.uni-combox__selector-item {
/* #ifndef APP-NVUE */
display: flex;
cursor: pointer;
/* #endif */
line-height: 36px;
font-size: 14px;
text-align: center;
// border-bottom: solid 1px #DDDDDD;
padding: 0px 10px;
}
.uni-combox__selector-item:hover {
background-color: #f9f9f9;
}
.uni-combox__selector-empty:last-child,
.uni-combox__selector-item:last-child {
/* #ifndef APP-NVUE */
border-bottom: none;
/* #endif */
}
// picker 弹出层通用的指示小三角
.uni-popper__arrow,
.uni-popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 6px;
}
.uni-popper__arrow {
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
top: -6px;
left: 10%;
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #EBEEF5;
}
.uni-popper__arrow::after {
content: " ";
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
.uni-combox__no-border {
border: none;
}
</style>

View File

@@ -0,0 +1,88 @@
{
"id": "uni-combox",
"displayName": "uni-combox 组合框",
"version": "1.0.2",
"description": "可以选择也可以输入的表单项 ",
"keywords": [
"uni-ui",
"uniui",
"combox",
"组合框",
"select"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [
"uni-scss",
"uni-icons"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
## Combox 组合框
> **组件名uni-combox**
> 代码块: `uCombox`
组合框组件。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-combox)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@@ -0,0 +1,51 @@
## 1.0.62024-10-22
- 新增 当 multiple 为 false 且传递的 value 为 数组时,使用数组第一项用作反显
## 1.0.52024-03-20
- 修复 单选模式下选中样式不生效的bug
## 1.0.42024-01-27
- 修复 修复错别字chagne为change
## 1.0.32022-09-16
- 可以使用 uni-scss 控制主题色
## 1.0.22022-06-30
- 优化 在 uni-forms 中的依赖注入方式
## 1.0.12022-02-07
- 修复 multiple 为 true 时v-model 的值为 null 报错的 bug
## 1.0.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-checkbox](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox)
## 0.2.52021-08-23
- 修复 在uni-forms中 modelValue 中不存在当前字段,当前字段必填写也不参与校验的问题
## 0.2.42021-08-17
- 修复 单选 list 模式下 icon 为 left 时,选中图标不显示的问题
## 0.2.32021-08-11
- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题
## 0.2.22021-07-30
- 优化 在uni-forms组件与label不对齐的问题
## 0.2.12021-07-27
- 修复 单选默认值为0不能选中的Bug
## 0.2.02021-07-13
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 0.1.112021-07-06
- 优化 删除无用日志
## 0.1.102021-07-05
- 修复 由 0.1.9 引起的非 nvue 端图标不显示的问题
## 0.1.92021-07-05
- 修复 nvue 黑框样式问题
## 0.1.82021-06-28
- 修复 selectedTextColor 属性不生效的Bug
## 0.1.72021-06-02
- 新增 map 属性可以方便映射text/value属性
## 0.1.62021-05-26
- 修复 不关联服务空间的情况下组件报错的Bug
## 0.1.52021-05-12
- 新增 组件示例地址
## 0.1.42021-04-09
- 修复 nvue 下无法选中的问题
## 0.1.32021-03-22
- 新增 disabled属性
## 0.1.22021-02-24
- 优化 默认颜色显示
## 0.1.12021-02-24
- 新增 支持nvue
## 0.1.02021-02-18
- “暂无数据”显示居中

View File

@@ -0,0 +1,316 @@
const events = {
load: 'load',
error: 'error'
}
const pageMode = {
add: 'add',
replace: 'replace'
}
const attrs = [
'pageCurrent',
'pageSize',
'collection',
'action',
'field',
'getcount',
'orderby',
'where'
]
export default {
data() {
return {
loading: false,
listData: this.getone ? {} : [],
paginationInternal: {
current: this.pageCurrent,
size: this.pageSize,
count: 0
},
errorMessage: ''
}
},
created() {
let db = null;
let dbCmd = null;
if(this.collection){
this.db = uniCloud.database();
this.dbCmd = this.db.command;
}
this._isEnded = false
this.$watch(() => {
let al = []
attrs.forEach(key => {
al.push(this[key])
})
return al
}, (newValue, oldValue) => {
this.paginationInternal.pageSize = this.pageSize
let needReset = false
for (let i = 2; i < newValue.length; i++) {
if (newValue[i] != oldValue[i]) {
needReset = true
break
}
}
if (needReset) {
this.clear()
this.reset()
}
if (newValue[0] != oldValue[0]) {
this.paginationInternal.current = this.pageCurrent
}
this._execLoadData()
})
// #ifdef H5
if (process.env.NODE_ENV === 'development') {
this._debugDataList = []
if (!window.unidev) {
window.unidev = {
clientDB: {
data: []
}
}
}
unidev.clientDB.data.push(this._debugDataList)
}
// #endif
// #ifdef MP-TOUTIAO
let changeName
let events = this.$scope.dataset.eventOpts
for (let i = 0; i < events.length; i++) {
let event = events[i]
if (event[0].includes('^load')) {
changeName = event[1][0][0]
}
}
if (changeName) {
let parent = this.$parent
let maxDepth = 16
this._changeDataFunction = null
while (parent && maxDepth > 0) {
let fun = parent[changeName]
if (fun && typeof fun === 'function') {
this._changeDataFunction = fun
maxDepth = 0
break
}
parent = parent.$parent
maxDepth--;
}
}
// #endif
// if (!this.manual) {
// this.loadData()
// }
},
// #ifdef H5
beforeDestroy() {
if (process.env.NODE_ENV === 'development' && window.unidev) {
let cd = this._debugDataList
let dl = unidev.clientDB.data
for (let i = dl.length - 1; i >= 0; i--) {
if (dl[i] === cd) {
dl.splice(i, 1)
break
}
}
}
},
// #endif
methods: {
loadData(args1, args2) {
let callback = null
if (typeof args1 === 'object') {
if (args1.clear) {
this.clear()
this.reset()
}
if (args1.current !== undefined) {
this.paginationInternal.current = args1.current
}
if (typeof args2 === 'function') {
callback = args2
}
} else if (typeof args1 === 'function') {
callback = args1
}
this._execLoadData(callback)
},
loadMore() {
if (this._isEnded) {
return
}
this._execLoadData()
},
refresh() {
this.clear()
this._execLoadData()
},
clear() {
this._isEnded = false
this.listData = []
},
reset() {
this.paginationInternal.current = 1
},
remove(id, {
action,
callback,
confirmTitle,
confirmContent
} = {}) {
if (!id || !id.length) {
return
}
uni.showModal({
title: confirmTitle || '提示',
content: confirmContent || '是否删除该数据',
showCancel: true,
success: (res) => {
if (!res.confirm) {
return
}
this._execRemove(id, action, callback)
}
})
},
_execLoadData(callback) {
if (this.loading) {
return
}
this.loading = true
this.errorMessage = ''
this._getExec().then((res) => {
this.loading = false
const {
data,
count
} = res.result
this._isEnded = data.length < this.pageSize
callback && callback(data, this._isEnded)
this._dispatchEvent(events.load, data)
if (this.getone) {
this.listData = data.length ? data[0] : undefined
} else if (this.pageData === pageMode.add) {
this.listData.push(...data)
if (this.listData.length) {
this.paginationInternal.current++
}
} else if (this.pageData === pageMode.replace) {
this.listData = data
this.paginationInternal.count = count
}
// #ifdef H5
if (process.env.NODE_ENV === 'development') {
this._debugDataList.length = 0
this._debugDataList.push(...JSON.parse(JSON.stringify(this.listData)))
}
// #endif
}).catch((err) => {
this.loading = false
this.errorMessage = err
callback && callback()
this.$emit(events.error, err)
})
},
_getExec() {
let exec = this.db
if (this.action) {
exec = exec.action(this.action)
}
exec = exec.collection(this.collection)
if (!(!this.where || !Object.keys(this.where).length)) {
exec = exec.where(this.where)
}
if (this.field) {
exec = exec.field(this.field)
}
if (this.orderby) {
exec = exec.orderBy(this.orderby)
}
const {
current,
size
} = this.paginationInternal
exec = exec.skip(size * (current - 1)).limit(size).get({
getCount: this.getcount
})
return exec
},
_execRemove(id, action, callback) {
if (!this.collection || !id) {
return
}
const ids = Array.isArray(id) ? id : [id]
if (!ids.length) {
return
}
uni.showLoading({
mask: true
})
let exec = this.db
if (action) {
exec = exec.action(action)
}
exec.collection(this.collection).where({
_id: dbCmd.in(ids)
}).remove().then((res) => {
callback && callback(res.result)
if (this.pageData === pageMode.replace) {
this.refresh()
} else {
this.removeData(ids)
}
}).catch((err) => {
uni.showModal({
content: err.message,
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
},
removeData(ids) {
let il = ids.slice(0)
let dl = this.listData
for (let i = dl.length - 1; i >= 0; i--) {
let index = il.indexOf(dl[i]._id)
if (index >= 0) {
dl.splice(i, 1)
il.splice(index, 1)
}
}
},
_dispatchEvent(type, data) {
if (this._changeDataFunction) {
this._changeDataFunction(data, this._isEnded)
} else {
this.$emit(type, data, this._isEnded)
}
}
}
}

View File

@@ -0,0 +1,853 @@
<template>
<view class="uni-data-checklist" :style="{'margin-top':isTop+'px'}">
<template v-if="!isLocal">
<view class="uni-data-loading">
<uni-load-more v-if="!mixinDatacomErrorMessage" status="loading" iconType="snow" :iconSize="18"
:content-text="contentText"></uni-load-more>
<text v-else>{{mixinDatacomErrorMessage}}</text>
</view>
</template>
<template v-else>
<checkbox-group v-if="multiple" class="checklist-group" :class="{'is-list':mode==='list' || wrap}"
@change="change">
<label class="checklist-box"
:class="['is--'+mode,item.selected?'is-checked':'',(disabled || !!item.disabled)?'is-disable':'',index!==0&&mode==='list'?'is-list-border':'']"
:style="item.styleBackgroud" v-for="(item,index) in dataList" :key="index">
<checkbox class="hidden" hidden :disabled="disabled || !!item.disabled" :value="item[map.value]+''"
:checked="item.selected" />
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')"
class="checkbox__inner" :style="item.styleIcon">
<view class="checkbox__inner-icon"></view>
</view>
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}">
<text class="checklist-text" :style="item.styleIconText">{{item[map.text]}}</text>
<view v-if="mode === 'list' && icon === 'right'" class="checkobx__list" :style="item.styleBackgroud"></view>
</view>
</label>
</checkbox-group>
<radio-group v-else class="checklist-group" :class="{'is-list':mode==='list','is-wrap':wrap}" @change="change">
<label class="checklist-box"
:class="['is--'+mode,item.selected?'is-checked':'',(disabled || !!item.disabled)?'is-disable':'',index!==0&&mode==='list'?'is-list-border':'']"
:style="item.styleBackgroud" v-for="(item,index) in dataList" :key="index">
<radio class="hidden" hidden :disabled="disabled || item.disabled" :value="item[map.value]+''"
:checked="item.selected" />
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')" class="radio__inner"
:style="item.styleBackgroud">
<view class="radio__inner-icon" :style="item.styleIcon"></view>
</view>
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}">
<text class="checklist-text" :style="item.styleIconText">{{item[map.text]}}</text>
<view v-if="mode === 'list' && icon === 'right'" :style="item.styleRightIcon" class="checkobx__list"></view>
</view>
</label>
</radio-group>
</template>
</view>
</template>
<script>
/**
* DataChecklist 数据选择器
* @description 通过数据渲染 checkbox 和 radio
* @tutorial https://ext.dcloud.net.cn/plugin?id=xxx
* @property {String} mode = [default| list | button | tag] 显示模式
* @value default 默认横排模式
* @value list 列表模式
* @value button 按钮模式
* @value tag 标签模式
* @property {Boolean} multiple = [true|false] 是否多选
* @property {Array|String|Number} value 默认值
* @property {Array} localdata 本地数据 ,格式 [{text:'',value:''}]
* @property {Number|String} min 最小选择个数 multiple为true时生效
* @property {Number|String} max 最大选择个数 multiple为true时生效
* @property {Boolean} wrap 是否换行显示
* @property {String} icon = [left|right] list 列表模式下icon显示位置
* @property {Boolean} selectedColor 选中颜色
* @property {Boolean} emptyText 没有数据时显示的文字 ,本地数据无效
* @property {Boolean} selectedTextColor 选中文本颜色,如不填写则自动显示
* @property {Object} map 字段映射, 默认 map={text:'text',value:'value'}
* @value left 左侧显示
* @value right 右侧显示
* @event {Function} change 选中发生变化触发
*/
export default {
name: 'uniDataChecklist',
mixins: [uniCloud.mixinDatacom || {}],
emits: ['input', 'update:modelValue', 'change'],
props: {
mode: {
type: String,
default: 'default'
},
multiple: {
type: Boolean,
default: false
},
value: {
type: [Array, String, Number],
default () {
return ''
}
},
// TODO vue3
modelValue: {
type: [Array, String, Number],
default () {
return '';
}
},
localdata: {
type: Array,
default () {
return []
}
},
min: {
type: [Number, String],
default: ''
},
max: {
type: [Number, String],
default: ''
},
wrap: {
type: Boolean,
default: false
},
icon: {
type: String,
default: 'left'
},
selectedColor: {
type: String,
default: ''
},
selectedTextColor: {
type: String,
default: ''
},
emptyText: {
type: String,
default: '暂无数据'
},
disabled: {
type: Boolean,
default: false
},
map: {
type: Object,
default () {
return {
text: 'text',
value: 'value'
}
}
}
},
watch: {
localdata: {
handler(newVal) {
this.range = newVal
this.dataList = this.getDataList(this.getSelectedValue(newVal))
},
deep: true
},
mixinDatacomResData(newVal) {
this.range = newVal
this.dataList = this.getDataList(this.getSelectedValue(newVal))
},
value(newVal) {
this.dataList = this.getDataList(newVal)
// fix by mehaotian is_reset 在 uni-forms 中定义
// if(!this.is_reset){
// this.is_reset = false
// this.formItem && this.formItem.setValue(newVal)
// }
},
modelValue(newVal) {
this.dataList = this.getDataList(newVal);
// if(!this.is_reset){
// this.is_reset = false
// this.formItem && this.formItem.setValue(newVal)
// }
}
},
data() {
return {
dataList: [],
range: [],
contentText: {
contentdown: '查看更多',
contentrefresh: '加载中',
contentnomore: '没有更多'
},
isLocal: true,
styles: {
selectedColor: '#2979ff',
selectedTextColor: '#666',
},
isTop: 0
};
},
computed: {
dataValue() {
if (this.value === '') return this.modelValue
if (this.modelValue === '') return this.value
return this.value
}
},
created() {
// this.form = this.getForm('uniForms')
// this.formItem = this.getForm('uniFormsItem')
// this.formItem && this.formItem.setValue(this.value)
// if (this.formItem) {
// this.isTop = 6
// if (this.formItem.name) {
// // 如果存在name添加默认值,否则formData 中不存在这个字段不校验
// if(!this.is_reset){
// this.is_reset = false
// this.formItem.setValue(this.dataValue)
// }
// this.rename = this.formItem.name
// this.form.inputChildrens.push(this)
// }
// }
if (this.localdata && this.localdata.length !== 0) {
this.isLocal = true
this.range = this.localdata
this.dataList = this.getDataList(this.getSelectedValue(this.range))
} else {
if (this.collection) {
this.isLocal = false
this.loadData()
}
}
},
methods: {
loadData() {
this.mixinDatacomGet().then(res => {
this.mixinDatacomResData = res.result.data
if (this.mixinDatacomResData.length === 0) {
this.isLocal = false
this.mixinDatacomErrorMessage = this.emptyText
} else {
this.isLocal = true
}
}).catch(err => {
this.mixinDatacomErrorMessage = err.message
})
},
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
change(e) {
const values = e.detail.value
let detail = {
value: [],
data: []
}
if (this.multiple) {
this.range.forEach(item => {
if (values.includes(item[this.map.value] + '')) {
detail.value.push(item[this.map.value])
detail.data.push(item)
}
})
} else {
const range = this.range.find(item => (item[this.map.value] + '') === values)
if (range) {
detail = {
value: range[this.map.value],
data: range
}
}
}
// this.formItem && this.formItem.setValue(detail.value)
// TODO 兼容 vue2
this.$emit('input', detail.value);
// // TOTO 兼容 vue3
this.$emit('update:modelValue', detail.value);
this.$emit('change', {
detail
})
if (this.multiple) {
// 如果 v-model 没有绑定 ,则走内部逻辑
// if (this.value.length === 0) {
this.dataList = this.getDataList(detail.value, true)
// }
} else {
this.dataList = this.getDataList(detail.value)
}
},
/**
* 获取渲染的新数组
* @param {Object} value 选中内容
*/
getDataList(value) {
// 解除引用关系,破坏原引用关系,避免污染源数据
let dataList = JSON.parse(JSON.stringify(this.range))
let list = []
if (this.multiple) {
if (!Array.isArray(value)) {
value = []
}
} else {
if (Array.isArray(value) && value.length) {
value = value[0]
}
}
dataList.forEach((item, index) => {
item.disabled = item.disable || item.disabled || false
if (this.multiple) {
if (value.length > 0) {
let have = value.find(val => val === item[this.map.value])
item.selected = have !== undefined
} else {
item.selected = false
}
} else {
item.selected = value === item[this.map.value]
}
list.push(item)
})
return this.setRange(list)
},
/**
* 处理最大最小值
* @param {Object} list
*/
setRange(list) {
let selectList = list.filter(item => item.selected)
let min = Number(this.min) || 0
let max = Number(this.max) || ''
list.forEach((item, index) => {
if (this.multiple) {
if (selectList.length <= min) {
let have = selectList.find(val => val[this.map.value] === item[this.map.value])
if (have !== undefined) {
item.disabled = true
}
}
if (selectList.length >= max && max !== '') {
let have = selectList.find(val => val[this.map.value] === item[this.map.value])
if (have === undefined) {
item.disabled = true
}
}
}
this.setStyles(item, index)
list[index] = item
})
return list
},
/**
* 设置 class
* @param {Object} item
* @param {Object} index
*/
setStyles(item, index) {
// 设置自定义样式
item.styleBackgroud = this.setStyleBackgroud(item)
item.styleIcon = this.setStyleIcon(item)
item.styleIconText = this.setStyleIconText(item)
item.styleRightIcon = this.setStyleRightIcon(item)
},
/**
* 获取选中值
* @param {Object} range
*/
getSelectedValue(range) {
if (!this.multiple) return this.dataValue
let selectedArr = []
range.forEach((item) => {
if (item.selected) {
selectedArr.push(item[this.map.value])
}
})
return this.dataValue.length > 0 ? this.dataValue : selectedArr
},
/**
* 设置背景样式
*/
setStyleBackgroud(item) {
let styles = {}
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff'
if (this.selectedColor) {
if (this.mode !== 'list') {
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6'
}
if (this.mode === 'tag') {
styles['background-color'] = item.selected ? selectedColor : '#f5f5f5'
}
}
let classles = ''
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
},
setStyleIcon(item) {
let styles = {}
let classles = ''
if (this.selectedColor) {
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff'
styles['background-color'] = item.selected ? selectedColor : '#fff'
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6'
if (!item.selected && item.disabled) {
styles['background-color'] = '#F2F6FC'
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6'
}
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
},
setStyleIconText(item) {
let styles = {}
let classles = ''
if (this.selectedColor) {
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff'
if (this.mode === 'tag') {
styles.color = item.selected ? (this.selectedTextColor ? this.selectedTextColor : '#fff') : '#666'
} else {
styles.color = item.selected ? (this.selectedTextColor ? this.selectedTextColor : selectedColor) : '#666'
}
if (!item.selected && item.disabled) {
styles.color = '#999'
}
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
},
setStyleRightIcon(item) {
let styles = {}
let classles = ''
if (this.mode === 'list') {
styles['border-color'] = item.selected ? this.styles.selectedColor : '#DCDFE6'
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
}
}
}
</script>
<style lang="scss">
$uni-primary: #2979ff !default;
$border-color: #DCDFE6;
$disable: 0.4;
@mixin flex {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
}
.uni-data-loading {
@include flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 36px;
padding-left: 10px;
color: #999;
}
.uni-data-checklist {
position: relative;
z-index: 0;
flex: 1;
// 多选样式
.checklist-group {
@include flex;
flex-direction: row;
flex-wrap: wrap;
&.is-list {
flex-direction: column;
}
.checklist-box {
@include flex;
flex-direction: row;
align-items: center;
position: relative;
margin: 5px 0;
margin-right: 25px;
.hidden {
position: absolute;
opacity: 0;
}
// 文字样式
.checklist-content {
@include flex;
flex: 1;
flex-direction: row;
align-items: center;
justify-content: space-between;
.checklist-text {
font-size: 14px;
color: #666;
margin-left: 5px;
line-height: 14px;
}
.checkobx__list {
border-right-width: 1px;
border-right-color: #007aff;
border-right-style: solid;
border-bottom-width: 1px;
border-bottom-color: #007aff;
border-bottom-style: solid;
height: 12px;
width: 6px;
left: -5px;
transform-origin: center;
transform: rotate(45deg);
opacity: 0;
}
}
// 多选样式
.checkbox__inner {
/* #ifndef APP-NVUE */
flex-shrink: 0;
box-sizing: border-box;
/* #endif */
position: relative;
width: 16px;
height: 16px;
border: 1px solid $border-color;
border-radius: 4px;
background-color: #fff;
z-index: 1;
.checkbox__inner-icon {
position: absolute;
/* #ifdef APP-NVUE */
top: 2px;
/* #endif */
/* #ifndef APP-NVUE */
top: 1px;
/* #endif */
left: 5px;
height: 8px;
width: 4px;
border-right-width: 1px;
border-right-color: #fff;
border-right-style: solid;
border-bottom-width: 1px;
border-bottom-color: #fff;
border-bottom-style: solid;
opacity: 0;
transform-origin: center;
transform: rotate(40deg);
}
}
// 单选样式
.radio__inner {
@include flex;
/* #ifndef APP-NVUE */
flex-shrink: 0;
box-sizing: border-box;
/* #endif */
justify-content: center;
align-items: center;
position: relative;
width: 16px;
height: 16px;
border: 1px solid $border-color;
border-radius: 16px;
background-color: #fff;
z-index: 1;
.radio__inner-icon {
width: 8px;
height: 8px;
border-radius: 10px;
opacity: 0;
}
}
// 默认样式
&.is--default {
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
.checkbox__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.radio__inner {
background-color: #F2F6FC;
border-color: $border-color;
}
.checklist-text {
color: #999;
}
}
// 选中
&.is-checked {
.checkbox__inner {
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
}
.radio__inner {
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $uni-primary;
}
}
.checklist-text {
color: $uni-primary;
}
// 选中禁用
&.is-disable {
.checkbox__inner {
opacity: $disable;
}
.checklist-text {
opacity: $disable;
}
.radio__inner {
opacity: $disable;
}
}
}
}
// 按钮样式
&.is--button {
margin-right: 10px;
padding: 5px 10px;
border: 1px $border-color solid;
border-radius: 3px;
transition: border-color 0.2s;
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
border: 1px #eee solid;
opacity: $disable;
.checkbox__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.radio__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.checklist-text {
color: #999;
}
}
&.is-checked {
border-color: $uni-primary;
.checkbox__inner {
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
}
.radio__inner {
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $uni-primary;
}
}
.checklist-text {
color: $uni-primary;
}
// 选中禁用
&.is-disable {
opacity: $disable;
}
}
}
// 标签样式
&.is--tag {
margin-right: 10px;
padding: 5px 10px;
border: 1px $border-color solid;
border-radius: 3px;
background-color: #f5f5f5;
.checklist-text {
margin: 0;
color: #666;
}
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
opacity: $disable;
}
&.is-checked {
background-color: $uni-primary;
border-color: $uni-primary;
.checklist-text {
color: #fff;
}
}
}
// 列表样式
&.is--list {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
padding: 10px 15px;
padding-left: 0;
margin: 0;
&.is-list-border {
border-top: 1px #eee solid;
}
// 禁用
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
.checkbox__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.checklist-text {
color: #999;
}
}
&.is-checked {
.checkbox__inner {
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
}
.radio__inner {
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $uni-primary;
}
}
.checklist-text {
color: $uni-primary;
}
.checklist-content {
.checkobx__list {
opacity: 1;
border-color: $uni-primary;
}
}
// 选中禁用
&.is-disable {
.checkbox__inner {
opacity: $disable;
}
.checklist-text {
opacity: $disable;
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,87 @@
{
"id": "uni-data-checkbox",
"displayName": "uni-data-checkbox 数据选择器",
"version": "1.0.6",
"description": "通过数据驱动的单选框和复选框",
"keywords": [
"uni-ui",
"checkbox",
"单选",
"多选",
"单选多选"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "^3.1.1"
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": ["uni-load-more","uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-harmony": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
## DataCheckbox 数据驱动的单选复选框
> **组件名uni-data-checkbox**
> 代码块: `uDataCheckbox`
本组件是基于uni-app基础组件checkbox的封装。本组件要解决问题包括
1. 数据绑定型组件给本组件绑定一个data会自动渲染一组候选内容。再以往开发者需要编写不少代码实现类似功能
2. 自动的表单校验组件绑定了data且符合[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)组件的表单校验规范,搭配使用会自动实现表单校验
3. 本组件合并了单选多选
4. 本组件有若干风格选择如普通的单选多选框、并列button风格、tag风格。开发者可以快速选择需要的风格。但作为一个封装组件样式代码虽然不用自己写了却会牺牲一定的样式自定义性
在uniCloud开发中`DB Schema`中配置了enum枚举等类型后在web控制台的[自动生成表单](https://uniapp.dcloud.io/uniCloud/schema?id=autocode)功能中,会自动生成``uni-data-checkbox``组件并绑定好data
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

45
utils/media.js Normal file
View File

@@ -0,0 +1,45 @@
/**
* 封装 uni.chooseImage返回 Promise便于 async/await 使用
* @param {Object} options - 透传给 uni.chooseImage 的配置
* @returns {Promise<Array<string>>} 返回选中的临时图片路径数组
*/
export function chooseImage(options = {}) {
// 默认配置
const defaultOptions = {
count: 1,
sourceType: ['album', 'camera'],
...options
}
return new Promise((resolve, reject) => {
uni.chooseImage({
...defaultOptions,
success(res) {
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
resolve(res.tempFilePaths)
} else {
reject(new Error('未选择任何图片'))
}
},
fail(err) {
// 统一错误处理(如权限被拒、用户取消等)
let msg = '选择图片失败'
if (err.errMsg.includes('cancel')) {
msg = '用户取消选择'
} else if (
err.errMsg.includes('auth deny') ||
err.errMsg.includes('permission')
) {
msg = '请在设置中开启相册或相机权限'
}
// 可选:自动弹出 toast 提示
uni.showToast({
title: msg,
icon: 'none',
duration: 2000
})
reject(new Error(msg))
}
})
})
}

View File

@@ -132,3 +132,4 @@ export const uploadMultipleFiles = (filePaths, config = {}) => {
)
return Promise.all(uploadPromises)
}

View File

@@ -31,7 +31,7 @@ const hideLoading = () => {
* @param {string} type - 'success' | 'error' | 'warning' | 'none'
* @param {number} duration - 持续时间(毫秒)
*/
const showToast = (message, type = 'none', duration = 2000) => {
const showToast = (message, type = 'none', duration = 1800) => {
let icon = 'none'
if (type === 'success') icon = 'success'
if (type === 'error') icon = 'error'

15
vue.config.js Normal file
View File

@@ -0,0 +1,15 @@
const ScriptSetup = require('unplugin-vue2-script-setup/webpack').default;
module.exports = {
parallel: false,
configureWebpack: {
plugins: [
ScriptSetup({
/* options */
}),
],
},
chainWebpack(config) {
// disable type check and let `vue-tsc` handles it
config.plugins.delete('fork-ts-checker');
},
};