Initial commit

This commit is contained in:
2025-07-04 16:18:58 +08:00
commit 2cf13f673d
770 changed files with 73394 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
<template>
<view class="contact_choose_container">
<custom-nav-bar title="联系人" />
<view class="search_bar_wrap">
<u-search
shape="square"
placeholder="搜索"
:showAction="false"
v-model="keyword"
/>
</view>
<view class="tab_container">
<template v-if="activeTab === 0">
<setting-item
@click="tabChange(tabs[0].idx)"
:title="tabs[0].title"
:border="false"
/>
<view class="tab_pane"></view>
</template>
<template v-else>
<view class="tab_pane" v-show="activeTab === 1">
<choose-index-list
@updateCheck="updateCheckedUser"
:indexList="getChooseData.indexList"
:itemArr="getChooseData.dataList"
:checkedIDList="checkedUserIDList"
:disabledIDList="disabledUserIDList"
:showCheck="true"
/>
</view>
</template>
</view>
<choose-index-footer
:comfirmLoading="comfirmLoading"
@removeItem="updateCheckedUserOrGroup"
@confirm="confirm"
:choosedData="getCheckedInfo"
/>
</view>
</template>
<script>
import { mapGetters } from "vuex";
import { ContactChooseTypes } from "@/constant";
import { formatChooseData, toastWithCallback } from "@/util/common";
import IMSDK from "openim-uniapp-polyfill";
import CustomNavBar from "@/components/CustomNavBar/index.vue";
import UserItem from "@/components/UserItem/index.vue";
import ChooseIndexList from "@/components/ChooseIndexList/index.vue";
import ChooseIndexFooter from "@/components/ChooseIndexFooter/index.vue";
import SettingItem from "@/components/SettingItem/index.vue";
export default {
components: {
CustomNavBar,
UserItem,
ChooseIndexList,
ChooseIndexFooter,
SettingItem,
},
data() {
return {
keyword: "",
type: ContactChooseTypes.Card,
activeTab: 0,
groupID: "",
checkedUserIDList: [],
disabledUserIDList: [],
comfirmLoading: false,
tabs: [
{
idx: 1,
title: "我的好友",
},
],
};
},
computed: {
...mapGetters([
"storeFriendList",
"storeCurrentConversation",
"storeCurrentUserID",
"storeConversationList",
]),
getChooseData() {
if (this.keyword) {
return {
indexList: ["#"],
dataList: [
this.storeFriendList.filter(
(friend) =>
friend.nickname.includes(this.keyword) ||
friend.remark.includes(this.keyword)
),
],
};
}
return formatChooseData(this.storeFriendList);
},
getCheckedInfo() {
const tmpUserIDList = [...this.checkedUserIDList];
const checkedFriends = this.storeFriendList.filter((friend) => {
const idx = tmpUserIDList.findIndex(
(userID) => userID === friend.userID
);
if (idx > -1) {
tmpUserIDList.splice(idx, 1);
}
return idx > -1;
});
return [...checkedFriends];
},
},
onLoad(options) {
const {
groupID,
type,
checkedUserIDList,
} = options;
this.type = type;
this.groupID = groupID;
this.checkedUserIDList = checkedUserIDList
? JSON.parse(checkedUserIDList)
: [];
if (this.type === ContactChooseTypes.Invite) {
this.checkDisabledUser();
}
},
methods: {
checkDisabledUser() {
const friendIDList = this.storeFriendList.map((friend) => friend.userID);
IMSDK.asyncApi("getUsersInGroup", IMSDK.uuid(), {
groupID: this.groupID,
userIDList: friendIDList,
}).then(({ data }) => {
this.disabledUserIDList = data;
});
},
tabChange(idx) {
this.keyword = "";
this.activeTab = idx;
},
updateCheckedUserOrGroup(item) {
if (item.userID) {
this.updateCheckedUser(item);
}
},
updateCheckedUser({ userID }) {
if (this.checkedUserIDList.includes(userID)) {
const idx = this.checkedUserIDList.findIndex((item) => item === userID);
const tmpArr = [...this.checkedUserIDList];
tmpArr.splice(idx, 1);
this.checkedUserIDList = [...tmpArr];
} else {
this.checkedUserIDList = [...this.checkedUserIDList, userID];
}
},
confirm() {
if (this.activeTab) {
this.activeTab = 0;
return;
}
this.comfirmLoading = true;
if (this.type === ContactChooseTypes.GetList) {
let pages = getCurrentPages();
let prevPage = pages[pages.length - 2];
prevPage.$vm.getCheckedUsers(this.getCheckedInfo);
this.comfirmLoading = false;
uni.navigateBack({
delta: 1,
});
return;
}
if (this.type === ContactChooseTypes.Invite) {
IMSDK.asyncApi(IMSDK.IMMethods.InviteUserToGroup, IMSDK.uuid(), {
groupID: this.groupID,
reason: "",
userIDList: this.getCheckedInfo.map((user) => user.userID),
})
.then(() => {
toastWithCallback("操作成功", () => uni.navigateBack());
this.comfirmLoading = false;
})
.catch(() => toastWithCallback("操作失败"));
return;
}
this.comfirmLoading = false;
},
},
onBackPress() {
if (this.activeTab) {
this.activeTab = 0;
return true;
}
return false;
},
};
</script>
<style lang="scss" scoped>
/deep/.u-popup {
flex: none;
}
.contact_choose_container {
height: 100vh;
display: flex;
flex-direction: column;
.search_bar_wrap {
height: 34px;
padding: 12px 22px;
}
.tab_container {
@include colBox(false);
flex: 1;
overflow: hidden;
.setting_item {
padding: 32rpx 36rpx;
}
.title {
height: 60rpx;
display: flex;
justify-content: start;
align-items: center;
// padding: 16rpx 8rpx;
background: #f8f9fa;
color: #8e9ab0;
font-size: 24rpx;
}
.tabs_bar {
@include vCenterBox();
justify-content: space-evenly;
.tab_item {
@include colBox(false);
align-items: center;
image {
width: 50px;
height: 50px;
}
}
}
.tab_pane {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.member_list {
flex: 1;
height: 80% !important;
/deep/uni-scroll-view {
max-height: 100% !important;
}
}
.user_list {
height: 100% !important;
}
.member_anchor {
background-color: #f8f8f8 !important;
border: none !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,213 @@
<template>
<view class="create_group_container">
<custom-nav-bar title="发起群聊" />
<u-toast ref="uToast"></u-toast>
<view class="main">
<view class="group_base_info">
<my-avatar
@click="chooseImage"
:isGroup="true"
:src="groupFaceUrl"
size="44"
/>
<u--input
placeholder="取个群名称方便后续搜索"
border="none"
maxlength="16"
v-model="groupName"
></u--input>
</view>
<view class="member_row" @click="toChooseMember">
<view class="desc_title">
<text>群成员</text>
<text>{{ `${checkedMemberList.length}` }}</text>
</view>
<view class="member_list">
<view
v-for="member in checkedMemberList.slice(0, 5)"
:key="member.userID"
class="member_item"
>
<my-avatar :src="member.userID" :desc="member.nickname" size="42" />
</view>
</view>
</view>
</view>
<view class="action_bar">
<u-button
:loading="createLoading"
:disabled="disabledCreate"
@click="complateCreate"
type="primary"
text="完成创建"
></u-button>
</view>
</view>
</template>
<script>
import { ContactChooseTypes } from "@/constant";
import IMSDK, {
GroupType,
IMMethods,
SessionType,
} from "openim-uniapp-polyfill";
import CustomNavBar from "@/components/CustomNavBar/index.vue";
import MyAvatar from "@/components/MyAvatar/index.vue";
import { navigateToDesignatedConversation } from "@/util/imCommon";
import { getPurePath, toastWithCallback } from "@/util/common";
export default {
components: {
CustomNavBar,
MyAvatar,
},
data() {
return {
groupName: "",
groupFaceUrl: "",
checkedMemberList: [],
fileList: [],
createLoading: false,
};
},
computed: {
disabledCreate() {
return !this.groupName || this.checkedMemberList.length === 0;
},
},
onLoad(options) {
const { checkedMemberList } = options;
this.checkedMemberList = checkedMemberList
? JSON.parse(checkedMemberList)
: [];
},
methods: {
toChooseMember() {
const checkedIDList = this.checkedMemberList.map(
(member) => member.userID,
);
uni.navigateTo({
url: `/pages/common/contactChoose/index?type=${
ContactChooseTypes.GetList
}&checkedUserIDList=${JSON.stringify(checkedIDList)}`,
});
},
complateCreate() {
this.createLoading = true;
const options = {
adminUserIDs: [],
memberUserIDs: this.checkedMemberList.map((member) => member.userID),
groupInfo: {
groupType: GroupType.WorkingGroup,
groupName: this.groupName,
faceURL: this.groupFaceUrl,
},
};
IMSDK.asyncApi(IMSDK.IMMethods.CreateGroup, IMSDK.uuid(), options)
.then(({ data }) => {
toastWithCallback("创建成功", () =>
navigateToDesignatedConversation(
data.groupID,
SessionType.WorkingGroup,
true,
),
);
})
.finally(() => (this.createLoading = false));
},
getCheckedUsers(list) {
this.checkedMemberList = [...list];
},
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ["compressed"],
success: async ({ tempFilePaths }) => {
const path = tempFilePaths[0];
const nameIdx = path.lastIndexOf("/") + 1;
const typeIdx = path.lastIndexOf(".") + 1;
const fileName = path.slice(nameIdx);
const fileType = path.slice(typeIdx);
try {
const {
data: { url },
} = await IMSDK.asyncApi(IMMethods.UploadFile, IMSDK.uuid(), {
filepath: getPurePath(tempFilePaths[0]),
name: fileName,
contentType: fileType,
uuid: IMSDK.uuid(),
});
this.groupFaceUrl = url;
} catch (error) {
uni.$u.toast("上传失败");
}
},
fail: function (err) {
uni.$u.toast("上传失败");
},
});
},
},
};
</script>
<style lang="scss">
.create_group_container {
@include colBox(false);
height: 100vh;
background-color: #f6f6f6;
.main {
display: flex;
flex-direction: column;
flex: 1;
}
.group_base_info {
@include vCenterBox();
padding: 44rpx;
background-color: #fff;
margin: 36rpx 0;
.u-input {
margin-left: 48rpx;
}
}
.member_row {
padding: 44rpx;
background-color: #fff;
color: #999;
.desc_title {
@include vCenterBox();
justify-content: space-between;
}
.member_list {
display: flex;
flex-wrap: wrap;
margin-top: 24rpx;
.member_item {
@include colBox(false);
align-items: center;
margin-right: 12rpx;
.member_name {
@include nomalEllipsis();
max-width: 42px;
margin-top: 12rpx;
}
}
}
}
.action_bar {
background-color: #fff;
padding: 44rpx 44rpx;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<view class="details_container">
<custom-nav-bar title="个人资料" />
<view class="info_list">
<user-info-row-item class="info_item" lable="头像" arrow>
<my-avatar
:src="sourceInfo.faceURL"
:desc="sourceInfo.nickname"
size="26"
/>
</user-info-row-item>
<user-info-row-item class="info_item" lable="昵称" arrow>
<text class="right_content">{{ sourceInfo.nickname }}</text>
</user-info-row-item>
<user-info-row-item class="info_item" lable="性别" arrow>
<text class="right_content">{{ getGender }}</text>
</user-info-row-item>
<user-info-row-item class="info_item" lable="生日" arrow>
<text class="right_content">{{ getBirthStr }}</text>
</user-info-row-item>
</view>
<view class="info_list">
<user-info-row-item class="info_item" lable="手机号码" arrow>
<text class="right_content">{{ sourceInfo.phoneNumber || "-" }}</text>
</user-info-row-item>
<user-info-row-item class="info_item" lable="邮箱" arrow>
<text class="right_content">{{ sourceInfo.email || "-" }}</text>
</user-info-row-item>
</view>
</view>
</template>
<script>
import dayjs from "dayjs";
import CustomNavBar from "@/components/CustomNavBar/index.vue";
import MyAvatar from "@/components/MyAvatar/index.vue";
import UserInfoRowItem from "../userCard/components/UserInfoRowItem.vue";
export default {
components: {
CustomNavBar,
MyAvatar,
UserInfoRowItem,
},
data() {
return {
sourceInfo: {},
};
},
computed: {
getGender() {
if (this.sourceInfo.gender === 1) {
return "男";
}
if (this.sourceInfo.gender === 2) {
return "女";
}
return "保密";
},
getBirthStr() {
const birth = this.sourceInfo.birth ?? 0;
return dayjs(birth).format("YYYY-MM-DD");
},
},
onLoad(options) {
const { sourceInfo } = options;
this.sourceInfo = JSON.parse(sourceInfo);
},
};
</script>
<style lang="scss" scoped>
.details_container {
@include colBox(false);
height: 100vh;
background-color: #f6f6f6;
.info_list {
border-radius: 6px;
overflow: hidden;
margin: 24rpx;
.info_item {
background-color: #fff;
// border-bottom: 1px solid rgba(153, 153, 153, 0.3);
.right_content {
color: #999;
}
}
}
}
</style>

View File

@@ -0,0 +1,309 @@
<template>
<view class="group_card_container">
<custom-nav-bar title="" />
<u-toast ref="uToast"></u-toast>
<view class="main">
<view class="base_info">
<my-avatar :src="sourceGroupInfo.faceURL" :isGroup="true" size="48" />
<view>
<view class="group_name">
<text>{{ sourceGroupInfo.groupName }}</text>
<text v-if="!!sourceGroupInfo.memberCount"
>{{ sourceGroupInfo.memberCount }}</text
>
</view>
<view class="create_time">
<u-icon name="clock" color="#999" size="14"></u-icon>
<text>{{ getCreateTime }}</text>
</view>
</view>
</view>
<view
v-if="!!sourceGroupInfo.memberCount"
@click="toMemberList"
class="member_row info_row"
>
<view class="member_desc">
<text>群成员</text>
<text class="member_count">{{
`${sourceGroupInfo.memberCount}`
}}</text>
<u-icon name="arrow-right" color="#999" size="18"></u-icon>
</view>
<view class="member_list">
<my-avatar
v-for="member in getRenderMemberList"
:key="member.userID"
class="member_item"
size="42"
:src="member.faceURL"
:desc="member.nickname"
></my-avatar>
<u-avatar
bgColor="#5496EB"
icon="more-dot-fill"
shape="square"
size="42"
></u-avatar>
</view>
</view>
<view class="info_row">
<user-info-row-item lable="群ID号" :content="sourceGroupInfo.groupID" />
</view>
</view>
<view class="action_row">
<u-button type="primary" v-if="!isJoinedGroup" @click="joinGroup"
>申请加入该群</u-button
>
<u-button type="primary" v-else @click="chatingInGroup">发消息</u-button>
</view>
</view>
</template>
<script>
import { GroupMemberListTypes } from "@/constant";
import { navigateToDesignatedConversation } from "@/util/imCommon";
import IMSDK, {
GroupVerificationType,
SessionType,
} from "openim-uniapp-polyfill";
import dayjs from "dayjs";
import MyAvatar from "@/components/MyAvatar/index.vue";
import CustomNavBar from "@/components/CustomNavBar/index.vue";
import UserInfoRowItem from "../userCard/components/UserInfoRowItem.vue";
import userIcon from "static/images/contact_my_friend.png";
export default {
components: {
CustomNavBar,
MyAvatar,
UserInfoRowItem,
},
data() {
return {
sourceID: "",
isScan: false,
sourceGroupInfo: {},
groupMemberList: [],
};
},
computed: {
isJoinedGroup() {
return (
this.$store.getters.storeGroupList.findIndex(
(group) => group.groupID === this.sourceID,
) !== -1
);
},
getCreateTime() {
return dayjs(this.sourceGroupInfo.createTime).format("YYYY-MM-DD");
},
getRenderMemberList() {
if (this.isJoinedGroup) {
this.groupMemberList;
return this.groupMemberList;
}
const memberCount = this.sourceGroupInfo.memberCount ?? 0;
return new Array(memberCount >= 6 ? 6 : memberCount)
.fill(1)
.map((item, idx) => ({
userID: idx,
src: userIcon,
}));
},
},
onLoad(options) {
const { sourceID, sourceInfo, isScan } = options;
this.isScan = !!isScan;
if (sourceID) {
this.sourceID = sourceID;
this.getSourceGroupInfo();
} else {
const info = JSON.parse(sourceInfo);
this.sourceID = info.groupID;
this.sourceGroupInfo = {
...info,
};
}
this.getGroupMemberList();
},
methods: {
toMemberList() {
if (this.isJoinedGroup) {
this.$store.dispatch("conversation/getCurrentGroup", this.sourceID);
this.$store.dispatch(
"conversation/getCurrentMemberInGroup",
this.sourceID,
);
uni.navigateTo({
url: `/pages/conversation/groupMemberList/index?type=${GroupMemberListTypes.Preview}&groupID=${this.sourceID}`,
});
}
},
joinGroup() {
uni.$u.route("/pages/common/sendAddRequest/index", {
isGroup: true,
sourceID: this.sourceID,
isScan: this.isScan,
notNeedVerification:
this.sourceGroupInfo.needVerification ===
GroupVerificationType.AllNot,
sessionType: SessionType.WorkingGroup,
});
},
chatingInGroup() {
navigateToDesignatedConversation(
this.sourceID,
SessionType.WorkingGroup,
).catch(() => this.showToast("获取会话信息失败"));
},
async getSourceGroupInfo() {
let info = null;
if (this.isJoinedGroup) {
info = this.$store.getters.storeGroupList.find(
(group) => group.groupID === this.sourceID,
);
} else {
try {
const { data } = await IMSDK.asyncApi(
IMSDK.IMMethods.GetSpecifiedGroupsInfo,
IMSDK.uuid(),
[this.sourceID],
);
info = data[0] ?? {};
} catch (e) {
info = {};
}
}
this.sourceGroupInfo = {
...info,
};
},
getGroupMemberList() {
if (this.isJoinedGroup) {
IMSDK.asyncApi(IMSDK.IMMethods.GetGroupMemberList, IMSDK.uuid(), {
groupID: this.sourceID,
filter: 0,
offset: 0,
count: 6,
}).then(({ data }) => {
this.groupMemberList = [...data];
});
}
},
showToast(message) {
this.$refs.uToast.show({
message,
});
},
},
};
</script>
<style lang="scss" scoped>
.group_card_container {
@include colBox(false);
height: 100vh;
background-color: #f6f6f6;
.main {
display: flex;
flex-direction: column;
flex: 1;
}
.base_info {
@include vCenterBox();
background-color: #fff;
padding: 44rpx;
margin-bottom: 18rpx;
.u-avatar {
margin-right: 24rpx;
}
.group_name {
display: flex;
margin-bottom: 12rpx;
}
.create_time {
@include vCenterBox();
justify-content: center;
color: #adadad;
font-size: 26rpx;
.u-icon {
margin-right: 12rpx;
}
}
}
.member_row {
padding: 24rpx 44rpx;
.member_desc {
margin-bottom: 24rpx;
position: relative;
.member_count {
font-size: 28rpx;
color: #adadad;
margin-left: 24rpx;
}
.u-icon {
position: absolute;
right: 0;
top: 0;
}
}
.member_list {
display: flex;
.member_item {
margin-right: 12rpx;
&:nth-child(7) {
margin-right: 0;
}
}
}
}
.info_row {
background-color: #fff;
margin-bottom: 24rpx;
/deep/ .content {
color: #adadad;
}
}
.action_row {
background-color: #fff;
padding: 44rpx 44rpx;
}
.online_state {
@include vCenterBox();
margin-left: 24rpx;
font-size: 24rpx;
color: #999;
.dot {
background-color: #10cc64;
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 12rpx;
}
}
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<view class="mark_id_container">
<custom-nav-bar :title="getTitle">
<view class="nav_right_action" slot="more">
<text v-show="!loading" @click="saveOrCopy">{{ getConfirmText }}</text>
<u-loading-icon v-show="loading" />
</view>
</custom-nav-bar>
<view class="content_row">
<u-input
:disabled="!isRemark && !isSelfNickname"
v-model="content"
disabledColor="transparent"
maxlength="16"
placeholder="请输入内容"
clearable
>
</u-input>
</view>
</view>
</template>
<script>
import IMSDK from "openim-uniapp-polyfill";
import CustomNavBar from "@/components/CustomNavBar/index.vue";
import { businessInfoUpdate } from "@/api/login";
export default {
components: {
CustomNavBar,
},
props: {},
data() {
return {
content: "",
isRemark: false,
isSelfNickname: false,
sourceInfo: {},
loading: false,
};
},
computed: {
getTitle() {
if (this.isRemark) {
return "设置备注";
}
if (this.isSelfNickname) {
return "我的姓名";
}
return "ID号";
},
getConfirmText() {
return this.isRemark || this.isSelfNickname ? "保存" : "复制";
},
},
onLoad(options) {
const { isRemark, isSelfNickname, sourceInfo } = options;
this.sourceInfo = JSON.parse(sourceInfo);
this.isRemark = !!isRemark;
if (this.isRemark) {
this.content = this.sourceInfo.remark;
}
this.isSelfNickname = !!isSelfNickname;
if (this.isSelfNickname) {
this.content = this.sourceInfo.nickname;
}
},
methods: {
async saveOrCopy() {
if (this.isRemark) {
this.loading = true;
IMSDK.asyncApi(IMSDK.IMMethods.SetFriendRemark, IMSDK.uuid(), {
toUserID: this.sourceInfo.userID,
remark: this.content,
})
.then(() => {
uni.$u.toast("设置成功");
setTimeout(() => uni.navigateBack(), 1000);
})
.catch((error) => {
console.log(error);
uni.$u.toast("设置失败");
})
.finally(() => (this.loading = false));
} else if (this.isSelfNickname) {
this.loading = true;
try {
await businessInfoUpdate({
userID: this.sourceInfo.userID,
nickname: this.content,
});
await this.$store.dispatch("user/updateBusinessInfo");
uni.$u.toast("修改成功");
setTimeout(() => uni.navigateBack(), 1000);
} catch (e) {
console.log(e);
uni.$u.toast("修改失败");
}
this.loading = false;
} else {
uni.setClipboardData({
data: this.sourceInfo.userID,
success: () => {
uni.hideToast();
this.$nextTick(() => {
uni.$u.toast("复制成功");
});
},
});
}
},
},
};
</script>
<style lang="scss" scoped>
.mark_id_container {
@include colBox(false);
height: 100vh;
.nav_right_action {
margin-right: 36rpx;
}
.content_row {
margin-top: 96rpx;
margin: 72rpx 44rpx 0;
.u-input {
background-color: #e8eaef;
}
.u-button {
height: 60rpx;
}
}
}
</style>

View File

@@ -0,0 +1,172 @@
<template>
<view class="search_container">
<custom-nav-bar :route="false">
<view slot="left"> </view>
<view class="search_bar" slot="center">
<u-search
actionText="取消"
@change="keywordChange"
@custom="cancel"
@search="startSearch"
shape="square"
:placeholder="getPlaceholder"
v-model="keyword"
/>
</view>
</custom-nav-bar>
<view v-show="!empty && !searching" @click="startSearch(keyword)" class="result_row">
<image class="icon" :src="getIcon" alt="" />
<view class="">
<text>查找</text>
<text>{{ keyword }}</text>
</view>
</view>
<view v-show="searching && !empty" class="result_row result_row_empty">
<u-loading-icon></u-loading-icon>
</view>
<view v-show="empty" class="result_row result_row_empty">
<text>未搜索到相关结果</text>
</view>
</view>
</template>
<script>
import IMSDK from "openim-uniapp-polyfill";
import CustomNavBar from "@/components/CustomNavBar/index.vue";
import searchGroup from "static/images/contact_add_join_group_fill.png";
import searchUser from "static/images/contact_add_search_user_fill.png";
import { businessSearchUserInfo } from "@/api/login";
export default {
components: {
CustomNavBar,
},
data() {
return {
keyword: "",
searching: false,
empty: false,
isSearchGroup: false,
};
},
computed: {
getIcon() {
return this.isSearchGroup ? searchGroup : searchUser;
},
getPlaceholder() {
return this.isSearchGroup ? "请输入群聊ID" : "搜索ID或手机号添加好友";
},
},
onLoad(options) {
const { isSearchGroup } = options;
this.isSearchGroup = JSON.parse(isSearchGroup);
},
methods: {
cancel() {
console.log("cancel");
uni.navigateBack();
},
keywordChange() {
if (this.empty) {
this.empty = !this.empty;
}
},
async startSearch(value) {
if (!value) return;
this.searching = true;
try {
if (this.isSearchGroup) {
let info = this.$store.getters.storeGroupList.find(
(item) => item.groupID === value,
);
if (!info) {
const { data } = await IMSDK.asyncApi(
IMSDK.IMMethods.GetSpecifiedGroupsInfo,
IMSDK.uuid(),
[value],
);
info = data[0];
}
if (info) {
uni.navigateTo({
url: `/pages/common/groupCard/index?sourceInfo=${JSON.stringify(
info,
)}`,
});
} else {
this.empty = true;
}
} else {
let info = this.$store.getters.storeFriendList.find(
(item) => item.userID === value,
);
if (!info) {
const { total, users } = await businessSearchUserInfo(value);
if (total > 0) {
const { data } = await IMSDK.asyncApi(
IMSDK.IMMethods.GetUsersInfo,
IMSDK.uuid(),
[users[0].userID],
);
const imData = data[0];
info = {
...imData,
...users[0],
};
}
}
if (info) {
uni.navigateTo({
url: `/pages/common/userCard/index?sourceInfo=${JSON.stringify(
info,
)}`,
});
} else {
this.empty = true;
}
}
} catch (e) {
//TODO handle the exception
}
this.searching = false;
},
},
};
</script>
<style lang="scss" scoped>
.search_container {
height: 100vh;
background-color: #f8f8f8;
.search_bar {
width: 100%;
padding: 0 44rpx;
}
.result_row {
@include vCenterBox();
padding: 24rpx 44rpx;
font-size: 28rpx;
color: $uni-text-color;
background-color: #fff;
.icon {
width: 20px;
height: 20px;
margin-right: 24rpx;
}
&_empty {
display: flex;
justify-content: center;
color: #999;
}
}
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<view class="request_join_container">
<custom-nav-bar :title="isGroup ? '群聊验证' : '好友验证'">
<view class="top_right_btn" slot="more">
<u-button @click="sendRequest" text="发送" type="primary"></u-button>
</view>
</custom-nav-bar>
<text class="title">{{ `发送${isGroup ? "入群" : "好友"}申请` }}</text>
<view class="input_container">
<u--textarea
height="120"
v-model="reason"
border="none"
placeholder="请输入内容"
maxlength="20"
count
>
</u--textarea>
</view>
</view>
</template>
<script>
import IMSDK, { GroupJoinSource } from "openim-uniapp-polyfill";
import CustomNavBar from "@/components/CustomNavBar/index.vue";
import { navigateToDesignatedConversation } from "@/util/imCommon";
export default {
components: {
CustomNavBar,
},
data() {
return {
reason: "",
sourceID: "",
isGroup: false,
isScan: false,
notNeedVerification: false,
sessionType: 0,
};
},
onLoad(options) {
const { isGroup, sourceID, isScan, notNeedVerification, sessionType } =
options;
this.isGroup = JSON.parse(isGroup);
this.isScan = JSON.parse(isScan);
this.sourceID = sourceID;
this.notNeedVerification = JSON.parse(notNeedVerification);
this.sessionType = sessionType ?? 0;
},
methods: {
sendRequest() {
let func;
if (this.isGroup) {
const joinSource = this.isScan
? GroupJoinSource.QrCode
: GroupJoinSource.Search;
func = IMSDK.asyncApi(IMSDK.IMMethods.JoinGroup, IMSDK.uuid(), {
groupID: this.sourceID,
reqMsg: this.reason,
joinSource,
});
} else {
func = IMSDK.asyncApi(IMSDK.IMMethods.AddFriend, IMSDK.uuid(), {
toUserID: this.sourceID,
reqMsg: this.reason,
});
}
func
.then(() => {
uni.$u.toast(this.notNeedVerification ? "你已加入该群" : "发送成功");
setTimeout(() => {
if (this.notNeedVerification) {
navigateToDesignatedConversation(
this.sourceID,
Number(this.sessionType),
).catch(() => this.showToast("获取会话信息失败"));
} else {
uni.navigateBack();
}
}, 1000);
})
.catch((err) => {
console.log(err);
uni.$u.toast("发送失败");
});
},
showToast(message) {
this.$refs.uToast.show({
message,
});
},
},
};
</script>
<style lang="scss">
.request_join_container {
@include colBox(false);
height: 100vh;
background-color: #f6f6f6;
.top_right_btn {
margin-right: 44rpx;
.u-button {
height: 48rpx;
}
}
.title {
font-size: 28rpx;
color: #999;
margin: 24rpx 44rpx;
}
.input_container {
/deep/.u-textarea {
padding: 24rpx 44rpx !important;
}
}
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<view @click="click" class="row_item" :class="{ arrow_right: arrow }">
<view class="title">
<text>{{ lable }}</text>
</view>
<view class="content">
<text>{{ content }}</text>
</view>
<slot>
<u-icon v-if="arrow" name="arrow-right" color="#999" size="20"></u-icon>
</slot>
</view>
</template>
<script>
export default {
name: "",
components: {},
props: {
lable: String,
content: String,
arrow: {
type: Boolean,
default: false,
},
},
data() {
return {};
},
methods: {
click() {
this.$emit("click");
},
},
};
</script>
<style lang="scss" scoped>
.row_item {
@include vCenterBox();
padding: 24rpx 44rpx;
}
.title {
margin-right: 24rpx;
}
.arrow_right {
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,298 @@
<template>
<view class="user_card_container">
<u-loading-page :loading="isLoading" loading-text="loading..."></u-loading-page>
<custom-nav-bar title="" />
<view v-if="!isLoading" style="flex: 1;display: flex;flex-direction: column;">
<view class="base_info">
<my-avatar
:desc="sourceUserInfo.remark || sourceUserInfo.nickname"
:src="sourceUserInfo.faceURL"
size="46"
/>
<view class="user_name">
<text class="text">{{ getShowName }}</text>
<text class="id" @click="copy(sourceUserInfo.userID)">{{
sourceUserInfo.userID
}}</text>
</view>
<view class="add_btn" @click="toAddFriend" v-if="trySendRequest">
<u-button type="primary" icon="man-add" text="添加"></u-button>
</view>
</view>
<view v-if="isFriend" class="info_row">
<user-info-row-item @click="toMoreInfo" lable="个人资料" arrow />
</view>
<view class="action_row" v-if="!isSelf">
<view @click="toDesignatedConversation" class="action_item">
<img src="static/images/user_card_message.png" alt="" />
<text>发消息</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { mapGetters } from "vuex";
import { navigateToDesignatedConversation } from "@/util/imCommon";
import IMSDK, {
SessionType,
} from "openim-uniapp-polyfill";
import MyAvatar from "@/components/MyAvatar/index.vue";
import CustomNavBar from "@/components/CustomNavBar/index.vue";
import UserInfoRowItem from "./components/UserInfoRowItem.vue";
import { businessSearchUserInfo } from "@/api/login";
export default {
components: {
CustomNavBar,
MyAvatar,
UserInfoRowItem,
},
data() {
return {
isLoading: false,
sourceID: "",
sourceUserInfo: {},
switchLoading: false,
showSetRole: false,
};
},
computed: {
...mapGetters([
"storeFriendList",
"storeSelfInfo",
]),
isFriend() {
return (
this.storeFriendList.findIndex(
(friend) => friend.userID === this.sourceID,
) !== -1
);
},
trySendRequest() {
return !this.isFriend && !this.isSelf
},
isSelf() {
return this.sourceID === this.storeSelfInfo.userID;
},
getShowName() {
let suffix = "";
if (this.sourceUserInfo.remark) {
suffix = `(${this.sourceUserInfo.remark})`;
}
return this.sourceUserInfo.nickname + suffix;
},
},
onLoad(options) {
const { sourceID, sourceInfo } = options;
if (sourceID) {
this.sourceID = sourceID;
} else {
const info = JSON.parse(sourceInfo);
this.sourceID = info.userID;
}
this.getSourceUserInfo();
},
methods: {
copy(userID) {
uni.setClipboardData({
showToast: false,
data: userID,
success: function () {
uni.showToast({
icon: "none",
title: "复制成功",
});
},
});
},
async getSourceUserInfo() {
let info = {};
const friendInfo = this.storeFriendList.find((item) => item.userID === this.sourceID);
if (friendInfo) {
info = { ...friendInfo };
}
else {
const { data } = await IMSDK.asyncApi(
IMSDK.IMMethods.GetUsersInfo,
IMSDK.uuid(),
[this.sourceID],
);
info = { ...(data[0] ?? {}) };
}
this.isLoading = true
try {
const { total, users } = await businessSearchUserInfo(this.sourceID);
if (total > 0) {
const { data } = await IMSDK.asyncApi(
IMSDK.IMMethods.GetUsersInfo,
IMSDK.uuid(),
[this.sourceID],
);
const imData = data[0]?.friendInfo ?? data[0]?.publicInfo ?? {};
info = {
...imData,
...users[0],
};
}
} catch (e) {
info = {};
}
this.isLoading = false
this.sourceUserInfo = {
...info,
};
},
toAddFriend() {
uni.$u.route("/pages/common/sendAddRequest/index", {
isGroup: false,
sourceID: this.sourceID,
isScan: false,
notNeedVerification: false,
});
},
toDesignatedConversation() {
navigateToDesignatedConversation(
this.sourceID,
SessionType.Single,
false,
).catch(() => uni.$u.toast("获取会话信息失败"));
},
toMoreInfo() {
uni.navigateTo({
url: `/pages/common/userCardMore/index?sourceInfo=${JSON.stringify(
this.sourceUserInfo,
)}`,
});
}
},
};
</script>
<style lang="scss" scoped>
.user_card_container {
@include colBox(false);
height: 100vh;
background-color: #f6f6f6;
overflow-y: auto;
position: relative;
.base_info {
@include vCenterBox();
background-color: #fff;
padding: 44rpx;
margin-bottom: 18rpx;
.add_btn {
width: 140rpx;
height: 60rpx;
margin-left: auto;
.u-button {
width: 140rpx;
height: 60rpx;
}
}
.u-avatar {
margin-right: 24rpx;
}
.user_name {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-bottom: 12rpx;
height: 46px;
.text {
@include nomalEllipsis();
max-width: 300rpx;
}
}
.company {
font-size: 28rpx;
color: $u-primary;
}
}
.info_row {
background-color: #fff;
margin-bottom: 24rpx;
}
.mute_right {
display: flex;
align-items: center;
}
.company_row {
padding: 20rpx 0;
.desc_title {
padding-left: 44rpx;
}
/deep/.title {
width: 200rpx;
color: #999 !important;
}
}
.action_row {
@include vCenterBox();
align-items: flex-end;
justify-content: space-around;
margin: 44rpx;
flex: 1;
.action_item {
width: 100%;
@include colBox(true);
flex-direction: row;
align-items: center;
justify-content: center;
padding: 22rpx 0;
background: $u-primary;
color: #fff;
border-radius: 12rpx;
img {
margin-right: 16rpx;
width: 40rpx;
height: 40rpx;
}
}
}
.id {
font-size: 24rpx;
color: #999;
}
.online_state {
@include vCenterBox();
margin-left: 24rpx;
font-size: 24rpx;
color: #999;
.dot {
background-color: #10cc64;
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 12rpx;
}
.online_str {
@include nomalEllipsis();
max-width: 280rpx;
}
}
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<view class="user_more_container">
<custom-nav-bar title="好友设置" />
<view class="info_row">
<user-info-row-item @click="toMark" lable="设置备注" arrow />
<user-info-row-item @click="toMore" lable="个人资料" arrow />
</view>
<view class="info_row">
<user-info-row-item lable="加入黑名单" arrow>
<u-switch
asyncChange
:loading="blackLoading"
size="20"
:value="isBlacked"
@change="change"
></u-switch>
</user-info-row-item>
</view>
<view v-if="isFriend" class="info_row">
<u-button
@click="() => (showConfirm = true)"
type="error"
plain
text="解除好友关系"
></u-button>
</view>
<u-toast ref="uToast"></u-toast>
<u-modal
:content="`确定要解除与${sourceInfo.nickname}的好友关系吗?`"
asyncClose
:show="showConfirm"
showCancelButton
@confirm="confirmRemove"
@cancel="() => (showConfirm = false)"
></u-modal>
</view>
</template>
<script>
import IMSDK from "openim-uniapp-polyfill";
import CustomNavBar from "@/components/CustomNavBar/index.vue";
import UserInfoRowItem from "../userCard/components/UserInfoRowItem.vue";
import { ContactChooseTypes } from "@/constant";
export default {
components: {
CustomNavBar,
UserInfoRowItem,
},
data() {
return {
blackLoading: false,
sourceInfo: {},
showConfirm: false,
};
},
computed: {
isFriend() {
return (
this.$store.getters.storeFriendList.findIndex(
(friend) => friend.userID === this.sourceInfo.userID,
) !== -1
);
},
isBlacked() {
return (
this.$store.getters.storeBlackList.findIndex(
(black) => black.userID === this.sourceInfo.userID,
) !== -1
);
},
},
onLoad(options) {
const { sourceInfo } = options;
this.sourceInfo = JSON.parse(sourceInfo);
},
methods: {
change(isBlack) {
this.blackLoading = true;
if (isBlack) {
IMSDK.asyncApi(IMSDK.IMMethods.AddBlack, IMSDK.uuid(), {
toUserID: this.sourceInfo.userID,
ex: "",
})
.catch(() => this.showToast("操作失败"))
.finally(() => (this.blackLoading = false));
return;
}
IMSDK.asyncApi(
IMSDK.IMMethods.RemoveBlack,
IMSDK.uuid(),
this.sourceInfo.userID
)
.catch(() => this.showToast("操作失败"))
.finally(() => (this.blackLoading = false));
},
confirmRemove() {
IMSDK.asyncApi(
IMSDK.IMMethods.DeleteFriend,
IMSDK.uuid(),
this.sourceInfo.userID,
)
.then(() => this.showToast("操作成功"))
.catch(() => this.showToast("操作失败"))
.finally(() => (this.showConfirm = false));
},
toMore() {
uni.navigateTo({
url: `/pages/common/detailsFileds/index?sourceInfo=${JSON.stringify(
this.sourceInfo,
)}`,
});
},
toMark() {
uni.navigateTo({
url: `/pages/common/markOrIDPage/index?isRemark=true&sourceInfo=${JSON.stringify(
this.sourceInfo,
)}`,
});
},
toShare() {
uni.navigateTo({
url: `/pages/common/contactChoose/index?type=${
ContactChooseTypes.ShareCard
}&cardInfo=${JSON.stringify(this.sourceInfo)}`,
});
},
showToast(message) {
this.$refs.uToast.show({
message,
});
},
},
};
</script>
<style lang="scss">
.user_more_container {
@include colBox(false);
height: 100vh;
background-color: #f6f6f6;
.info_row {
background-color: #fff;
margin: 24rpx;
border-radius: 6px;
overflow: hidden;
.u-button {
border: none;
}
}
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<web-view :src="url"></web-view>
</template>
<script>
export default {
data() {
return {
url: "",
};
},
onLoad(options) {
this.url = decodeURIComponent(options.url);
},
};
</script>
<style lang="scss"></style>