9.16下午更新 包含im

This commit is contained in:
zoujiandong 2025-09-16 16:19:29 +08:00
parent 215b3298d0
commit b884a8f6fc
127 changed files with 59972 additions and 182 deletions

418
App.vue
View File

@ -1,15 +1,410 @@
<script>
export default {
onLaunch: function() {
console.log('App Launch')
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
<script >
import RootStore from "@xkit-yx/im-store-v2";
/** esm 版本 */
//@ts-ignore
// import { V2NIMConst, NIM } from './esmNim.js'
/** 常规版本*/
import NIM from "nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK";
import { V2NIMConst } from "nim-web-sdk-ng/dist/esm/nim";
import {
customRedirectTo,
customReLaunch,
customSwitchTab,
} from "@/utils/im/customNavigate";
import { getMsgContentTipByType } from "@/utils/im/msg";
import { STORAGE_KEY } from "@/utils/im/constants";
import { isWxApp } from "@/utils/im/index";
/** 国际化*/
import { setLanguage } from "@/utils/im/i18n";
// #ifdef APP-PLUS
/** 推送插件 */
const nimPushPlugin = uni.requireNativePlugin("NIMUniPlugin-PluginModule");
/** 音视频通话插件 */
const nimCallKit = (uni.$UIKitCallKit =
uni.requireNativePlugin("netease-CallKit"));
// #endif
let startByNotificationId = "";
export default {
onLaunch() {
// #ifdef APP-PLUS
/** 关闭启动画面,锁定竖屏 */
plus.navigator.closeSplashscreen();
plus.screen.lockOrientation("portrait-primary");
// #endif
/** 设置语言 此处为了方便demo切换语言将其存到本地实际需根据业务情况设置*/
setLanguage(
uni.getStorageSync("switchToEnglishFlag") == "en" ? "en" : "zh"
);
/** 已经登录了 不用走初始化逻辑*/
if (
uni?.$UIKitStore?.connectStore?.connectStatus ===
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
) {
return;
}
let storage_token='';
let storage_accid='';
if (process.env.UNI_PLATFORM == "h5") {
if (window.location.href.indexOf('dev') > -1) {
storage_accid=uni.getetStorageSync('AUTH_YX_ACCID_App');
storage_token=uni.setStorageSync('AUTH_YX_TOKEN_App');
} else {
storage_accid=uni.getStorageSync('DEV_AUTH_YX_ACCID_App');
storage_token=uni.getStorageSync('DEV_AUTH_YX_TOKEN_App');
}
} else if(process.env.UNI_PLATFORM == "mp-weixin") {
const {
envVersion
} = uni.getAccountInfoSync().miniProgram;
if (envVersion == "release") {
storage_accid=uni.getetStorageSync('AUTH_YX_ACCID_App');
storage_token=uni.setStorageSync('AUTH_YX_TOKEN_App');
} else {
storage_accid=uni.getStorageSync('DEV_AUTH_YX_ACCID_App');
storage_token=uni.getStorageSync('DEV_AUTH_YX_TOKEN_App');
}
}else{
if (BASE_URL.indexOf('dev') == -1) {
storage_accid=uni.getStorageSync('DEV_AUTH_YX_ACCID_App');
storage_token=uni.getStorageSync('DEV_AUTH_YX_TOKEN_App');
} else {
storage_accid=uni.getetStorageSync('AUTH_YX_ACCID_App');
storage_token=uni.setStorageSync('AUTH_YX_TOKEN_App');
}
}
const account =storage_accid;
const token = storage_token;
const imOptions = {
appkey: "885dea390870814acf3ba8558c717572", // appkey
account: account,//"9ufkll2xo57km6224xe", // account
token: token//"4918605da57e573cff93209df56f351d", // 请填写你的token
};
if (imOptions) {
this.initNim(imOptions);
} else {
/** 未登录 跳转登录页 */
// customRedirectTo({
// url: isWxApp ? "/pages/index/index" : "/pages/Login/index",
// });
}
},
onShow() {
// #ifdef APP-PLUS
uni?.$UIKitNIM?.V2NIMSettingService?.setAppBackground(false);
//
nimPushPlugin.addOpenNotificationListener((res) => {
if (typeof res == "object" && res?.sessionId && res?.sessionType) {
// id
const imOptions = uni.getStorageSync(STORAGE_KEY);
//
const type = res?.sessionType;
// ID
startByNotificationId = `${imOptions.account}|${type}|${res?.sessionId}`;
}
});
// #endif
},
onHide() {
// #ifdef APP-PLUS
uni?.$UIKitNIM?.V2NIMSettingService?.setAppBackground(true);
// #endif
// startByNotificationId
startByNotificationId = "";
},
methods: {
initNim(opts) {
/** 保存登录信息 demo 层逻辑 具体根据您的业务调整*/
uni.setStorage({
key: STORAGE_KEY,
data: opts,
});
/** 是否开启云端会话此处为了方便demo切换云端/本地会话,将其存到本地,实际需根据业务情况设置)*/
const enableV2CloudConversation =
uni.getStorageSync("enableV2CloudConversation") === "on";
/** 初始化 nim sdk */
//@ts-ignore
const nim = (uni.$UIKitNIM = NIM.getInstance(
{
appkey: opts.appkey,
needReconnect: true,
debugLevel: "debug",
apiVersion: "v2",
enableV2CloudConversation: enableV2CloudConversation,
},
{
V2NIMLoginServiceConfig: {
/**
* 微信小程序需要使用单独的lbsUrls和linkUrl
*/
lbsUrls: isWxApp
? ["https://lbs.netease.im/lbs/wxwebconf.jsp"]
: ["https://lbs.netease.im/lbs/webconf.jsp"],
linkUrl: isWxApp ? "wlnimsc0.netease.im" : "weblink.netease.im",
/**
* 使用固定设备ID
*/
isFixedDeviceId: true,
},
}
));
/** 初始化 im store */
// @ts-ignore
const store = (uni.$UIKitStore = new RootStore(
// @ts-ignore
nim,
{
//
addFriendNeedVerify: true,
// p2p p2p false
p2pMsgReceiptVisible: true,
// false
teamMsgReceiptVisible: true,
// 线线
loginStateVisible: true,
//
teamAgreeMode:
V2NIMConst.V2NIMTeamAgreeMode.V2NIM_TEAM_AGREE_MODE_NO_AUTH,
// ,
// @ts-ignore
sendMsgBefore: async (options) => {
const pushContent = getMsgContentTipByType({
text: options.msg.text,
messageType: options.msg.messageType,
});
const yxAitMsg = options.serverExtension
? options.serverExtension.yxAitMsg
: { forcePushIDsList: "[]", needForcePush: false };
// at 线
// @ts-ignore
const { forcePushIDsList, needForcePush } = yxAitMsg
? // @ts-ignore
store.msgStore._formatExtAitToPushInfo(
yxAitMsg,
options.msg.text
)
: { forcePushIDsList: "[]", needForcePush: false };
const { conversationId } = options;
const conversationType =
nim.V2NIMConversationIdUtil.parseConversationType(conversationId);
const targetId =
nim.V2NIMConversationIdUtil.parseConversationTargetId(
conversationId
);
// 线
// https://doc.yunxin.163.com/messaging2/guide/zc4MTg5MDY?platform=client#%E7%AC%AC%E4%B8%80%E6%AD%A5%E4%B8%8A%E4%BC%A0%E6%8E%A8%E9%80%81%E8%AF%81%E4%B9%A6
const pushPayload = JSON.stringify({
pushTitle: "", //
notify_effect: "2", //1appLauncher Activity2appActivityintent_uri3web_uri
intent_uri:
"intent:#Intent;action=com.netease.nimlib.uniapp.push.NotificationClickActivity;component=com.netease.nim.demo/com.netease.nimlib.uniapp.push.NotificationClickActivity;launchFlags=0x04000000;i.sessionType=0;S.sessionId=cs1;end", //app
hwField: {
click_action: {
//
type: 1, //1 2URL 3
// intentintenttype1intentactionschemeactivity
intent:
"intent:#Intent;action=com.netease.nimlib.uniapp.push.NotificationClickActivity;component=com.netease.nim.demo/com.netease.nimlib.uniapp.push.NotificationClickActivity;launchFlags=0x04000000;i.sessionType=0;S.sessionId=cs1;end",
},
androidConfig: {
category: "IM", // AndroidConfig.category
},
},
honorField: {
notification: {
// AndroidNotification
clickAction: {
//
type: 1, //1 2URL 3
//intentintenttype1intentaction
intent: "",
},
importance: "NORMAL", //AndroidLOW NORMAL
},
},
vivoField: {
skipType: "4", // 1APP 2 3 4:app1
skipContent: "",
classification: "1", // 010
category: "IM", //
},
oppoField: {
channel_id: "", //ID
category: "IM", //
notify_level: 2, //1-2-+16-++++
click_action_type: "1", //00.1.action2.4.5.Intent scheme URL: "",
click_action_activity: "",
action_parameters: "",
},
fcmFieldV1: {
message: {
android: {
priority: "",
data: {
sessionType: "0",
sessionId: "cs1",
},
notification: {
click_action: "",
},
},
},
},
// IOS apns
sessionId:
conversationType == 1
? uni.$UIKitStore.userStore.myUserInfo.accountId
: targetId,
sessionType: conversationType,
});
// @ts-ignore
const pushConfig = {
pushEnabled: true,
pushNickEnabled: true,
forcePush: needForcePush,
forcePushContent: pushContent,
forcePushAccountIds: forcePushIDsList,
pushPayload,
pushContent,
};
return { ...options, pushConfig };
},
},
"UniApp"
));
// #ifdef APP-PLUS
/** 注册推送 实际根据您在推送厂商申请的证书进行配置,具体参考文档 https://doc.yunxin.163.com/messaging2/guide/zc4MTg5MDY?platform=client#%E7%AC%AC%E4%B8%80%E6%AD%A5%E4%B8%8A%E4%BC%A0%E6%8E%A8%E9%80%81%E8%AF%81%E4%B9%A6
*/
nim.V2NIMSettingService.setOfflinePushConfig(nimPushPlugin, {
// miPush: {
// appId: "",
// appKey: "",
// certificateName: "",
// },
// hwPush: {
// appId: "",
// certificateName: "",
// },
// oppoPush: {
// appId: "",
// appKey: "",
// certificateName: "",
// secret: "",
// },
// vivoPush: {
// appId: "",
// appKey: "",
// certificateName: "",
// },
// fcmPush: {
// certificateName: "",
// },
// mzPush: {
// appId: "",
// appKey: "",
// certificateName: "",
// },
// apns: {
// certificateName: "",
// },
});
// #endif
/** nim sdk 登录 */
nim.V2NIMLoginService.login(opts.account, opts.token).then(async () => {
// #ifdef APP-PLUS
/** 初始化音视频通话插件*/
nimCallKit.initConfig(
{
appKey: opts.appkey, // appkey
account: opts.account, // account
token: opts.token, // token
apnsCername: "",
pkCername: "",
},
(ret) => {
if (ret.code != 200) {
// callkit init
} else {
nimCallKit.login(
{
account: opts.account,
token: opts.token,
},
function (ret) {
if (ret.code != 200) {
//
} else {
//
}
}
);
}
}
);
// #endif
// ,
if (!startByNotificationId) {
// customSwitchTab({
// url: "/pages/Conversation/index",
// });
} else {
if (startByNotificationId) {
await uni.$UIKitStore.uiStore.selectConversation(
startByNotificationId
);
uni.navigateTo({
url: `/pages/Chat/index?conversationId=${startByNotificationId}`,
});
startByNotificationId = "";
}
}
});
},
logout() {
uni.removeStorageSync(STORAGE_KEY);
try {
nimCallKit.logout({}, (ret) => {
if (ret.code != 200) {
console.log("音视频通话插件退出失败");
} else {
console.log("音视频通话插件退出成功");
}
});
} catch (error) {
console.log("音视频通话插件退出失败", error);
}
// 退
uni.$UIKitNIM.V2NIMLoginService.logout().then(() => {
uni.$UIKitStore.destroy();
customReLaunch({
url: "/pages/Login/index",
});
});
},
},
};
</script>
<style lang="scss">
@ -50,4 +445,7 @@
font-size: 50rpx!important;
}
::-webkit-scrollbar { display: none; }
.nav-right{
margin-top: -20px;
}
</style>

View File

@ -439,6 +439,64 @@ const api = {
getOfficeList(data){
return request('/expertAPI/officeList', data, 'post', false);
},
newConsultList(data){
return request('/expertAPI/newConsultList', data, 'post', false);
},
consultListHis(data){
return request('/expertAPI/consultListHis', data, 'post', false);
},
listNewInterrogation(data){
return request('/expertAPI/listNewInterrogation', data, 'post', false);
},
listMyAnsweredInterrogation(data){
return request('/expertAPI/listMyAnsweredInterrogation', data, 'post', false);
},
getInterrogation(data){
return request('/expertAPI/getInterrogation', data, 'post', false);
},
interrogationPatientInfo(data){
return request('/expertAPI/InterrogationPatientInfo', data, 'post', false);
},
updateInterrogationAnswer(data){
return request('/expertAPI/updateInterrogationAnswer', data, 'post', false);
},
addInterrogationAnswer(data){
return request('/expertAPI/addInterrogationAnswer', data, 'post', false);
},
videoDetail(data){
return request('/expertAPI/videoDetail', data, 'post', false);
},
addVideoWatchRecord(data){
return request('/expertAPI/addVideoWatchRecord', data, 'post', false);
},
videoCommentListV2(data){
return request('/expertAPI/videoCommentListV2', data, 'post', false);
},
isVideoDownloadRecord(data){
return request('/expertAPI/isVideoDownloadRecord', data, 'post', false);
},
getWelfareNum(data){
return request('/expertAPI/getWelfareNum', data, 'post', false);
},
payVideoDownload(data){
return request('/expertAPI/payVideoDownload', data, 'post', false);
},
addCommentV2(data){
return request('/expertAPI/addCommentV2', data, 'post', false);
},
collection(data){
return request('/expertAPI/collection', data, 'post', false);
},
discollection(data){
return request('/expertAPI/discollection', data, 'post', false);
},
meetingV2Video(data){
return request('/expertAPI/meetingV2Video', data, 'post', false);
},
}

View File

@ -0,0 +1,54 @@
<template>
<span
class="appellation"
:style="{ color: color, fontSize: fontSize + 'px' }"
>{{ appellation }}</span
>
</template>
<script lang="ts" setup>
import { autorun } from 'mobx'
import { onUnmounted, ref, withDefaults } from 'vue'
const appellation = ref('')
const { account, teamId, ignoreAlias, nickFromMsg } = withDefaults(
defineProps<{
account: string
teamId?: string
ignoreAlias?: boolean
nickFromMsg?: string
color?: string
fontSize?: number
}>(),
{
teamId: undefined,
ignoreAlias: false,
nickFromMsg: undefined,
color: '#333',
fontSize: 16,
}
)
const uninstallAppellationWatch = autorun(() => {
appellation.value = uni.$UIKitStore.uiStore.getAppellation({
account,
teamId,
ignoreAlias,
nickFromMsg,
})
})
onUnmounted(() => {
uninstallAppellationWatch()
})
</script>
<style scoped lang="scss">
.appellation {
color: #333;
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

193
components/Avatar.vue Normal file
View File

@ -0,0 +1,193 @@
<template>
<div
class="avatar"
:style="{ width: avatarSize + 'px', height: avatarSize + 'px' }"
@click="handleAvatarClick"
@longpress="longpress"
@touchend="touchend"
>
<!-- 使用遮罩层避免android长按头像会出现保存图片的弹窗 -->
<div class="img-mask"></div>
<image
:lazy-load="true"
class="avatar-img"
v-if="avatarUrl"
:src="docUrl+avatarUrl.replaceAll('null','')"
mode="aspectFill"
/>
<image
:lazy-load="true"
class="avatar-img"
v-else-if="userInfo.uuid && account==userInfo.uuid.toLowerCase()"
:src="docUrl+userInfo.photo"
mode="aspectFill"
/>
<div class="avatar-name-wrapper" :style="{ backgroundColor: color }">
<div class="avatar-name-text" :style="{ fontSize: fontSize + 'px' }">
{{ appellation }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { customNavigateTo, customRedirectTo } from '@/utils/im/customNavigate'
import { autorun } from 'mobx'
import { ref, computed, onUnmounted, withDefaults } from 'vue'
import docUrl from "@/utils/docUrl"
import { onShow,onLoad,onReady } from "@dcloudio/uni-app";
import { V2NIMUser } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMUserService'
const props = withDefaults(
defineProps<{
account: string
teamId?: string
avatar?: string
size?: string
gotoUserCard?: boolean
fontSize?: string
isRedirect?: boolean
}>(),
{
teamId: '',
avatar: '',
size: '',
gotoUserCard: false,
fontSize: '',
isRedirect: false,
}
)
const userInfo=ref({
uuid:''
})
const $emit = defineEmits(['onLongpress'])
const avatarSize = props.size || 42
const user = ref<V2NIMUser>()
let isLongPress = false // uniapp
const appellation = ref<string>('')
const appellationWatch = autorun(() => {
appellation.value = uni.$UIKitStore.uiStore
.getAppellation({
account: props.account,
ignoreAlias: false,
})
?.slice(0, 2)
})
const userInfoWatch = autorun(() => {
uni.$UIKitStore?.userStore?.getUserActive(props.account).then((data) => {
user.value = data
})
})
const avatarUrl = computed(() => {
user.value = uni.$UIKitStore?.userStore?.users?.get(props.account)
return props.avatar || user.value?.avatar
})
const key = `__yx_avatar_color_${props.account}__`
let color = uni.getStorageSync(key)
if (!color) {
const colorMap: { [key: number]: string } = {
0: '#60CFA7',
1: '#53C3F3',
2: '#537FF4',
3: '#854FE2',
4: '#BE65D9',
5: '#E9749D',
6: '#F9B751',
}
const _color = colorMap[Math.floor(Math.random() * 7)]
uni.setStorageSync(key, _color)
color = _color
}
const handleAvatarClick = () => {
if (props.gotoUserCard && !isLongPress) {
if (props.isRedirect) {
if (props.account === uni.$UIKitStore?.userStore?.myUserInfo.accountId) {
customRedirectTo({
url: `/pages/User/my-detail/index`,
})
} else {
customRedirectTo({
url: `/pages/User/friend/index?account=${props.account}`,
})
}
} else {
if (props.account === uni.$UIKitStore?.userStore?.myUserInfo.accountId) {
customNavigateTo({
url: `/pages/User/my-detail/index`,
})
} else {
customNavigateTo({
url: `/pages/User/friend/index?account=${props.account}`,
})
}
}
}
}
const longpress = () => {
isLongPress = true
$emit('onLongpress')
}
const touchend = () => {
const timeOut = setTimeout(() => {
isLongPress = false
clearTimeout(timeOut)
}, 200)
}
onShow(()=>{
userInfo.value=uni.getStorageSync('userInfo');
})
onReady(()=>{
userInfo.value=uni.getStorageSync('userInfo');
})
onUnmounted(() => {
userInfoWatch()
appellationWatch()
})
</script>
<style scoped lang="scss">
.avatar {
overflow: hidden;
border-radius: 50%;
flex-shrink: 0;
position: relative;
}
.img-mask {
position: absolute;
z-index: 10;
left: 0;
right: 0;
top: 0;
bottom: 0;
opacity: 0;
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.avatar-name-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-name-text {
color: #fff;
size: 14px;
}
</style>

58
components/Badge.vue Normal file
View File

@ -0,0 +1,58 @@
<template>
<div>
<div class="dot" v-if="dot" :style="customStyle"></div>
<div class="badge" v-else-if="text" :style="customStyle">{{ text }}</div>
<div class="hidden">{{ props.num }}</div>
</div>
</template>
<script lang="ts" setup>
import { StyleValue, computed, withDefaults } from 'vue'
const props = withDefaults(
defineProps<{
num: number
max?: number
dot?: boolean
customStyle?: StyleValue
}>(),
{
max: 99,
dot: false,
}
)
const max = props.max || 99
const text = computed(() => {
return props.num > 0 ? (props.num > max ? `${max}+` : props.num + '') : ''
})
</script>
<style scoped lang="scss">
.dot {
background-color: #ff4d4f;
color: #fff;
width: 10px;
height: 10px;
border-radius: 5px;
box-sizing: border-box;
z-index: 99;
}
.badge {
background-color: #ff4d4f;
color: #fff;
font-size: 12px;
min-width: 20px;
height: 20px;
line-height: 19px;
border-radius: 10px;
padding: 0 5px;
box-sizing: border-box;
text-align: center;
z-index: 99;
position: relative;
}
.hidden {
display: none;
}
</style>

41
components/Empty.vue Normal file
View File

@ -0,0 +1,41 @@
<template>
<div class="empty-wrapper">
<image
class="empty-img"
src="https://yx-web-nosdn.netease.im/common/e0f58096f06c18cdd101f2614e6afb09/empty.png"
/>
<div class="empty-text">{{ text }}</div>
</div>
</template>
<script lang="ts" setup>
withDefaults(
defineProps<{
text?: string
}>(),
{
text: '',
}
)
</script>
<style lang="scss" scoped>
.empty-wrapper {
margin: 75px 10px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.empty-img {
display: block;
width: 125px;
height: 100px;
}
.empty-text {
display: block;
color: #a6adb6;
margin: 10px;
}
}
</style>

139
components/FormInput.vue Normal file
View File

@ -0,0 +1,139 @@
<template>
<div>
<div :class="inputClass">
<slot name="addonBefore" />
<input
class="input"
:type="type"
:value="inputValue"
@input="handleInput"
:focus="inputFocus"
@focus="handleFocus"
@blur="handleBlur"
:placeholder="placeholder"
:maxlength="maxlength"
/>
<div class="clear-icon" @tap="clearInput()">
<icon v-show="modelValue && allowClear" type="clear" size="16" />
</div>
<slot name="addonAfter" />
</div>
<div v-if="inputError && rule" class="error-tips">{{ rule.message }}</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
const $emit = defineEmits([
'update:modelValue',
'input',
'focus',
'blur',
'clear',
])
const props = withDefaults(
defineProps<{
className?: string
type?: string
modelValue?: string
placeholder?: string
allowClear?: boolean
rule?: any
maxlength?: number
}>(),
{
className: '',
type: 'text',
modelValue: '',
placeholder: '',
allowClear: false,
rule: null,
maxlength: 140,
}
)
const inputFocus = ref(false)
const inputError = ref(false)
// const inputKey = ref(0);
const inputClass = computed(() => {
return [
props.className,
'form-input-item',
{ focus: inputFocus.value, error: inputError.value },
]
})
const inputValue = computed(() => {
return props.modelValue || ''
})
const handleBlur = (event: any) => {
inputFocus.value = false
if (props.rule && props.rule.trigger === 'blur') {
inputError.value = !props.rule.reg.test(props.modelValue || '')
}
$emit('blur', event)
}
const handleFocus = (event: any) => {
inputFocus.value = true
$emit('blur', event)
}
const handleInput = (event: any) => {
if (!(props.maxlength && event.detail.value.length > props.maxlength)) {
$emit('update:modelValue', event.detail.value)
$emit('input', event.detail.value)
}
// input
// inputKey.value++;
}
const clearInput = () => {
$emit('update:modelValue', null)
$emit('input', null)
$emit('clear', '')
inputFocus.value = true
}
</script>
<style lang="scss" scoped>
$primary-color: #337eff;
$error-color: #f56c6c;
.form-input-item {
border-bottom: 1px solid #dcdfe5;
padding: 10px 10px 5px 0px;
display: flex;
height: 44px;
align-items: center;
&.focus {
border-color: $primary-color;
}
&.error {
border-color: $error-color;
}
}
.input {
flex: 1;
height: 30px;
border: none;
outline: none;
}
.clear-icon {
width: 40px;
text-align: right;
}
.error-tips {
color: $error-color;
font-size: 12px;
margin-top: 5px;
}
</style>

373
components/Icon.vue Normal file
View File

@ -0,0 +1,373 @@
<template>
<view :class="className" :style="iconStyle">
<!-- #ifdef APP-PLUS -->
<image
:src="iconUrl"
:style="{
width: (width || size) + 'px',
height: (height || size) + 'px',
}"
class="icon"
/>
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<img
:src="iconUrl"
:style="{
width: (width || size) + 'px',
height: (height || size) + 'px',
}"
class="icon"
/>
<!-- #endif -->
</view>
</template>
<script lang="ts" setup>
import { computed, withDefaults } from 'vue'
const props = withDefaults(
defineProps<{
type: string
size?: number
width?: number
height?: number
iconClassName?: string
iconStyle?: any
}>(),
{
size: 16,
}
)
const urlMap: any = {
'icon-a-1':
'https://yx-web-nosdn.netease.im/common/7c2e2c6ff08f4ed60f3ca7d5ab6d38ac/icon-a-1.png',
'icon-a-2':
'https://yx-web-nosdn.netease.im/common/bb61c05013350e980b3acf77a00aa7b7/icon-a-2.png',
'icon-a-3':
'https://yx-web-nosdn.netease.im/common/bfe80222635ec3f07e41b2819d002a4c/icon-a-3.png',
'icon-a-4':
'https://yx-web-nosdn.netease.im/common/e6dd252af94a4716edc191f7d3812042/icon-a-4.png',
'icon-a-5':
'https://yx-web-nosdn.netease.im/common/4e01e93f3e795d8aeb140e924d8b1206/icon-a-5.png',
'icon-a-6':
'https://yx-web-nosdn.netease.im/common/a1fa1174a588a984845ba871b1f1e3b0/icon-a-6.png',
'icon-a-7':
'https://yx-web-nosdn.netease.im/common/e0e30470561cf90446ec31c6d5b206a5/icon-a-7.png',
'icon-a-8':
'https://yx-web-nosdn.netease.im/common/e44ceea2c764bfbe773e61483ae857eb/icon-a-8.png',
'icon-a-9':
'https://yx-web-nosdn.netease.im/common/f4e5b303cb77d9ce67a0baf4efaa1125/icon-a-9.png',
'icon-a-10':
'https://yx-web-nosdn.netease.im/common/28e46e6cc468c4ff6f44532bd6b7f464/icon-a-10.png',
'icon-a-11':
'https://yx-web-nosdn.netease.im/common/f265ea5a782cebabae08a079045caf2c/icon-a-11.png',
'icon-a-12':
'https://yx-web-nosdn.netease.im/common/20043e6f8d619ab7bca69f1a0d5a0e56/icon-a-12.png',
'icon-a-13':
'https://yx-web-nosdn.netease.im/common/c2b2d52a97eea83efb10c9bbc34dc104/icon-a-13.png',
'icon-a-14':
'https://yx-web-nosdn.netease.im/common/37486551a8bd31e3446ae862cddd3067/icon-a-14.png',
'icon-a-15':
'https://yx-web-nosdn.netease.im/common/e383cc57dcb7a903887031ed0d341fef/icon-a-15.png',
'icon-a-16':
'https://yx-web-nosdn.netease.im/common/73e2f9507f8aac7238896a3f1ee8305a/icon-a-16.png',
'icon-a-17':
'https://yx-web-nosdn.netease.im/common/51a80e8e81a8aa531d1c68e6f79f7d58/icon-a-17.png',
'icon-a-18':
'https://yx-web-nosdn.netease.im/common/20f8c6153ba0f51c038e04f2fbedf767/icon-a-18.png',
'icon-a-19':
'https://yx-web-nosdn.netease.im/common/47d420b5251ca7b9ddc564b42564eb01/icon-a-19.png',
'icon-a-20':
'https://yx-web-nosdn.netease.im/common/8fad6b9427b49f5bfd1a8ec0f8a77fac/icon-a-20.png',
'icon-a-21':
'https://yx-web-nosdn.netease.im/common/6fa17035135cebe4ca1512b454fa38c2/icon-a-21.png',
'icon-a-22':
'https://yx-web-nosdn.netease.im/common/12189563eea22258b847cf900dcbc4d4/icon-a-22.png',
'icon-a-23':
'https://yx-web-nosdn.netease.im/common/3b7dcfe14788bd7d8c2da8aad6c24baa/icon-a-23.png',
'icon-a-24':
'https://yx-web-nosdn.netease.im/common/6bb0879272c8a3c73524f3b728a93c10/icon-a-24.png',
'icon-a-25':
'https://yx-web-nosdn.netease.im/common/1f93dac2bdef1f91ca2f7302065954d5/icon-a-25.png',
'icon-a-26':
'https://yx-web-nosdn.netease.im/common/aa3c8e63ee6c6d605f4606a8094f2094/icon-a-26.png',
'icon-a-27':
'https://yx-web-nosdn.netease.im/common/000447b3172d70d1b739a170c377eae2/icon-a-27.png',
'icon-a-28':
'https://yx-web-nosdn.netease.im/common/27c3b8d1acb6f234a7591c2358b549bf/icon-a-28.png',
'icon-a-29':
'https://yx-web-nosdn.netease.im/common/fbae68926f43367022d0b0c95c15adcf/icon-a-29.png',
'icon-a-30':
'https://yx-web-nosdn.netease.im/common/61c0b6c12bc6ad2b7e0511d843b03265/icon-a-30.png',
'icon-a-31':
'https://yx-web-nosdn.netease.im/common/202e7abeaecf651f6da3f7f695ef8752/icon-a-31.png',
'icon-a-32':
'https://yx-web-nosdn.netease.im/common/6ef4958d39866165212c1b60bc7c6a55/icon-a-32.png',
'icon-a-33':
'https://yx-web-nosdn.netease.im/common/18f8b53d57a6b47c3876b6938f2f57b9/icon-a-33.png',
'icon-a-34':
'https://yx-web-nosdn.netease.im/common/84d7491e6d5a6d53848b6a3d12a739b2/icon-a-34.png',
'icon-a-35':
'https://yx-web-nosdn.netease.im/common/f03fe01986e9e25f3faf1a17c0ef78c8/icon-a-35.png',
'icon-a-36':
'https://yx-web-nosdn.netease.im/common/4617253245617bcd38ed7165bdb592d8/icon-a-36.png',
'icon-a-37':
'https://yx-web-nosdn.netease.im/common/9168b11333f59b0ff948003dedb24c84/icon-a-37.png',
'icon-a-38':
'https://yx-web-nosdn.netease.im/common/1a3cd5d62e92895dbab3d356112b5079/icon-a-38.png',
'icon-a-39':
'https://yx-web-nosdn.netease.im/common/b465db737c737ab4308c9272a00a1fdf/icon-a-39.png',
'icon-a-40':
'https://yx-web-nosdn.netease.im/common/6bca766a4904e10934ea2f2450c341b5/icon-a-40.png',
'icon-a-41':
'https://yx-web-nosdn.netease.im/common/12d5055618b619b568342f0619cacf0c/icon-a-41.png',
'icon-a-42':
'https://yx-web-nosdn.netease.im/common/8851ccd815375180bd7f8728ead1cdf1/icon-a-42.png',
'icon-a-43':
'https://yx-web-nosdn.netease.im/common/1ac7cacc9d0063d165f8e2bc3020bfff/icon-a-43.png',
'icon-a-44':
'https://yx-web-nosdn.netease.im/common/5735260c7e240beaabc360365e6d60c5/icon-a-44.png',
'icon-a-45':
'https://yx-web-nosdn.netease.im/common/07372c06915a7d0a977a4f042522e2a7/icon-a-45.png',
'icon-a-46':
'https://yx-web-nosdn.netease.im/common/40c5b2b5769166b51c968942f4abed95/icon-a-46.png',
'icon-a-47':
'https://yx-web-nosdn.netease.im/common/9334c3ec8c8d54a9a3271a3536ff7c62/icon-a-47.png',
'icon-a-48':
'https://yx-web-nosdn.netease.im/common/5630ab646533dd6de7e3c1accc3d2ca1/icon-a-48.png',
'icon-a-49':
'https://yx-web-nosdn.netease.im/common/f2412538fa0da38549c23fd44b25bdfb/icon-a-49.png',
'icon-a-50':
'https://yx-web-nosdn.netease.im/common/1f28ffd1413aa3fcce1c17aff3694d41/icon-a-50.png',
'icon-a-51':
'https://yx-web-nosdn.netease.im/common/a99b57b586a85c23cbd8ed65d1a16765/icon-a-51.png',
'icon-a-52':
'https://yx-web-nosdn.netease.im/common/ce6f64bbe45a42108fd1b1a7b1dae606/icon-a-52.png',
'icon-a-53':
'https://yx-web-nosdn.netease.im/common/ba5dc56bf8f550da8c2dc1c94e2ee7fb/icon-a-53.png',
'icon-a-54':
'https://yx-web-nosdn.netease.im/common/81f5b3b693d9133030c013f0f21462ab/icon-a-54.png',
'icon-a-55':
'https://yx-web-nosdn.netease.im/common/758ed38dea207d60855969689a5dd68b/icon-a-55.png',
'icon-a-56':
'https://yx-web-nosdn.netease.im/common/a20bee2372cbe399191af3d76a5aad31/icon-a-56.png',
'icon-a-57':
'https://yx-web-nosdn.netease.im/common/a17fbdbc64cf4b55be377f18ed81879a/icon-a-57.png',
'icon-a-58':
'https://yx-web-nosdn.netease.im/common/784b31cda55da76d8278a5bd53c2c7e4/icon-a-58.png',
'icon-a-59':
'https://yx-web-nosdn.netease.im/common/c716afbc25b8809dfb9ca80a09051f6f/icon-a-59.png',
'icon-a-60':
'https://yx-web-nosdn.netease.im/common/54de44be20c5d21ffad08176b24560a0/icon-a-60.png',
'icon-a-61':
'https://yx-web-nosdn.netease.im/common/0628fd4507e2a3455028f376d8d5d80a/icon-a-61.png',
'icon-a-62':
'https://yx-web-nosdn.netease.im/common/fa336dc373d5e395a1e9a541dc9953d2/icon-a-62.png',
'icon-a-63':
'https://yx-web-nosdn.netease.im/common/868ee8c664b32b34d554d02fc406ab70/icon-a-63.png',
'icon-a-64':
'https://yx-web-nosdn.netease.im/common/4e33aadf3ca3b918e73ef07e21eb96ac/icon-a-64.png',
'icon-a-65':
'https://yx-web-nosdn.netease.im/common/c6e5563811d94c82426036b9f96d6de5/icon-a-65.png',
'icon-a-66':
'https://yx-web-nosdn.netease.im/common/8baa1f43b4e523e524c54f3340ef21cb/icon-a-66.png',
'icon-a-67':
'https://yx-web-nosdn.netease.im/common/307c3426dccf1252b5967956bcdcf58a/icon-a-67.png',
'icon-a-68':
'https://yx-web-nosdn.netease.im/common/196f62aa8e8a38bbbcd818ad42729714/icon-a-68.png',
'icon-a-70':
'https://yx-web-nosdn.netease.im/common/fb54482390faf9d8d9d607d7e3ab691f/icon-a-70.png',
'icon-a-Frame7':
'https://yx-web-nosdn.netease.im/common/c5c3d0ee0a4000736827cedfd0610172/icon-a-Frame7.png',
'icon-a-Frame8':
'https://yx-web-nosdn.netease.im/common/9f77f834a99c29ed5c4b4b54b7ade468/icon-a-Frame8.png',
'icon-addition':
'https://yx-web-nosdn.netease.im/common/6302fcf17e9c4ac65b392553aaccf9b1/icon-addition.png',
'icon-biaoqing':
'https://yx-web-nosdn.netease.im/common/1a98df356ed629193e50bea570e02a53/icon-biaoqing.png',
'icon-chehui':
'https://yx-web-nosdn.netease.im/common/958cb6797f69bdfbf5441da25ed997c3/icon-chehui.png',
'icon-chuangjianqunzu':
'https://yx-web-nosdn.netease.im/common/7b00839704359b7c10f32af7cc8b5911/icon-chuangjianqunzu.png',
'icon-computed':
'https://yx-web-nosdn.netease.im/common/352cd94c93347f01936b50bb1487b5a4/icon-computed.png',
'icon-erfenzhiyiyidu':
'https://yx-web-nosdn.netease.im/common/aa6b529567ee6aeb801f34f228600b04/icon-erfenzhiyiyidu.png',
'icon-Excel':
'https://yx-web-nosdn.netease.im/common/a9793bbb8a7237e9e92f57ad1b469baf/icon-Excel.png',
'icon-fasong':
'https://yx-web-nosdn.netease.im/common/f8d855dde4840989b4c88e904e1c23bf/icon-fasong.png',
'icon-fuzhi1':
'https://yx-web-nosdn.netease.im/common/9e0ba675eb4548bffe19da823fb712e3/icon-fuzhi1.png',
'icon-guanbi':
'https://yx-web-nosdn.netease.im/common/2d07f146ecb4b616632f0fcfdd02b5be/icon-guanbi.png',
'icon-guanbi1':
'https://yx-web-nosdn.netease.im/common/51bad0b424c2462df8213883e211fe07/icon-guanbi1.png',
'icon-guanyu':
'https://yx-web-nosdn.netease.im/common/24d2344c49b551d5a605b3e5e3e6f6da/icon-guanyu.png',
'icon-huifu':
'https://yx-web-nosdn.netease.im/common/153c273cb7b075fc0c37487655c5cbfc/icon-huifu.png',
'icon-im-xuanzhong':
'https://yx-web-nosdn.netease.im/common/468aaecf148e23c84b821835021dfee1/icon-im-xuanzhong.png',
'icon-jiantou':
'https://yx-web-nosdn.netease.im/common/c9bb28670c41e208b1d9bcedd6de88a1/icon-jiantou.png',
'icon-jiaruqunzu':
'https://yx-web-nosdn.netease.im/common/a27ba47b93131b7c52d829b7012fa966/icon-jiaruqunzu.png',
'icon-kefu':
'https://yx-web-nosdn.netease.im/common/4ddc1b9557e6672b0b45fb8fb992c992/icon-kefu.png',
'icon-lahei':
'https://yx-web-nosdn.netease.im/common/e9054279ca62db944ef6e5a76687a93a/icon-lahei.png',
'icon-lishixiaoxi':
'https://yx-web-nosdn.netease.im/common/77888d982110f3c709a3ff4fd24d48dd/icon-lishixiaoxi.png',
'icon-More':
'https://yx-web-nosdn.netease.im/common/137ad07f3245dc220d5546433db63786/icon-More.png',
'icon-PPT':
'https://yx-web-nosdn.netease.im/common/1991f8d57d1d432fcd930e4049403958/icon-PPT.png',
'icon-qita':
'https://yx-web-nosdn.netease.im/common/c6f687c792ef029bef1e59ccce986922/icon-qita.png',
'icon-quxiaoxiaoximiandarao':
'https://yx-web-nosdn.netease.im/common/f6f54973789b69939c57ae94a38e8d25/icon-quxiaoxiaoximiandarao.png',
'icon-quxiaozhiding':
'https://yx-web-nosdn.netease.im/common/ad2d451e8f1ee36ec9cec6787d6074c0/icon-quxiaozhiding.png',
'icon-RAR1':
'https://yx-web-nosdn.netease.im/common/27a743e5696c1ca4416b75c5dde0252c/icon-RAR1.png',
'icon-shanchu':
'https://yx-web-nosdn.netease.im/common/d96f9c0113af4d86dd4e5ecf2e49711b/icon-shanchu.png',
'icon-shandiao':
'https://yx-web-nosdn.netease.im/common/14eea1edc7801449a30700a3c9d604c6/icon-shandiao.png',
'icon-shezhi':
'https://yx-web-nosdn.netease.im/common/698d483a7aacade68ea976f727184e1e/setting.png',
'icon-shezhi1':
'https://yx-web-nosdn.netease.im/common/eced271ac35864b7c716262f1a37217e/icon-shezhi1.png',
'icon-shipin':
'https://yx-web-nosdn.netease.im/common/455c333219318f3b96f748d6753eda4a/icon-shipin.png',
'icon-shipin8':
'https://yx-web-nosdn.netease.im/common/73fededbe5b97dec0246e438d33ae614/icon-shipin8.png',
'icon-shipinyuyin':
'https://yx-web-nosdn.netease.im/common/09b16fe9a2b824ded4162f25333e44e1/icon-shipinyuyin.png',
'icon-sifenzhisanyidu':
'https://yx-web-nosdn.netease.im/common/b1234a4255d3187fa9a767bfed8f4d96/icon-sifenzhisanyidu.png',
'icon-sifenzhiyiyidu':
'https://yx-web-nosdn.netease.im/common/11c7a7e79c57eda20c5ca9d98b0d7e8a/icon-sifenzhiyiyidu.png',
'icon-sousuo':
'https://yx-web-nosdn.netease.im/common/2ccfdfa640c72167f228e7d76068e5f5/icon-sousuo.png',
'icon-team':
'https://yx-web-nosdn.netease.im/common/140e8fbd1cc2df4c878acb17405471f7/icon-team.png',
'icon-zuojiantou':
'https://yx-web-nosdn.netease.im/common/9ab796030ac24a126dedc60fd60613ce/icon-zuojiantou.png',
'icon-zhuanfa':
'https://yx-web-nosdn.netease.im/common/163a3343b0262a76d72d00bbbb4f8ac9/icon-zhuanfa.png',
'icon-zhongyingwen':
'https://yx-web-nosdn.netease.im/common/80a7dbae8c0207f1896e63746ac1a18a/icon-zhongyingwen.png',
'icon-zhankai':
'https://yx-web-nosdn.netease.im/common/d4f655bff9e278ea732adb6a5317bbca/icon-zhankai.png',
'icon-yinle':
'https://yx-web-nosdn.netease.im/common/98a2a366c7e06ab1b1b6ddf9b0c01d73/icon-yinle.png',
'icon-yidu':
'https://yx-web-nosdn.netease.im/common/d17d4aa7866faca55a6a1180c1e15bf6/icon-yidu.png',
'icon-yanzheng':
'https://yx-web-nosdn.netease.im/common/5a0c2769626284ff646298a7ef1f66c2/icon-yanzheng.png',
'icon-xiaoxizhiding':
'https://yx-web-nosdn.netease.im/common/cef2e824e603dde3d333d128434f90c1/icon-xiaoxizhiding.png',
'icon-xiaoximiandarao':
'https://yx-web-nosdn.netease.im/common/1c92731bb3fa91fa3fc5ff45bf9e4dbe/icon-xiaoximiandarao.png',
'icon-Word':
'https://yx-web-nosdn.netease.im/common/af0e4fa22c4b30a263a7f534b0504c23/icon-Word.png',
'icon-wenjian':
'https://yx-web-nosdn.netease.im/common/d3b36fc953447f9d9630f3b73aaa6ef1/icon-wenjian.png',
'icon-weizhiwenjian':
'https://yx-web-nosdn.netease.im/common/d51b39a07b3b482ab3b50a4b068588c6/icon-weizhiwenjian.png',
'icon-weidu':
'https://yx-web-nosdn.netease.im/common/5d50477d2afa387a59a67e30fcdceabd/icon-weidu.png',
'icon-tupian2':
'https://yx-web-nosdn.netease.im/common/51eb954ad971eb6890d0934858f950aa/icon-tupian2.png',
'icon-tupian1':
'https://yx-web-nosdn.netease.im/common/0737f1e187aa250d5090f38925672485/icon-tupian1.png',
'icon-tupian':
'https://yx-web-nosdn.netease.im/common/aa93aa9ffd0197b9a961455506f75078/icon-tupian.png',
'icon-tuigejian':
'https://yx-web-nosdn.netease.im/common/7bca7dffd1f8c3cd66b8ede7f176e4a8/icon-tuigejian.png',
'icon-tuichudenglu':
'https://yx-web-nosdn.netease.im/common/ce42192020620522a46763c758951b76/icon-tuichudenglu.png',
'icon-touxiang5':
'https://yx-web-nosdn.netease.im/common/769a3ba0615b3157d6b493fa5d2352c4/icon-touxiang5.png',
'icon-touxiang4':
'https://yx-web-nosdn.netease.im/common/c6ea2b6557913d2fe8017b68eb688515/icon-touxiang4.png',
'icon-touxiang3':
'https://yx-web-nosdn.netease.im/common/89bbfa21ce6d43fda25a8c4121284db6/icon-touxiang3.png',
'icon-touxiang2':
'https://yx-web-nosdn.netease.im/common/e2e3fdafb9201a0b693b36514eb378ae/icon-touxiang2.png',
'icon-touxiang1':
'https://yx-web-nosdn.netease.im/common/4e639e380f246e804b2f0f115f84215f/icon-touxiang1.png',
'icon-tongxunlu-xuanzhong':
'https://yx-web-nosdn.netease.im/common/f49a558e193325d185223f55b65711bf/icon-tongxunlu-xuanzhong.png',
'icon-tongxunlu-weixuanzhong':
'https://yx-web-nosdn.netease.im/common/e33c24318c1faeb73c79fa1b0b1c9c53/icon-tongxunlu-weixuanzhong.png',
'icon-tianjiahaoyou':
'https://yx-web-nosdn.netease.im/common/c5f19ef12df64f466bba0a611cf224d5/icon-tianjiahaoyou.png',
'icon-tianjiaanniu':
'https://yx-web-nosdn.netease.im/common/181feb34fc6324198f4d9d887a8759e1/icon-tianjiaanniu.png',
'icon-team2':
'https://yx-web-nosdn.netease.im/common/f9d8ea13b9b5d769f75e7f01edcab1df/icon-team2.png',
'icon-lahei2':
'https://yx-web-nosdn.netease.im/common/1ee2a3bffb33b81727583189a2562658/icon-lahei2.png',
'icon-yuyin1':
'https://yx-web-nosdn.netease.im/common/2c785931baec5a9b00cd5f59f81f4482/语音1.png',
'icon-yuyin2':
'https://yx-web-nosdn.netease.im/common/20043a54056311986c867edf93f51f62/语音2.png',
'icon-yuyin3':
'https://yx-web-nosdn.netease.im/common/786476a9bd129f6a7b027621f9818883/语音3.png',
'icon-yuyin8':
'https://yx-web-nosdn.netease.im/common/40d631410c18983f6ee0ca880976c2e9/icon-yuyin8.png',
'icon-audio':
'https://yx-web-nosdn.netease.im/common/27c4c8b528fac12d3f79bb3154be87d4/audio1.png',
'audio-btn':
'https://yx-web-nosdn.netease.im/common/abfd3577b9d29bb6d3445979336a5770/Vector.png',
'audio-btn-selected':
'https://yx-web-nosdn.netease.im/common/7f17c648e9b63f40b91832664668ac7a/Frame.png',
'send-more':
'https://yx-web-nosdn.netease.im/common/270ebff9ad75056b857c21f40b55d72d/send-more.png',
'icon-paishe':
'https://yx-web-nosdn.netease.im/common/be9638b843a70f307ecb6803ffe5775c/paishe.png',
'icon-shipin2':
'https://yx-web-nosdn.netease.im/common/3865bf597f9f5ca03b2b222ca07344e1/icon-shipin2.png',
'icon-audio-call':
'https://yx-web-nosdn.netease.im/common/99438364d757b51e7e36c18d254e70e7/icon-audio-call.png',
'icon-video-call':
'https://yx-web-nosdn.netease.im/common/ed7c85a59de3e247d10ecfc684b05226/icon-video-call.png',
'icon-read':
'https://yx-web-nosdn.netease.im/common/271c53e493cfd6ea98c51650d8b39b79/read.png',
'icon-file':
'https://yx-web-nosdn.netease.im/common/90485f277f50fc081970ded8772ec7c5/file.png',
'icon-pin':
'https://yx-web-nosdn.netease.im/common/6eb4fafcca008d2e93e90311696d6b96/black-pin.png',
'icon-green-pin':
'https://yx-web-nosdn.netease.im/common/4a1f15eff2f53563c4f1cf6ecde82d2c/green-pin.png',
'choose-picture':
'https://yx-web-nosdn.netease.im/common/97b3ca79a589d5753cbc0e8e8ec09501/choose-picture.png',
'icon-collection':
'https://yx-web-nosdn.netease.im/common/aa1bad3410009dea83d34d513dcd20f3/add-collection.png',
'blue-collection':
'https://yx-web-nosdn.netease.im/common/fb3836a8731b57720fcfdd3b589b3d5f/collection.png',
}
//访
const iconUrl = computed(() => {
return urlMap[props.type]
})
const className = `${props.iconClassName || ''} icon-wrapper`
</script>
<style scoped lang="scss">
.icon-wrapper {
display: inline-block;
line-height: 0;
}
.icon {
display: inline-block;
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<div class="wrapper">
<template v-for="item in textArr" :key="item.key">
<template v-if="item.type === 'text'" class="msg-reply-text">
{{ item.value }}
</template>
<template class="icon" v-else-if="item.type === 'emoji'">
<Icon
:type="EMOJI_ICON_MAP_CONFIG[item.value]"
:size="14"
:iconStyle="{
margin: '3px',
verticalAlign: 'bottom',
display: 'inline-block',
}"
/>
</template>
</template>
</div>
</template>
<script lang="ts" setup>
import Icon from './Icon.vue'
import { EMOJI_ICON_MAP_CONFIG, emojiRegExp } from '@/utils/im/emoji'
import { computed, withDefaults } from 'vue'
const props = withDefaults(defineProps<{ text: string }>(), {})
//
const parseText = (text: string) => {
if (!text) return []
const regexEmoji = emojiRegExp
const matches: {
type: 'emoji' | 'text'
value: string
index: number
}[] = []
let match
while ((match = regexEmoji.exec(text)) !== null) {
matches.push({
type: 'emoji',
value: match[0],
index: match.index,
})
const fillText = ' '.repeat(match[0].length)
text = text.replace(match[0], fillText)
}
text = text.replace(regexEmoji, ' ')
if (text) {
text
.split(' ')
.filter((item) => item.trim())
.map((item) => {
const index = text?.indexOf(item)
matches.push({
type: 'text',
value: item,
index,
})
const fillText = ' '.repeat(item.length)
text = text.replace(item, fillText)
})
}
return matches
.sort((a, b) => a.index - b.index)
.map((item, index) => {
return {
...item,
key: index + item.type,
}
})
}
const textArr = computed(() => {
return parseText(props.text)
})
</script>
<style lang="scss" scoped>
.wrapper {
flex: 1;
font-size: 13px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.msg-reply-text {
font-size: 13px !important;
height: 18px;
line-height: 18px;
width: 100%;
display: inline;
}
.ellipsis {
display: inline-block;
flex-basis: 25px;
font-size: 13px;
}
</style>

112
components/Modal.vue Normal file
View File

@ -0,0 +1,112 @@
<template>
<div class="modal" v-if="visible">
<div class="mask" @tap="handleMaskClick"></div>
<div class="content">
<div class="title">{{ title }}</div>
<slot></slot>
<div class="buttons">
<div class="button cancel" @tap="handleCancelClick">
{{ cancelText }}
</div>
<div class="button confirm" @tap="handleConfirmClick">
{{ confirmText }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
withDefaults(
defineProps<{
title: string
confirmText: string
cancelText: string
visible: boolean
}>(),
{}
)
const emit = defineEmits(['confirm', 'cancel'])
const handleMaskClick = () => {
emit('cancel')
}
const handleConfirmClick = () => {
emit('confirm')
}
const handleCancelClick = () => {
emit('cancel')
}
</script>
<style>
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
z-index: 99999999999;
}
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
touch-action: none;
z-index: 9999;
}
.content {
width: 276px;
background-color: #fff;
border-radius: 4px;
overflow: hidden;
z-index: 99999;
}
.title {
font-size: 16px;
font-weight: 500;
text-align: left;
margin: 16px 0 0 16px;
}
.buttons {
display: flex;
justify-content: center;
align-items: center;
height: 52px;
border-top: 1px solid #e1e6e8;
position: relative;
bottom: 0;
margin-top: 16px;
}
.cancel {
color: #666666;
height: 52px;
line-height: 52px;
border-right: 1px solid #e1e6e8;
}
.confirm {
color: #337eff;
}
.button {
flex: 1;
text-align: center;
font-size: 16px;
}
</style>

103
components/NavBar.vue Normal file
View File

@ -0,0 +1,103 @@
<template>
<!-- 样式兼容微信小程序 -->
<div>
<div
class="nav-bar-wrapper"
:style="{
backgroundColor: backgroundColor || '#ffffff',
backgroundImage: `url(${title})`,
height: isWeixinApp ? '55px' : '40px',
alignItems: isWeixinApp ? 'flex-end' : 'center',
}"
>
<slot v-if="showLeft" name="left"></slot>
<div v-else @tap="back">
<Icon type="icon-zuojiantou" :size="22"></Icon>
</div>
<div class="title-container">
<div class="title">{{ title }}</div>
<div class="subTitle" v-if="subTitle">{{ subTitle }}</div>
<slot name="icon"></slot>
</div>
<div>
<slot name="right"></slot>
</div>
</div>
<div :class="isWeixinApp ? 'block-wx' : 'block'"></div>
</div>
</template>
<script lang="ts" setup>
import { getUniPlatform } from '../utils'
import Icon from './Icon.vue'
import { withDefaults } from 'vue'
withDefaults(
defineProps<{
title: string
subTitle?: string
backgroundColor?: string
showLeft?: boolean
}>(),
{
subTitle: '',
backgroundColor: '',
}
)
const isWeixinApp = getUniPlatform() === 'mp-weixin'
const back = () => {
uni.navigateBack({
delta: 1,
})
}
</script>
<style lang="scss" scoped>
@import '../pages/styles/common.scss';
.nav-bar-wrapper {
position: fixed;
display: flex;
justify-content: space-between;
padding: var(--status-bar-height) 10px 5px 10px;
z-index: 9999;
top: 0;
left: 0;
right: 0;
.title-container {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 230px;
display: flex;
justify-content: center;
align-items: center;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
white-space: nowrap;
font-weight: 500;
}
.subTitle {
white-space: nowrap;
font-weight: 500;
}
}
.block {
width: 100%;
height: calc(45px + var(--status-bar-height));
}
.block-wx {
width: 100%;
height: calc(55px + var(--status-bar-height));
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div v-if="!isConnected && text" class="network-alert">
{{ text }}
</div>
</template>
<script lang="ts" setup>
import { autorun } from 'mobx'
import { ref, onMounted, onUnmounted } from 'vue'
import { t } from '@/utils/im/i18n'
import { V2NIMConst } from '@/utils/im/nim'
const isConnected = ref(true)
const text = ref(t('connectingText'))
// uni.onNetworkStatusChange((res) => {
// if (!res.isConnected) {
// isConnected.value = false;
// text.value = t('offlineText');
// } else {
// text.value = t('connectingText');
// }
// });
onMounted(() => {
if (
uni.$UIKitStore?.connectStore?.connectStatus ===
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
) {
isConnected.value = true
} else if (
uni.$UIKitStore?.connectStore?.connectStatus ===
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_DISCONNECTED
) {
isConnected.value = false
text.value = t('offlineText')
} else {
isConnected.value = false
text.value = t('connectingText')
}
})
const uninstallConnectWatch = autorun(() => {
if (
uni.$UIKitStore?.connectStore?.connectStatus ===
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
) {
isConnected.value = true
} else if (
uni.$UIKitStore?.connectStore?.connectStatus ===
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_DISCONNECTED
) {
isConnected.value = false
text.value = t('offlineText')
} else {
isConnected.value = false
text.value = t('connectingText')
}
})
onUnmounted(() => {
uninstallConnectWatch()
})
</script>
<style>
.network-alert {
font-size: 14px;
background: #fee3e6;
color: #fc596a;
text-align: center;
padding: 8px 0;
}
</style>

164
components/PersonSelect.vue Normal file
View File

@ -0,0 +1,164 @@
<template>
<div v-if="personList.length > 0" class="friend-select-wrapper">
<div class="member-wrapper">
<radio-group v-if="radio" @change="checkboxChange">
<div
class="member-item"
v-for="item in personList"
:key="item.accountId"
>
<radio
class="checkbox"
:value="item.accountId"
:checked="item.checked"
:disabled="
item.disabled ||
(selectAccount.length >= max &&
!selectAccount.includes(item.accountId))
"
/>
<Avatar class="user-avatar" size="36" :account="item.accountId" />
<div class="user-name">
<Appellation :account="item.accountId" :teamId="item.teamId" />
</div>
</div>
</radio-group>
<checkbox-group v-else @change="checkboxChange">
<div
class="member-item"
v-for="item in personList"
:key="item.accountId"
>
<checkbox
class="checkbox"
:value="item.accountId"
:checked="item.checked"
:disabled="
item.disabled ||
(selectAccount.length >= max &&
!selectAccount.includes(item.accountId))
"
/>
<Avatar class="user-avatar" size="36" :account="item.accountId" />
<div class="user-name">
<Appellation :account="item.accountId" :teamId="item.teamId" />
</div>
</div>
</checkbox-group>
</div>
<div
:style="{ border: '1px solid #ccc' }"
v-if="!!showBtn"
@tap="onBtnClick"
class="ok-btn"
>
{{ btnText || t('okText') }}
</div>
</div>
<Empty v-else :text="t('noFriendText')"></Empty>
</template>
<script lang="ts" setup>
import Avatar from './Avatar.vue'
import Appellation from './Appellation.vue'
import Empty from './Empty.vue'
import { t } from '../utils/i18n'
import { events } from '../utils/constants'
import { ref, onMounted } from 'vue'
export type PersonSelectItem = {
accountId: string
teamId?: string
disabled?: boolean
checked?: boolean
}
const props = withDefaults(
defineProps<{
personList: PersonSelectItem[]
showBtn?: boolean
btnText?: string
radio?: boolean
max?: number
}>(),
{
showBtn: true,
btnText: '',
radio: false,
max: Number.MAX_SAFE_INTEGER,
}
)
const $emit = defineEmits<{
(event: 'checkboxChange', selectList: string | string[]): void
(event: 'onBtnClick'): void
}>()
const onBtnClick = () => {
uni.$emit(events.FRIEND_SELECT)
$emit('onBtnClick')
}
const checkboxChange = (event: any) => {
const value = event.detail.value
selectAccount.value = value
$emit('checkboxChange', value)
}
const selectAccount = ref<string[]>([])
onMounted(() => {
selectAccount.value = props.personList
.filter((item) => item.checked)
.map((item) => item.accountId)
})
</script>
<style lang="scss" scoped>
@import '../pages/styles/common.scss';
.friend-select-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
}
.member-wrapper {
padding-top: 10px;
display: flex;
max-height: 80vh;
overflow-y: auto;
.member-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 8px 20px;
// width: 100vw;
.user-avatar {
margin: 0 14px;
}
.user-name {
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #333;
font-size: 16px;
}
.checkbox {
margin-right: 8px;
}
}
}
.ok-btn {
margin-bottom: 15px;
}
.ok-btn-mp {
margin-bottom: 15px;
}
</style>

353
components/Tooltip.vue Normal file
View File

@ -0,0 +1,353 @@
<template>
<view
class="zb-tooltip"
:style="{
'--theme-bg-color': color,
}"
>
<view class="zb_tooltip_content" @longpress.stop="handleClick">
<slot></slot>
<view
class="zb_tooltip__mask"
@longpress.stop
v-show="isShow"
@touchstart="close"
></view>
<view
class="zb_tooltip__popper"
@tap.stop="() => {}"
:style="[
style,
{
visibility: isShow ? 'visible' : 'hidden',
color: color === 'white' ? '' : '#fff',
boxShadow:
color === 'white'
? '0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d'
: '',
},
]"
>
<slot name="content">{{ content }}</slot>
<!-- <view
class="zb_popper__icon"
:style="[arrowStyle]"
:class="[
{
zb_popper__up: placement.indexOf('bottom') === 0,
zb_popper__arrow: placement.indexOf('top') === 0,
zb_popper__right: placement.indexOf('right') === 0,
zb_popper__left: placement.indexOf('left') === 0,
},
]"
>
</view> -->
</view>
</view>
</view>
</template>
<script>
export default {
name: 'Tooltip',
props: {
visible: Boolean,
align: Boolean,
color: {
type: String,
default: '#303133',
},
// placement: {
// type: String,
// default: 'top',
// },
content: {
type: String,
default: '',
},
show: {
type: Boolean,
default: false,
},
},
data() {
return {
isShow: this.visible,
title: 'Hello',
arrowLeft: 0,
query: null,
style: {},
arrowStyle: {},
placement: 'top',
}
},
onLoad() {},
watch: {
isShow: {
handler(val) {
this.$emit('update:visible', val)
},
immediate: true,
},
visible: {
handler(val) {
if (val) {
this.$nextTick(() => {
this.getPosition()
})
}
this.isShow = val
},
immediate: true,
},
},
mounted() {
// #ifdef H5
window.addEventListener('click', () => {
this.isShow = false
})
// #endif
this.getPosition()
},
methods: {
close() {
this.isShow = false
},
fixedWrap() {
this.isShow = false
},
async handleClick() {
if (this.isShow) {
return (this.isShow = false)
}
await this.getPosition()
this.isShow = true
},
getPosition() {
return new Promise((resolve) => {
uni
.createSelectorQuery()
.in(this)
.selectAll('.zb_tooltip_content,.zb_tooltip__popper')
.boundingClientRect(async (data) => {
let { left, bottom, right, top, width, height } = data[0] || {}
let obj1 = data[1] || {}
let objStyle = {}
let objStyle1 = {}
if (top <= 300) {
this.placement = 'bottom'
} else {
this.placement = 'top'
}
switch (this.placement) {
case 'top':
// if (obj1.width > width) {
// objStyle.left = `-${(obj1.width - width + 120) / 2}px`
// } else {
// objStyle.left = `${Math.abs(obj1.width - width) / 2}px`
// }
if (this.align) {
objStyle.left = '-100px'
if (width < 90) {
objStyle.left = '-200px'
}
} else {
objStyle.left = '50px'
}
objStyle.bottom = `${height + 8}px`
// objStyle.left = '30%'
// objStyle1.left = obj1.width - 6 + 'px'
break
case 'bottom':
// if (obj1.width > width) {
// objStyle.left = `-${(obj1.width - width) / 2}px`
// } else {
// objStyle.left = `${Math.abs(obj1.width - width) / 2}px`
// }
if (this.align) {
objStyle.left = '-100px'
if (width < 100) {
objStyle.left = '-200px'
}
} else {
objStyle.left = '50px'
}
// objStyle.left = `-${obj1.width - width - 100}px`
objStyle.top = `${height + 8}px`
// objStyle1.left = obj1.width / 2 - 6 + 'px'
break
// case 'top-start':
// objStyle.left = `0px`
// objStyle.bottom = `${height + 8}px`
// break;
// case 'top-end':
// objStyle.right = `0px`
// objStyle.bottom = `${height + 8}px`
// objStyle1.right = `8px`
// break;
// case 'bottom-start':
// objStyle.left = `0px`
// objStyle.top = `${height + 8}px`
// objStyle1.left = `8px`
// break;
// case 'bottom-end':
// objStyle.right = `0px`
// objStyle.top = `${height + 8}px`
// objStyle1.right = `8px`
// break;
// case 'right':
// objStyle.left = `${width + 8}px`
// if (obj1.height > height) {
// objStyle.top = `-${(obj1.height - height) / 2}px`
// } else {
// objStyle.top = `${Math.abs((obj1.height - height) / 2)}px`
// }
// objStyle1.top = `${obj1.height / 2 - 6}px`
// break;
// case 'right-start':
// objStyle.left = `${width + 8}px`
// objStyle.top = `0px`
// objStyle1.top = `8px`
// break;
// case 'right-end':
// objStyle.left = `${width + 8}px`
// objStyle.bottom = `0px`
// objStyle1.bottom = `8px`
// break;
// case 'left':
// objStyle.right = `${width + 8}px`
// if (obj1.height > height) {
// objStyle.top = `-${(obj1.height - height) / 2}px`
// } else {
// objStyle.top = `${Math.abs((obj1.height - height) / 2)}px`
// }
// objStyle1.top = `${obj1.height / 2 - 6}px`
// break;
// case 'left-start':
// objStyle.right = `${width + 8}px`
// objStyle.top = `0px`
// objStyle1.top = `8px`
// break;
// case 'left-end':
// objStyle.right = `${width + 8}px`
// objStyle.bottom = `0px`
// objStyle1.bottom = `8px`
// break;
}
this.style = objStyle
//
this.arrowStyle = objStyle1
resolve()
})
.exec()
})
},
},
}
</script>
<style lang="scss" scoped>
$theme-bg-color: var(--theme-bg-color);
.zb-tooltip {
position: relative;
}
.zb_tooltip_content {
height: 100%;
/* float: left; */
position: relative;
display: inline-block;
// display: flex;
// flex-direction: row;
// align-items: center;
/* overflow: hidden; */
}
.zb_tooltip__popper {
/* transform-origin: center top; */
background: $theme-bg-color;
visibility: hidden;
// color:'#fff';
position: absolute;
border-radius: 4px;
font-size: 12px;
padding: 10px;
min-width: 10px;
word-wrap: break-word;
display: inline-block;
white-space: nowrap;
z-index: 99;
}
.zb_tooltip__mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: rgba(256, 256, 256, 0);
z-index: 8;
}
.zb_popper__icon {
width: 0;
height: 0;
z-index: 9;
position: absolute;
}
.zb_popper__arrow {
bottom: -5px;
/* transform-origin: center top; */
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid $theme-bg-color;
}
.zb_popper__right {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid $theme-bg-color;
left: -5px;
}
.zb_popper__left {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid $theme-bg-color;
right: -5px;
}
.zb_popper__up {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid $theme-bg-color;
top: -5px;
}
.fixed {
position: absolute;
width: 100vw;
height: 100vh;
position: fixed;
left: 0;
top: 0;
pointer-events: auto;
background: red;
z-index: -1;
}
</style>

117
components/UserCard.vue Normal file
View File

@ -0,0 +1,117 @@
<template>
<div class="user-wrapper">
<div class="avatar-wrapper">
<Avatar v-if="props.account" size="70" :account="props.account"></Avatar>
</div>
<div class="account-wrapper">
<div v-if="alias">
<div class="main">{{ alias }}</div>
<div class="deputy">{{ t('name') }}:{{ nick || account }}</div>
</div>
<div v-else class="main">{{ nick || account }}</div>
<div class="deputy">
{{ t('accountText') }}:{{ props.account }}
<div @tap.stop="copyAccount">
<Icon
class="copy-icon"
type="icon-fuzhi1"
color="#A6ADB6"
:size="20"
></Icon>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import Avatar from './Avatar.vue'
import Icon from './Icon.vue'
import { onUnmounted, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { autorun } from 'mobx'
import { t } from '../utils/i18n'
const props = withDefaults(
defineProps<{
account?: string
nick?: string
}>(),
{
account: '',
nick: '',
}
)
const alias = ref<string>()
let uninstallFriendsWatch = () => {}
onLoad((props) => {
let account = props ? props.account : ''
uninstallFriendsWatch = autorun(() => {
const friend = { ...uni.$UIKitStore.friendStore.friends.get(account) }
alias.value = friend ? friend.alias : ''
})
})
onUnmounted(() => {
uninstallFriendsWatch()
})
const copyAccount = () => {
uni.setClipboardData({
data: props.account,
showToast: false,
success: () => {
uni.showToast({
title: t('copySuccessText'),
icon: 'none',
})
},
fail: () => {
uni.showToast({
title: t('copyFailText'),
icon: 'none',
})
},
})
}
</script>
<style lang="scss" scoped>
@import '../pages/styles/common.scss';
.user-wrapper {
background-color: #fff;
display: flex;
height: 140px;
align-items: center;
.avatar-wrapper {
margin: 0 15px;
flex: 0 0 70px;
}
.account-wrapper {
flex: 1;
overflow: hidden;
padding-right: 40px;
.main {
font-size: 20px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 500;
}
.deputy {
font-size: 14px;
display: flex;
align-items: center;
}
.copy-icon {
margin-left: 2px;
}
}
}
</style>

View File

@ -13,8 +13,11 @@
<style scoped lang="scss">
.emptybox{
width:100%;
height:800rpx;
min-width:600rpx;
display: flex;
margin:0 auto;
align-items: center;
justify-content: center;
flex-direction: column;

View File

@ -15,7 +15,7 @@
<script setup>
import { ref } from 'vue';
import { onShow } from "@dcloudio/uni-app";
cosnt props=defineProps({
const props=defineProps({
title:{
type:String,
default:''

View File

@ -0,0 +1,150 @@
<template>
<a
v-if="isShowA"
class="uni-link"
:href="href"
:class="{
'uni-link--withline': showUnderLine === true || showUnderLine === 'true',
}"
:style="{ color, fontSize: fontSize + 'px' }"
:download="download"
>
<slot>{{ text }}</slot>
</a>
<!-- #ifndef APP-NVUE -->
<text
v-else
class="uni-link"
:class="{
'uni-link--withline': showUnderLine === true || showUnderLine === 'true',
}"
:style="{ color, fontSize: fontSize + 'px' }"
@click="openURL"
>
<slot>{{ text }}</slot>
</text>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<text
v-else
class="uni-link"
:class="{
'uni-link--withline': showUnderLine === true || showUnderLine === 'true',
}"
:style="{ color, fontSize: fontSize + 'px' }"
@click="openURL"
>
{{ text }}
</text>
<!-- #endif -->
</template>
<script>
/**
* Link 外部网页超链接组件
* @description uni-link是一个外部网页超链接组件在小程序内复制url在app内打开外部浏览器在h5端打开新网页
* @tutorial https://ext.dcloud.net.cn/plugin?id=1182
* @property {String} href 点击后打开的外部网页url
* @property {String} text 显示的文字
* @property {String} downlaod H5平台下载文件名
* @property {Boolean} showUnderLine 是否显示下划线
* @property {String} copyTips 在小程序端复制链接时显示的提示语
* @property {String} color 链接文字颜色
* @property {String} fontSize 链接文字大小
* @example * <uni-link href="https://ext.dcloud.net.cn" text="https://ext.dcloud.net.cn"></uni-link>
*/
export default {
name: 'uniLink',
props: {
href: {
type: String,
default: '',
},
text: {
type: String,
default: '',
},
download: {
type: String,
default: '',
},
showUnderLine: {
type: [Boolean, String],
default: true,
},
copyTips: {
type: String,
default: '已自动复制网址,请在手机浏览器里粘贴该网址',
},
color: {
type: String,
default: '#999999',
},
fontSize: {
type: [Number, String],
default: 14,
},
},
computed: {
isShowA() {
// #ifdef H5
this._isH5 = true
// #endif
if ((this.isMail() || this.isTel()) && this._isH5 === true) {
return true
}
return false
},
},
created() {
this._isH5 = null
},
methods: {
isMail() {
return this.href.startsWith('mailto:')
},
isTel() {
return this.href.startsWith('tel:')
},
openURL() {
// #ifdef APP-PLUS
if (this.isTel()) {
this.makePhoneCall(this.href.replace('tel:', ''))
} else {
plus.runtime.openURL(this.href)
}
// #endif
// #ifdef H5
window.open(this.href)
// #endif
// #ifdef MP
uni.setClipboardData({
data: this.href,
showToast: false,
})
uni.showModal({
content: this.copyTips,
showCancel: false,
})
// #endif
},
makePhoneCall(phoneNumber) {
uni.makePhoneCall({
phoneNumber,
})
},
},
}
</script>
<style>
/* #ifndef APP-NVUE */
.uni-link {
cursor: pointer;
}
/* #endif */
.uni-link--withline {
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,49 @@
// #ifdef H5
export default {
name: 'Keypress',
props: {
disable: {
type: Boolean,
default: false,
},
},
mounted() {
const keyNames = {
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
space: [' ', 'Spacebar'],
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
delete: ['Backspace', 'Delete', 'Del'],
}
const listener = ($event) => {
if (this.disable) {
return
}
const keyName = Object.keys(keyNames).find((key) => {
const keyName = $event.key
const value = keyNames[key]
return (
value === keyName || (Array.isArray(value) && value.includes(keyName))
)
})
if (keyName) {
// 避免和其他按键事件冲突
setTimeout(() => {
this.$emit(keyName, {})
}, 0)
}
}
document.addEventListener('keyup', listener)
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keyup', listener)
})
},
render: () => {
return null
},
}
// #endif

View File

@ -0,0 +1,275 @@
<template>
<view class="uni-popup-dialog">
<view class="uni-dialog-title">
<text class="uni-dialog-title-text" :class="['uni-popup__'+dialogType]">{{titleText}}</text>
</view>
<view v-if="mode === 'base'" class="uni-dialog-content">
<slot>
<text class="uni-dialog-content-text">{{content}}</text>
</slot>
</view>
<view v-else class="uni-dialog-content">
<slot>
<input class="uni-dialog-input" v-model="val" :type="inputType" :placeholder="placeholderText" :focus="focus" >
</slot>
</view>
<view class="uni-dialog-button-group">
<view class="uni-dialog-button" @click="closeDialog">
<text class="uni-dialog-button-text">{{closeText}}</text>
</view>
<view class="uni-dialog-button uni-border-left" @click="onOk">
<text class="uni-dialog-button-text uni-button-color">{{okText}}</text>
</view>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from '../uni-popup/i18n/index.js'
const { t } = initVueI18n(messages)
/**
* PopUp 弹出层-对话框样式
* @description 弹出层-对话框样式
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} value input 模式下的默认值
* @property {String} placeholder input 模式下输入提示
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} mode = [base|input] 模式
* @value base 基础对话框
* @value input 可输入对话框
* @property {String} content 对话框内容
* @property {Boolean} beforeClose 是否拦截取消事件
* @event {Function} confirm 点击确认按钮触发
* @event {Function} close 点击取消按钮触发
*/
export default {
name: "uniPopupDialog",
mixins: [popup],
emits:['confirm','close'],
props: {
inputType:{
type: String,
default: 'text'
},
value: {
type: [String, Number],
default: ''
},
placeholder: {
type: [String, Number],
default: ''
},
type: {
type: String,
default: 'error'
},
mode: {
type: String,
default: 'base'
},
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
beforeClose: {
type: Boolean,
default: false
},
cancelText:{
type: String,
default: ''
},
confirmText:{
type: String,
default: ''
}
},
data() {
return {
dialogType: 'error',
focus: false,
val: ""
}
},
computed: {
okText() {
return this.confirmText || t("uni-popup.ok")
},
closeText() {
return this.cancelText || t("uni-popup.cancel")
},
placeholderText() {
return this.placeholder || t("uni-popup.placeholder")
},
titleText() {
return this.title || t("uni-popup.title")
}
},
watch: {
type(val) {
this.dialogType = val
},
mode(val) {
if (val === 'input') {
this.dialogType = 'info'
}
},
value(val) {
this.val = val
}
},
created() {
//
this.popup.disableMask()
// this.popup.closeMask()
if (this.mode === 'input') {
this.dialogType = 'info'
this.val = this.value
} else {
this.dialogType = this.type
}
},
mounted() {
this.focus = true
},
methods: {
/**
* 点击确认按钮
*/
onOk() {
if (this.mode === 'input'){
this.$emit('confirm', this.val)
}else{
this.$emit('confirm')
}
if(this.beforeClose) return
this.popup.close()
},
/**
* 点击取消按钮
*/
closeDialog() {
this.$emit('close')
if(this.beforeClose) return
this.popup.close()
},
close(){
this.popup.close()
}
}
}
</script>
<style lang="scss" >
.uni-popup-dialog {
width: 300px;
border-radius: 11px;
background-color: #fff;
}
.uni-dialog-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 25px;
}
.uni-dialog-title-text {
font-size: 16px;
font-weight: 500;
}
.uni-dialog-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
padding: 20px;
}
.uni-dialog-content-text {
font-size: 14px;
color: #6C6C6C;
}
.uni-dialog-button-group {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
border-top-color: #f5f5f5;
border-top-style: solid;
border-top-width: 1px;
}
.uni-dialog-button {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
height: 45px;
}
.uni-border-left {
border-left-color: #f0f0f0;
border-left-style: solid;
border-left-width: 1px;
}
.uni-dialog-button-text {
font-size: 16px;
color: #333;
}
.uni-button-color {
color: #007aff;
}
.uni-dialog-input {
flex: 1;
font-size: 14px;
border: 1px #eee solid;
height: 40px;
padding: 0 10px;
border-radius: 5px;
color: #555;
}
.uni-popup__success {
color: #4cd964;
}
.uni-popup__warn {
color: #f0ad4e;
}
.uni-popup__error {
color: #dd524d;
}
.uni-popup__info {
color: #909399;
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<view class="uni-popup-message">
<view class="uni-popup-message__box fixforpc-width" :class="'uni-popup__'+type">
<slot>
<text class="uni-popup-message-text" :class="'uni-popup__'+type+'-text'">{{message}}</text>
</slot>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
/**
* PopUp 弹出层-消息提示
* @description 弹出层-消息提示
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} message 消息提示文字
* @property {String} duration 显示时间设置为 0 则不会自动关闭
*/
export default {
name: 'uniPopupMessage',
mixins:[popup],
props: {
/**
* 主题 success/warning/info/error 默认 success
*/
type: {
type: String,
default: 'success'
},
/**
* 消息文字
*/
message: {
type: String,
default: ''
},
/**
* 显示时间设置为 0 则不会自动关闭
*/
duration: {
type: Number,
default: 3000
},
maskShow:{
type:Boolean,
default:false
}
},
data() {
return {}
},
created() {
this.popup.maskShow = this.maskShow
this.popup.messageChild = this
},
methods: {
timerClose(){
if(this.duration === 0) return
clearTimeout(this.timer)
this.timer = setTimeout(()=>{
this.popup.close()
},this.duration)
}
}
}
</script>
<style lang="scss" >
.uni-popup-message {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
}
.uni-popup-message__box {
background-color: #e1f3d8;
padding: 10px 15px;
border-color: #eee;
border-style: solid;
border-width: 1px;
flex: 1;
}
@media screen and (min-width: 500px) {
.fixforpc-width {
margin-top: 20px;
border-radius: 4px;
flex: none;
min-width: 380px;
/* #ifndef APP-NVUE */
max-width: 50%;
/* #endif */
/* #ifdef APP-NVUE */
max-width: 500px;
/* #endif */
}
}
.uni-popup-message-text {
font-size: 14px;
padding: 0;
}
.uni-popup__success {
background-color: #e1f3d8;
}
.uni-popup__success-text {
color: #67C23A;
}
.uni-popup__warn {
background-color: #faecd8;
}
.uni-popup__warn-text {
color: #E6A23C;
}
.uni-popup__error {
background-color: #fde2e2;
}
.uni-popup__error-text {
color: #F56C6C;
}
.uni-popup__info {
background-color: #F2F6FC;
}
.uni-popup__info-text {
color: #909399;
}
</style>

View File

@ -0,0 +1,187 @@
<template>
<view class="uni-popup-share">
<view class="uni-share-title"><text class="uni-share-title-text">{{shareTitleText}}</text></view>
<view class="uni-share-content">
<view class="uni-share-content-box">
<view class="uni-share-content-item" v-for="(item,index) in bottomData" :key="index" @click.stop="select(item,index)">
<image class="uni-share-image" :src="item.icon" mode="aspectFill"></image>
<text class="uni-share-text">{{item.text}}</text>
</view>
</view>
</view>
<view class="uni-share-button-box">
<button class="uni-share-button" @click="close">{{cancelText}}</button>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from '../uni-popup/i18n/index.js'
const { t } = initVueI18n(messages)
export default {
name: 'UniPopupShare',
mixins:[popup],
emits:['select'],
props: {
title: {
type: String,
default: ''
},
beforeClose: {
type: Boolean,
default: false
}
},
data() {
return {
bottomData: [{
text: '微信',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/c2b17470-50be-11eb-b680-7980c8a877b8.png',
name: 'wx'
},
{
text: '支付宝',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/d684ae40-50be-11eb-8ff1-d5dcf8779628.png',
name: 'wx'
},
{
text: 'QQ',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/e7a79520-50be-11eb-b997-9918a5dda011.png',
name: 'qq'
},
{
text: '新浪',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/0dacdbe0-50bf-11eb-8ff1-d5dcf8779628.png',
name: 'sina'
},
// {
// text: '',
// icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/1ec6e920-50bf-11eb-8a36-ebb87efcf8c0.png',
// name: 'copy'
// },
// {
// text: '',
// icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/2e0fdfe0-50bf-11eb-b997-9918a5dda011.png',
// name: 'more'
// }
]
}
},
created() {},
computed: {
cancelText() {
return t("uni-popup.cancel")
},
shareTitleText() {
return this.title || t("uni-popup.shareTitle")
}
},
methods: {
/**
* 选择内容
*/
select(item, index) {
this.$emit('select', {
item,
index
})
this.close()
},
/**
* 关闭窗口
*/
close() {
if(this.beforeClose) return
this.popup.close()
}
}
}
</script>
<style lang="scss" >
.uni-popup-share {
background-color: #fff;
border-top-left-radius: 11px;
border-top-right-radius: 11px;
}
.uni-share-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
height: 40px;
}
.uni-share-title-text {
font-size: 14px;
color: #666;
}
.uni-share-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 10px;
}
.uni-share-content-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex-wrap: wrap;
width: 360px;
}
.uni-share-content-item {
width: 90px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
padding: 10px 0;
align-items: center;
}
.uni-share-content-item:active {
background-color: #f5f5f5;
}
.uni-share-image {
width: 30px;
height: 30px;
}
.uni-share-text {
margin-top: 10px;
font-size: 14px;
color: #3B4144;
}
.uni-share-button-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
padding: 10px 15px;
}
.uni-share-button {
flex: 1;
border-radius: 50px;
color: #666;
font-size: 16px;
}
.uni-share-button::after {
border-radius: 50px;
}
</style>

View File

@ -0,0 +1,7 @@
{
"uni-popup.cancel": "cancel",
"uni-popup.ok": "ok",
"uni-popup.placeholder": "pleace enter",
"uni-popup.title": "Hint",
"uni-popup.shareTitle": "Share to"
}

View File

@ -0,0 +1,8 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
import zhHant from './zh-Hant.json'
export default {
en,
'zh-Hans': zhHans,
'zh-Hant': zhHant,
}

View File

@ -0,0 +1,7 @@
{
"uni-popup.cancel": "取消",
"uni-popup.ok": "确定",
"uni-popup.placeholder": "请输入",
"uni-popup.title": "提示",
"uni-popup.shareTitle": "分享到"
}

View File

@ -0,0 +1,7 @@
{
"uni-popup.cancel": "取消",
"uni-popup.ok": "確定",
"uni-popup.placeholder": "請輸入",
"uni-popup.title": "提示",
"uni-popup.shareTitle": "分享到"
}

View File

@ -0,0 +1,49 @@
// #ifdef H5
export default {
name: 'Keypress',
props: {
disable: {
type: Boolean,
default: false,
},
},
mounted() {
const keyNames = {
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
space: [' ', 'Spacebar'],
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
delete: ['Backspace', 'Delete', 'Del'],
}
const listener = ($event) => {
if (this.disable) {
return
}
const keyName = Object.keys(keyNames).find((key) => {
const keyName = $event.key
const value = keyNames[key]
return (
value === keyName || (Array.isArray(value) && value.includes(keyName))
)
})
if (keyName) {
// 避免和其他按键事件冲突
setTimeout(() => {
this.$emit(keyName, {})
}, 0)
}
}
document.addEventListener('keyup', listener)
// this.$once('hook:beforeDestroy', () => {
// document.removeEventListener('keyup', listener)
// })
},
render: () => {
return null
},
}
// #endif

View File

@ -0,0 +1,23 @@
export default {
data() {
return {}
},
created() {
this.popup = this.getParent()
},
methods: {
/**
* 获取父元素实例
*/
getParent(name = 'uniPopup') {
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
},
},
}

View File

@ -0,0 +1,511 @@
<template>
<view
v-if="showPopup"
class="uni-popup"
:class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']"
>
<view @touchstart="touchstart">
<UniTransition
key="1"
v-if="maskShow"
name="mask"
mode-class="fade"
:styles="maskClass"
:duration="duration"
:show="showTrans"
@click="onTap"
/>
<UniTransition
key="2"
:mode-class="ani"
name="content"
:styles="transClass"
:duration="duration"
:show="showTrans"
@click="onTap"
>
<view
class="uni-popup__wrapper"
:style="{ backgroundColor: bg }"
:class="[popupstyle]"
@click="clear"
>
<slot />
</view>
</UniTransition>
</view>
<!-- #ifdef H5 -->
<keypress v-if="maskShow" @esc="onTap" />
<!-- #endif -->
</view>
</template>
<script>
// #ifdef H5
import keypress from './keypress.js'
// #endif
import UniTransition from '../../../uni-transition/components/uni-transition/uni-transition.vue'
/**
* PopUp 弹出层
* @description 弹出层组件为了解决遮罩弹层的问题
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
* @value top 顶部弹出
* @value center 中间弹出
* @value bottom 底部弹出
* @value left 左侧弹出
* @value right 右侧弹出
* @value message 消息提示
* @value dialog 对话框
* @value share 底部分享示例
* @property {Boolean} animation = [true|false] 是否开启动画
* @property {Boolean} maskClick = [true|false] 蒙版点击是否关闭弹窗(废弃)
* @property {Boolean} isMaskClick = [true|false] 蒙版点击是否关闭弹窗
* @property {String} backgroundColor 主窗口背景色
* @property {String} maskBackgroundColor 蒙版颜色
* @property {Boolean} safeArea 是否适配底部安全区
* @event {Function} change 打开关闭弹窗触发e={show: false}
* @event {Function} maskClick 点击遮罩触发
*/
export default {
name: 'uniPopup',
components: {
UniTransition,
// #ifdef H5
keypress,
// #endif
},
emits: ['change', 'maskClick'],
props: {
//
animation: {
type: Boolean,
default: true,
},
// top: bottomcenter
// message: ; dialog :
type: {
type: String,
default: 'center',
},
// maskClick
isMaskClick: {
type: Boolean,
default: null,
},
// TODO 2 使 isMaskClick
maskClick: {
type: Boolean,
default: null,
},
backgroundColor: {
type: String,
default: 'none',
},
safeArea: {
type: Boolean,
default: true,
},
maskBackgroundColor: {
type: String,
default: 'rgba(0, 0, 0, 0.4)',
},
},
watch: {
/**
* 监听type类型
*/
type: {
handler: function (type) {
if (!this.config[type]) return
this[this.config[type]](true)
},
immediate: true,
},
isDesktop: {
handler: function (newVal) {
if (!this.config[newVal]) return
this[this.config[this.type]](true)
},
immediate: true,
},
/**
* 监听遮罩是否可点击
* @param {Object} val
*/
maskClick: {
handler: function (val) {
this.mkclick = val
},
immediate: true,
},
isMaskClick: {
handler: function (val) {
this.mkclick = val
},
immediate: true,
},
// H5
showPopup(show) {
// #ifdef H5
// fix by mehaotian h5 穿
document.getElementsByTagName('body')[0].style.overflow = show
? 'hidden'
: 'visible'
// #endif
},
},
data() {
return {
duration: 300,
ani: [],
showPopup: false,
showTrans: false,
popupWidth: 0,
popupHeight: 0,
config: {
top: 'top',
bottom: 'bottom',
center: 'center',
left: 'left',
right: 'right',
message: 'top',
dialog: 'center',
share: 'bottom',
},
maskClass: {
position: 'fixed',
bottom: 0,
top: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)',
},
transClass: {
position: 'fixed',
left: 0,
right: 0,
},
maskShow: true,
mkclick: true,
popupstyle: this.isDesktop ? 'fixforpc-top' : 'top',
}
},
computed: {
isDesktop() {
return this.popupWidth >= 500 && this.popupHeight >= 500
},
bg() {
if (this.backgroundColor === '' || this.backgroundColor === 'none') {
return 'transparent'
}
return this.backgroundColor
},
},
mounted() {
const fixSize = () => {
const {
windowWidth,
windowHeight,
windowTop,
safeArea,
screenHeight,
safeAreaInsets,
} = uni.getSystemInfoSync()
this.popupWidth = windowWidth
this.popupHeight = windowHeight + (windowTop || 0)
// TODO fix by mehaotian ,ios app ios
if (safeArea && this.safeArea) {
// #ifdef MP-WEIXIN
this.safeAreaInsets = screenHeight - safeArea.bottom
// #endif
// #ifndef MP-WEIXIN
this.safeAreaInsets = safeAreaInsets.bottom
// #endif
} else {
this.safeAreaInsets = 0
}
}
fixSize()
// #ifdef H5
// window.addEventListener('resize', fixSize)
// this.$once('hook:beforeDestroy', () => {
// window.removeEventListener('resize', fixSize)
// })
// #endif
},
// #ifndef VUE3
// TODO vue2
destroyed() {
this.setH5Visible()
},
// #endif
// #ifdef VUE3
// TODO vue3
unmounted() {
this.setH5Visible()
},
// #endif
created() {
// this.mkclick = this.isMaskClick || this.maskClick
if (this.isMaskClick === null && this.maskClick === null) {
this.mkclick = true
} else {
this.mkclick =
this.isMaskClick !== null ? this.isMaskClick : this.maskClick
}
if (this.animation) {
this.duration = 300
} else {
this.duration = 0
}
// TODO message
this.messageChild = null
// TODO
this.clearPropagation = false
this.maskClass.backgroundColor = this.maskBackgroundColor
},
methods: {
setH5Visible() {
// #ifdef H5
// fix by mehaotian h5 穿
document.getElementsByTagName('body')[0].style.overflow = 'visible'
// #endif
},
/**
* 公用方法不显示遮罩层
*/
closeMask() {
this.maskShow = false
},
/**
* 公用方法遮罩层禁止点击
*/
disableMask() {
this.mkclick = false
},
// TODO nvue
clear(e) {
// #ifndef APP-NVUE
e.stopPropagation()
// #endif
this.clearPropagation = true
},
open(direction) {
// fix by mehaotian
if (this.showPopup) {
return
}
let innerType = [
'top',
'center',
'bottom',
'left',
'right',
'message',
'dialog',
'share',
]
if (!(direction && innerType?.indexOf(direction) !== -1)) {
direction = this.type
}
if (!this.config[direction]) {
console.error('缺少类型:', direction)
return
}
this[this.config[direction]]()
this.$emit('change', {
show: true,
type: direction,
})
},
close(type) {
this.showTrans = false
this.$emit('change', {
show: false,
type: this.type,
})
clearTimeout(this.timer)
// //
// this.customOpen && this.customClose()
this.timer = setTimeout(() => {
this.showPopup = false
}, 300)
},
// TODO
touchstart() {
this.clearPropagation = false
},
onTap() {
if (this.clearPropagation) {
// fix by mehaotian nvue
this.clearPropagation = false
return
}
this.$emit('maskClick')
if (!this.mkclick) return
this.close()
},
/**
* 顶部弹出样式处理
*/
top(type) {
this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top'
this.ani = ['slide-top']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
backgroundColor: this.bg,
}
// TODO type
if (type) return
this.showPopup = true
this.showTrans = true
this.$nextTick(() => {
if (this.messageChild && this.type === 'message') {
this.messageChild.timerClose()
}
})
},
/**
* 底部弹出样式处理
*/
bottom(type) {
this.popupstyle = 'bottom'
this.ani = ['slide-bottom']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
paddingBottom: this.safeAreaInsets + 'px',
backgroundColor: this.bg,
}
// TODO type
if (type) return
this.showPopup = true
this.showTrans = true
},
/**
* 中间弹出样式处理
*/
center(type) {
this.popupstyle = 'center'
this.ani = ['zoom-out', 'fade']
this.transClass = {
position: 'fixed',
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column',
/* #endif */
bottom: 0,
left: 0,
right: 0,
top: 0,
justifyContent: 'center',
alignItems: 'center',
}
// TODO type
if (type) return
this.showPopup = true
this.showTrans = true
},
left(type) {
this.popupstyle = 'left'
this.ani = ['slide-left']
this.transClass = {
position: 'fixed',
left: 0,
bottom: 0,
top: 0,
backgroundColor: this.bg,
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column',
/* #endif */
}
// TODO type
if (type) return
this.showPopup = true
this.showTrans = true
},
right(type) {
this.popupstyle = 'right'
this.ani = ['slide-right']
this.transClass = {
position: 'fixed',
bottom: 0,
right: 0,
top: 0,
backgroundColor: this.bg,
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column',
/* #endif */
}
// TODO type
if (type) return
this.showPopup = true
this.showTrans = true
},
},
}
</script>
<style lang="scss">
.uni-popup {
position: fixed;
/* #ifndef APP-NVUE */
z-index: 999999;
/* #endif */
&.top,
&.left,
&.right {
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
}
.uni-popup__wrapper {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
z-index: 999;
/* iphonex 等安全区设置,底部安全区适配 */
/* #ifndef APP-NVUE */
// padding-bottom: constant(safe-area-inset-bottom);
// padding-bottom: env(safe-area-inset-bottom);
/* #endif */
&.left,
&.right {
/* #ifdef H5 */
padding-top: var(--window-top);
/* #endif */
/* #ifndef H5 */
padding-top: 0;
/* #endif */
flex: 1;
}
}
}
.fixforpc-z-index {
/* #ifndef APP-NVUE */
z-index: 999;
/* #endif */
}
.fixforpc-top {
top: 0;
}
</style>

View File

@ -0,0 +1,15 @@
## 1.0.32022-01-21
- 优化 组件示例
## 1.0.22021-11-22
- 修复 / 符号在 vue 不同版本兼容问题引起的报错问题
## 1.0.12021-11-22
- 修复 vue3 中 scss 语法兼容问题
## 1.0.02021-11-18
- init

View File

@ -0,0 +1 @@
@import './styles/index.scss';

View File

@ -0,0 +1,82 @@
{
"id": "uni-scss",
"displayName": "uni-scss 辅助样式",
"version": "1.0.3",
"description": "uni-sass是uni-ui提供的一套全局样式 通过一些简单的类名和sass变量实现简单的页面布局操作比如颜色、边距、圆角等。",
"keywords": [
"uni-scss",
"uni-ui",
"辅助样式"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"category": [
"JS SDK",
"通用 SDK"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "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"
},
"快应用": {
"华为": "n",
"联盟": "n"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@ -0,0 +1,5 @@
`uni-sass``uni-ui`提供的一套全局样式 ,通过一些简单的类名和`sass`变量,实现简单的页面布局操作,比如颜色、边距、圆角等。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-sass)
#### 如使用过程中有任何问题,或者您对 uni-ui 有一些好的建议,欢迎加入 uni-ui 交流群871950839

View File

@ -0,0 +1,7 @@
@import './setting/_variables.scss';
@import './setting/_border.scss';
@import './setting/_color.scss';
@import './setting/_space.scss';
@import './setting/_radius.scss';
@import './setting/_text.scss';
@import './setting/_styles.scss';

View File

@ -0,0 +1,3 @@
.uni-border {
border: 1px $uni-border-1 solid;
}

View File

@ -0,0 +1,66 @@
// TODO 暂时不需要 class 需要用户使用变量实现 如果使用类名其实并不推荐
// @mixin get-styles($k,$c) {
// @if $k == size or $k == weight{
// font-#{$k}:#{$c}
// }@else{
// #{$k}:#{$c}
// }
// }
$uni-ui-color:(
// 主色
primary: $uni-primary,
primary-disable: $uni-primary-disable,
primary-light: $uni-primary-light,
// 辅助色
success: $uni-success,
success-disable: $uni-success-disable,
success-light: $uni-success-light,
warning: $uni-warning,
warning-disable: $uni-warning-disable,
warning-light: $uni-warning-light,
error: $uni-error,
error-disable: $uni-error-disable,
error-light: $uni-error-light,
info: $uni-info,
info-disable: $uni-info-disable,
info-light: $uni-info-light,
// 中性色
main-color: $uni-main-color,
base-color: $uni-base-color,
secondary-color: $uni-secondary-color,
extra-color: $uni-extra-color,
// 背景色
bg-color: $uni-bg-color,
// 边框颜色
border-1: $uni-border-1,
border-2: $uni-border-2,
border-3: $uni-border-3,
border-4: $uni-border-4,
// 黑色
black:$uni-black,
// 白色
white:$uni-white,
// 透明
transparent:$uni-transparent
) !default;
@each $key, $child in $uni-ui-color {
.uni-#{"" + $key} {
color: $child;
}
.uni-#{"" + $key}-bg {
background-color: $child;
}
}
.uni-shadow-sm {
box-shadow: $uni-shadow-sm;
}
.uni-shadow-base {
box-shadow: $uni-shadow-base;
}
.uni-shadow-lg {
box-shadow: $uni-shadow-lg;
}
.uni-mask {
background-color:$uni-mask;
}

View File

@ -0,0 +1,55 @@
@mixin radius($r,$d:null ,$important: false){
$radius-value:map-get($uni-radius, $r) if($important, !important, null);
// Key exists within the $uni-radius variable
@if (map-has-key($uni-radius, $r) and $d){
@if $d == t {
border-top-left-radius:$radius-value;
border-top-right-radius:$radius-value;
}@else if $d == r {
border-top-right-radius:$radius-value;
border-bottom-right-radius:$radius-value;
}@else if $d == b {
border-bottom-left-radius:$radius-value;
border-bottom-right-radius:$radius-value;
}@else if $d == l {
border-top-left-radius:$radius-value;
border-bottom-left-radius:$radius-value;
}@else if $d == tl {
border-top-left-radius:$radius-value;
}@else if $d == tr {
border-top-right-radius:$radius-value;
}@else if $d == br {
border-bottom-right-radius:$radius-value;
}@else if $d == bl {
border-bottom-left-radius:$radius-value;
}
}@else{
border-radius:$radius-value;
}
}
@each $key, $child in $uni-radius {
@if($key){
.uni-radius-#{"" + $key} {
@include radius($key)
}
}@else{
.uni-radius {
@include radius($key)
}
}
}
@each $direction in t, r, b, l,tl, tr, br, bl {
@each $key, $child in $uni-radius {
@if($key){
.uni-radius-#{"" + $direction}-#{"" + $key} {
@include radius($key,$direction,false)
}
}@else{
.uni-radius-#{$direction} {
@include radius($key,$direction,false)
}
}
}
}

View File

@ -0,0 +1,56 @@
@mixin fn($space,$direction,$size,$n) {
@if $n {
#{$space}-#{$direction}: #{$size*$uni-space-root}px
} @else {
#{$space}-#{$direction}: #{-$size*$uni-space-root}px
}
}
@mixin get-styles($direction,$i,$space,$n){
@if $direction == t {
@include fn($space, top,$i,$n);
}
@if $direction == r {
@include fn($space, right,$i,$n);
}
@if $direction == b {
@include fn($space, bottom,$i,$n);
}
@if $direction == l {
@include fn($space, left,$i,$n);
}
@if $direction == x {
@include fn($space, left,$i,$n);
@include fn($space, right,$i,$n);
}
@if $direction == y {
@include fn($space, top,$i,$n);
@include fn($space, bottom,$i,$n);
}
@if $direction == a {
@if $n {
#{$space}:#{$i*$uni-space-root}px;
} @else {
#{$space}:#{-$i*$uni-space-root}px;
}
}
}
@each $orientation in m,p {
$space: margin;
@if $orientation == m {
$space: margin;
} @else {
$space: padding;
}
@for $i from 0 through 16 {
@each $direction in t, r, b, l, x, y, a {
.uni-#{$orientation}#{$direction}-#{$i} {
@include get-styles($direction,$i,$space,true);
}
.uni-#{$orientation}#{$direction}-n#{$i} {
@include get-styles($direction,$i,$space,false);
}
}
}
}

View File

@ -0,0 +1,167 @@
/* #ifndef APP-NVUE */
$-color-white:#fff;
$-color-black:#000;
@mixin base-style($color) {
color: #fff;
background-color: $color;
border-color: mix($-color-black, $color, 8%);
&:not([hover-class]):active {
background: mix($-color-black, $color, 10%);
border-color: mix($-color-black, $color, 20%);
color: $-color-white;
outline: none;
}
}
@mixin is-color($color) {
@include base-style($color);
&[loading] {
@include base-style($color);
&::before {
margin-right:5px;
}
}
&[disabled] {
&,
&[loading],
&:not([hover-class]):active {
color: $-color-white;
border-color: mix(darken($color,10%), $-color-white);
background-color: mix($color, $-color-white);
}
}
}
@mixin base-plain-style($color) {
color:$color;
background-color: mix($-color-white, $color, 90%);
border-color: mix($-color-white, $color, 70%);
&:not([hover-class]):active {
background: mix($-color-white, $color, 80%);
color: $color;
outline: none;
border-color: mix($-color-white, $color, 50%);
}
}
@mixin is-plain($color){
&[plain] {
@include base-plain-style($color);
&[loading] {
@include base-plain-style($color);
&::before {
margin-right:5px;
}
}
&[disabled] {
&,
&:active {
color: mix($-color-white, $color, 40%);
background-color: mix($-color-white, $color, 90%);
border-color: mix($-color-white, $color, 80%);
}
}
}
}
.uni-btn {
margin: 5px;
color: #393939;
border:1px solid #ccc;
font-size: 16px;
font-weight: 200;
background-color: #F9F9F9;
// TODO 暂时处理边框隐藏一边的问题
overflow: visible;
&::after{
border: none;
}
&:not([type]),&[type=default] {
color: #999;
&[loading] {
background: none;
&::before {
margin-right:5px;
}
}
&[disabled]{
color: mix($-color-white, #999, 60%);
&,
&[loading],
&:active {
color: mix($-color-white, #999, 60%);
background-color: mix($-color-white,$-color-black , 98%);
border-color: mix($-color-white, #999, 85%);
}
}
&[plain] {
color: #999;
background: none;
border-color: $uni-border-1;
&:not([hover-class]):active {
background: none;
color: mix($-color-white, $-color-black, 80%);
border-color: mix($-color-white, $-color-black, 90%);
outline: none;
}
&[disabled]{
&,
&[loading],
&:active {
background: none;
color: mix($-color-white, #999, 60%);
border-color: mix($-color-white, #999, 85%);
}
}
}
}
&:not([hover-class]):active {
color: mix($-color-white, $-color-black, 50%);
}
&[size=mini] {
font-size: 16px;
font-weight: 200;
border-radius: 8px;
}
&.uni-btn-small {
font-size: 14px;
}
&.uni-btn-mini {
font-size: 12px;
}
&.uni-btn-radius {
border-radius: 999px;
}
&[type=primary] {
@include is-color($uni-primary);
@include is-plain($uni-primary)
}
&[type=success] {
@include is-color($uni-success);
@include is-plain($uni-success)
}
&[type=error] {
@include is-color($uni-error);
@include is-plain($uni-error)
}
&[type=warning] {
@include is-color($uni-warning);
@include is-plain($uni-warning)
}
&[type=info] {
@include is-color($uni-info);
@include is-plain($uni-info)
}
}
/* #endif */

View File

@ -0,0 +1,24 @@
@mixin get-styles($k,$c) {
@if $k == size or $k == weight{
font-#{$k}:#{$c}
}@else{
#{$k}:#{$c}
}
}
@each $key, $child in $uni-headings {
/* #ifndef APP-NVUE */
.uni-#{$key} {
@each $k, $c in $child {
@include get-styles($k,$c)
}
}
/* #endif */
/* #ifdef APP-NVUE */
.container .uni-#{$key} {
@each $k, $c in $child {
@include get-styles($k,$c)
}
}
/* #endif */
}

View File

@ -0,0 +1,146 @@
// @use "sass:math";
@import '../tools/functions.scss';
// 间距基础倍数
$uni-space-root: 2 !default;
// 边框半径默认值
$uni-radius-root:5px !default;
$uni-radius: () !default;
// 边框半径断点
$uni-radius: map-deep-merge(
(
0: 0,
// TODO 当前版本暂时不支持 sm 属性
// 'sm': math.div($uni-radius-root, 2),
null: $uni-radius-root,
'lg': $uni-radius-root * 2,
'xl': $uni-radius-root * 6,
'pill': 9999px,
'circle': 50%
),
$uni-radius
);
// 字体家族
$body-font-family: 'Roboto', sans-serif !default;
// 文本
$heading-font-family: $body-font-family !default;
$uni-headings: () !default;
$letterSpacing: -0.01562em;
$uni-headings: map-deep-merge(
(
'h1': (
size: 32px,
weight: 300,
line-height: 50px,
// letter-spacing:-0.01562em
),
'h2': (
size: 28px,
weight: 300,
line-height: 40px,
// letter-spacing: -0.00833em
),
'h3': (
size: 24px,
weight: 400,
line-height: 32px,
// letter-spacing: normal
),
'h4': (
size: 20px,
weight: 400,
line-height: 30px,
// letter-spacing: 0.00735em
),
'h5': (
size: 16px,
weight: 400,
line-height: 24px,
// letter-spacing: normal
),
'h6': (
size: 14px,
weight: 500,
line-height: 18px,
// letter-spacing: 0.0125em
),
'subtitle': (
size: 12px,
weight: 400,
line-height: 20px,
// letter-spacing: 0.00937em
),
'body': (
font-size: 14px,
font-weight: 400,
line-height: 22px,
// letter-spacing: 0.03125em
),
'caption': (
'size': 12px,
'weight': 400,
'line-height': 20px,
// 'letter-spacing': 0.03333em,
// 'text-transform': false
)
),
$uni-headings
);
// 主色
$uni-primary: #2979ff !default;
$uni-primary-disable:lighten($uni-primary,20%) !default;
$uni-primary-light: lighten($uni-primary,25%) !default;
// 辅助色
// 除了主色外的场景色需要在不同的场景中使用例如危险色表示危险的操作
$uni-success: #18bc37 !default;
$uni-success-disable:lighten($uni-success,20%) !default;
$uni-success-light: lighten($uni-success,25%) !default;
$uni-warning: #f3a73f !default;
$uni-warning-disable:lighten($uni-warning,20%) !default;
$uni-warning-light: lighten($uni-warning,25%) !default;
$uni-error: #e43d33 !default;
$uni-error-disable:lighten($uni-error,20%) !default;
$uni-error-light: lighten($uni-error,25%) !default;
$uni-info: #8f939c !default;
$uni-info-disable:lighten($uni-info,20%) !default;
$uni-info-light: lighten($uni-info,25%) !default;
// 中性色
// 中性色用于文本背景和边框颜色通过运用不同的中性色来表现层次结构
$uni-main-color: #3a3a3a !default; // 主要文字
$uni-base-color: #6a6a6a !default; // 常规文字
$uni-secondary-color: #909399 !default; // 次要文字
$uni-extra-color: #c7c7c7 !default; // 辅助说明
// 边框颜色
$uni-border-1: #F0F0F0 !default;
$uni-border-2: #EDEDED !default;
$uni-border-3: #DCDCDC !default;
$uni-border-4: #B9B9B9 !default;
// 常规色
$uni-black: #000000 !default;
$uni-white: #ffffff !default;
$uni-transparent: rgba($color: #000000, $alpha: 0) !default;
// 背景色
$uni-bg-color: #f7f7f7 !default;
/* 水平间距 */
$uni-spacing-sm: 8px !default;
$uni-spacing-base: 15px !default;
$uni-spacing-lg: 30px !default;
// 阴影
$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5) !default;
$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2) !default;
$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5) !default;
// 蒙版
$uni-mask: rgba($color: #000000, $alpha: 0.4) !default;

View File

@ -0,0 +1,19 @@
// 合并 map
@function map-deep-merge($parent-map, $child-map){
$result: $parent-map;
@each $key, $child in $child-map {
$parent-has-key: map-has-key($result, $key);
$parent-value: map-get($result, $key);
$parent-type: type-of($parent-value);
$child-type: type-of($child);
$parent-is-map: $parent-type == map;
$child-is-map: $child-type == map;
@if (not $parent-has-key) or ($parent-type != $child-type) or (not ($parent-is-map and $child-is-map)){
$result: map-merge($result, ( $key: $child ));
}@else {
$result: map-merge($result, ( $key: map-deep-merge($parent-value, $child) ));
}
}
@return $result;
};

View File

@ -0,0 +1,31 @@
// 间距基础倍数
$uni-space-root: 2;
// 边框半径默认值
$uni-radius-root:5px;
// 主色
$uni-primary: #2979ff;
// 辅助色
$uni-success: #4cd964;
// 警告色
$uni-warning: #f0ad4e;
// 错误色
$uni-error: #dd524d;
// 描述色
$uni-info: #909399;
// 中性色
$uni-main-color: #303133;
$uni-base-color: #606266;
$uni-secondary-color: #909399;
$uni-extra-color: #C0C4CC;
// 背景色
$uni-bg-color: #f5f5f5;
// 边框颜色
$uni-border-1: #DCDFE6;
$uni-border-2: #E4E7ED;
$uni-border-3: #EBEEF5;
$uni-border-4: #F2F6FC;
// 常规色
$uni-black: #000000;
$uni-white: #ffffff;
$uni-transparent: rgba($color: #000000, $alpha: 0);

View File

@ -0,0 +1,62 @@
@import './styles/setting/_variables.scss';
// 间距基础倍数
$uni-space-root: 2;
// 边框半径默认值
$uni-radius-root:5px;
// 主色
$uni-primary: #2979ff;
$uni-primary-disable:mix(#fff,$uni-primary,50%);
$uni-primary-light: mix(#fff,$uni-primary,80%);
// 辅助色
// 除了主色外的场景色需要在不同的场景中使用例如危险色表示危险的操作
$uni-success: #18bc37;
$uni-success-disable:mix(#fff,$uni-success,50%);
$uni-success-light: mix(#fff,$uni-success,80%);
$uni-warning: #f3a73f;
$uni-warning-disable:mix(#fff,$uni-warning,50%);
$uni-warning-light: mix(#fff,$uni-warning,80%);
$uni-error: #e43d33;
$uni-error-disable:mix(#fff,$uni-error,50%);
$uni-error-light: mix(#fff,$uni-error,80%);
$uni-info: #8f939c;
$uni-info-disable:mix(#fff,$uni-info,50%);
$uni-info-light: mix(#fff,$uni-info,80%);
// 中性色
// 中性色用于文本背景和边框颜色通过运用不同的中性色来表现层次结构
$uni-main-color: #3a3a3a; // 主要文字
$uni-base-color: #6a6a6a; // 常规文字
$uni-secondary-color: #909399; // 次要文字
$uni-extra-color: #c7c7c7; // 辅助说明
// 边框颜色
$uni-border-1: #F0F0F0;
$uni-border-2: #EDEDED;
$uni-border-3: #DCDCDC;
$uni-border-4: #B9B9B9;
// 常规色
$uni-black: #000000;
$uni-white: #ffffff;
$uni-transparent: rgba($color: #000000, $alpha: 0);
// 背景色
$uni-bg-color: #f7f7f7;
/* 水平间距 */
$uni-spacing-sm: 8px;
$uni-spacing-base: 15px;
$uni-spacing-lg: 30px;
// 阴影
$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5);
$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2);
$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5);
// 蒙版
$uni-mask: rgba($color: #000000, $alpha: 0.4);

View File

@ -0,0 +1,153 @@
// const defaultOption = {
// duration: 300,
// timingFunction: 'linear',
// delay: 0,
// transformOrigin: '50% 50% 0'
// }
// #ifdef APP-NVUE
const nvueAnimation = uni.requireNativePlugin('animation')
// #endif
class MPAnimation {
constructor(options, _this) {
this.options = options
// 在iOS10+QQ小程序平台下传给原生的对象一定是个普通对象而不是Proxy对象否则会报parameter should be Object instead of ProxyObject的错误
this.animation = uni.createAnimation({
...options,
})
this.currentStepAnimates = {}
this.next = 0
this.$ = _this
}
_nvuePushAnimates(type, args) {
let aniObj = this.currentStepAnimates[this.next]
let styles = {}
if (!aniObj) {
styles = {
styles: {},
config: {},
}
} else {
styles = aniObj
}
if (animateTypes1.includes(type)) {
if (!styles.styles.transform) {
styles.styles.transform = ''
}
let unit = ''
if (type === 'rotate') {
unit = 'deg'
}
styles.styles.transform += `${type}(${args + unit}) `
} else {
styles.styles[type] = `${args}`
}
this.currentStepAnimates[this.next] = styles
}
_animateRun(styles = {}, config = {}) {
let ref = this.$.$refs['ani'].ref
if (!ref) return
return new Promise((resolve, reject) => {
nvueAnimation.transition(
ref,
{
styles,
...config,
},
(res) => {
resolve()
}
)
})
}
_nvueNextAnimate(animates, step = 0, fn) {
let obj = animates[step]
if (obj) {
let { styles, config } = obj
this._animateRun(styles, config).then(() => {
step += 1
this._nvueNextAnimate(animates, step, fn)
})
} else {
this.currentStepAnimates = {}
typeof fn === 'function' && fn()
this.isEnd = true
}
}
step(config = {}) {
// #ifndef APP-NVUE
this.animation.step(config)
// #endif
// #ifdef APP-NVUE
this.currentStepAnimates[this.next].config = Object.assign(
{},
this.options,
config
)
this.currentStepAnimates[this.next].styles.transformOrigin =
this.currentStepAnimates[this.next].config.transformOrigin
this.next++
// #endif
return this
}
run(fn) {
// #ifndef APP-NVUE
this.$.animationData = this.animation.export()
this.$.timer = setTimeout(() => {
typeof fn === 'function' && fn()
}, this.$.durationTime)
// #endif
// #ifdef APP-NVUE
this.isEnd = false
let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref
if (!ref) return
this._nvueNextAnimate(this.currentStepAnimates, 0, fn)
this.next = 0
// #endif
}
}
const animateTypes1 = [
'matrix',
'matrix3d',
'rotate',
'rotate3d',
'rotateX',
'rotateY',
'rotateZ',
'scale',
'scale3d',
'scaleX',
'scaleY',
'scaleZ',
'skew',
'skewX',
'skewY',
'translate',
'translate3d',
'translateX',
'translateY',
'translateZ',
]
const animateTypes2 = ['opacity', 'backgroundColor']
const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom']
animateTypes1.concat(animateTypes2, animateTypes3).forEach((type) => {
MPAnimation.prototype[type] = function (...args) {
// #ifndef APP-NVUE
this.animation[type](...args)
// #endif
// #ifdef APP-NVUE
this._nvuePushAnimates(type, args)
// #endif
return this
}
})
export function createAnimation(option, _this) {
if (!_this) return
clearTimeout(_this.timer)
return new MPAnimation(option, _this)
}

View File

@ -0,0 +1,286 @@
<template>
<!-- #ifndef APP-NVUE -->
<view v-show="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
<!-- #endif -->
</template>
<script>
import { createAnimation } from './createAnimation'
/**
* Transition 过渡动画
* @description 简单过渡动画组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
* @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-right 由右至左过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {Number} duration 过渡动画持续时间
* @property {Object} styles 组件样式 css 样式注意带-连接符的属性需要使用小驼峰写法如`backgroundColor:red`
*/
export default {
name: 'uniTransition',
emits:['click','change'],
props: {
show: {
type: Boolean,
default: false
},
modeClass: {
type: [Array, String],
default() {
return 'fade'
}
},
duration: {
type: Number,
default: 300
},
styles: {
type: Object,
default() {
return {}
}
},
customClass:{
type: String,
default: ''
},
onceRender:{
type:Boolean,
default:false
},
},
data() {
return {
isShow: false,
transform: '',
opacity: 1,
animationData: {},
durationTime: 300,
config: {}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open()
} else {
// close,
if (this.isShow) {
this.close()
}
}
},
immediate: true
}
},
computed: {
//
stylesObject() {
let styles = {
...this.styles,
'transition-duration': this.duration / 1000 + 's'
}
let transform = ''
for (let i in styles) {
let line = this.toLine(i)
transform += line + ':' + styles[i] + ';'
}
return transform
},
//
transformStyles() {
return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
}
},
created() {
//
this.config = {
duration: this.duration,
timingFunction: 'ease',
transformOrigin: '50% 50%',
delay: 0
}
this.durationTime = this.duration
},
methods: {
/**
* ref 触发 初始化动画
*/
init(obj = {}) {
if (obj.duration) {
this.durationTime = obj.duration
}
this.animation = createAnimation(Object.assign(this.config, obj),this)
},
/**
* 点击组件触发回调
*/
onClick() {
this.$emit('click', {
detail: this.isShow
})
},
/**
* ref 触发 动画分组
* @param {Object} obj
*/
step(obj, config = {}) {
if (!this.animation) return
for (let i in obj) {
try {
if(typeof obj[i] === 'object'){
this.animation[i](...obj[i])
}else{
this.animation[i](obj[i])
}
} catch (e) {
console.error(`方法 ${i} 不存在`)
}
}
this.animation.step(config)
return this
},
/**
* ref 触发 执行动画
*/
run(fn) {
if (!this.animation) return
this.animation.run(fn)
},
//
open() {
clearTimeout(this.timer)
this.transform = ''
this.isShow = true
let { opacity, transform } = this.styleInit(false)
if (typeof opacity !== 'undefined') {
this.opacity = opacity
}
this.transform = transform
// nextTick wx
this.$nextTick(() => {
// TODO
this.timer = setTimeout(() => {
this.animation = createAnimation(this.config, this)
this.tranfromInit(false).step()
this.animation.run()
this.$emit('change', {
detail: this.isShow
})
}, 20)
})
},
//
close(type) {
if (!this.animation) return
this.tranfromInit(true)
.step()
.run(() => {
this.isShow = false
this.animationData = null
this.animation = null
let { opacity, transform } = this.styleInit(false)
this.opacity = opacity || 1
this.transform = transform
this.$emit('change', {
detail: this.isShow
})
})
},
//
styleInit(type) {
let styles = {
transform: ''
}
let buildStyle = (type, mode) => {
if (mode === 'fade') {
styles.opacity = this.animationType(type)[mode]
} else {
styles.transform += this.animationType(type)[mode] + ' '
}
}
if (typeof this.modeClass === 'string') {
buildStyle(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildStyle(type, mode)
})
}
return styles
},
//
tranfromInit(type) {
let buildTranfrom = (type, mode) => {
let aniNum = null
if (mode === 'fade') {
aniNum = type ? 0 : 1
} else {
aniNum = type ? '-100%' : '0'
if (mode === 'zoom-in') {
aniNum = type ? 0.8 : 1
}
if (mode === 'zoom-out') {
aniNum = type ? 1.2 : 1
}
if (mode === 'slide-right') {
aniNum = type ? '100%' : '0'
}
if (mode === 'slide-bottom') {
aniNum = type ? '100%' : '0'
}
}
this.animation[this.animationMode()[mode]](aniNum)
}
if (typeof this.modeClass === 'string') {
buildTranfrom(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildTranfrom(type, mode)
})
}
return this.animation
},
animationType(type) {
return {
fade: type ? 1 : 0,
'slide-top': `translateY(${type ? '0' : '-100%'})`,
'slide-right': `translateX(${type ? '0' : '100%'})`,
'slide-bottom': `translateY(${type ? '0' : '100%'})`,
'slide-left': `translateX(${type ? '0' : '-100%'})`,
'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
}
},
//
animationMode() {
return {
fade: 'opacity',
'slide-top': 'translateY',
'slide-right': 'translateX',
'slide-bottom': 'translateY',
'slide-left': 'translateX',
'zoom-in': 'scale',
'zoom-out': 'scale'
}
},
// 线
toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase()
}
}
}
</script>
<style></style>

View File

@ -77,7 +77,7 @@
this.videoVid = event.detail.value;
},
setVid() {
setVid(vid) {
if(!this.videoVid) {
uni.showToast({
title: "请输入视频Vid",
@ -87,7 +87,7 @@
}
const { vodPlayer } = this;
vodPlayer.setVid({
vid:this.videoVid,
vid:this.videoVid || vid,
level:0
}, (ret) => {
this.text = JSON.stringify(ret);

29079
esmNim.js Normal file

File diff suppressed because it is too large Load Diff

5
locale/en.json Normal file
View File

@ -0,0 +1,5 @@
{
"messageText": "Message",
"cantactsText": "Cantact",
"mineText": "Me"
}

367
locale/en.ts Normal file
View File

@ -0,0 +1,367 @@
// en.js
export default {
appText: 'NIM',
messageText: 'Message',
contactText: 'Contacts',
mineText: 'Me',
logoutText: 'Log out',
commsEaseText: 'About NIM',
logoutConfirmText: 'Are you sure you want to log out?',
avatarText: 'Avatar',
accountText: 'Account',
name: 'Nickname',
genderText: 'Gender',
remark: 'remark',
mobile: 'Phone',
email: 'Email',
sign: 'Signature',
man: 'Male',
woman: 'Female',
unknow: 'Unknown',
birthText: 'Birthday',
userOnlineText: 'Online',
userOfflineText: 'Offline',
updateAvatarText: 'Update Avatar',
updateNameText: 'Update Name',
FriendPageText: 'Friend Card',
startConversationText: 'Start Chat',
addText: 'Add',
applyFriendSuccessText: 'Request Sent',
applyFriendFailText: 'Request Failed',
addFriendText: 'Add Friend',
addFriendFailText: 'Add Friend Failed',
deleteFriendText: 'Delete Friend',
deleteFriendConfirmText: 'Are you sure you want to delete this friend?',
deleteFriendSuccessText: 'Delete Friend Success',
deleteFriendFailText: 'Delete Friend Failed',
setBlackFailText: 'Add to blacklist failed',
removeBlackFailText: 'Remove from blacklist failed',
removeBlackSuccessText: 'Remove from blacklist success',
addPermissionText:
'Please add relevant permissions in the system settings page',
FailAvatarText: 'Avatar upload failed',
chooseDefaultImage: 'Choose default image',
chatButtonText: 'Go to Chat',
gotoChatFailText: 'Go to chat failed',
chatWithFriendText: 'Chat',
okText: 'OK',
updateText: 'Update',
cancelText: 'Cancel',
friendSelectText: 'Select Friend',
telErrorText: 'Invalid phone number format',
emailErrorText: 'Invalid email format',
createButtonText: 'Create',
createP2PText: 'Start Chat',
createTeamText: 'Create Group',
addTeamMemberText: 'Add Group Member',
discussionMemberText: '讨论组成员',
addTeamMemberFailText: 'Add Group Member Failed',
teamTitlePlaceholder: 'Enter Group Name',
maxSelectedText: 'You can only select up to 200 friends',
createTeamSuccessText: 'Create Group Success',
createTeamFailedText: 'Create Group Failed',
PersonalPageText: 'PersonalCard',
deleteSessionText: 'Delete',
sessionMuteText: 'Mute Notifications',
sessionMuteFailText: 'Mute Notifications Failed',
sessionUnMuteFailText: 'Unmute Notifications Failed',
stickTopText: 'Pin Chat',
deleteStickTopText: 'Unpin Chat',
addStickTopText: 'Pin',
deleteSessionFailText: 'Delete Conversation Failed',
deleteStickTopFailText: 'Unpin Chat Failed',
addStickTopFailText: 'Pin Message Failed',
teamMemberText: 'Group Members',
saveText: 'Save',
setText: 'Settings',
failText: 'Failed',
saveSuccessText: 'Save Success',
saveFailedText: 'Save Failed',
searchTeamPlaceholder: 'Enter Group Name',
teamTitle: 'Group Name',
discussionTitle: 'Discussion Group Name',
teamInfoText: 'Group Information',
discussionInfoText: 'Discussion Group Information',
teamAvatar: 'Group Avatar',
discussionAvatarText: 'Discussion Group Avatar',
networkError: 'Current network error',
searchFailText: 'Search Failed',
offlineText: 'Network is unavailable, please check network settings.',
connectingText:
'The current IM connection has been disconnected and is reconnecting…',
teamMenuText: 'My Groups',
teamChooseText: 'My Group Chats',
chooseText: 'Choose',
friendSelect: 'Please select a contact',
getHistoryMsgFailedText: 'Failed to get message history',
deleteText: 'Delete',
recallText: 'Recall',
copyText: 'Copy',
forwardText: 'Forward',
forwardComment: 'forward comment',
replyText: 'Reply',
replyNotFindText: 'Message has recalled or deleted',
forwardToTeamText: 'Forward to Group',
forwardToFriendText: 'Forward to Contact',
sessionRecordText: 'Conversation Record',
chatInputPlaceHolder: 'What would you like to say?',
enableV2CloudConversationText: 'Enable V2 Cloud Conversation',
SwitchToEnglishText: 'Switch to English',
pinText: 'Mark',
pinFailedText: 'Mark Failed',
unpinText: 'Unmark',
unpinFailedText: 'Unmark Failed',
noPinListText: 'No Mark',
collectionText: 'Collection',
deleteCollectionText: 'Delete collection',
addCollectionSuccessText: 'Add collection success',
addCollectionFailedText: 'Add collection Failed',
deleteCollectionFailedText: 'Delete collection Failed',
noCollectionsText: 'No collection',
teamBannedText: 'Message Banned',
you: 'You',
pinThisText: 'Marked this message',
delete: 'Delete this message',
recall: 'Recalled a message',
recall2: 'This message has been recalled',
recall3: 'Recall this message',
recallMsgFailText: 'Recall Message Failed',
reeditText: 'Edit',
loadingMoreText: 'Loading…',
noMoreText: 'No more',
deleteMsgSuccessText: 'Delete Success',
deleteMsgFailText: 'Delete Failed',
conversationEmptyText: 'No conversation',
teamOwner: 'Group Owner',
teamManager: 'Group Manager',
teamEmptyText: 'NoGroup Chats',
blacklistEmptyText: 'No Blocked Contacts',
blacklistSubTitle: 'You will not get messages from anyone on the list. ',
validEmptyText: 'No Verification Messages',
dismissTeamText: 'Dismiss Group',
dismissTeamConfirmText: 'Are you sure you want to dismiss this group?',
leaveTeamTitle: 'Leave Group',
leaveDiscussionTitle: 'Leave Discussion Group',
leaveTeamConfirmText: 'Are you sure you want to leave this group?',
leaveTeamSuccessText: 'You have left this group',
leaveTeamFailedText: 'Failed to leave this group',
leaveDiscussionSuccessText: 'Successfully left the discussion group',
leaveDiscussionFailedText: 'Failed to leave the discussion group',
dismissTeamSuccessText: 'Dismiss Group Success',
dismissTeamFailedText: 'Dismiss Group Failed',
updateTeamSuccessText: 'Update Success',
updateTeamFailedText: 'Update Failed',
teamTitleConfirmText: 'Group name cannot be empty',
teamIntroConfirmText: 'Group introduction cannot be empty',
aliasConfirmText: 'Alias cannot be empty',
sendImageFailedText: 'Failed to send Image',
sendVideoFailedText: 'Failed to send Video',
sendAudioFailedText: 'Failed to send Audio',
sendFileFailedText: 'Failed to send File',
sendMsgFailedText: 'Failed to send Message',
weekText: 'Weeks ago',
dayText: 'Days ago',
hourText: 'Hours ago',
minuteText: 'Minutes ago',
nowText: 'Just now',
sendText: 'Send',
sendToText: 'Send to',
textMsgText: 'Text Message',
audioMsgText: 'Audio Message',
videoMsgText: 'Video Message',
fileMsgText: 'File Message',
callMsgText: 'Call Message',
geoMsgText: 'Location Message',
imgMsgText: 'Image Message',
notiMsgText: 'Notification Message',
robotMsgText: 'Robot Message',
tipMsgText: 'Tip Message',
customMsgText: 'Custom Message',
unknowMsgText: 'Unknown Message',
noFriendText: 'No Friends',
onDismissTeamText: 'This group has been dismissed',
onRemoveTeamText: 'You have left the group',
selectSessionFailText: 'Failed to select conversation',
noExistUser: 'This user does not exist',
enterAccount: 'Enter account',
validMsgText: 'Verification Message',
blacklistText: 'Blocked Contacts',
applyTeamText: 'Apply to Join Group',
acceptResultText: 'Accepted',
rejectResultText: 'Rejected',
rejectTeamInviteText: 'Rejected group invitation',
beRejectResultText: 'Rejected friend request',
passResultText: 'Accepted friend request',
acceptedText: 'This request has been accepted',
acceptFailedText: 'Failed to accept this request',
rejectedText: 'This request has been rejected',
rejectFailedText: 'Failed to reject this request',
passFriendAskText:
"I have accepted your friend request, let's start chatting!",
applyFriendText: 'Add you as a friend',
rejectText: 'Reject',
acceptText: 'Accept',
inviteTeamText: 'Invite to Group',
addBlacklist: 'Block',
removeBlacklist: 'Unblock',
forwardSuccessText: 'Forward Success',
forwardFailText: 'Forward Failed',
getMessageFailed: 'Fail to get message',
getForwardMessageFailed: 'Fail to get forward message',
sendFailWithInBlackText:
'The other party has blocked you, message sending failed',
sendFailWithDeleteText:
'The relationship between both parties has been terminated, please reapply for contact',
friendVerificationText: 'Friend Verification',
teamAll: 'all',
chooseMentionText: 'Choose Mention',
someoneText: 'someone',
meText: 'mention',
teamMutePlaceholder: 'The current group owner has set the group to mute',
viewMoreText: 'View More',
resendMsgFailText: 'Resend Failed',
audioBtnText: 'Press and hold to speak',
audioRemindText: 'Release to send',
audioErrorText: 'Recording failed',
videoPlayText: 'Video Play',
voiceCallText: 'Voice Call',
videoCallText: 'Video Call',
callDurationText: 'call duration',
callCancelText: 'canceled',
callRejectedText: 'rejected',
callTimeoutText: 'Missed call',
callBusyText: 'busy line',
callFailedText: 'call failed',
wxAppFileCopyText: 'Please paste the URL in the mobile browser.',
shootText: 'shoot',
albumText: 'album',
msgReadPageTitleText: 'Message Reading Status',
readText: 'Read',
unreadText: 'Unread',
fileText: 'file',
allUnReadText: 'All Unread',
msgRecallTimeErrorText: 'Message recall time exceeded',
allReadText: 'All Read',
searchText: 'Search',
searchTitleText: 'Search',
friendText: 'Friend',
teamText: 'Group',
discussionText: 'Discussion Group',
searchResultNullText: 'User does not exist',
conversationSendFailText: '[Send Failed]',
conversationNotificationText: '[Notification]',
fileMsgTitleText: '[File Message]',
collectionFromText: 'Collection from',
copySuccessText: 'Copy Success',
openUrlText: 'Copied. Open in browser.',
teamManagerText: 'Team Manager',
teamManagerSettingText: 'Team Manager Setting',
updateTeamInfoText: 'Who is allowed to edit group details',
teamOwnerAndManagerText: 'TeamOwnerAndManager',
updateTeamInviteText: 'Who is allowed to invite group members',
updateTeamAtText: 'Who is allowed to @',
addMemberText: 'Add Member',
removeText: 'remove',
securityTipText:
'For test only. Beware of money transfer, lottery winnings & strange call scams.',
createDiscussionText: 'Create Discussion',
// emoji 不能随便填,要用固定 key
Laugh: '[Laugh]',
Happy: '[Happy]',
Sexy: '[Sexy]',
Cool: '[Cool]',
Mischievous: '[Mischievous]',
Kiss: '[Kiss]',
Spit: '[Spit]',
Squint: '[Squint]',
Cute: '[Cute]',
Grimace: '[Grimace]',
Snicker: '[Snicker]',
Joy: '[Joy]',
Ecstasy: '[Ecstasy]',
Surprise: '[Surprise]',
Tears: '[Tears]',
Sweat: '[Sweat]',
Angle: '[Angle]',
Funny: '[Funny]',
Awkward: '[Awkward]',
Thrill: '[Thrill]',
Cry: '[Cry]',
Fretting: '[Fretting]',
Terrorist: '[Terrorist]',
Halo: '[Halo]',
Shame: '[Shame]',
Sleep: '[Sleep]',
Tired: '[Tired]',
Mask: '[Mask]',
ok: '[ok]',
AllRight: '[All right]',
Despise: '[Despise]',
Uncomfortable: '[Uncomfortable]',
Disdain: '[Disdain]',
ill: '[ill]',
Mad: '[Mad]',
Ghost: '[Ghost]',
Angry: '[Angry]',
Unhappy: '[Unhappy]',
Frown: '[Frown]',
Broken: '[Broken]',
Beckoning: '[Beckoning]',
Ok: '[Ok]',
Low: '[Low]',
Nice: '[Nice]',
Applause: '[Applause]',
GoodJob: '[Good job]',
Hit: '[Hit]',
Please: '[Please]',
Bye: '[Bye]',
First: '[First]',
Fist: '[Fist]',
GiveMeFive: '[Give me five]',
Knife: '[Knife]',
Hi: '[Hi]',
No: '[No]',
Hold: '[Hold]',
Think: '[Think]',
Pig: '[Pig]',
NoListen: '[No listen]',
NoLook: '[No look]',
NoWords: '[No words]',
Monkey: '[Monkey]',
Bomb: '[Bomb]',
Cloud: '[Cloud]',
Rocket: '[Rocket]',
Ambulance: '[Ambulance]',
Poop: '[Poop]',
// 错误码及提示内容
// 通用错误码
190001: 'internal error',
190002: 'illegal state',
191001: 'misuse',
191002: 'cancelled',
191003: 'callback failed',
191004: 'invalid parameter',
191005: 'timeout',
191006: 'resource not exist',
191007: 'resource already exist',
// 连接相关
192001: 'connect failed',
192002: 'connect timeout',
192003: 'disconnected',
192004: 'protocol timeout',
192005: 'protocol send failed',
192006: 'request failed',
// 消息相关
107319: 'PIN limit exceeded',
107320: 'PIN not exist',
107322: 'PIN already exist',
107327: 'PIN function disabled',
189301: 'collection limit exceeded',
189302: 'collection not exist',
189449: 'collection concurrent operation failed',
// 群相关
109432: 'no permission',
}

5
locale/zh-Hans.json Normal file
View File

@ -0,0 +1,5 @@
{
"messageText": "消息",
"cantactsText": "通讯录",
"mineText": "我的"
}

407
locale/zh-Hans.ts Normal file
View File

@ -0,0 +1,407 @@
// zn.js
export default {
appText: '云信IM',
messageText: '消息',
contactText: '通讯录',
mineText: '我的',
logoutText: '退出登录',
commsEaseText: '关于云信',
logoutConfirmText: '确定退出登录吗?',
avatarText: '头像',
updateAvatarText: '修改头像',
updateNameText: '修改名称',
accountText: '账号',
name: '昵称',
genderText: '性别',
remarkText: '备注名',
mobile: '手机',
email: '邮箱',
sign: '签名',
man: '男',
woman: '女',
unknow: '未知',
birthText: '生日',
userOnlineText: '在线',
userOfflineText: '离线',
FriendPageText: '好友名片',
startConversationText: '发起单聊',
addText: '添加',
removeText: '移除',
addTeamManagerSuccessText: '添加管理员成功',
addTeamManagerFailText: '添加管理员失败',
confirmRemoveText: '是否移除?',
removeManagerExplain: '移除后该成员将无管理权限',
removeMemberExplain: '将移除该成员',
removeSuccessText: '移除成功',
removeFailText: '移除失败',
userNotInTeam: '该用户不在群里',
noTeamManager: '暂无群管理员',
noTeamMember: '暂无群成员',
applyFriendSuccessText: '好友申请已发送',
applyFriendFailText: '发送申请失败',
addFriendText: '添加好友',
addMemberText: '添加成员',
addFriendFailText: '添加好友失败',
deleteFriendText: '删除好友',
deleteFriendConfirmText: '是否确认删除好友',
deleteFriendSuccessText: '删除好友成功',
deleteFriendFailText: '删除好友失败',
setBlackFailText: '加入黑名单失败',
removeBlackFailText: '解除黑名单失败',
removeBlackSuccessText: '解除黑名单成功',
addPermissionText: '请在系统设置页添加相关权限',
FailAvatarText: '上传头像失败',
chooseDefaultImage: '选择默认图标',
chatButtonText: '去聊天',
gotoChatFailText: '去聊天失败',
chatWithFriendText: '聊天',
okText: '确定',
updateText: '修改',
cancelText: '取消',
friendSelectText: '选择好友',
telErrorText: '手机格式不正确',
emailErrorText: '邮箱格式不正确',
createButtonText: '创建',
createP2PText: '发起单聊',
createTeamText: '创建群聊',
addTeamMemberText: '添加群成员',
addTeamMemberFailText: '添加群成员失败',
teamTitlePlaceholder: '请输入群名称',
maxSelectedText: '最多只能选择200位好友',
createTeamSuccessText: '创建群组成功',
createDiscussionSuccessText: '创建讨论组成功',
createTeamFailedText: '创建群组失败',
createDiscussionFailedText: '创建讨论组失败',
PersonalPageText: '个人名片',
deleteSessionText: '删除会话',
sessionMuteText: '开启消息提醒',
sessionMuteFailText: '开启消息提醒失败',
sessionUnMuteFailText: '关闭消息提醒失败',
stickTopText: '聊天置顶',
deleteStickTopText: '取消置顶',
addStickTopText: '置顶消息',
deleteSessionFailText: '删除会话失败',
deleteStickTopFailText: '取消置顶失败',
addStickTopFailText: '置顶消息失败',
teamMemberText: '群成员',
discussionMemberText: '讨论组成员',
teamManagerText: '群管理',
teamManagerSettingText: '管理管理员',
updateTeamInfoText: '谁可以编辑群信息',
updateTeamInviteText: '谁可以邀请新成员',
updateTeamAtText: '谁可以@所有人',
transformTeam: '转让群主',
transformTeamContent: '确认转让群主给',
doubt: '吗?',
transformTeamSuccessText: '转让群主成功',
transformTeamFailedText: '转让群主失败',
transformTeamText: '选择转让成员',
teamBannedText: '群禁言',
saveText: '保存',
setText: '设置',
failText: '失败',
saveSuccessText: '保存成功',
saveFailedText: '保存失败',
searchTeamPlaceholder: '请输入群名称',
nickInTeamEmptyText: '请输入群昵称',
teamTitle: '群名称',
discussionTitle: '讨论组名称',
teamInfoText: '群信息',
discussionInfoText: '讨论组信息',
teamIntro: '群介绍',
teamAvatar: '群头像',
discussionAvatarText: '讨论组头像',
networkError: '当前网络错误',
searchFailText: '查找失败',
offlineText: '当前网络不可用,请检查你当前网络设置。',
connectingText: '当前IM连接已断开正在重连...',
teamMenuText: '我的群组',
teamChooseText: '我的群聊',
chooseText: '选择',
friendSelect: '请选择联系人',
pleaseSelectMember: '请选择成员',
teamMemebrSelect: '人员选择',
getHistoryMsgFailedText: '获取失败',
deleteText: '删除',
recallText: '撤回',
copyText: '复制',
forwardText: '转发',
forwardComment: '留言',
forwardSuccessText: '转发成功',
forwardFailedText: '转发失败',
getMessageFailed: '获取消息失败',
replyText: '回复',
replyNotFindText: '该消息已撤回或删除',
msgRecallTimeErrorText: '已超过时间无法撤回',
enableV2CloudConversationText: '是否开启云端会话',
SwitchToEnglish: '切换语言为英文',
copySuccessText: '复制成功',
forwardToTeamText: '转发到群组',
forwardToFriendText: '转发到个人',
sessionRecordText: '的会话记录',
chatInputPlaceHolder: '说点什么呢~',
pinText: '标记',
pinFailedText: '标记失败',
unpinText: '取消标记',
unpinFailedText: '取消标记失败',
noPinListText: '暂无标记消息',
collectionText: '收藏',
deleteCollectionText: '删除收藏',
addCollectionSuccessText: '收藏成功',
addCollectionFailedText: '添加收藏失败',
deleteCollectionFailedText: '删除收藏失败',
noCollectionsText: '暂无收藏',
you: '你',
pinThisText: '标记了这条消息',
delete: '删除此消息',
recall: '撤回了一条消息',
recall2: '此消息已撤回',
recall3: '撤回此消息',
recallMsgFailText: '撤回消息失败',
reeditText: '重新编辑',
loadingMoreText: '加载更多……',
noMoreText: '没有更多了',
deleteMsgSuccessText: '删除成功',
deleteMsgFailText: '删除失败',
conversationEmptyText: '暂无会话',
teamOwner: '群主',
teamManager: '群管理员',
manager: '管理员',
teamEmptyText: '暂无群聊',
blacklistEmptyText: '暂无黑名单',
blacklistSubTitle: '你不会收到列表中任何联系人的消息',
validEmptyText: '暂无验证消息',
dismissTeamText: '解散群组',
dismissTeamConfirmText: '是否确认解散该群组',
leaveTeamTitle: '退出群聊',
leaveDiscussionTitle: '退出讨论组',
leaveTeamConfirmText: '是否确认退出该群组',
leaveDiscussionConfirmText: '是否确认退出该讨论组',
leaveTeamSuccessText: '已成功退出此群',
leaveDiscussionSuccessText: '已成功退出此讨论组',
leaveTeamFailedText: '退出此群失败',
leaveDiscussionFailedText: '退出此讨论组失败',
dismissTeamSuccessText: '群解散成功',
dismissTeamFailedText: '群解散失败',
updateTeamSuccessText: '修改成功',
updateTeamFailedText: '修改失败',
teamTitleConfirmText: '群名称不能为空',
teamIntroConfirmText: '群介绍不能为空',
aliasConfirmText: '备注名不能为空',
sendImageFailedText: '发送图片失败',
sendVideoFailedText: '发送视频失败',
sendAudioFailedText: '发送音频失败',
sendFileFailedText: '发送文件失败',
sendMsgFailedText: '发送消息失败',
weekText: '周前',
dayText: '天前',
hourText: '小时前',
minuteText: '分钟前',
nowText: '刚刚',
sendText: '发送',
sendToText: '发送给',
textMsgText: '文本消息',
audioMsgText: '音频消息',
videoMsgText: '视频消息',
fileMsgText: '文件消息',
callMsgText: '话单消息',
geoMsgText: '地理位置消息',
imgMsgText: '图片消息',
notiMsgText: '通知消息',
robotMsgText: '机器消息',
tipMsgText: '提示消息',
customMsgText: '自定义消息',
unknowMsgText: '未知消息体',
noFriendText: '暂无好友',
onDismissTeamText: '该群聊已解散',
onRemoveTeamText: '您已离开群组',
selectSessionFailText: '选择会话失败',
noExistUser: '该用户不存在',
enterAccount: '请输入账号',
validMsgText: '验证消息',
blacklistText: '黑名单',
applyTeamText: '申请入群',
acceptResultText: '已同意',
rejectResultText: '已拒绝',
rejectTeamInviteText: '拒绝了群邀请',
beRejectResultText: '拒绝了好友申请',
passResultText: '通过了好友申请',
acceptedText: '已同意该申请',
acceptFailedText: '同意该申请失败',
rejectedText: '已拒绝该申请',
rejectFailedText: '拒绝该申请失败',
passFriendAskText: '我已经同意了你的申请,现在开始聊天吧~',
applyFriendText: '添加您为好友',
rejectText: '拒绝',
acceptText: '同意',
inviteTeamText: '邀请入群',
addBlacklist: '加入黑名单',
removeBlacklist: '解除',
teamAll: '所有人',
sendFailWithInBlackText: '对方已将你拉黑,消息发送失败',
sendFailWithDeleteText: '双方关系已解除,如需沟通,请申请',
friendVerificationText: '好友验证',
chooseMentionText: '选择提醒',
someoneText: '有人',
meText: '我',
teamMutePlaceholder: '当前群主已经设置为群禁言',
noPermission: '您暂无权限操作',
muteAllTeamFailedText: '全员禁言失败',
unmuteAllTeamFailedText: '解除全员禁言失败',
nickInTeam: '我在群里的昵称',
viewMoreText: '查看更多',
resendMsgFailText: '重发失败',
audioBtnText: '按住说话',
audioRemindText: '松开发送',
audioErrorText: '录音失败,请检查是否开启权限',
videoPlayText: '视频播放',
updateTeamAvatar: '更新了群头像',
updateTeamName: '更新群名称为',
updateTeamIntro: '更新了群介绍',
updateTeamInviteMode: '更新了群权限“邀请他人权限”为',
updateTeamUpdateTeamMode: '更新了群权限“群资料修改权限”为',
updateAllowAt: '更新了“@所有人权限”为',
updateTeamMute: '更新了“群禁言”为',
onlyTeamOwner: '仅群主',
teamOwnerAndManagerText: '群主和管理员',
closeText: '关闭',
openText: '开启',
joinTeamText: '加入群组',
leaveTeamText: '离开了群组',
beRemoveTeamText: '被移出群组',
beAddTeamManagersText: '被任命为管理员',
beRemoveTeamManagersText: '被移除管理员',
newGroupOwnerText: '成为新群主',
callDurationText: '通话时长',
callCancelText: '已取消',
callRejectedText: '已拒绝',
callTimeoutText: '超时未接听',
callBusyText: '忙线未接听',
callFailedText: '呼叫失败',
wxAppFileCopyText: '已自动复制网址,请在手机浏览器里粘贴该网址',
shootText: '拍摄',
albumText: '相册',
voiceCallText: '语音通话',
videoCallText: '视频通话',
msgReadPageTitleText: '消息阅读状态',
readText: '已读',
unreadText: '未读',
fileText: '文件',
allUnReadText: '全部成员未读',
allReadText: '全部成员已读',
searchText: '请输入你要搜索的关键字',
searchTitleText: '搜索',
friendText: '好友',
teamText: '群组',
discussionText: '讨论组',
searchResultNullText: '该用户不存在',
conversationSendFailText: '[发送失败]',
conversationNotificationText: '[通知消息]',
getForwardMessageFailed: '该消息已撤回或删除',
fileMsgTitleText: '[文件消息]',
collectionFromText: '来自',
openUrlText: '已复制链接,请在浏览器打开链接',
SwitchToEnglishText: '是否切换为英文',
securityTipText:
'仅用于体验云信IM 产品功能,请勿轻信汇款、中奖等涉及钱款的信息,勿轻易拨打陌生电话,谨防上当受骗。',
createDiscussionText: '创建讨论组',
// emoji 不能随便填,要用固定 key
Laugh: '[大笑]',
Happy: '[开心]',
Sexy: '[色]',
Cool: '[酷]',
Mischievous: '[奸笑]',
Kiss: '[亲]',
Spit: '[伸舌头]',
Squint: '[眯眼]',
Cute: '[可爱]',
Grimace: '[鬼脸]',
Snicker: '[偷笑]',
Joy: '[喜悦]',
Ecstasy: '[狂喜]',
Surprise: '[惊讶]',
Tears: '[流泪]',
Sweat: '[流汗]',
Angle: '[天使]',
Funny: '[笑哭]',
Awkward: '[尴尬]',
Thrill: '[惊恐]',
Cry: '[大哭]',
Fretting: '[烦躁]',
Terrorist: '[恐怖]',
Halo: '[两眼冒星]',
Shame: '[害羞]',
Sleep: '[睡着]',
Tired: '[冒星]',
Mask: '[口罩]',
ok: '[OK]',
AllRight: '[好吧]',
Despise: '[鄙视]',
Uncomfortable: '[难受]',
Disdain: '[不屑]',
ill: '[不舒服]',
Mad: '[愤怒]',
Ghost: '[鬼怪]',
Angry: '[发怒]',
Unhappy: '[不高兴]',
Frown: '[皱眉]',
Broken: '[心碎]',
Beckoning: '[心动]',
Ok: '[好的]',
Low: '[低级]',
Nice: '[赞]',
Applause: '[鼓掌]',
GoodJob: '[给力]',
Hit: '[打你]',
Please: '[阿弥陀佛]',
Bye: '[拜拜]',
First: '[第一]',
Fist: '[拳头]',
GiveMeFive: '[手掌]',
Knife: '[剪刀]',
Hi: '[招手]',
No: '[不要]',
Hold: '[举着]',
Think: '[思考]',
Pig: '[猪头]',
NoListen: '[不听]',
NoLook: '[不看]',
NoWords: '[不说]',
Monkey: '[猴子]',
Bomb: '[炸弹]',
Cloud: '[筋斗云]',
Rocket: '[火箭]',
Ambulance: '[救护车]',
Poop: '[便便]',
// 错误码及提示内容
// 通用错误码
190001: '内部错误',
190002: '非法状态',
191001: '使用姿势错误',
191002: '操作取消',
191003: '回调失败',
191004: '参数错误',
191005: '请求超时',
191006: '资源不存在',
191007: '资源已存在',
// 连接相关
192001: '连接错误',
192002: '连接超时',
192003: '连接断开',
192004: '协议超时',
192005: '协议发送失败',
192006: '请求失败',
// 消息相关
107319: 'PIN 数量超限',
107320: 'PIN 消息不存在',
107322: '重复 PIN',
107327: '未开启 PIN 功能',
189301: '收藏数量超限',
189302: '收藏不存在',
189449: '并发操作收藏失败',
// 群相关
109432: '没有权限',
}

View File

@ -9,6 +9,10 @@
"description": "",
"dependencies": {
"crypto-js": "^4.2.0",
"@xkit-yx/im-store-v2": "^0.8.3",
"@xkit-yx/utils": "^0.7.2",
"mobx": "^6.6.1",
"nim-web-sdk-ng": "^10.9.30",
"dayjs": "^1.11.18",
"js-base64": "^3.7.8",
"js-md5": "^0.8.3",

View File

@ -208,6 +208,47 @@
}
}
},
{
"path": "patientInfo/patientInfo",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "患者信息",
"app": {
"bounce": "none"
}
}
},
{
"path": "myAnswer/myAnswer",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "我的意见",
"app": {
"bounce": "none"
}
}
},
{
"path": "consultDetail/consultDetail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "consult/consult",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "searchNews/searchNews",
"style": {
@ -361,6 +402,7 @@
}
}
},
{
"path": "selectPatient/selectPatient",
"style": {
@ -394,6 +436,16 @@
}
}
},
{
"path": "liveReplay/liveReplay",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "videoDetail/videoDetail",
"style": {
@ -885,12 +937,23 @@
{
"path": "feedback/feedback-logoff",
"style": {
"navigationStyle": "custom",
"navigationStyle": "custom",
"navigationBarTitleText": "注销账户",
"app": {
"bounce": "none"
}
}
},
{
"path": "reply/reply",
"style": {
"navigationStyle": "custom",
"navigationBarRightButton":{ "hide": true},
"navigationBarTitleText": "回复",
"app": {
"bounce": "none"
}
}
}
@ -970,6 +1033,32 @@
}
}
]
},
{
"root": "pages_chat",
"pages": [
{
"path": "chat/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "聊天页",
"app": {
"bounce": "none"
}
}
},
{
"path": "groupMessage/groupMessage",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
}
]
}],
"globalStyle": {
"navigationBarTextStyle": "black",

View File

@ -56,7 +56,7 @@
<view class="grid-section">
<uni-grid :column="4" :highlight="true" >
<uni-grid-item v-for="(item, index) in gridList" :key="index">
<view class="grid-item" @click="onClick(index)">
<view class="grid-item" @click="onClick(index,item.text)">
<up-image :src="item.icon" width="75rpx" height="75rpx" ></up-image>
<text class="grid-text">{{ item.text }}</text>
</view>
@ -254,6 +254,7 @@
import sign from "@/static/home_qiandao_icon.png"
import signImg from "@/static/sign_in_bng_big.png"
import dayjs from 'dayjs'
//import imLogin from "@/utils/IM.js"
const expertDetail=reactive({})
const showSign=ref(false)
const signInfo=reactive({
@ -446,28 +447,31 @@
const onReplayClick = (item) => {
console.log('点击精彩回放:', item);
navTo({
url: '/pages_course/course_detail/course_detail?id=' + item.id
url: '/pages_app/videoDetail/videoDetail?id='+item.id
})
};
//
const onClick = (index) => {
const onClick = (index,name) => {
console.log(name)
console.log('点击了第' + index + '个');
let url=''
if(index==0){
if(name=='我的患者'){
url='/pages_app/patientMsg/patientMsg'
}else if(index==1){
}else if(name=='肝胆视频'){
url='/pages_app/video/video'
}else if(index==2){
}else if(name=='公益咨询'){
url='/pages_app/consult/consult'
}else if(index==3){
}else if(name=='指南杂志'){
url='/pages_app/zhinan/zhinan'
}else if(index==4){
url='/pages_app/news/news'
}else if(index==5){
}else if(name=='肝胆新闻'){
url='/pages_app/newsList/newsList'
}else if(name=='肝胆课件'){
url='/pages_app/ppt/ppt'
}else if(index==6){
}else if(name=='精品课'){
url='/pages_course/course/course'
}else if(name=='积分商城'){
url='/pages_goods/pointMall/pointMall'
}else{
url='/pages_app/myApplication/myApplication'
}
@ -831,16 +835,18 @@
//
onLoad(() => {
console.log('首页加载完成');
});
//
onShow(() => {
console.log('首页显示');
//
getHomeData();
// tabbar
testTabbar();
});
//
onShow(() => {
//imLogin();
console.log('首页显示');
// 5tabbar
setTimeout(() => {
getTabbarStatus();

View File

@ -13,7 +13,7 @@
<up-image :src="select" width="26rpx" height="26rpx" ></up-image>
</view>
<view class="filter-divider"></view>
<view class="filter-item">
<view class="filter-item" @click="goLiveReplay">
<text>会议回放</text>
</view>
</view>
@ -39,7 +39,7 @@
<view class="meeting-list" v-if="meetingList.length > 0">
<template v-for="(group, groupIndex) in groupedMeetings" :key="groupIndex">
<view class="time-header">{{ group.monthYear }}</view>
<view class="meetcell" v-for="(item, index) in group.items" :key="item.id || index">
<view class="meetcell" v-for="(item, index) in group.items" :key="item.id || index" @click="goDetail(item)">
<view class="meeting-item">
<!-- 左侧日期标识 -->
<view class="date-tag" :style="{backgroundColor: item.tagColor}">
@ -167,7 +167,7 @@
import timeImg from "@/static/play_long.png"
import docUrl from "@/utils/docUrl"
import dayjs from 'dayjs'
import navTo from '@/utils/navTo.js';
//
const isTimePopupShow = ref(false);
const isLocationPopupShow = ref(false);
@ -230,6 +230,12 @@
monthList.value = months;
console.log('生成的月份列表:', monthList.value);
};
const goDetail=(item)=>{
const encoded = encodeURIComponent(item.path);
uni.navigateTo({
url: `/pages_app/webview/webview?url=${encoded}`
});
}
//
const provinceList = ref([
{ code: '', name: '全国' },
@ -268,7 +274,11 @@
{ code: '香港', name: '香港特别行政区' },
{ code: '澳门', name: '澳门特别行政区' }
]);
const goLiveReplay = () => {
navTo({
url: '/pages_app/liveReplay/liveReplay'
});
};
//
const meetingList = ref([]);
@ -869,7 +879,7 @@ $shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-top: 231rpx;
position: relative;
background-color: $white;
box-sizing: border-box;
width: 100%;
height: calc(100vh - 433rpx);
padding:40rpx;
@ -932,6 +942,7 @@ $shadow: 0 2px 8px rgba(0,0,0,0.1);
display: block;
white-space: nowrap;
overflow: hidden;
width:106rpx;
text-overflow: ellipsis;
word-break: break-all;
}

View File

@ -0,0 +1,479 @@
<template>
<view class="consult-page">
<!-- 顶部导航与标签 -->
<navBar title="公益咨询" />
<view class="tabs">
<view :class="['tab', activeTab==='new' ? 'active' : '']" @tap="switchTab('new')">新的咨询</view>
<view :class="['tab', activeTab==='mine' ? 'active' : '']" @tap="switchTab('mine')">我已回答</view>
</view>
<view class="tabs-spacer"></view>
<!-- 列表 -->
<scroll-view
scroll-y
class="list-scroll"
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onReachBottom"
lower-threshold="80"
>
<view v-for="(item, idx) in displayList" :key="idx" class="consult-card" @click="goDetail(item.id)">
<view class="card-head">
<text class="user-name">{{ item.maskName }}</text>
<text class="date">{{ item.date }}</text>
</view>
<view class="card-body">
<text class="content">{{ item.content }}</text>
</view>
<view class="card-foot" v-if="bottomActive==='multi'">
<text class="reply-count">{{ item.answer_num }}位医生已回答</text>
<view v-if="item.tag" class="tag">{{ item.tag }}</view>
</view>
<view class="card-foot" v-else>
<view class="left">
<view class="detail">问题详情</view>
<view v-if="item.tag" class="tag">{{ item.tag }}</view>
</view>
</view>
</view>
<empty v-if="displayList.length===0" />
<view v-if="isLoading" style="text-align:center;color:#9aa0a6;padding:10px 0;">加载中...</view>
<view v-else-if="!hasMore && displayList.length>0" style="text-align:center;color:#9aa0a6;padding:10px 0;">没有更多了</view>
</scroll-view>
<!-- 底部操作条双按钮 Tab -->
<view class="bottom-bar" :style="{height: bottomBarHeight+'px'}">
<view :class="['bottom-tab', bottomActive==='quick' ? 'active' : '']" @click="switchBottomTab('quick')">快速问医生</view>
<view :class="['bottom-tab', bottomActive==='multi' ? 'active' : '']" @click="switchBottomTab('multi')">多对一解惑</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import navBar from '@/components/navBar/navBar.vue'
import empty from '@/components/empty/empty.vue'
import api from '@/api/api.js'
import navTo from '@/utils/navTo.js'
const activeTab = ref('new');
const bottomBarHeight = 56
const page=ref(1)
const pageSize=ref(10)
const hasMore = ref(true)
const isLoading = ref(false)
const isRefreshing = ref(false)
const goDetail=async(uuid)=>{
if(bottomActive.value==='quick'){
let userId=uni.getStorageSync('userInfo').uuid.toLowerCase();
let conversationId=userId+'|1|'+uuid.toLowerCase();
await uni.$UIKitStore.uiStore.selectConversation(conversationId)
navTo({
url:'/pages_chat/chat/index?from=consult'
})
}else{
let status=0;
if(activeTab.value==='new'){
status=0;
}else{
status=1;
}
navTo({
url:'/pages_app/consultDetail/consultDetail?uuid='+uuid+'&status='+status
})
}
};
function maskName(name){
if(!name) return '**'
const first = name.slice(0,1)
return `${first}**`
}
const newConsultList=async(isRefresh=false)=>{
if(isLoading.value) return
isLoading.value = true
if(isRefresh){
page.value = 1
hasMore.value = true
listNew.value = []
}
const res=await api.newConsultList({
page:page.value,
pageSize:pageSize.value
})
console.log(res)
if(res && res.code===200 && res.data && res.data.consult_list){
const list = Array.isArray(res.data.consult_list.list) ? res.data.consult_list.list : []
const mapped = list.map(item=>({
maskName: maskName(item.realName || ''),
date: (item.createDate || '').slice(0,10),
content: item.content || '',
replyCount: 0,
tag: item.diseaseName || '',
id:item.patientUuid || ''
}))
if(isRefresh){
listNew.value = mapped
}else{
listNew.value = page.value===1 ? mapped : [...listNew.value, ...mapped]
}
const totalPage = Number(res.data.consult_list.totalPage || 1)
hasMore.value = page.value < totalPage
}
isLoading.value = false
if(isRefresh){
isRefreshing.value = false
}
}
const consultListHis=async(isRefresh=false)=>{
if(isLoading.value) return
isLoading.value = true
if(isRefresh){
page.value = 1
hasMore.value = true
listMine.value = []
}
const res=await api.consultListHis({
page:page.value,
pageSize:pageSize.value
})
console.log(res)
if(res.code==200){
const list = Array.isArray(res.data.list) ? res.data.list : []
const mapped = list.map(item=>({
maskName: maskName(item.realName || ''),
date: (item.createDate || '').slice(0,10),
content: item.content || '',
replyCount: 0,
tag: item.diseaseName || '',
id:item.patientUuid || ''
}))
if(isRefresh){
listMine.value = mapped
}else{
listMine.value = page.value===1 ? mapped : [...listMine.value, ...mapped]
}
const totalPage = Number(res.data.totalPage || 1)
hasMore.value = page.value < totalPage
}
isLoading.value = false
if(isRefresh){
isRefreshing.value = false
}
}
const listNewInterrogation=async(isRefresh=false)=>{
if(isLoading.value) return
isLoading.value = true
if(isRefresh){
page.value = 1
hasMore.value = true
listNew.value = []
}
const res=await api.listNewInterrogation({
page:page.value,
pageSize:pageSize.value
})
if(res.code ==200){
const list =res.data.list;
console.log(1111)
console.log(list)
const mapped = list.map(item=>({
maskName: maskName(item.name || ''),
date: (item.create_date || '').slice(0,10),
content: item.your_question || '',
disease_describe:item.your_question || '',
answer_num: item.answer_num || 0,
tag: item.disease_name || '',
id:item.step1_uuid || ''
}))
if(isRefresh){
listNew.value = mapped
}else{
listNew.value = page.value===1 ? mapped : [...listNew.value, ...mapped]
}
const totalPage = Number(res.data.pages || 1)
hasMore.value = page.value < totalPage
}
isLoading.value = false
if(isRefresh){
isRefreshing.value = false
}
}
const listMyAnsweredInterrogation=async(isRefresh=false)=>{
if(isLoading.value) return
isLoading.value = true
if(isRefresh){
page.value = 1
hasMore.value = true
listMine.value = []
}
const res=await api.listMyAnsweredInterrogation({
page:page.value,
pageSize:pageSize.value
})
console.log(res)
if(res.code==200){
const list = Array.isArray(res.data.list) ? res.data.list : []
const mapped = list.map(item=>({
maskName: maskName(item.name || ''),
date: (item.create_date || '').slice(0,10),
content: item.your_question || '',
disease_describe:item.disease_describe || '',
answer_num: item.answer_num || 0,
tag: item.disease_name || '',
id:item.step1_uuid || ''
}))
if(isRefresh){
listMine.value = mapped
}else{
listMine.value = page.value===1 ? mapped : [...listMine.value, ...mapped]
}
const totalPage = Number(res.data.pages || 1)
hasMore.value = page.value < totalPage
}
isLoading.value = false
if(isRefresh){
isRefreshing.value = false
}
};
onShow(()=>{
page.value = 1
hasMore.value = true;
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(true)
}else{
consultListHis(true)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(true)
}else{
listMyAnsweredInterrogation(true)
}
}
})
const listNew = ref([])
const listMine = ref([])
const displayList = computed(() => (activeTab.value === 'new' ? listNew.value : listMine.value))
function switchTab(key) {
activeTab.value = key;
isLoading.value = false;
listNew.value = [];
listMine.value=[];
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(true)
}else{
consultListHis(true)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(true)
}else{
listMyAnsweredInterrogation(true)
}
}
}
function goBack() {
// #ifdef H5
history.back()
// #endif
// #ifndef H5
uni.navigateBack({ delta: 1 })
// #endif
}
// tab
const bottomActive = ref('quick')
const switchBottomTab=(key)=>{
isLoading.value = false;
bottomActive.value = key;
listNew.value = [];
listMine.value=[];
if(key=='quick'){
if(activeTab.value==='new'){
newConsultList(true)
}else{
consultListHis(true)
}
}else{
if(activeTab.value==='new'){
console.log('listNewInterrogation')
listNewInterrogation(true);
}else{
listMyAnsweredInterrogation(true)
}
}
}
//
function onRefresh(){
if(isRefreshing.value) return
isRefreshing.value = true
page.value = 1
hasMore.value = true
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(true)
}else{
consultListHis(true)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(true)
}else{
listMyAnsweredInterrogation(true)
}
}
}
//
function onReachBottom(){
if(!hasMore.value || isLoading.value) return
page.value += 1
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(false)
}else{
consultListHis(false)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(false)
}else{
listMyAnsweredInterrogation(false)
}
}
}
</script>
<style lang="scss" scoped>
.consult-page {
background-color: #f7f7f7;
height: 100vh;
display: flex;
flex-direction: column;
}
.tabs {
display: flex;
justify-content: space-around;
align-items: center;
height: 44px;
padding: 0 16px;
position: fixed;
top: 140rpx;
left: 0;
right: 0;
z-index: 10;
background-color: #ffffff;
box-shadow: 0 1px 0 rgba(0,0,0,0.06);
.tab {
flex: 1;
text-align: center;
font-size: 16px;
color: #7a7a7a;
padding: 10px 0;
&.active { color: #8B2316; position: relative; }
&.active::after { content: ''; position: absolute; left: 25%; right: 25%; bottom: 2px; height: 3px; background-color: #8B2316; border-radius: 2px; }
}
}
.tabs-spacer { height: 44px; }
.list-scroll {
flex: 1;
position: fixed;
top: 228rpx;
bottom: 136rpx;
padding: 8px 0px 0 0px;
margin: 20rpx 30rpx 0;
box-sizing: border-box;
width:auto;
}
.consult-card {
background-color: #ffffff;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
.card-head {
display: flex;
justify-content: space-between;
align-items: center;
.user-name { font-size: 16px; color: #a31712; }
.date { font-size: 14px; color: #9aa0a6; }
}
.card-body {
background: #efefef;
padding: 20rpx;
margin: 10px 0;
.content {
word-break: break-all;
font-size: 15px; color: #2b2f33; line-height: 1.6;
} }
.card-foot {
.left{
display: flex;
.detail{
font-size: 28rpx;
background: #a31712;
color:#fff;
border-radius: 14rpx;
margin-right: 10rpx;
padding: 8rpx 16rpx;
border-radius: 28rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
border:2rpx solid #a31712;
}
}
display: flex; align-items: center; justify-content: space-between; margin-top: 8px;
.reply-count { font-size:28rpx; color: #8B2316; }
.tag {
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx 16rpx; background: #fff5f5; color: #a31712; border-radius: 28rpx; font-size: 24rpx; border:2rpx solid #a31712;}
}
}
.bottom-bar {
position: fixed;
left: 0; right: 0; bottom: 0;
background-color: #ffffff;
box-shadow: 0 -2px 8px rgba(0,0,0,0.06);
display: flex; align-items: center; justify-content: center;
padding: 8px 12px;
gap: 12px;
.bottom-tab {
flex: 1;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 6px;
font-size: 16px;
color: #a31712;
background-color: #fff5f5;
}
.bottom-tab.active {
background-color: #a31712;
color: #ffffff;
}
}
</style>

View File

@ -0,0 +1,575 @@
<template>
<view class="consult-detail-page">
<!-- 导航栏 -->
<navBar title="问题详情" />
<!-- 内容区域 -->
<scroll-view scroll-y class="content-scroll">
<!-- 用户信息区域 -->
<view class="user-section">
<view class="user-info">
<view class="user-name">
<text class="name">{{ userInfo.name }}</text>
<text class="gender-age">{{ userInfo.gender }} {{ userInfo.age }}</text>
</view>
<view class="detail-btn" @click="goInfo">
<up-image :src="detailImg" width="183rpx" height="34rpx" ></up-image>
</view>
</view>
</view>
<!-- 疾病标签和日期 -->
<view class="tag-date-row">
<view class="disease-tag">
<text class="tag-text">{{ questionInfo.diseaseTag }}</text>
</view>
<view class="date">{{ questionInfo.date }}</view>
</view>
<!-- 问题内容 -->
<view class="question-content">
<text class="content-text">{{ questionInfo.diseaseDescribe }}</text>
</view>
<!-- 疾病描述 -->
<!-- <view v-if="questionInfo.diseaseDescribe" class="disease-describe">
<view class="describe-title">疾病描述</view>
<text class="describe-text">{{ questionInfo.diseaseDescribe }}</text>
</view> -->
<!-- 图片网格 -->
<view class="image-grid" v-if="questionInfo.images">
<image
v-if="questionInfo.images && questionInfo.images.split(',').length>0"
v-for="(img, index) in questionInfo.images.split(',')"
:key="index"
:src="docUrl+img"
class="grid-image"
mode="aspectFill"
@click="previewImage(docUrl+img, index)"
/>
</view>
<view class="bar"></view>
<!-- 医生回答区域 -->
<view class="doctor-reply-section">
<view class="section-title">医生回答</view>
<view class="doctor-cell" v-for="item in questionInfo.AnswerList" :key="item.answer_uuid">
<view class="doctor-card">
<view class="doctor-info">
<image :src="doctorReply.avatar" class="doctor-avatar" />
<view class="doctor-details">
<view class="doctor-name">{{ item.realname }}</view>
<view class="hospital-time-row">
<view class="doctor-hospital" >{{ item.hospital }}</view>
<view class="reply-time">{{ item.create_date }}</view>
</view>
</view>
</view>
<view class="reply-content">
<text class="reply-text">{{ item.note }}</text>
</view>
<view class="reply-content" style="background:none;pading:0">
<view v-if="item.imgs" class="reply-images">
<image
v-for="(img, idx) in item.imgs.split(',')"
:key="idx"
:src="docUrl+img"
class="reply-image"
@click="previewReplyImages(item, idx)"
/>
</view>
</view>
</view>
<view class="smallbar"></view>
</view>
</view>
</scroll-view>
<!-- 底部固定区域 -->
<view class="bottom-fixed">
<!-- 特别声明 -->
<view class="disclaimer">
<text class="disclaimer-title">特别声明</text>
<text class="disclaimer-text">答案仅为医生个人经验或建议分享不能视为诊断依据如有诊疗需求请务必前往正规医院就诊</text>
</view>
<!-- 编辑按钮 -->
<view class="edit-btn" @click="editQuestion">
{{status==1?'我要编辑':'我要回答'}}
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import navBar from '@/components/navBar/navBar.vue'
import detailImg from "@/static/iv_jiwangshi.png"
import navTo from '@/utils/navTo.js'
import { onLoad,onShow } from '@dcloudio/uni-app'
import docUrl from '@/utils/docUrl'
import api from "@/api/api.js"
const uuid = ref('');
const step1_uuid = ref('');
const answer_uuid = ref('');
const status = ref(0)
onLoad((options) => {
uuid.value = options.uuid || '125891f8c12145a99de01e29729978fb'
status.value = options.status || 0
console.log(uuid.value)
})
const goInfo=()=>{
navTo({
url:'/pages_app/patientInfo/patientInfo?step1_uuid='+step1_uuid.value
})
}
const getInterrogation=()=>{
api.getInterrogation({
uuid:uuid.value
}).then(res=>{
console.log(res)
if(res.code === '200' && res.data) {
step1_uuid.value = res.data.step1_uuid || ''
//
userInfo.value = {
name: res.data.name || '提**',
gender: res.data.sex === 1 ? '男' : '女',
age: res.data.birthday ? calculateAge(res.data.birthday) : '未知'
}
//
questionInfo.value = {
date: res.data.create_date ? formatDate(res.data.create_date) : '未知',
diseaseTag: res.data.disease_name || '未知疾病',
content: res.data.your_question || '暂无问题描述',
images: res.data.imgs || '',
AnswerList: res.data.AnswerList || [],
your_question: res.data.your_question || ''
}
//
if(res.data.disease_describe) {
questionInfo.value.diseaseDescribe = res.data.disease_describe
}
let user=uni.getStorageSync('userInfo');
let arr=res.data.AnswerList.filter(item=>{
return user.uuid == item.expert_uuid
})
if(arr.length>0){
answer_uuid.value = arr[0].answer_uuid;
}
}
})
}
onShow(()=>{
getInterrogation()
})
//
function calculateAge(birthday) {
const birthDate = new Date(birthday)
const today = new Date()
let age = today.getFullYear() - birthDate.getFullYear()
const monthDiff = today.getMonth() - birthDate.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--
}
return age.toString()
}
//
function formatDate(dateString) {
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
//
const userInfo = ref({
name: '提**',
gender: '男',
age: '15'
})
//
const questionInfo = ref({
date: '2022-11-09',
diseaseTag: '甲型肝炎',
content: '为什么程序员总是分不清万圣节和圣诞节因为Oct31==Dec25。\n任何我写的代码超过6个月不去看它当我再看时都像是别人写的。',
diseaseDescribe: '', //
images: [
'/static/images/placeholder1.jpg',
'/static/images/placeholder2.jpg',
'/static/images/placeholder3.jpg',
'/static/images/placeholder4.jpg',
'/static/images/placeholder5.jpg',
'/static/images/placeholder6.jpg',
'/static/images/placeholder7.jpg',
'/static/images/placeholder8.jpg'
]
})
//
const doctorReply = ref({
avatar: '/static/images/doctor-avatar.jpg',
name: 'los测试',
hospital: '隆福医院',
time: '28天前',
content: '徐徐,喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵',
image: '/static/images/reply-image.jpg'
})
//
function goBack() {
uni.navigateBack()
}
//
function previewImage(current, index) {
uni.previewImage({
urls: questionInfo.value.images?questionInfo.value.images.split(',').map(path=> docUrl + path):[],
current: index
})
}
//
function previewReplyImages(item, index){
if(!item || !item.imgs){
return
}
const urls = item.imgs.split(',').map(path=> docUrl + path)
uni.previewImage({
urls,
current: index
})
}
//
function editQuestion() {
//
navTo({
url:'/pages_app/myAnswer/myAnswer?answer_uuid='+answer_uuid.value+'&uuid='+uuid.value
})
}
</script>
<style lang="scss" scoped>
.consult-detail-page {
background-color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 32rpx;
background-color: #ffffff;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
.nav-left {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 48rpx;
color: #8B2316;
font-weight: bold;
}
}
.nav-title {
font-size: 36rpx;
font-weight: 500;
color: #8B2316;
}
.nav-right {
width: 80rpx;
}
}
.bar{
width:100%;
height:20rpx;
background-color: #efefef;
}
.smallbar{
width:100%;
height:10rpx;
background-color: #efefef;
}
.content-scroll {
flex: 1;
position: fixed;
top: 135rpx;
width:auto;
box-sizing: border-box;
padding: 30rpx 0;
bottom: 313rpx;
}
.user-section {
background-color: #ffffff;
border-radius: 16rpx;
margin-bottom: 24rpx;
padding: 0 30rpx;
margin: 0 30rpx;
.user-info {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.user-name {
display: flex;
align-items: center;
.name {
font-size: 32rpx;
color: #8B2316;
font-weight: 500;
}
.gender-age {
font-size: 28rpx;
color: #666;
margin-left: 8rpx;
}
}
.detail-btn {
display: flex;
margin-left: 10rpx;
.detail-text {
color: #ffffff;
font-size: 24rpx;
margin-right: 8rpx;
}
.detail-icon {
color: #ffffff;
font-size: 24rpx;
}
}
}
}
.tag-date-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
margin: 0 30rpx 30rpx;
.disease-tag {
margin-bottom: 0;
.tag-text {
display: inline-block;
background-color: #ffffff;
color: #8B2316;
border: 2rpx solid #8B2316;
border-radius: 40rpx;
padding: 7rpx 22rpx;
font-size: 24rpx;
}
}
.date {
font-size: 28rpx;
color: #999;
}
}
.question-content {
background-color: #f0f0f0;
border-radius: 16rpx;
padding: 32rpx;
margin: 0 30rpx;
margin-bottom: 24rpx;
.content-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
}
.disease-describe {
background-color: #f8f9fa;
border-radius: 16rpx;
padding: 32rpx;
margin: 0 30rpx 24rpx;
border-left: 6rpx solid #8B2316;
.describe-title {
font-size: 30rpx;
color: #8B2316;
font-weight: 500;
margin-bottom: 16rpx;
}
.describe-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
}
.image-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8rpx;
margin: 0 30rpx 40rpx;
.grid-image {
width: 100%;
height: 160rpx;
border-radius: 8rpx;
}
}
.doctor-reply-section {
margin: 0 0rpx 40rpx;
.section-title {
font-size: 32rpx;
padding:24rpx 30rpx;
border-bottom:1rpx solid #efefef;
color: #8B2316;
font-weight: 500;
}
.doctor-card {
margin:20rpx 30rpx 0;
background-color: #ffffff;
border-radius: 16rpx;
.doctor-info {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.doctor-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 24rpx;
}
.doctor-details {
flex: 1;
.doctor-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.hospital-time-row {
display: flex;
align-items: center;
justify-content: space-between;
.doctor-hospital {
font-size: 28rpx;
color: #999;
}
.reply-time {
font-size: 28rpx;
color: #999;
}
}
}
}
.reply-content {
margin-bottom: 24rpx;
background-color: #f0f0f0;
border-radius: 16rpx;
font-size: 28rpx;
padding: 32rpx;
.reply-text {
font-size: 30rpx;
color: #333;
line-height: 1.6;
}
.reply-images{
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8rpx;
margin-top: 16rpx;
}
}
.reply-image {
width: 100%;
height:150rpx;
border-radius: 8rpx;
object-fit: cover;
}
}
}
.bottom-fixed {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: #ffffff;
padding: 24rpx 32rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.disclaimer {
background-color: #fff5f5;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
.disclaimer-title {
font-size: 28rpx;
color: #8B2316;
font-weight: 500;
}
.disclaimer-text {
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
}
.edit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background-color: #00bcd4;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border-radius: 16rpx;
}
</style>

View File

@ -1,15 +1,9 @@
<template>
<view class="feedback-container">
<!-- 状态栏占位 -->
<view class="status-bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="back-arrow"></text>
</view>
<text class="nav-title">意见反馈</text>
</view>
<navBar title="意见反馈"></navBar>
<!-- 反馈输入区域 -->
<view class="feedback-input-container">
@ -41,7 +35,7 @@
import { ref } from 'vue';
import api from '@/api/api';
const feedbackText = ref('');
import navBar from "@/components/navBar/navBar.vue"
//
const goBack = () => {
uni.navigateBack();

View File

@ -29,6 +29,7 @@
</view>
</view>
</view>
<empty v-if="bankCards.length === 0"></empty>
<!-- 底部导航指示器 -->
<view class="bottom-indicator"></view>
@ -39,6 +40,7 @@
import { ref, onMounted } from 'vue';
import navTo from '@/utils/navTo';
import api from '@/api/api';
import empty from "@/components/empty/empty.vue"
const bankCards = ref([]);
//

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,273 @@
<template>
<view class="my-answer-page">
<navBar title="我的意见" />
<scroll-view scroll-y class="content-scroll">
<!-- 文本输入 -->
<view class="card">
<view class="card-title">我的意见 <text class="required">*</text></view>
<view class="textarea-wrap">
<textarea
v-model="form.note"
class="textarea"
:maxlength="300"
placeholder="请依据患者的个人信息、疾病资料及患者所咨询的问题详细解答患者的问题信息仅提问患者及医生可见最多输入300个字"
placeholder-class="ph"
auto-height
/>
</view>
</view>
<!-- 图片上传 -->
<view class="card">
<view class="card-title">相关图片</view>
<view class="sub-tip">可以用部分科普或文献来协助回答问题最多6张</view>
<view class="img-grid">
<view
v-if="imgList.length>0"
v-for="(img, index) in imgList"
:key="index"
class="img-item"
@click="preview(index)"
>
<image :src="docUrl+img" mode="aspectFill" class="img" />
<view class="del" @click.stop="remove(index)">×</view>
</view>
<view v-if="imgList.length < maxImages" class="img-item add" @click="addImages">
<view class="plus"></view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部提交 -->
<view class="bottom-fixed">
<view class="submit-btn" @click="submit">提交</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import navBar from '@/components/navBar/navBar.vue'
import api from '@/api/api'
import docUrl from '@/utils/docUrl'
import { onLoad,onShow } from '@dcloudio/uni-app'
const uuid=ref('');
const maxImages = 6;
const imgList = ref([]);
const form = ref({
note: '',
images: []
})
onLoad((options) => {
uuid.value = options.uuid || ''
})
const getInterrogation=()=>{
api.getInterrogation({
uuid:uuid.value
}).then(res=>{
console.log(res)
if(res.code === '200' && res.data) {
let user=uni.getStorageSync('userInfo');
let arr=res.data.AnswerList.filter(item=>{
return user.uuid == item.expert_uuid
})
form.value= arr[0];
imgList.value= form.value.imgs?form.value.imgs.split(','):[];
}
})
}
const updateInterrogationAnswer=()=>{
let imgobj={};
if(imgList.value.length>0){
let count=0;
imgList.value.forEach(item=>{
imgobj['img'+count]=docUrl+item;
})
}
api.updateInterrogationAnswer({
answer_uuid: answer_uuid.value,
note: form.value.note,
imgsBean: imgobj
}).then(res=>{
if(res.code == 200){
uni.showToast({title: '提交成功', icon: 'none'})
uni.navigateBack()
}
})
}
const addInterrogationAnswer=()=>{
api.addInterrogationAnswer({
note: form.value.note,
imgsBean: imgobj
}).then(res=>{
if(res.code == 200){
uni.showToast({title: '提交成功', icon: 'none'})
uni.navigateBack()
}
})
}
onShow(()=>{
getInterrogation()
})
function addImages(){
const remain = maxImages - form.value.images.length
if(remain <= 0){
uni.showToast({title: '最多6张', icon: 'none'})
return
}
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album','camera'],
success: (res)=>{
const paths = (res.tempFilePaths || res.tempFiles?.map(f=>f.path) || [])
imgList.value = imgList.value.concat(paths).slice(0, maxImages)
}
})
}
function preview(index){
uni.previewImage({
urls:imgList.map(path=> docUrl + path),
current: index
})
}
function remove(index){
imgList.valuesplice(index, 1)
}
function submit(){
if(!form.value.note.trim()){
uni.showToast({title:'请输入意见', icon:'none'})
return
}
if(answer_uuid.value){
updateInterrogationAnswer()
}else{
addInterrogationAnswer()
}
}
</script>
<style lang="scss" scoped>
.my-answer-page{
background-color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
}
.content-scroll{
flex: 1;
position: fixed;
top: 135rpx;
left: 0;
right: 0;
bottom: 160rpx;
padding: 24rpx 24rpx 0;
box-sizing: border-box;
}
.card{
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.card-title{
font-size: 32rpx;
color: #8B2316;
font-weight: 500;
margin-bottom: 16rpx;
}
.required{ color: #ff4d4f; }
.textarea-wrap{
position: relative;
background: #f7f7f7;
border-radius: 12rpx;
padding: 16rpx 88rpx 16rpx 16rpx;
}
.textarea{
min-height: 180rpx;
width: 100%;
font-size: 28rpx;
color: #333;
}
.ph{ color:#999; }
.voice-btn{
position: absolute;
right: 16rpx;
bottom: 16rpx;
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
background: #b90f0f;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.sub-tip{
font-size: 26rpx;
color: #999;
margin-bottom: 16rpx;
}
.img-grid{
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.img-item{
position: relative;
width:150rpx;
height: 150rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f0f0f0;
}
.img{ width: 100%; height: 100%; }
.add{ display:flex; align-items:center; justify-content:center; }
.plus{ font-size: 80rpx; color:#c0c0c0; line-height: 1; }
.del{
position: absolute;
top: 8rpx;
right: 8rpx;
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
background: rgba(0,0,0,.5);
color: #fff;
text-align: center;
line-height: 44rpx;
font-size: 28rpx;
}
.bottom-fixed{
position: fixed;
left: 0; right: 0; bottom: 0;
background: #fff;
padding: 24rpx;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,.06);
}
.submit-btn{
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background: #00bcd4;
color: #fff;
font-size: 32rpx;
border-radius: 16rpx;
}
</style>

View File

@ -83,8 +83,9 @@
<script setup>
import { ref } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import api from "@/api/api";
import docUrl from '@/utils/docUrl';
import api from "@/api/api";
import docUrl from '@/utils/docUrl';
import navTo from '@/utils/navTo';
//
const isEditMode = ref(false);
@ -124,9 +125,7 @@
const allApps = sourceList.map(item => ({
id: item.id,
name: item.name,
icon: docUrl + item.img, // 使docUrl
bgColor: bgColorMap[item.id] || '#999999', // 使
url: urlMap[item.id] || '', // 使
icon: docUrl + item.img, // 使docUrl // 使// 使
selected: item.selected
}));
@ -322,9 +321,7 @@
.app-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
border: 1rpx solid #e0e0e0;
border-left: none;
border-right: none;
}
.app-item {
@ -333,8 +330,9 @@
flex-direction: column;
align-items: center;
padding: 20rpx 10rpx;
border-right: 1rpx solid #e0e0e0;
border-bottom: 1rpx solid #e0e0e0;
border: 1rpx solid #e0e0e0;
border-right:none;
transition: all 0.3s ease;
.iconbox {
position: absolute;
@ -348,12 +346,12 @@
//
&:nth-child(3n) {
border-right: none;
}
//
&:nth-last-child(-n + 3) {
border-bottom: none;
&:nth-last-child(1){
border-right: 1rpx solid #e0e0e0;
}
}

View File

@ -1,20 +1,7 @@
<template>
<view class="my-code-page">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="我的二维码"
@cviewckLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
>
<template v-slot:right>
<uni-icons type="redo" color="#8B2316" size="22"></uni-icons>
</template>
</uni-nav-bar>
<navBar title="我的二维码" />
<!-- 内容 -->
<scroll-view scroll-y class="page-scroll">
@ -30,10 +17,10 @@
<view class="rightCircle"></view>
<view class="halfCircle"></view>
<view class="avatar-wrapper">
<image class="avatar" :src="avatarImg" mode="aspectFill" />
<image class="avatar" :src="docUrl+userInfo.photo" mode="aspectFill" />
</view>
<view class="name-viewne">邹建东 主任医师</view>
<view class="org-viewne">北京肝胆相照公益基金会</view>
<view class="name-viewne">{{ userInfo.realName }} {{ userInfo.positionName }}</view>
<view class="org-viewne">{{ userInfo.hospitalName }}</view>
<view class="dash-viewne"></view>
<view class="slogan">
<text class="h1">不方便到医院就诊</text>
@ -44,7 +31,7 @@
<up-image :src="viewnkImg" width="430rpx" height="131rpx" ></up-image>
</view>
<image class="qr-img" :src="qrImg" mode="aspectFit" />
<image class="qr-img" :src="docUrl+userInfo.qrcode" mode="aspectFit" />
</view>
</view>
</view>
@ -72,25 +59,91 @@
<!-- 底部保存按钮 -->
<view class="save-bar">
<button class="save-btn" @cviewck="onSave">保存二维码到手机</button>
<button class="save-btn" @click="onSave">保存二维码到手机</button>
</view>
</view>
</template>
<script setup>
import navBar from '@/components/navBar/navBar.vue'
import { onShow } from "@dcloudio/uni-app";
import { ref } from 'vue';
const avatarImg = '/static/xxtx.png';
const qrImg = '/static/sfewm.png';
import docUrl from '@/utils/docUrl'
import bgImg from "@/static/background.jpg"
import viewnkImg from "@/static/arr.png"
const userInfo = ref({})
const goBack = () => {
uni.navigateBack();
};
onShow(()=>{
userInfo.value = uni.getStorageSync('userInfo')
})
const onSave = () => {
uni.showToast({ title: '已保存(示例)', icon: 'none' });
//
if (!userInfo.value.qrcode) {
uni.showToast({ title: '二维码不存在', icon: 'none' });
return;
}
//
uni.showLoading({ title: '保存中...' });
//
uni.downloadFile({
url: docUrl + userInfo.value.qrcode,
success: (res) => {
if (res.statusCode === 200) {
//
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading();
uni.showToast({
title: '保存成功',
icon: 'success',
duration: 2000
});
},
fail: (err) => {
uni.hideLoading();
console.error('保存失败:', err);
//
if (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize')) {
uni.showModal({
title: '权限提示',
content: '需要相册权限才能保存图片,请在设置中开启权限',
showCancel: false,
confirmText: '知道了'
});
} else {
uni.showToast({
title: '保存失败,请重试',
icon: 'none',
duration: 2000
});
}
}
});
} else {
uni.hideLoading();
uni.showToast({
title: '下载失败,请检查网络',
icon: 'none',
duration: 2000
});
}
},
fail: (err) => {
uni.hideLoading();
console.error('下载失败:', err);
uni.showToast({
title: '下载失败,请检查网络',
icon: 'none',
duration: 2000
});
}
});
};
</script>
@ -182,7 +235,7 @@ const onSave = () => {
width:100%;
color: #ffffff;
text-aviewgn: center;
text-align: center;
.banner-title-small {
font-size: 26rpx;
@ -279,10 +332,10 @@ const onSave = () => {
font-weight:bold;
letter-spacing: 8rpx;
flex-direction: column;
text-aviewgn: center;
text-align: center;
font-size: 40rpx;
color: #1e88e5;
viewne-height: 1.6;
line-height: 1.6;
text{
text-align: center;
}
@ -293,7 +346,7 @@ const onSave = () => {
}
.contact-qr {
display: flex;
aviewgn-items: center;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-top: 30rpx;
@ -305,12 +358,12 @@ const onSave = () => {
color: #ffffff;
display: flex;
aviewgn-items: center;
align-items: center;
justify-content: space-between;
.contact-text {
white-space: pre-viewne;
font-size: 26rpx;
viewne-height: 1.5;
line-height: 1.5;
}
.arrow {
font-size: 36rpx;

View File

@ -0,0 +1,269 @@
<template>
<view class="patient-info-container">
<!-- 导航栏 -->
<navBar title="患者信息" />
<!-- 标签页 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: activeTab === 'basic' }"
@click="switchTab('basic')"
>
基本资料
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'history' }"
@click="switchTab('history')"
>
病史信息
</view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-area" scroll-y>
<!-- 基本资料内容 -->
<view v-if="activeTab === 'basic'" class="basic-info">
<view class="info-item">
<text class="info-label">姓名</text>
<text class="info-value">{{ patientInfo.name || '提**' }}</text>
</view>
<view class="info-item">
<text class="info-label">性别</text>
<text class="info-value">{{ patientInfo.gender || '男' }}</text>
</view>
<view class="info-item">
<text class="info-label">年龄</text>
<text class="info-value">{{ patientInfo.age || '15' }}</text>
</view>
<view class="info-item">
<text class="info-label">地址</text>
<text class="info-value">{{ patientInfo.address || '北京市东城区' }}</text>
</view>
<view class="info-item">
<text class="info-label">肝硬化或肝癌家族史</text>
<text class="info-value">{{ patientInfo.familyHistory || '无' }}</text>
</view>
</view>
<!-- 病史信息内容 -->
<view v-if="activeTab === 'history'" class="history-info">
<view class="info-item">
<text class="info-label">既往病史</text>
<text class="info-value">暂无</text>
</view>
<view class="info-item">
<text class="info-label">过敏史</text>
<text class="info-value">暂无</text>
</view>
<view class="info-item">
<text class="info-label">手术史</text>
<text class="info-value">暂无</text>
</view>
<view class="info-item">
<text class="info-label">用药史</text>
<text class="info-value">暂无</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import navBar from '@/components/navBar/navBar.vue'
import { onLoad } from '@dcloudio/uni-app'
import api from '@/api/api.js'
//
const statusBarHeight = ref(0)
const activeTab = ref('basic');
const step1_uuid = ref('');
const patientInfo = reactive({
name: '提**',
gender: '男',
age: '15',
address: '北京市东城区',
familyHistory: '未知'
})
onLoad((options) => {
step1_uuid.value = options.step1_uuid || ''
interrogationPatientInfo()
})
const interrogationPatientInfo=()=>{
api.interrogationPatientInfo({
step1_uuid: step1_uuid.value
}).then(res=>{
if(res.code == 200 && res.data){
const d = res.data
patientInfo.name = d.name || '提**'
patientInfo.gender = d.sex === 1 ? '男' : '女'
patientInfo.age = d.birthday ? calculateAge(d.birthday) : '未知'
patientInfo.address = d.address || ''
//
patientInfo.familyHistory = '未知'
}
})
}
function calculateAge(birthday){
const birth = new Date(birthday)
const today = new Date()
let age = today.getFullYear() - birth.getFullYear()
const m = today.getMonth() - birth.getMonth()
if(m < 0 || (m === 0 && today.getDate() < birth.getDate())){
age--
}
return String(age)
}
//
const statusBarStyle = computed(() => ({
height: statusBarHeight.value + 'px'
}))
//
const goBack = () => {
uni.navigateBack()
}
const switchTab = (tab) => {
activeTab.value = tab
}
//
onMounted(() => {
//
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
})
</script>
<style lang="scss" scoped>
//
$primary-color: #ff0000;
$text-color: #333333;
$text-color-light: #666666;
$border-color: #f0f0f0;
$border-color-light: #e0e0e0;
$background-color: #ffffff;
$nav-height: 88rpx;
$tab-height: 88rpx;
$info-item-height: 100rpx;
$padding-horizontal: 30rpx;
$padding-small: 10rpx;
.patient-info-container {
background-color: $background-color;
min-height: 100vh;
}
.status-bar {
background-color: $background-color;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: $nav-height;
padding: 0 $padding-horizontal;
background-color: $background-color;
border-bottom: 2rpx solid $border-color;
.nav-left {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.back-arrow {
font-size: 48rpx;
color: $primary-color;
font-weight: bold;
}
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: $primary-color;
}
.nav-right {
width: 60rpx;
}
}
.tab-bar {
display: flex;
background-color: $background-color;
border-bottom: 2rpx solid $border-color-light;
.tab-item {
flex: 1;
height: $tab-height;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $text-color;
position: relative;
&.active {
color: $primary-color;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: $primary-color;
}
}
}
}
.content-area {
flex: 1;
background-color: $background-color;
}
.basic-info,
.history-info {
padding: 0 $padding-horizontal;
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
height: $info-item-height;
border-bottom: 2rpx solid $border-color;
padding: 0 $padding-small;
&:last-child {
border-bottom: none;
}
.info-label {
font-size: 32rpx;
color: $text-color;
flex: 1;
}
.info-value {
font-size: 32rpx;
color: $text-color-light;
text-align: right;
flex: 1;
}
}
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<div
class="p2p-msg-receipt-wrapper"
v-if="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
p2pMsgReceiptVisible
"
>
<div v-if="p2pMsgRotateDeg == 360" class="icon-read-wrapper">
<Icon type="icon-read" :size="16"></Icon>
</div>
<div v-else class="sector">
<span
class="cover-1"
:style="`transform: rotate(${p2pMsgRotateDeg}deg)`"
></span>
<span
:class="p2pMsgRotateDeg >= 180 ? 'cover-2 cover-3' : 'cover-2'"
></span>
</div>
</div>
</template>
<script lang="ts" setup>
/** 会话列表已读未读组件 */
import { computed } from 'vue'
import Icon from '@/components/Icon.vue'
import { V2NIMConst } from '@/utils/im/nim'
import {
V2NIMConversationForUI,
V2NIMLocalConversationForUI,
} from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMConversationType } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMConversationService'
const props = withDefaults(
defineProps<{
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
}>(),
{}
)
/** 是否需要显示 p2p 消息、p2p会话列表消息已读未读默认 false*/
const p2pMsgReceiptVisible = uni.$UIKitStore.localOptions.p2pMsgReceiptVisible
/** 会话类型 */
const conversationType =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
props.conversation.conversationId
) as unknown as V2NIMConversationType
const p2pMsgRotateDeg = computed(() => {
return (props?.conversation?.msgReceiptTime || 0) >=
(props?.conversation?.lastMessage?.messageRefer?.createTime || 0)
? 360
: 0
})
</script>
<style scoped lang="scss">
.p2p-msg-receipt-wrapper {
width: 22px;
height: 22px;
overflow: hidden;
line-height: 18px;
vertical-align: bottom;
display: flex;
align-items: center;
justify-content: center;
}
.icon-read-wrapper {
margin: 0px 3px 0px 0;
width: 18px;
height: 18px;
overflow: hidden;
display: flex;
align-items: center;
}
.sector {
display: inline-block;
position: relative;
overflow: hidden;
border: 2px solid #4c84ff;
width: 12px;
height: 12px;
background-color: #ffffff;
border-radius: 50%;
margin: 0px 3px 0 0;
.cover-1,
.cover-2 {
position: absolute;
top: 0;
width: 50%;
height: 100%;
background-color: #ffffff;
}
.cover-1 {
background-color: #4c84ff;
transform-origin: right;
}
.cover-3 {
right: 0;
background-color: #4c84ff;
}
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<div>
<div
v-if="
props.lastMessage.lastMessageState ===
V2NIMConst.V2NIMLastMessageState.V2NIM_MESSAGE_STATUS_REVOKE
"
>
{{ t('recall') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION
"
>
{{ t('conversationNotificationText') }}
</div>
<div
v-else-if="
props.lastMessage.sendingState ===
V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_FAILED
"
>
{{ t('conversationSendFailText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE
"
>
{{ translateMsg('fileMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE
"
>
{{ translateMsg('imgMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM
"
>
{{ props.lastMessage.text || translateMsg('customMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
"
>
{{ translateMsg('audioMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
>
{{ translateMsg('callMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION
"
>
{{ translateMsg('geoMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_ROBOT
"
>
{{ translateMsg('robotMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TIPS
"
>
{{ translateMsg('tipMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO
"
>
{{ translateMsg('videoMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
class="msg-conversation-text-wrap"
>
<template v-for="item in textArr" :key="item.key">
<template v-if="item.type === 'text'">
<span class="msg-conversation-text">{{ item.value }}</span>
</template>
<template v-else-if="item.type === 'emoji'">
<span
:class="
isWxApp
? 'msg-conversation-text-emoji-wx'
: 'msg-conversation-text-emoji'
"
>
<Icon :type="EMOJI_ICON_MAP_CONFIG[item.value]" :size="16" />
</span>
</template>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
/** 会话列表Item 外漏消息组件 */
import { computed } from 'vue'
import Icon from '@/components/Icon.vue'
import { V2NIMConst } from '@/utils/im/nim'
import { t } from '@/utils/im/i18n'
import { V2NIMLastMessage } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMConversationService'
import { EMOJI_ICON_MAP_CONFIG, emojiRegExp } from '@/utils/im/emoji'
import { isWxApp } from '@/utils/im/index'
const props = withDefaults(
defineProps<{
lastMessage: V2NIMLastMessage
}>(),
{}
)
/** 筛选出文本和表情 */
const parseTextWithEmoji = (text: string) => {
if (!text) return []
const matches: {
type: 'emoji' | 'text'
value: string
index: number
}[] = []
let match
const regexEmoji = emojiRegExp
while ((match = regexEmoji.exec(text)) !== null) {
matches.push({
type: 'emoji',
value: match[0],
index: match.index,
})
const fillText = ' '.repeat(match[0].length)
text = text.replace(match[0], fillText)
}
text = text.replace(regexEmoji, ' ')
if (text) {
text
.split(' ')
.filter((item) => item.trim())
.map((item) => {
const index = text?.indexOf(item)
matches.push({
type: 'text',
value: item,
index,
})
const fillText = ' '.repeat(item.length)
text = text.replace(item, fillText)
})
}
return matches
.sort((a, b) => a.index - b.index)
.map((item, index) => {
return {
...item,
key: index + item.type,
}
})
}
/** 解析的消息数组 */
const textArr = computed(() => {
return parseTextWithEmoji(props.lastMessage.text as string)
})
/** 消息映射 */
const translateMsg = (key: string): string => {
const text =
{
textMsgText: t('textMsgText'),
customMsgText: t('customMsgText'),
audioMsgText: t('audioMsgText'),
videoMsgText: t('videoMsgText'),
fileMsgText: t('fileMsgText'),
callMsgText: t('callMsgText'),
geoMsgText: t('geoMsgText'),
imgMsgText: t('imgMsgText'),
notiMsgText: t('notiMsgText'),
robotMsgText: t('robotMsgText'),
tipMsgText: t('tipMsgText'),
unknowMsgText: t('unknowMsgText'),
}[key] || ''
return `[${text}]`
}
</script>
<style scoped lang="scss">
.wrapper {
flex: 1;
font-size: 13px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.msg-conversation-text {
font-size: 13px !important;
height: 22px;
line-height: 22px;
width: 100%;
display: inline;
}
.msg-conversation-text-wrap {
width: 80%;
line-height: 22px;
height: 22px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 14px;
overflow: hidden;
}
.msg-conversation-text-emoji {
display: inline-flex;
width: 18px;
height: 18px;
box-sizing: border-box;
align-items: center;
justify-content: center;
}
.msg-conversation-text-emoji-wx {
display: inline-flex;
width: 18px;
height: 18px;
box-sizing: border-box;
align-items: center;
justify-content: center;
position: relative;
bottom: 4px;
}
</style>

View File

@ -0,0 +1,496 @@
<template>
<view
:class="[
'conversation-item-container',
{
'show-action-list': showMoreActions,
'stick-on-top': conversation.stickTop,
},
]"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@click="handleConversationItemClick()"
>
<view class="conversation-item-content">
<view class="conversation-item-left">
<!-- 会话Item未读数 -->
<view class="unread" v-if="unread">
<view class="dot" v-if="isMute"></view>
<view class="badge" v-else>{{ unread }}</view>
</view>
<!-- 会话头像 -->
<Avatar :account="to" :avatar="teamAvatar" />
<!-- 用户在线离线状态 -->
<view
class="login-state-icon"
v-if="
loginStateVisible &&
!isAiUser &&
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
status
"
></view>
<view
class="unlogin-state-icon"
v-if="
loginStateVisible &&
!isAiUser &&
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
!status
"
></view>
</view>
<view class="conversation-item-right">
<!-- 会话名称 -->
<view class="conversation-item-top">
<Appellation
class="conversation-item-title"
v-if="
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
"
:account="to"
/>
<span v-else class="conversation-item-title">
{{ sessionName }}
</span>
<span class="conversation-item-time">{{ date }}</span>
</view>
<view class="conversation-item-desc">
<!-- 是否有人@ -->
<span v-if="beMentioned" class="beMentioned">
{{ '[' + t('someoneText') + '@' + t('meText') + ']' }}
</span>
<!-- 会话最后一条消息是否已读 -->
<!-- <ConversationItemIsRead
v-if="showConversationUnread"
:conversation="props.conversation"
></ConversationItemIsRead> -->
<!-- 会话最后一条消息外露 -->
<span
v-if="props.conversation.lastMessage"
class="conversation-item-desc-content"
>
<LastMsgContent :lastMessage="props.conversation.lastMessage" />
</span>
<!-- 消息免打扰 -->
<span class="conversation-item-desc-ait">
<Icon
v-if="isMute"
iconClassName="conversation-item-desc-state"
type="icon-xiaoximiandarao"
color="#ccc"
/>
</span>
</view>
</view>
</view>
<!-- 消息右键操作列表 -->
<view class="right-action-list">
<view
v-for="action in moreActions"
:key="action.type"
:class="['right-action-item', action.class]"
@click="() => handleClick(action.type)"
>
{{ action.name }}
</view>
</view>
</view>
</template>
<script lang="ts" setup>
/** 会话列表Item组件 */
import Avatar from '@/components/Avatar.vue'
import Appellation from '@/components/Appellation.vue'
import Icon from '@/components/Icon.vue'
import { computed, onUnmounted, withDefaults } from 'vue'
import dayjs from 'dayjs'
import { t } from '@/utils/im/i18n'
import { V2NIMConst } from '@/utils/im/nim'
import {
V2NIMConversationForUI,
V2NIMLocalConversationForUI,
} from '@xkit-yx/im-store-v2/dist/types/types'
import ConversationItemIsRead from './conversation-item-isRead.vue'
import LastMsgContent from './conversation-item-last-msg-content.vue'
import { ref } from 'vue'
import { autorun } from 'mobx'
const props = withDefaults(
defineProps<{
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
showMoreActions?: boolean
}>(),
{ showMoreActions: false }
)
const emit = defineEmits(['click', 'delete', 'stickyToTop', 'leftSlide'])
/** 右滑操作列表 */
const moreActions = computed(() => {
return [
{
name: props.conversation.stickTop
? t('deleteStickTopText')
: t('addStickTopText'),
class: 'action-top',
type: 'action-top',
},
{
name: t('deleteSessionText'),
class: 'action-delete',
type: 'action-delete',
},
]
})
/** 对话方 */
const to = computed(() => {
const res = uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
props.conversation.conversationId
)
return res
})
/** 全局配置的是否需要展示在线离线状态 */
const loginStateVisible = uni.$UIKitStore.localOptions.loginStateVisible
/** 当前会话方在线离线状态 */
const status = ref<boolean>(false)
/** 右滑操作点击 */
const handleClick = (type: string) => {
if (type === 'action-top') {
emit('stickyToTop', props.conversation)
} else {
emit('delete', props.conversation)
}
}
/** 群头像 */
const teamAvatar = computed(() => {
if (
props.conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
const { avatar } = props.conversation
return avatar
}
})
/** 会话昵称 */
const sessionName = computed(() => {
if (props.conversation.name) {
return props.conversation.name
}
return props.conversation.conversationId
})
/** 是否是机器人 */
const isAiUser = ref(false)
/** 时间 */
const date = computed(() => {
const time =
props.conversation.lastMessage?.messageRefer.createTime ||
props.conversation.updateTime
//
if (!time) {
return ''
}
const _d = dayjs(time)
const isCurrentDay = _d.isSame(dayjs(), 'day')
const isCurrentYear = _d.isSame(dayjs(), 'year')
return _d.format(
isCurrentDay ? 'HH:mm' : isCurrentYear ? 'MM-DD HH:mm' : 'YYYY-MM-DD HH:mm'
)
})
const max = 99
/** 未读数 */
const unread = computed(() => {
return props.conversation.unreadCount > 0
? props.conversation.unreadCount > max
? `${max}+`
: props.conversation.unreadCount + ''
: ''
})
/** 是否免打扰 */
const isMute = computed(() => {
return !!props.conversation.mute
})
/** 是否被@ */
const beMentioned = computed(() => {
return !!props.conversation.aitMsgs?.length
})
/** 是否展示 未读数 */
const showConversationUnread = computed(() => {
const myUserAccountId = uni.$UIKitNIM.V2NIMLoginService.getLoginUser()
if (
props.conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
return (
props?.conversation?.lastMessage?.messageRefer.senderId ===
myUserAccountId &&
props?.conversation?.lastMessage?.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL &&
props?.conversation?.lastMessage?.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION &&
props?.conversation?.lastMessage?.sendingState ===
V2NIMConst.V2NIMMessageSendingState
.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED &&
props?.conversation?.lastMessage?.lastMessageState !==
V2NIMConst.V2NIMLastMessageState.V2NIM_MESSAGE_STATUS_REVOKE
)
} else {
return false
}
})
// action
let startX = 0,
startY = 0
//
function handleTouchStart(event: TouchEvent) {
startX = event.changedTouches[0].pageX
startY = event.changedTouches[0].pageY
}
//
function handleTouchMove(event: TouchEvent) {
const moveEndX = event.changedTouches[0].pageX
const moveEndY = event.changedTouches[0].pageY
const X = moveEndX - startX + 20
const Y = moveEndY - startY
if (Math.abs(X) > Math.abs(Y) && X > 0) {
emit('leftSlide', null)
} else if (Math.abs(X) > Math.abs(Y) && X < 0) {
emit('leftSlide', props.conversation)
}
}
/** 会话列表点击事件 */
function handleConversationItemClick() {
if (props.showMoreActions) {
emit('leftSlide', null)
return
}
emit('click', props.conversation)
}
/** 监听是否是Ai 数字人 */
const isAiUserWatch = autorun(() => {
isAiUser.value = uni.$UIKitStore.aiUserStore.isAIUser(to.value)
})
/** 监听会话方在线离线状态 */
const statusWatch = autorun(() => {
if (
props.conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
const stateMap = uni.$UIKitStore?.subscriptionStore.stateMap
if (
stateMap.get(to.value) &&
uni.$UIKitStore.localOptions.loginStateVisible
) {
status.value =
stateMap.get(to.value)?.statusType ===
V2NIMConst.V2NIMUserStatusType.V2NIM_USER_STATUS_TYPE_LOGIN
} else {
status.value = false
}
}
})
onUnmounted(() => {
isAiUserWatch()
statusWatch()
})
</script>
<style lang="scss" scoped>
$cellHeight: 72px;
.conversation-item-container {
position: relative;
transition: transform 0.3s;
&.show-action-list {
transform: translateX(-200px);
}
&.stick-on-top {
background: #f3f5f7;
}
.beMentioned {
color: #ff4d4f;
}
.content {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.right-action-list {
position: absolute;
top: 0;
right: -200px;
bottom: 0;
width: 200px;
white-space: nowrap;
.right-action-item {
width: 100px;
display: inline-block;
color: #fff;
text-align: center;
height: $cellHeight;
line-height: $cellHeight;
}
.action-top {
background: #337eff;
}
.action-delete {
background: #a8abb6;
}
}
.conversation-item-content {
display: flex;
align-items: center;
padding: 10px 20px;
height: $cellHeight;
box-sizing: border-box;
}
.conversation-item-left {
position: relative;
.conversation-item-badge {
position: absolute;
top: 0px;
right: 0px;
z-index: 10;
}
}
.conversation-item-right {
flex: 1;
width: 0;
margin-left: 10px;
}
.conversation-item-top {
display: flex;
justify-content: space-between;
align-items: center;
.conversation-item-title {
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
}
.conversation-item-time {
font-size: 12px;
color: #cccccc;
text-align: right;
width: 90px;
flex-shrink: 0;
}
}
.conversation-item-desc {
font-size: 13px;
color: #999999;
display: flex;
align-items: center;
justify-content: space-between;
height: 22px;
.conversation-item-desc-content {
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
flex: 1;
}
.conversation-item-desc-state {
margin-left: 10px;
}
}
.dot {
background-color: #ff4d4f;
color: #fff;
width: 10px;
height: 10px;
border-radius: 5px;
box-sizing: border-box;
z-index: 99;
}
.badge {
background-color: #ff4d4f;
color: #fff;
font-size: 12px;
min-width: 20px;
height: 20px;
line-height: 19px;
border-radius: 10px;
padding: 0 5px;
box-sizing: border-box;
text-align: center;
z-index: 99;
position: relative;
}
.unread {
position: absolute;
right: -4px;
top: -2px;
z-index: 99;
}
.conversation-item-desc-ait {
display: inline-block;
}
.login-state-icon {
width: 8px;
height: 8px;
box-sizing: content-box;
background-color: #84ed85;
border: 2px solid #fff;
position: absolute;
right: -2px;
bottom: -2px;
border-radius: 50%;
}
.unlogin-state-icon {
width: 8px;
height: 8px;
box-sizing: content-box;
background-color: #d4d9da;
border: 2px solid #fff;
position: absolute;
right: -2px;
bottom: -2px;
border-radius: 50%;
}
</style>

View File

@ -0,0 +1,570 @@
<template>
<div class="conversation-wrapper">
<div
v-if="addDropdownVisible"
class="dropdown-mark"
@touchstart="hideAddDropdown"
></div>
<div class="navigation-bar">
<div :class="isWxApp ? 'button-box-mp' : 'button-box'">
<!-- #ifdef MP -->
<image
src="https://yx-web-nosdn.netease.im/common/9ae07d276ba2833b678a4077960e2d1e/Group 1899.png"
class="button-icon"
@tap="showAddDropdown"
/>
<!-- #endif -->
<!-- #ifndef MP -->
<div class="button-icon-add" @tap="showAddDropdown">
<Icon type="icon-More" />
</div>
<!-- #endif -->
<div v-if="addDropdownVisible" class="dropdown-container">
<div class="add-menu-list">
<div class="add-menu-item" @tap="onDropdownClick('addFriend')">
<Icon type="icon-tianjiahaoyou" :style="{ marginRight: '5px' }" />
{{ t('addFriendText') }}
</div>
<div class="add-menu-item" @tap="onDropdownClick('createGroup')">
<Icon
type="icon-chuangjianqunzu"
:style="{ marginRight: '5px' }"
/>
{{ t('createTeamText') }}
</div>
<div
class="add-menu-item"
@tap="onDropdownClick('createDiscussion')"
>
<Icon
type="icon-chuangjianqunzu"
:style="{ marginRight: '5px' }"
/>
{{ t('createDiscussionText') }}
</div>
</div>
</div>
</div>
</div>
<div class="block"></div>
<NetworkAlert />
<div v-if="!conversationList || conversationList.length === 0">
<div class="conversation-search" @tap="goToSearchPage">
<div class="search-input-wrapper">
<div class="search-icon-wrapper">
<Icon
iconClassName="search-icon"
:size="16"
color="#A6ADB6"
type="icon-sousuo"
></Icon>
</div>
<div class="search-input">{{ t('searchText') }}</div>
</div>
</div>
<!-- 页面初始化的过程中sessionList编译到小程序和h5出现sessionList为undefined的情况即使给了默认值为空数组故在此处进行判断 -->
<Empty
v-if="!conversationList || conversationList.length === 0"
:text="t('conversationEmptyText')"
/>
</div>
<div v-else class="conversation-list-wrapper">
<div class="security-tip">
<div>
{{ t('securityTipText') }}
</div>
</div>
<div class="conversation-search" @click="goToSearchPage">
<div class="search-input-wrapper">
<div class="search-icon-wrapper">
<Icon
iconClassName="search-icon"
:size="16"
color="#A6ADB6"
type="icon-sousuo"
></Icon>
</div>
<div class="search-input">{{ t('searchText') }}</div>
</div>
</div>
<!-- 此处的key如果用conversationId会在ios上渲染存在问题会出现会话列表显示undefined -->
<div
v-for="conversation in conversationList"
:key="conversation.renderKey"
>
<ConversationItem
:key="conversation.renderKey"
:showMoreActions="
currentMoveSessionId === conversation.conversationId
"
:conversation="conversation"
@delete="handleSessionItemDeleteClick"
@stickyToTop="handleSessionItemStickTopChange"
@click="handleSessionItemClick"
@leftSlide="handleSessionItemLeftSlide"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
/** 会话列表主界面 */
import { onUnmounted, ref, watch } from 'vue'
import { autorun } from 'mobx'
import { onShow, onHide } from '@dcloudio/uni-app'
import Icon from '@/components/Icon.vue'
import NetworkAlert from '@/components/NetworkAlert.vue'
import Empty from '@/components/Empty.vue'
import ConversationItem from './conversation-item.vue'
import { setContactTabUnread, setTabUnread } from '@/utils/im/msg'
import { t } from '@/utils/im/i18n'
import { customNavigateTo } from '@/utils/im/customNavigate'
import { V2NIMConst } from '@/utils/im/nim'
import { isWxApp } from '@/utils/im/index'
import { trackInit } from '@/utils/im/reporter'
import {
V2NIMConversationForUI,
V2NIMLocalConversationForUI,
} from '@xkit-yx/im-store-v2/dist/types/types'
/**会话列表 */
const conversationList = ref<
(
| (V2NIMConversationForUI & { renderKey: string })
| (V2NIMLocalConversationForUI & { renderKey: string })
)[]
>([])
/** 右上角更多 */
const addDropdownVisible = ref(false)
/** 当前左滑会话ID */
const currentMoveSessionId = ref('')
/**是否是云端会话 */
const enableV2CloudConversation =
uni.$UIKitStore?.sdkOptions?.enableV2CloudConversation
/** 会话左滑 */
const handleSessionItemLeftSlide = (
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI | null
) => {
// conversation null
if (conversation) {
currentMoveSessionId.value = conversation.conversationId
} else {
currentMoveSessionId.value = ''
}
}
let flag = false
//
const handleSessionItemClick = async (
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
) => {
console.log(conversation)
if (flag) return
currentMoveSessionId.value = ''
try {
flag = true
// @
if (
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM ||
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_SUPER_TEAM
) {
if (enableV2CloudConversation) {
await uni.$UIKitStore.conversationStore?.markConversationReadActive(
conversation.conversationId
)
} else {
await uni.$UIKitStore.localConversationStore?.markConversationReadActive(
conversation.conversationId
)
}
}
await uni.$UIKitStore.uiStore.selectConversation(
conversation.conversationId
)
customNavigateTo({
url: '/pages_chat/chat/index',
})
} catch {
uni.showToast({
title: t('selectSessionFailText'),
icon: 'error',
})
} finally {
flag = false
}
}
//
const handleSessionItemDeleteClick = async (
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
) => {
try {
if (enableV2CloudConversation) {
await uni.$UIKitStore.conversationStore?.deleteConversationActive(
conversation.conversationId
)
} else {
await uni.$UIKitStore.localConversationStore?.deleteConversationActive(
conversation.conversationId
)
}
currentMoveSessionId.value = ''
} catch {
uni.showToast({
title: t('deleteSessionFailText'),
icon: 'error',
})
}
}
//
const handleSessionItemStickTopChange = async (
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
) => {
if (conversation.stickTop) {
try {
if (enableV2CloudConversation) {
await uni.$UIKitStore?.conversationStore?.stickTopConversationActive(
conversation.conversationId,
false
)
} else {
await uni.$UIKitStore?.localConversationStore?.stickTopConversationActive(
conversation.conversationId,
false
)
}
} catch {
uni.showToast({
title: t('deleteStickTopFailText'),
icon: 'error',
})
}
} else {
try {
if (enableV2CloudConversation) {
await uni.$UIKitStore?.conversationStore?.stickTopConversationActive(
conversation.conversationId,
true
)
} else {
await uni.$UIKitStore?.localConversationStore?.stickTopConversationActive(
conversation.conversationId,
true
)
}
} catch {
uni.showToast({
title: t('addStickTopFailText'),
icon: 'error',
})
}
}
}
/** 显示添加好友、群聊 Dropdown */
const showAddDropdown = () => {
addDropdownVisible.value = true
}
/** 隐藏添加好友、群聊 Dropdown */
const hideAddDropdown = () => {
addDropdownVisible.value = false
}
/** 点击Dropdown */
const onDropdownClick = (
urlType: 'addFriend' | 'createGroup' | 'createDiscussion'
) => {
const urlMap = {
//
addFriend: '/pages/User/friend/add-friend',
//
createGroup: '/pages/Team/team-create/index',
// im_ui_kit_group
createDiscussion: `/pages/Team/team-create/index?createDiscussion=${true}`,
}
addDropdownVisible.value = false
customNavigateTo({
url: urlMap[urlType],
})
}
/** 跳转至搜索页面 */
const goToSearchPage = () => {
customNavigateTo({
url: '/pages/Conversation/conversation-search/index',
})
}
/** 订阅当前会话方在线离线状态 */
const subscribeUserStatus = (
conversations: (V2NIMConversationForUI | V2NIMLocalConversationForUI)[]
) => {
const loginStateVisible = uni.$UIKitStore.localOptions.loginStateVisible
if (loginStateVisible) {
// 线线
const accounts = conversations
.filter(
(item) =>
item.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
)
.map((item) => {
return uni.$UIKitNIM?.V2NIMConversationIdUtil.parseConversationTargetId(
item.conversationId
)
})
// accounts 100
const chunkSize = 100
const length = accounts.length
for (let i = 0; i < length; i += chunkSize) {
const chunk = accounts.slice(i, i + chunkSize)
if (chunk.length > 0) {
uni.$UIKitStore.subscriptionStore.subscribeUserStatusActive(chunk)
}
}
}
}
trackInit('ConversationUIKit')
/** 监听会话列表数据变更,实时更新 conversationList */
const conversationListWatch = autorun(() => {
const _conversationList = enableV2CloudConversation
? uni.$UIKitStore?.uiStore?.conversations
: uni.$UIKitStore?.uiStore?.localConversations
conversationList.value = _conversationList
?.map(
(conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI) => {
return {
...conversation,
// renderKey :key = conversation.conversationId
// undefineduniapp
renderKey: conversation.conversationId,
}
}
)
.sort(
(
a: V2NIMConversationForUI | V2NIMLocalConversationForUI,
b: V2NIMConversationForUI | V2NIMLocalConversationForUI
) => b.sortOrder - a.sortOrder
)
setTabUnread()
})
/** 连接状态监听 断网重连后重新订阅 */
const connectWatch = autorun(() => {
if (
uni.$UIKitStore?.connectStore.loginStatus ===
V2NIMConst.V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGINED &&
uni.$UIKitStore?.connectStore.connectStatus ===
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
) {
subscribeUserStatus(conversationList?.value)
}
})
/** 监听系统消息未读 */
const getTotalUnreadMsgsCountWatch = autorun(() => {
//
uni.$UIKitStore?.sysMsgStore?.getTotalUnreadMsgsCount()
setContactTabUnread()
})
//
watch(
() => conversationList?.value?.length, // length
() => {
subscribeUserStatus(conversationList?.value)
}
)
// 线线
onShow(() => {
if (conversationList.value?.length) {
subscribeUserStatus(conversationList?.value)
}
})
onHide(() => {
addDropdownVisible.value = false
currentMoveSessionId.value = ''
})
/**卸载监听 */
onUnmounted(() => {
conversationListWatch()
getTotalUnreadMsgsCountWatch()
connectWatch()
})
</script>
<style lang="scss" scoped>
@import '@/styles/common.scss';
.conversation-wrapper {
height: 100vh;
overflow: hidden;
}
.navigation-bar {
position: fixed;
height: 60px;
border-bottom: 1rpx solid #e9eff5;
padding: 0 20px;
display: none;
align-items: center;
justify-content: space-between;
padding-top: var(--status-bar-height);
background-color: #fff;
width: 100%;
opacity: 1;
z-index: 999;
}
.conversation-search {
display: none;
align-items: center;
height: 54px;
box-sizing: border-box;
overflow: hidden;
padding: 10px;
}
.security-tip {
padding: 0 10px;
background: #fff5e1;
height: 50px;
width: 100%;
box-sizing: border-box;
font-size: 14px;
text-align: center;
white-space: wrap;
color: #eb9718;
text-align: left;
display: none;
align-items: center;
}
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
height: 34px;
overflow: hidden;
box-sizing: border-box;
padding: 8px 10px;
background: #f3f5f7;
border-radius: 5px;
}
.search-input {
margin-left: 5px;
color: #999999;
font-size: 14px;
}
.search-icon-wrapper {
height: 22px;
display: flex;
align-items: center;
}
.block {
height: 60px;
width: 100%;
display: none;
padding-top: var(--status-bar-height);
}
.conversation-list-wrapper {
height: calc(100% - 60px - var(--status-bar-height));
box-sizing: border-box;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.logo-box {
display: flex;
align-items: center;
font-size: 20px;
font-weight: 500;
.logo-img {
width: 32px;
height: 32px;
margin-right: 10px;
}
}
.button-icon-add {
position: relative;
right: 20px;
}
.dropdown-mark {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.dropdown-container {
position: absolute;
// #ifdef MP
top: -105px;
// #endif
// #ifndef MP
top: 100%;
// #endif
right: 30px;
min-width: 122px;
min-height: 40px;
background-color: #fff;
border: 1px solid #e6e6e6;
box-shadow: 0px 4px 7px rgba(133, 136, 140, 0.25);
border-radius: 8px;
z-index: 99;
}
.add-menu-list {
padding: 15px 10px;
.add-menu-item {
white-space: nowrap;
font-size: 16px;
padding-left: 5px;
margin-bottom: 10px;
height: 30px;
line-height: 30px;
display: flex;
align-items: center;
&:last-child {
margin-bottom: 0;
}
}
}
.conversation-block {
width: 100%;
height: 72px;
}
</style>

View File

@ -28,8 +28,9 @@
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
>
<ConversationList />
<!-- 消息项 -->
<view class="message-item" v-for="(item, index) in messageList" :key="item.id || index" @click="openMessage(item)">
<!-- <view class="message-item" v-for="(item, index) in messageList" :key="item.id || index" @click="openMessage(item)">
<view class="message-avatar">
<view class="avatar-placeholder" v-if="!item.avatar">
<uni-icons type="person" size="32" color="#ffffff"></uni-icons>
@ -48,14 +49,14 @@
</view>
<!-- 空状态提示 -->
<view class="empty-state" v-if="messageList.length === 0 && !isRefreshing">
<!-- <view class="empty-state" v-if="messageList.length === 0 && !isRefreshing">
<uni-icons type="chat" size="80" color="#cccccc"></uni-icons>
<text class="empty-text">暂无患者消息</text>
<text class="empty-subtext">下拉刷新获取最新申请</text>
<view class="debug-actions">
<button class="debug-btn" @click="getApplyList">测试API调用</button>
</view>
</view>
</view> -->
</scroll-view>
<!-- 患者列表区域 -->
@ -141,7 +142,10 @@
<up-image :src="lineImg" width="14rpx" height="140rpx" ></up-image>
</view>
<view class="right-content">
<view class="note">{{ item.note }}</view>
<view class="leftcontent">
<view class="note">{{ item.note }}</view>
<view class="name">{{ item.patientname }}</view>
</view>
<uni-icons type="forward" size="20" color="#999"></uni-icons>
</view>
</view>
@ -207,7 +211,7 @@
<script setup>
import { ref, getCurrentInstance, computed } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import { onShow,onLoad} from "@dcloudio/uni-app";
import dayImg from "@/static/visit_data11.png"
import planImg from "@/static/visitplan.png"
import api from '@/api/api.js';
@ -217,6 +221,7 @@
import pinyin from 'pinyin';
import dayjs from 'dayjs'
import lineImg from "@/static/item_visitplan_fg.png"
import ConversationList from './conversation-list/index.vue'
const goPatientDetail = (uuid) => {
navTo({
@ -581,13 +586,21 @@
// 使 up-index-list
//
onShow(() => {
activeTab.value='message';
onLoad(() => {
loadMessageList();
computeListHeight();
getApplyList();
patientListByGBK();
getFollowUpList();
});
onShow(() => {
followUpList.value = [];
page.value = 1;
followUpHasMore.value = true;
followUpLoading.value = false;
followUpRefreshing.value = false;
getFollowUpList(true);
});
//
@ -598,7 +611,7 @@
const goFollowDetail = (raw) => {
if(!raw) return;
navTo({
url: `/pages_app/followDetail/followDetail?followUpUuid=${encodeURIComponent(raw.uuid || '')}&patient_name=${encodeURIComponent(raw.patient_name || '')}`
url: `/pages_app/followDetail/followDetail?followUpUuid=${encodeURIComponent(raw.uuid || '')}&patient_name=${raw.patientname}`
});
};
</script>
@ -1081,7 +1094,11 @@
font-size: 30rpx;
color:#333;
}
.right-content .name{
margin-top: 30rpx;
font-size: 28rpx;
color:#8B2316;
}
/* 加载状态样式 */
.load-more {
display: flex;

View File

@ -148,6 +148,7 @@
&:last-child{ border-bottom: none; }
.cell-left{
font-size: 32rpx;
white-space:nowrap;
color: #333;
}
.cell-right{

397
pages_app/reply/reply.vue Normal file
View File

@ -0,0 +1,397 @@
<template>
<view class="reply-page">
<!-- 导航栏 -->
<uni-nav-bar
left-icon="left"
title="回复"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right" @click="confirmReply">
<view class="btn-confirm" >确定</view>
</view>
</template>
</uni-nav-bar>
<!-- 主内容区域 -->
<view class="main-content">
<view class="input-container">
<textarea
class="reply-input"
v-model="replyText"
:placeholder="placeholder"
:auto-height="true"
:maxlength="maxLength"
:disabled="isSubmitting"
></textarea>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
import api from '@/api/api'
import { onLoad } from "@dcloudio/uni-app";
const video_uuid=ref('');
const name=ref('');
const comment_partent=ref('');
onLoad((options) => {
video_uuid.value=options.video_uuid;
placeholder.value='回复 '+options.name+':';
comment_partent.value=options.comment_partent;
name.value=options.name;
})
//
const statusBarHeight = ref(0)
const replyText = ref('')
const isSubmitting = ref(false)
const maxLength = 500
const placeholder = ref('')
//
const replyLength = computed(() => replyText.value.length)
const canSubmit = computed(() => replyText.value.trim().length > 0 && !isSubmitting.value)
const remainingChars = computed(() => maxLength - replyLength.value)
//
watch(replyText, (newValue) => {
if (newValue.length > maxLength) {
replyText.value = newValue.slice(0, maxLength)
}
})
const addCommentV2=async()=>{
const res=await api.addCommentV2({
article_uuid:video_uuid.value,
type: "8",
comment:replyText.value+'||'+name.value+''+comment_partent.value
})
if(res.code==200){
uni.showToast({ title: '回复成功', icon: 'none' })
replyText.value = '';
uni.navigateBack();
}
}
onUnmounted(() => {
//
console.log('回复页面已卸载')
})
//
const goBack = () => {
uni.navigateBack()
}
const confirmReply = async () => {
console.log(1111)
if (!canSubmit.value) return
isSubmitting.value = true
try {
addCommentV2();
} catch (error) {
console.error('回复失败:', error)
uni.showToast({
title: '回复失败,请重试',
icon: 'error'
})
} finally {
isSubmitting.value = false
}
}
const startVoiceInput = () => {
//
uni.showToast({
title: '语音输入功能',
icon: 'none'
})
}
const clearText = () => {
replyText.value = ''
}
</script>
<style lang="scss" scoped>
//
$primary-color: #ff2442;
$text-color: #000000;
$text-light: #999999;
$text-medium: #333333;
$border-color: #e0e0e0;
$border-light: #f0f0f0;
$white: #ffffff;
$green: #00a86b;
$orange: #ff6900;
$blue: #007aff;
.reply-page {
background-color: $white;
min-height: 100vh;
}
//
.status-bar {
background-color: $white;
.status-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
height: 20px;
.time {
font-size: 14px;
font-weight: 600;
color: $text-color;
}
.status-icons {
display: flex;
align-items: center;
gap: 8px;
.icon-group {
display: flex;
gap: 4px;
.app-icon {
width: 16px;
height: 16px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: $white;
&.green {
background-color: $green;
}
&.orange {
background-color: $orange;
}
&.red {
background-color: $primary-color;
}
&.blue {
background-color: $blue;
}
}
}
.network-info {
display: flex;
align-items: center;
gap: 4px;
.bluetooth-icon {
width: 12px;
height: 8px;
background-color: $text-color;
border-radius: 2px;
}
.speed {
font-size: 10px;
color: $text-color;
}
.signal-bars {
display: flex;
gap: 1px;
align-items: end;
.bar {
width: 2px;
background-color: $text-color;
&:nth-child(1) { height: 3px; }
&:nth-child(2) { height: 5px; }
&:nth-child(3) { height: 7px; }
&:nth-child(4) { height: 9px; }
}
}
.network-type {
font-size: 10px;
color: $text-color;
font-weight: 600;
}
.wifi-icon {
width: 12px;
height: 8px;
background-color: $text-color;
border-radius: 2px;
}
}
.battery {
font-size: 12px;
color: $text-color;
font-weight: 600;
}
}
}
}
.btn-confirm {
font-size: 28rpx;
color: #8B2316;
transition: opacity 0.3s ease;
}
//
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: $white;
border-bottom: 1px solid $border-light;
.nav-left, .nav-right {
width: 60px;
display: flex;
align-items: center;
}
.nav-right {
justify-content: flex-end;
}
.back-arrow {
font-size: 24px;
color: $primary-color;
font-weight: bold;
}
.nav-title {
font-size: 18px;
color: $primary-color;
font-weight: 600;
}
.confirm-btn {
font-size: 16px;
color: $primary-color;
font-weight: 600;
transition: opacity 0.3s ease;
&.disabled {
opacity: 0.5;
color: $text-light;
}
}
}
//
.main-content {
flex: 1;
padding: 20px 16px;
.input-container {
position: relative;
background-color: $white;
border: 1px solid $border-color;
border-radius: 8px;
min-height: 200px;
padding: 16px;
.input-placeholder {
font-size: 14px;
color: $text-light;
margin-bottom: 8px;
}
.reply-input {
width: 100%;
min-height: 120px;
font-size: 16px;
color: $text-medium;
line-height: 1.5;
border: none;
outline: none;
resize: none;
}
.input-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid $border-light;
.char-count {
font-size: 12px;
color: $text-light;
transition: color 0.3s ease;
&.warning {
color: $primary-color;
}
}
.action-buttons {
display: flex;
align-items: center;
gap: 8px;
.clear-btn {
width: 32px;
height: 32px;
background-color: $border-light;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
&:active {
background-color: $border-color;
}
.clear-icon {
font-size: 14px;
color: $text-light;
}
}
.voice-btn {
width: 40px;
height: 40px;
background-color: $primary-color;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(255, 36, 66, 0.3);
transition: transform 0.2s ease;
&:active {
transform: scale(0.95);
}
.voice-icon {
font-size: 18px;
color: $white;
}
}
}
}
}
}
</style>

View File

@ -102,11 +102,13 @@
<script setup>
import { ref, onMounted } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import navTo from '@/utils/navTo.js';
import api from '@/api/api.js';
//
const show=ref(false);
const selectedPatient = ref('');
const selectedDate = ref('');
const datetime = ref('');
const followUpContent = ref('请于近日来院复诊、复查');
const remindMe = ref(false);
const remindPatient = ref(true);
@ -117,7 +119,22 @@
const goBack = () => {
uni.navigateBack();
};
const addFollowUps=()=>{
api.addFollowUps({
patient_uuid: patientUuid.value,
note: followUpContent.value,
datetime: datetime.value,
isremindpatient: remindPatient.value?1:0,
isremindme:remindMe.value?1:0,
type:1
}).then(res=>{
console.log(res)
if(res.code==200){
uni.showToast({ title: '提交成功', icon: 'success' });
setTimeout(()=>uni.navigateBack(),700);
}
})
}
//
const submitSchedule = () => {
if (!selectedPatient.value) {
@ -127,16 +144,7 @@
});
return;
}
uni.showToast({
title: '日程添加成功',
icon: 'success'
});
//
setTimeout(() => {
uni.navigateBack();
}, 1500);
addFollowUps();
};
//
@ -184,6 +192,7 @@
const w = weekdays[d.getDay()];
headerYear.value = `${y}`;
headerDay.value = `${m}${dd}日周${w}`;
datetime.value = `${y}-${m}-${dd}`;
selectedDate.value = `${y}${m}${dd}日(星期${w})`;
}
};
@ -201,6 +210,7 @@
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
const weekday = weekdays[today.getDay()];
selectedDate.value = `${year}${month}${day}日(星期${weekday})`;
datetime.value = `${year}-${month}-${day}`;
headerYear.value = `${year}`;
headerDay.value = `${month}${day}日周${weekday}`;
};

View File

@ -66,6 +66,7 @@
import { ref } from 'vue'
import { onLoad,onShow } from '@dcloudio/uni-app'
import api from '@/api/api.js'
import navTo from '@/utils/navTo.js'
const keywords=ref('')
const title = ref('肝胆视频')
const activeTab = ref(0)
@ -187,7 +188,7 @@ const switchTab = (index) => {
const openDetail = (item) => {
// /
navTo({
url: `/pages_app/videoDetail/videoDetail?uuid=${item.uuid}`
url: `/pages_app/videoDetail/videoDetail?id=${item.uuid}`
})
}

View File

@ -95,6 +95,7 @@
const toggle = (id) => {
const i = selectedIds.value.indexOf(id)
if (i > -1) {
return false;
//
selectedIds.value.splice(i, 1)
const di = selectedDetail.value.findIndex(it => it.uuid === id)
@ -104,6 +105,12 @@
selectedIds.value.push(id)
const p = patientList.value.find(x => x.uuid === id)
selectedDetail.value.push({ uuid: id, realName: p?.realName || '', photo: p?.photo || '' })
let payload = { uuid: id, realName: p?.realName || '', photo: p?.photo || '' }
const pages = getCurrentPages()
const curr = pages[pages.length - 1]
const ec = curr?.getOpenerEventChannel?.()
ec?.emit && ec.emit('onPatientsSelected', payload)
uni.navigateBack()
}
}

View File

@ -344,6 +344,19 @@
uni.setStorageSync('DEV_AUTH_YX_TOKEN_App', result.YX_token);
uni.setStorageSync('userInfo', result.data);
}
}else{
if (BASE_URL.indexOf('dev') == -1) {
uni.setStorageSync('AUTH_TOKEN_App',result.access_token);
uni.setStorageSync('AUTH_YX_ACCID_App', result.YX_accid);
uni.setStorageSync('AUTH_YX_TOKEN_App', result.YX_token);
uni.setStorageSync('userInfo', result.data);
} else {
uni.setStorageSync('DEV_AUTH_TOKEN_App', result.access_token);
uni.setStorageSync('DEV_AUTH_YX_ACCID_App', result.YX_accid);
uni.setStorageSync('DEV_AUTH_YX_TOKEN_App', result.YX_token);
uni.setStorageSync('userInfo', result.data);
}
}

View File

@ -12,7 +12,10 @@
<template #right>
<view class="nav-actions">
<uni-icons type="share" size="22" color="#8B2316" />
<uni-icons type="heart" size="22" color="#8B2316" />
<view class="collect-img" @click="toCollection">
<image class="collect-img-icon" :src="videoInfo.isCollection?collectImg:discollectImg" mode="aspectFill" />
</view>
</view>
</template>
</uni-nav-bar>
@ -42,8 +45,8 @@
<view class="video-title">{{ decodeURIComponent(pageParams.title) }}</view>
<view v-if="pageParams.author" class="video-author">{{ decodeURIComponent(pageParams.author) }}</view>
</view>
<view class="speaker">陈煜 教授</view>
<text class="intro-text">{{ introText }}</text>
<view class="speaker">{{videoInfo.public_name}}</view>
<text class="intro-text">{{ videoInfo.note }}</text>
</view>
<view v-else class="comments">
<view v-if="commentList.length === 0" class="empty">暂无评论</view>
@ -54,6 +57,16 @@
<view class="name">{{ c.name }}</view>
<view class="content">{{ c.content }}</view>
<view class="time">{{ c.time }}</view>
<view v-if="c.children && c.children.length" class="child-list">
<view class="child-item" v-for="(r,i) in c.children" :key="i">
<image class="avatar small" :src="r.avatar" mode="aspectFill" />
<view class="meta">
<view class="name">{{ r.name }}</view>
<view class="content">{{ r.content }}</view>
<view class="time">{{ r.time }}</view>
</view>
</view>
</view>
</view>
<view class="reply-btn" @click="onReply(c)">回复</view>
</view>
@ -70,38 +83,142 @@
<!-- 底部区域信息页为下载条评论页为上传图片+输入+发送 -->
<view v-if="activeTab === 'info'" class="bottom-download" @click="onDownload">
<text class="download-text">点击下载(限时<text class="discount">5</text>仅需50积分)</text>
<text class="download-text">点击下载(限时<text class="discount">5</text>仅需{{videoInfo.point-welfareNum}}积分)</text>
</view>
<view v-else class="bottom-comment">
<input class="comment-input" v-model="commentText" placeholder="我也说一句" confirm-type="send" @confirm="sendComment" />
<view class="send-btn" @click="sendComment">发送</view>
</view>
<unidialog :visible="networkVisible" :content="networkContent" @close="networkVisible=false" @confirm="networkConfirm"></unidialog>
<unidialog :visible="pointVisible" :content="pointContent" @close="pointVisible=false" @confirm="pointConfirm"></unidialog>
<unidialog :visible="notEnoughVisible" :content="notEnoughContent" @close="notEnoughVisible=false" @confirm="notEnoughConfirm"></unidialog>
</template>
<script setup>
import { ref } from 'vue';
import uniVideo from '@/components/uniVideo/uniVideo.vue';
import { onLoad,onShow } from "@dcloudio/uni-app";
import unidialog from '@/components/dialog/dialog.vue';
import collectImg from '@/static/icon_book_collect_sel.png';
import discollectImg from '@/static/icon_book_collect_nor.png';
import api from '@/api/api';
import docUrl from '@/utils/docUrl'
import navTo from '@/utils/navTo'
const video_uuid=ref('');
const videoInfo=ref({});
const networkVisible=ref(false);
const networkContent=ref('');
const pointVisible=ref(false);
const pointContent=ref('');
const notEnoughVisible=ref(false);
const notEnoughContent=ref('');
const welfareNum=ref(0);
const notEnoughConfirm=()=>{
notEnoughVisible.value=false;
navTo({
url:'/pages_app/buyPoint/buyPoint'
})
}
const pointConfirm=()=>{
pointVisible.value=false;
payVideoDownload();
}
const toCollection=()=>{
if(videoInfo.value.isCollection==1){
discollection();
}else{
collection();
}
}
const networkConfirm=()=>{
networkVisible.value=false;
pointVisible.value=true;
}
const addVideoWatchRecord=async()=>{
const res=await api.addVideoWatchRecord({
video_uuid:video_uuid.value
})
if(res.code==200){
}
}
//
const pageParams = ref({});
// 使uni-apponLoad
const onLoad = (options) => {
pageParams.value = options;
console.log('接收到的参数:', pageParams.value);
//
if (options.title) {
//
console.log('视频标题:', decodeURIComponent(options.title));
const videoDetail=async()=>{
const res=await api.videoDetail({video_uuid:video_uuid.value})
if(res.code==200){
videoInfo.value=res.video;
}
if (options.author) {
console.log('视频作者:', decodeURIComponent(options.author));
}
const collection=async()=>{
const res=await api.collection({
other_uuid:video_uuid.value,
type:5
})
if(res.code==200){
uni.showToast({ title: '收藏成功', icon: 'none' })
videoDetail()
}
if (options.id) {
console.log('视频ID:', options.id);
}
const discollection=async()=>{
const res=await api.discollection({
other_uuid:video_uuid.value,
type:5
})
if(res.code==200){
uni.showToast({ title: '取消收藏成功', icon: 'none' })
videoDetail()
}
};
const isVideoDownloadRecord=ref(false);
const VideoDownloadRecord=async()=>{
const res=await api.isVideoDownloadRecord({video_uuid:video_uuid.value})
if(res.code==200){
isVideoDownloadRecord.value=res.result==0?false:true;
}
}
const getWelfareNum=async()=>{
const res=await api.getWelfareNum({
type:1
})
if(res.code==1){
welfareNum.value=res.WelfareNum;
}
}
const videoCommentListV2= async()=>{
loading.value = true;
try{
const res = await api.videoCommentListV2({
uuid: video_uuid.value
});
if(res && res.code==200 && Array.isArray(res.data)){
const mapped = res.data.map(mapComment);
commentList.value = mapped.reverse();
noMore.value = true;
}else{
noMore.value = true;
}
}finally{
loading.value = false;
}
}
onShow(()=>{
if(activeTab.value=='comment'){
videoCommentListV2();
}
videoDetail()
})
// 使uni-apponLoad
onLoad((options) => {
console.log(options)
video_uuid.value=options.id;
addVideoWatchRecord();
VideoDownloadRecord();
getWelfareNum();
});
const videoSrc = ref('');
const poster = ref('/static/livebg.png');
@ -112,41 +229,68 @@ const introText = ref(
);
//
const commentList = ref([
{
avatar: '/static/icon_home_my_public.png',
name: '肝胆相照测试号4',
content: 'hhjh',
time: '2025-07-01 17:17:13'
},
{
avatar: '/static/icon_home_my_public.png',
name: '肝胆相照测试号4',
content: 'kkkk',
time: '2025-07-01 17:17:18'
},
{
avatar: '/static/icon_home_my_public.png',
name: '肝胆相照测试号4',
content: 'nnj',
time: '2025-07-01 17:17:22'
}
]);
const commentList = ref([]);
const switchTab = (tab) => {
activeTab.value = tab;
if(tab=='comment'){
videoCommentListV2();
}
};
const payVideoDownload=async()=>{
const res=await api.payVideoDownload({video_uuid:video_uuid.value})
if(res.code==200){
navTo({
url:'/pages_app/myDownLoad/myDownLoad'
})
}else if(res.code==106){
notEnoughVisible.value=true;
notEnoughContent.value=`您的积分不足,是否购买积分?`
}
};
const onDownload = () => {
uni.showToast({ title: '前往下载页', icon: 'none' });
console.log(isVideoDownloadRecord.value);
if(!isVideoDownloadRecord.value){
pointContent.value=`当前需要${videoInfo.value.point-welfareNum.value}积分兑换,若删除可以再次缓存`
uni.getNetworkType({
success: function (res) {
console.log(res);
if(res.networkType!='none'){
networkVisible.value=true;
networkContent.value=`当前为${res.networkType}网络,确定下载?`
console.log(11111)
}else{
uni.showToast({title:'当前未联网,请检查网络',icon:'none'})
}
}
});
}else{
navTo({
url:'/pages_app/myDownLoad/myDownLoad'
})
}
};
const goBack = () => {
uni.navigateBack();
};
const addCommentV2=async()=>{
const res=await api.addCommentV2({
article_uuid:video_uuid.value,
type: "8",
comment:commentText.value
})
if(res.code==200){
uni.showToast({ title: '评论成功', icon: 'none' })
commentText.value = '';
videoCommentListV2();
}
}
const onReply = (c) => {
uni.showToast({ title: `回复:${c.name}`, icon: 'none' });
navTo({
url:'/pages_app/reply/reply?comment_partent='+c.content+'&name='+c.name+'&video_uuid='+video_uuid.value
})
};
//
@ -156,13 +300,7 @@ const sendComment = () => {
uni.showToast({ title: '请输入内容', icon: 'none' });
return;
}
commentList.value.unshift({
avatar: '/static/icon_home_my_public.png',
name: '我',
content: commentText.value,
time: new Date().toISOString().slice(0, 19).replace('T', ' ')
});
commentText.value = '';
addCommentV2();
};
//
@ -171,30 +309,25 @@ const pageSize = ref(10);
const loading = ref(false);
const noMore = ref(false);
const mockMore = Array.from({ length: 30 }).map((_, i) => ({
avatar: '/static/icon_home_my_public.png',
name: '肝胆相照测试号4',
content: `更多评论 ${i + 1}`,
time: `2025-07-01 17:${20 + Math.floor(i / 2)}:${10 + (i % 2) * 5}`
}));
const onScrollToLower = async () => {
if (activeTab.value !== 'comment' || loading.value || noMore.value) return;
loading.value = true;
try {
await new Promise(r => setTimeout(r, 600));
const start = (page.value - 1) * pageSize.value;
const chunk = mockMore.slice(start, start + pageSize.value);
if (!chunk.length) {
noMore.value = true;
} else {
commentList.value.push(...chunk);
page.value += 1;
}
} finally {
loading.value = false;
}
// push
};
const toFullUrl=(p)=>{
if(!p) return '/static/icon_home_my_public.png';
return p.startsWith('http')?p:(docUrl+p);
}
const mapComment=(item)=>{
return {
avatar: toFullUrl(item.photo||''),
name: item.name || '匿名',
content: item.content || '',
time: item.create_date || '',
children: Array.isArray(item.childs)? item.childs.map(mapComment): []
}
}
</script>
<style lang="scss" scoped>
@ -204,7 +337,10 @@ $bg-color: #f7f7f7;
$text-primary: #333;
$text-secondary: #666;
$theme-color: #8B2316;
.collect-img-icon{
width: 40rpx;
height: 40rpx;
}
.nav-actions {
display: flex;
align-items: center;
@ -371,6 +507,21 @@ $theme-color: #8B2316;
padding: 20rpx 0;
font-size: 26rpx;
}
.child-list {
margin-top: 8rpx;
margin-left: -10rpx;
}
.child-item {
display: flex;
align-items: flex-start;
margin-top: 16rpx;
}
.avatar.small {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
margin-right: 16rpx;
}
}
.bottom-spacer {

329
pages_chat/chat/forward.vue Normal file
View File

@ -0,0 +1,329 @@
<template>
<!-- 处理滚动穿透 此为官方推荐做法 https://uniapp.dcloud.net.cn/component/uniui/uni-popup.html#%E4%BB%8B%E7%BB%8D -->
<page-meta
:page-style="'overflow:' + (moveThrough ? 'hidden' : 'visible')"
></page-meta>
<div>
<NavBar
:title="
forwardConversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? t('teamChooseText')
: t('chooseText')
"
:showLeft="true"
>
<template v-slot:left>
<div @tap="backToChat">
<Icon type="icon-zuojiantou" :size="22"></Icon>
</div>
</template>
</NavBar>
<div
v-if="
forwardConversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
"
>
<div class="group-list-content">
<Empty v-if="teamList.length === 0" :text="t('TeamEmptyText')" />
<div v-else>
<div
class="group-item"
v-for="team in teamList"
:key="team.teamId"
@click="() => handleItemClick(team.teamId)"
>
<Avatar :account="team.teamId" :avatar="team.avatar" />
<span class="group-name">{{ team.name }}</span>
</div>
</div>
</div>
</div>
<div v-else>
<div v-if="friendGroupList.length > 0" class="friend-list-container">
<div class="friend-group-list">
<div
class="friend-group-item"
v-for="friendGroup in friendGroupList"
:key="friendGroup.key"
>
<div class="friend-group-title">
{{ friendGroup.key }}
</div>
<div
class="friend-item"
v-for="friend in friendGroup.data"
:key="friend.account"
@click="() => handleItemClick(friend.account)"
>
<Avatar :account="friend.account" size="36" />
<div class="friend-name">{{ friend.appellation }}</div>
</div>
</div>
</div>
</div>
<Empty v-else :text="t('noFriendText')" />
</div>
<!-- 转发弹窗 -->
<ForwardModal
:forward-modal-visible="forwardModalVisible"
:forward-to="forwardTo"
:forward-msg="forwardMsg"
:forward-conversation-type="forwardConversationType"
:forward-to-team-info="forwardToTeamInfo"
@confirm="handleForwardConfirm"
@cancel="handleForwardCancel"
/>
</div>
</template>
<script lang="ts" setup>
/**消息转发页面 */
import { ref, computed, onUnmounted } from 'vue'
import NavBar from '@/components/NavBar.vue'
import { t } from '@/utils/im/i18n'
import { onLoad } from '@dcloudio/uni-app'
import Avatar from '@/components/Avatar.vue'
import { friendGroupByPy } from '@/utils/im/friend'
import { autorun } from 'mobx'
import Empty from '@/components/Empty.vue'
import Icon from '@/components/Icon.vue'
import ForwardModal from './message/message-forward-modal.vue'
import { V2NIMConst } from 'nim-web-sdk-ng/dist/esm/nim'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMTeam } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMTeamService'
/** 好友列表 */
const friendGroupList = ref<
{ key: string; data: { account: string; appellation: string }[] }[]
>([])
/** 转发类型 */
const forwardConversationType = ref<V2NIMConst.V2NIMConversationType>(
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
)
/** 群列表 */
const teamList = ref<V2NIMTeam[]>([])
/** 会话ID */
const conversationId = uni.$UIKitStore?.uiStore.selectedConversation
/** 转发消息idClient*/
let msgIdClient = ''
/** 转发消息来源 */
let origin = ''
/** 转发相关 */
const forwardModalVisible = ref(false)
/** 转发到 */
const forwardTo = ref('')
/** 转发消息内容 */
const forwardMsg = ref<V2NIMMessageForUI>()
/** 转发到的群信息 */
const forwardToTeamInfo = ref<V2NIMTeam>()
const moveThrough = computed(() => {
return forwardModalVisible.value
})
/**转发消息确认 */
const handleForwardConfirm = (forwardComment: string) => {
forwardModalVisible.value = false
if (!forwardMsg.value) {
uni.showToast({
title: t('getForwardMessageFailed'),
icon: 'error',
})
setTimeout(() => {
backToChat()
}, 1000)
return
}
const forwardConversationId = uni.$UIKitNIM.V2NIMConversationIdUtil[
forwardConversationType.value ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
? 'p2pConversationId'
: 'teamConversationId'
](forwardTo.value)
uni.$UIKitStore.msgStore
.forwardMsgActive(forwardMsg.value, forwardConversationId, forwardComment)
.then(() => {
uni.showToast({
title: t('forwardSuccessText'),
icon: 'none',
duration: 1000,
})
setTimeout(() => {
backToChat()
}, 1000)
})
.catch(() => {
uni.showToast({
title: t('forwardFailedText'),
icon: 'error',
duration: 1000,
})
})
}
/**
* 取消转发弹窗
*/
const handleForwardCancel = () => {
forwardModalVisible.value = false
}
onLoad((props) => {
forwardConversationType.value = Number(props?.forwardConversationType)
msgIdClient = props?.msgIdClient
origin = props?.origin
})
/**群监听 */
const teamListWatch = autorun(() => {
teamList.value = uni.$UIKitStore.uiStore.teamList
})
/**好友监听 */
const friendsWatch = autorun(() => {
const friendsWithoutBlacklist = uni.$UIKitStore.uiStore.friends
.filter(
(item) =>
!uni.$UIKitStore.relationStore.blacklist.includes(item.accountId)
)
.map((item) => ({
account: item.accountId,
appellation: uni.$UIKitStore.uiStore.getAppellation({
account: item.accountId,
teamId: forwardTo.value,
}),
}))
friendGroupList.value = friendGroupByPy(
friendsWithoutBlacklist,
{
firstKey: 'appellation',
},
false
)
})
/**回到聊天 */
const backToChat = () => {
uni.navigateBack({
delta: 1,
})
}
/**点击转发选择列表 */
const handleItemClick = (_forwardTo: string) => {
if (_forwardTo && msgIdClient) {
forwardTo.value = _forwardTo
forwardMsg.value = uni.$UIKitStore.msgStore.getMsg(conversationId, [
msgIdClient,
])?.[0]
if (origin === 'pin') {
const curPinMsgsMap = uni.$UIKitStore.msgStore.pinMsgs.get(conversationId)
//@ts-ignore
const pinInfo = [...curPinMsgsMap.values()].find((pinInfo) => {
if (pinInfo.message) {
return pinInfo.message.messageClientId === msgIdClient
} else {
return false
}
})
if (pinInfo) {
forwardMsg.value = pinInfo.message
}
} else if (origin === 'collection') {
const msg = uni.$UIKitStore.msgStore.collectionMsgs.get(msgIdClient)
if (msg) {
forwardMsg.value = msg
}
}
forwardModalVisible.value = true
if (
forwardConversationType.value ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
forwardToTeamInfo.value = uni.$UIKitStore.teamStore.teams.get(_forwardTo)
}
}
}
onUnmounted(() => {
teamListWatch()
friendsWatch()
})
</script>
<style lang="scss" scoped>
@import '../styles/common.scss';
.nav-bar-text {
color: #337eff;
margin-right: 20px;
}
.group-list-content {
height: calc(100% - 60px - var(--status-bar-height));
width: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.group-item {
display: flex;
align-items: center;
height: 60px;
padding: 0 20px;
}
.group-name {
margin-left: 10px;
font-size: 16px;
padding-right: 20px;
color: #333333;
flex: 1;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
}
.friend-group-item {
padding-left: 20px;
}
.friend-group-title {
height: 40px;
line-height: 40px;
font-size: 14px;
color: #b3b7bc;
border-bottom: 1rpx solid #e1e6e8;
}
.friend-item {
margin-top: 16px;
display: flex;
align-items: center;
.friend-name {
margin-left: 12px;
padding-right: 20px;
font-size: 14px;
color: #333333;
flex: 1;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
}
}
</style>

683
pages_chat/chat/index.vue Normal file
View File

@ -0,0 +1,683 @@
<template>
<!-- 处理滚动穿透 此为官方推荐做法 https://uniapp.dcloud.net.cn/component/uniui/uni-popup.html#%E4%BB%8B%E7%BB%8D -->
<page-meta
:page-style="'overflow:' + (moveThrough ? 'hidden' : 'visible')"
></page-meta>
<div :class="isH5 ? 'msg-page-wrapper-h5' : 'msg-page-wrapper'">
<navBar :title="title"></navBar>
<!-- <NavBar :title="title" :subTitle="subTitle" :showLeft="true">
<template v-slot:left>
<div @click="backToConversation">
<Icon type="icon-zuojiantou" :size="22"></Icon>
</div>
</template>
</NavBar> -->
<div class="msg-alert">
<NetworkAlert />
</div>
<div :class="isH5 ? 'msg-wrapper-h5' : 'msg-wrapper'">
<MessageList
:conversationType="conversationType"
:to="to"
:msgs="msgs"
:loading-more="loadingMore"
:no-more="noMore"
:reply-msgs-map="replyMsgsMap"
/>
</div>
<div style="height: 'auto'">
<MessageInput
:reply-msgs-map="replyMsgsMap"
:conversation-type="conversationType"
:to="to"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { onShow, onHide } from '@dcloudio/uni-app'
import { events } from '@/utils/im/constants'
import { trackInit } from '@/utils/im/reporter'
import { autorun } from 'mobx'
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { getUniPlatform } from '@/utils/im/index'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { customSwitchTab } from '@/utils/im/customNavigate'
import NetworkAlert from '@/components/NetworkAlert.vue'
//import NavBar from './message/nav-bar.vue'
import navBar from "@/components/navBar/navBar.vue"
import Icon from '@/components/Icon.vue'
import MessageList from './message/message-list.vue'
import MessageInput from './message/message-input.vue'
import { HISTORY_LIMIT } from '@/utils/im/constants'
import { t } from '@/utils/im/i18n'
import { V2NIMMessage } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
import { V2NIMConst } from '@/utils/im/nim'
import { V2NIMConversationType } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMConversationService'
import { V2NIMMessageRefer } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
export interface YxReplyMsg {
messageClientId: string
scene: V2NIMConst.V2NIMConversationType
from: string
receiverId: string
to: string
idServer: string
time: number
}
const fromPage=ref('')
trackInit('ChatUIKit')
const title = ref('')
const subTitle = ref('')
/**会话ID */
const conversationId = uni.$UIKitStore.uiStore.selectedConversation
/**会话类型 */
const conversationType =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
conversationId
) as unknown as V2NIMConversationType
/**对话方 */
const to =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
conversationId
)
const isH5 = getUniPlatform() === 'web'
/**处理uni-popup 引起的滚动穿透 */
const moveThrough = ref(false)
/**回到会话列表 */
const backToConversation = () => {
uni.navigateBack()
}
/**读取是否需要显示群组消息已读未读的全局配置,默认 false */
const teamManagerVisible = uni.$UIKitStore.localOptions.teamMsgReceiptVisible
/**读取是否需要显示 p2p 消息、p2p会话列表消息已读未读的全局配置默认 false */
const p2pMsgReceiptVisible = uni.$UIKitStore.localOptions.p2pMsgReceiptVisible
/** 读取是否需要显示在线离线的全局配置默认true*/
const loginStateVisible = uni.$UIKitStore.localOptions.loginStateVisible
let isMounted = false
const loadingMore = ref(false)
/**是否还有更多历史消息 */
const noMore = ref(false)
/**消息列表 */
const msgs = ref<V2NIMMessage[]>([])
/**回复消息map用于回复消息的解析处理 */
const replyMsgsMap = ref<Record<string, V2NIMMessage>>()
/** 解散群组回调 */
const onTeamDismissed = (data: any) => {
if (data.teamId === to) {
uni.showModal({
content: t('onDismissTeamText'),
showCancel: false,
success(data) {
if (data.confirm) {
backToConversation()
}
},
})
}
}
/** 自己主动离开群组或被管理员踢出回调 */
const onTeamLeft = (data: any) => {
uni
.showToast({
title: t('onRemoveTeamText'),
icon: 'none',
duration: 1000,
})
.then(() => {
backToConversation()
})
}
/** 收到新消息 */
const onReceiveMessages = (msgs: V2NIMMessage[]) => {
const routes = getCurrentPages()
const curRoute = routes[routes.length - 1].route
//
if (
msgs.length &&
!msgs[0]?.isSelf &&
msgs[0].conversationId == conversationId &&
curRoute?.includes('Chat/index')
) {
handleMsgReceipt(msgs)
}
uni.$emit(events.ON_SCROLL_BOTTOM, msgs)
}
/** 处理收到消息的已读回执 */
const handleMsgReceipt = (msg: V2NIMMessage[]) => {
if (
msg[0].conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
p2pMsgReceiptVisible
) {
uni.$UIKitStore.msgStore.sendMsgReceiptActive(msg[0])
} else if (
msg[0].conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM &&
teamManagerVisible
) {
uni.$UIKitStore.msgStore.sendTeamMsgReceiptActive(msg)
}
}
/** 处理历史消息的已读未读 */
const handleHistoryMsgReceipt = (msgs: V2NIMMessage[]) => {
/** 如果是单聊 */
if (
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
p2pMsgReceiptVisible
) {
const myUserAccountId = uni.$UIKitNIM.V2NIMLoginService.getLoginUser()
const othersMsgs = msgs
.filter(
(item: V2NIMMessage) =>
// @ts-ignore
!['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
)
.filter((item: V2NIMMessage) => item.senderId !== myUserAccountId)
/** 发送单聊消息已读回执 */
if (othersMsgs.length > 0) {
uni.$UIKitStore.msgStore.sendMsgReceiptActive(othersMsgs?.[0])
}
/** 如果是群聊 */
} else if (
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM &&
teamManagerVisible
) {
const myUserAccountId = uni.$UIKitNIM.V2NIMLoginService.getLoginUser()
const myMsgs = msgs
.filter(
(item: V2NIMMessage) =>
// @ts-ignore
!['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
)
.filter((item: V2NIMMessage) => item.senderId === myUserAccountId)
uni.$UIKitStore.msgStore.getTeamMsgReadsActive(myMsgs, conversationId)
//
// sdk 50
const othersMsgs = msgs
.filter(
(item: V2NIMMessage) =>
// @ts-ignore
!['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
)
.filter((item: V2NIMMessage) => item.senderId !== myUserAccountId)
if (othersMsgs.length > 0 && othersMsgs.length < 50) {
uni.$UIKitStore.msgStore.sendTeamMsgReceiptActive(othersMsgs)
}
}
}
/** 拉取历史消息 */
const getHistory = async (endTime: number, lastMsgId?: string) => {
try {
if (noMore.value) {
return []
}
if (loadingMore.value) {
return []
}
loadingMore.value = true
if (conversationId) {
const historyMsgs = await uni.$UIKitStore.msgStore.getHistoryMsgActive({
conversationId,
endTime,
lastMsgId,
limit: HISTORY_LIMIT,
})
// pin msg
await uni.$UIKitStore.msgStore.getPinnedMessageListActive(conversationId)
loadingMore.value = false
if (historyMsgs.length < HISTORY_LIMIT) {
noMore.value = true
}
//
handleHistoryMsgReceipt(historyMsgs)
return historyMsgs
}
} catch (error) {
//@ts-ignore
// 线
if (error.code === 109404) {
uni.showModal({
content: t('onDismissTeamText'),
showCancel: false,
success(data) {
if (data.confirm) {
backToConversation()
}
},
})
}
loadingMore.value = false
throw error
}
}
/** 加载更多消息 */
const loadMoreMsgs = (lastMsg: V2NIMMessage) => {
if (lastMsg) {
getHistory(lastMsg.createTime, lastMsg.messageServerId)
} else {
getHistory(Date.now())
}
}
/** 设置页面标题 */
const setNavTitle = () => {
//
if (
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
if (loginStateVisible) {
subTitle.value =
uni.$UIKitStore?.subscriptionStore.stateMap.get(to)?.statusType ===
V2NIMConst.V2NIMUserStatusType.V2NIM_USER_STATUS_TYPE_LOGIN
? `(${t('userOnlineText')})`
: `(${t('userOfflineText')})`
}
console.log('to:'+to);
if(!fromPage.value){
title.value = uni.$UIKitStore.uiStore.getAppellation({ account: to })
}
;
//
} else if (
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
const team = uni.$UIKitStore.teamStore.teams.get(to)
subTitle.value = `(${team?.memberCount || 0})`
title.value = team?.name || ''
}
}
/** 监听当前聊天页面的会话类型 */
const conversationTypeWatch = autorun(() => {
setNavTitle()
})
/** 监听连接状态 */
const connectedWatch = autorun(() => {
if (
uni.$UIKitStore.connectStore.connectStatus ===
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
) {
if (
uni.$UIKitStore.connectStore.loginStatus ==
V2NIMConst.V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGINED
) {
getHistory(Date.now()).then(() => {
if (!isMounted) {
uni.$emit(events.ON_SCROLL_BOTTOM)
isMounted = true
}
})
}
}
})
/** 处理回复消息 */
const handleReplyMsg = (messages: V2NIMMessage[]) => {
// map
if (messages.length !== 0) {
const replyMsgsMapForExt: any = {}
const replyMsgsMapForThreadReply: any = {}
const extReqMsgs: YxReplyMsg[] = []
const threadReplyReqMsgs: V2NIMMessageRefer[] = []
const messageClientIds: Record<string, string> = {}
msgs.value.forEach((msg) => {
if (msg.serverExtension) {
try {
// yxReplyMsg
const { yxReplyMsg } = JSON.parse(msg.serverExtension)
if (yxReplyMsg) {
// replyMsg
const replyMsg = msgs.value.find(
(item) => item.messageClientId === yxReplyMsg.idClient
)
// map
if (replyMsg) {
replyMsgsMapForExt[msg.messageClientId] = replyMsg
// 1. 2. 3.
} else {
replyMsgsMapForExt[msg.messageClientId] = {
messageClientId: 'noFind',
}
const {
scene,
from,
to,
idServer,
messageClientId,
time,
receiverId,
} = yxReplyMsg
if (
scene &&
from &&
to &&
idServer &&
messageClientId &&
time &&
receiverId
) {
extReqMsgs.push({
scene,
from,
to,
idServer,
messageClientId,
time,
receiverId,
})
messageClientIds[idServer] = msg.messageClientId
}
}
}
} catch {}
}
if (msg.threadReply) {
//
const beReplyMsg = msgs.value.find(
(item) => item.messageClientId === msg.threadReply?.messageClientId
)
if (beReplyMsg) {
replyMsgsMapForThreadReply[msg.messageClientId] = beReplyMsg
} else {
replyMsgsMapForThreadReply[msg.messageClientId] = {
messageClientId: 'noFind',
}
messageClientIds[msg.threadReply.messageServerId] =
msg.messageClientId
threadReplyReqMsgs.push(msg.threadReply)
}
}
})
if (extReqMsgs.length > 0) {
// ,
uni.$UIKitNIM.V2NIMMessageService.getMessageListByRefers(
//@ts-ignore
extReqMsgs.map((item) => ({
senderId: item.from,
receiverId: item.receiverId,
messageClientId: item.messageClientId,
messageServerId: item.idServer,
createTime: item.time,
conversationType: item.scene,
conversationId: item.to,
}))
)
.then((res) => {
if (res?.length > 0) {
res.forEach((item) => {
if (item.messageServerId) {
replyMsgsMapForExt[messageClientIds[item.messageServerId]] =
item
}
})
}
replyMsgsMap.value = { ...replyMsgsMapForExt }
})
.catch(() => {
replyMsgsMap.value = { ...replyMsgsMapForExt }
})
}
replyMsgsMap.value = {
...replyMsgsMap.value,
...replyMsgsMapForThreadReply,
}
if (threadReplyReqMsgs.length > 0) {
uni.$UIKitNIM.V2NIMMessageService.getMessageListByRefers(
//@ts-ignore
threadReplyReqMsgs
)
.then((res) => {
if (res?.length > 0) {
res.forEach((item) => {
if (item.messageServerId) {
replyMsgsMapForThreadReply[
messageClientIds[item.messageServerId]
] = item
}
})
}
replyMsgsMap.value = {
...replyMsgsMap.value,
...replyMsgsMapForThreadReply,
}
})
.catch((error) => {
replyMsgsMap.value = {
...replyMsgsMap.value,
...replyMsgsMapForThreadReply,
}
})
}
}
}
/** 动态更新消息 */
const msgsWatch = autorun(() => {
// Clone pinState
const messages = [...uni.$UIKitStore.msgStore.getMsg(conversationId)]
if (messages.length !== 0) {
msgs.value = messages
}
//
handleReplyMsg(messages)
// 6
if (messages.length < 6) {
uni.hideKeyboard()
}
})
/** 监听会话方在线离线状态 */
const statusWatch = autorun(() => {
const stateMap = uni.$UIKitStore?.subscriptionStore.stateMap
if (
uni.$UIKitStore.localOptions.loginStateVisible &&
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
subTitle.value =
stateMap.get(to)?.statusType ===
V2NIMConst.V2NIMUserStatusType.V2NIM_USER_STATUS_TYPE_LOGIN
? `(${t('userOnlineText')})`
: `(${t('userOfflineText')})`
}
})
/** 滚动到底部*/
const scrollToBottom = () => {
const timer = setTimeout(() => {
uni.$emit(events.ON_SCROLL_BOTTOM)
clearTimeout(timer)
}, 300)
}
/** 订阅在线离线状态 */
const subscribeUserStatus = () => {
if (
loginStateVisible &&
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
uni.$UIKitStore.subscriptionStore.subscribeUserStatusActive([to])
}
}
onShow(()=>{
setNavTitle();
//console.log(uni.$UIKitStore);
console.log(3333);
setTimeout(()=>{
console.log(1111);
uni.$UIKitStore?.userStore._getUserInfo(to).then(res=>{
console.log(res)
title.value=res.name;
});
console.log(22222);
})
// 使 uni.navigateBackonload
if (msgs.value.length) {
const _msgs = [...msgs.value].reverse()
handleHistoryMsgReceipt(_msgs)
}
})
onLoad((options) => {
fromPage.value=options.from;
uni.$on(events.HANDLE_MOVE_THROUGH, (flag) => {
moveThrough.value = flag
})
})
onMounted(() => {
setNavTitle()
scrollToBottom()
subscribeUserStatus()
/** 收到消息 */
uni.$UIKitNIM.V2NIMMessageService.on(
'onReceiveMessages',
//@ts-ignore
onReceiveMessages
)
/** 解散群组回调 */
uni.$UIKitNIM.V2NIMTeamService.on('onTeamDismissed', onTeamDismissed)
/** 自己主动离开群组或被管理员踢出回调 */
uni.$UIKitNIM.V2NIMTeamService.on('onTeamLeft', onTeamLeft)
/** 加载更多消息 */
uni.$on(events.GET_HISTORY_MSG, loadMoreMsgs)
})
//
onUnmounted(() => {
uni.$UIKitNIM.V2NIMTeamService.off('onTeamDismissed', onTeamDismissed)
uni.$UIKitNIM.V2NIMTeamService.off('onTeamLeft', onTeamLeft)
uni.$UIKitNIM.V2NIMMessageService.off(
'onReceiveMessages',
//@ts-ignore
onReceiveMessages
)
uni.$off(events.GET_HISTORY_MSG, loadMoreMsgs)
/** 移除store的数据监听 */
connectedWatch()
msgsWatch()
statusWatch()
conversationTypeWatch()
})
onHide(() => {
uni.hideKeyboard()
})
onUnload(() => {
uni.$off(events.CONFIRM_FORWARD_MSG)
uni.$off(events.CANCEL_FORWARD_MSG)
})
</script>
<style scoped lang="scss">
page {
height: 100%;
overflow: hidden;
}
.msg-page-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.msg-page-wrapper-h5 {
width: 100%;
height: 100%;
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
}
.msg-alert {
display: flex;
flex-direction: column;
width: 100%;
height: auto;
z-index: 1;
}
.msg-wrapper {
width: 100%;
overflow: hidden;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
flex: 1;
}
.msg-wrapper-h5 {
width: 100%;
height: 100%;
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
}
.msg-wrapper > message-list {
height: 100%;
}
</style>

View File

@ -0,0 +1,181 @@
<template>
<div class="msg-page-wrapper">
<div class="msg-nav">
<NavBar :title="t('msgReadPageTitleText')" :showLeft="true">
<template v-slot:left>
<div @click="backToConversation">
<Icon type="icon-zuojiantou" :size="22"></Icon>
</div>
</template>
</NavBar>
</div>
<div class="msg-alert">
<NetworkAlert />
</div>
<div class="msg-read-header">
<div
class="msg-read-header-item"
:class="selectedType === 'read' ? 'active' : ''"
@click="selectedType = 'read'"
>
{{ `${t('readText')}(${readCount})` }}
</div>
<div
class="msg-read-header-item"
:class="selectedType === 'unread' ? 'active' : ''"
@click="selectedType = 'unread'"
>
{{ `${t('unreadText')}(${unReadCount})` }}
</div>
</div>
<div v-show="selectedType === 'read'" class="list-wrapper">
<div
v-if="readList.length"
class="list-item"
v-for="item in readList"
:key="item"
>
<div class="avatar-wrapper">
<Avatar
size="40"
:account="item"
:goto-user-card="true"
:teamId="teamId"
:goto-team-card="false"
/>
</div>
<Appellation :account="item" :teamId="teamId"></Appellation>
</div>
<div v-else>
<Empty :text="t('allUnReadText')"></Empty>
</div>
</div>
<div v-show="selectedType === 'unread'" class="list-wrapper">
<div
v-if="unReadList.length"
class="list-item"
v-for="item in unReadList"
:key="item"
>
<div class="avatar-wrapper">
<Avatar
size="40"
:account="item"
:goto-user-card="true"
:teamId="teamId"
:goto-team-card="false"
/>
</div>
<Appellation :account="item" :teamId="teamId"></Appellation>
</div>
<div v-else>
<Empty :text="t('allReadText')"></Empty>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
/** 消息已读未读详情页面 */
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { customNavigateTo } from '@/utils/im/customNavigate'
import NetworkAlert from '@/components/NetworkAlert.vue'
import NavBar from './message/nav-bar.vue'
import Icon from '@/components/Icon.vue'
import { t } from '@/utils/im/i18n'
import Avatar from '@/components/Avatar.vue'
import Appellation from '@/components/Appellation.vue'
import Empty from '@/components/Empty.vue'
/** 已读人数 */
const readCount = ref(0)
/** 未读人数 */
const unReadCount = ref(0)
/** 已读列表 */
const readList = ref<string[]>([])
/** 未读列表 */
const unReadList = ref<string[]>([])
/** 已读未读类型 */
const selectedType = ref<string>('read')
/** 群ID */
const teamId = ref<string>('')
/** 返回会话列表 */
const backToConversation = () => {
uni.navigateBack({
delta: 1,
})
}
onLoad((props) => {
const messageClientId = props?.messageClientId
const conversationId = props?.conversationId
if (messageClientId && conversationId) {
teamId.value =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
conversationId
)
const msg = uni.$UIKitStore.msgStore.getMsg(conversationId, [
messageClientId,
])
if (msg.length) {
//
uni.$UIKitStore.msgStore
.getTeamMessageReceiptDetailsActive(msg[0])
.then((res) => {
readCount.value = res?.readReceipt.readCount
unReadCount.value = res?.readReceipt.unreadCount
readList.value = res?.readAccountList
setTimeout(() => {
unReadList.value = res?.unreadAccountList
})
})
}
}
})
</script>
<style scoped lang="scss">
.msg-page-wrapper {
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
.msg-nav {
flex-basis: 45px;
}
.msg-read-header {
display: flex;
align-items: center;
width: 100%;
height: 40px;
margin-top: 5px;
margin-bottom: 10px;
.msg-read-header-item {
flex: 1;
text-align: center;
line-height: 40px;
}
.active {
border-bottom: 1px solid #007aff;
}
}
.list-wrapper {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
.list-item {
height: 50px;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
padding: 0 20px;
}
.avatar-wrapper {
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div class="msg-face-wrapper">
<div class="msg-face">
<div class="msg-face-row" v-for="(emojiRow, rowIndex) in emojiMatrix">
<div
@tap.stop="
() => {
handleEmojiClick({ key, type: emojiMap[key] })
}
"
v-for="key in emojiRow"
:key="key"
class="msg-face-item"
>
<Icon :size="27" :type="emojiMap[key]"></Icon>
</div>
<!-- 下面放三个看不到的 Icon 占个位 -->
<Icon
v-if="rowIndex + 1 === Math.ceil(emojiArr.length / emojiColNum)"
class="msg-face-delete"
:size="27"
type="icon-tuigejian"
></Icon>
<Icon
v-if="rowIndex + 1 === Math.ceil(emojiArr.length / emojiColNum)"
class="msg-face-delete"
:size="27"
type="icon-tuigejian"
></Icon>
<Icon
v-if="rowIndex + 1 === Math.ceil(emojiArr.length / emojiColNum)"
class="msg-face-delete"
:size="27"
type="icon-tuigejian"
></Icon>
</div>
</div>
<div class="emoji-block"></div>
<div class="msg-face-control">
<div @tap="handleEmojiDelete" class="msg-delete-btn">
<Icon type="icon-tuigejian" :size="25" :color="'#333'" />
</div>
<div @tap="handleEmojiSend" class="msg-send-btn">{{ t('sendText') }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
/** 表情组件 */
import { emojiMap } from '@/utils/im/emoji'
import { calculateMatrix } from '@/utils/im/matrix'
import Icon from '@/components/Icon.vue'
import { t } from '@/utils/im/i18n'
//
const emojiArr = Object.keys(emojiMap)
const emojiColNum = 7
//
const emojiMatrix = calculateMatrix(emojiArr, emojiColNum)
const emit = defineEmits(['emojiClick', 'emojiSend', 'emojiDelete'])
//
const handleEmojiClick = (emoji: any) => {
emit('emojiClick', emoji)
}
//
const handleEmojiDelete = () => {
emit('emojiDelete')
}
//
const handleEmojiSend = () => {
emit('emojiSend')
}
</script>
<style scoped lang="scss">
.msg-face-wrapper {
box-sizing: border-box;
}
.msg-face-control {
position: fixed;
bottom: 8px;
right: 10px;
z-index: 8;
}
.emoji-block {
width: 100%;
height: 40px;
background-color: transparent;
}
.msg-face {
display: flex;
flex-direction: column;
padding-bottom: 10px;
// flex-wrap: wrap;
&-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 5px 12px;
&:last-child {
flex-basis: 57.14%;
}
}
&-item {
font-size: 27px;
}
&-delete {
font-size: 27px;
visibility: hidden;
}
}
.msg-face-control {
display: flex;
flex-direction: row;
align-items: center;
}
.msg-send-btn {
padding: 6px 16px;
color: #fff;
background-color: #337eff;
}
.msg-delete-btn {
background-color: #fff;
margin-right: 10px;
padding: 0 16px;
}
</style>

View File

@ -0,0 +1,293 @@
<template>
<div class="mention-member-list-wrapper">
<div class="header">
<div @tap="onClosePopup" class="close">
<Icon color="#000" type="icon-jiantou" />
</div>
<div class="title">{{ t('chooseMentionText') }}</div>
</div>
<div class="member-list-content">
<div style="display: none">{{ teamExt }}</div>
<div
v-if="allowAtAll"
class="member-item"
@tap="
() =>
handleItemClick({
accountId: AT_ALL_ACCOUNT,
appellation: t('teamAll'),
})
"
>
<Icon :size="42" type="icon-team2" color="#fff" />
<span class="member-name">
{{ t('teamAll') }}
</span>
</div>
<div
class="member-item"
v-for="member in teamMembersWithoutSelf"
:key="member.accountId"
@tap="() => handleItemClick(member)"
>
<Avatar :account="member.accountId" />
<div class="member-name">
<Appellation
:account="member.accountId"
:teamId="member.teamId"
></Appellation>
</div>
<div
v-if="
member.memberRole ===
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER
"
class="owner"
>
{{ t('teamOwner') }}
</div>
<div
v-else-if="
member.memberRole ===
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER
"
class="manager"
>
{{ t('teamManager') }}
</div>
</div>
<div class="member-item-block"></div>
</div>
</div>
</template>
<script lang="ts" setup>
/**@ 列表组件,用于在群里@ 成员列表 */
import { ref, computed, onUnmounted, withDefaults } from 'vue'
import { t } from '@/utils/im/i18n'
import { autorun } from 'mobx'
import Avatar from '@/components/Avatar.vue'
import Icon from '@/components/Icon.vue'
import { ALLOW_AT, AT_ALL_ACCOUNT } from '@/utils/im/constants'
import { events } from '@/utils/im/constants'
import Appellation from '@/components/Appellation.vue'
import {
V2NIMTeam,
V2NIMTeamMember,
} from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMTeamService'
import { V2NIMConst } from '@/utils/im/nim'
import { MentionedMember } from './message-input.vue'
const props = withDefaults(
defineProps<{
teamId: string
}>(),
{}
)
const team = ref<V2NIMTeam>()
const teamMembers = ref<V2NIMTeamMember[]>([])
const teamExt = ref('')
/** 群成员 不包括当前登录用户 */
const teamMembersWithoutSelf = computed(() => {
return teamMembers.value.filter(
(item) => item.accountId !== uni.$UIKitStore.userStore.myUserInfo.accountId
)
})
/** 是否是群主 */
const isGroupOwner = computed(() => {
const myUser = uni.$UIKitStore.userStore.myUserInfo
return (
(team.value ? team.value.ownerAccountId : '') ===
(myUser ? myUser.accountId : '')
)
})
/** 是否是群管理员 */
const isGroupManager = computed(() => {
const myUser = uni.$UIKitStore.userStore.myUserInfo
return teamMembers.value
.filter(
(item) =>
item.memberRole ===
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER
)
.some((member) => member.accountId === (myUser ? myUser.accountId : ''))
})
/** 是否允许@ 所有人 */
const allowAtAll = computed(() => {
let ext: any = {}
try {
ext = JSON.parse(teamExt.value || '{}')
} catch (error) {
//
}
if (ext[ALLOW_AT] === 'manager') {
return isGroupOwner.value || isGroupManager.value
}
return true
})
/** 群成员排序 群主 > 管理员 > 成员 */
const sortGroupMembers = (members: V2NIMTeamMember[], teamId: string) => {
const owner = members.filter(
(item) =>
item.memberRole ===
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER
)
const manager = members
.filter(
(item) =>
item.memberRole ===
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER
)
.sort((a, b) => a.joinTime - b.joinTime)
const other = members
.filter(
(item) =>
![
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_OWNER,
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER,
].includes(item.memberRole)
)
.sort((a, b) => a.joinTime - b.joinTime)
const result = [...owner, ...manager, ...other].map((item) => {
return {
...item,
name: uni.$UIKitStore.uiStore.getAppellation({
account: item.accountId,
teamId,
}),
}
})
return result
}
/**
* 群成员点击函数
*/
const handleItemClick = (member: V2NIMTeamMember | MentionedMember) => {
const _member: MentionedMember =
member.accountId === AT_ALL_ACCOUNT
? (member as MentionedMember)
: {
accountId: member.accountId,
appellation: uni.$UIKitStore.uiStore.getAppellation({
account: member.accountId,
teamId: (member as V2NIMTeamMember).teamId,
ignoreAlias: true,
}),
}
uni.$emit(events.HANDLE_AIT_MEMBER, _member)
}
const onClosePopup = () => {
uni.$emit(events.CLOSE_AIT_POPUP)
}
/** 监听群成员 */
const teamMemberWatch = autorun(() => {
if (props.teamId) {
teamMembers.value = sortGroupMembers(
//@ts-ignore
uni.$UIKitStore.teamMemberStore.getTeamMember(props.teamId),
props.teamId
)
// @ts-ignore
const _team: V2NIMTeam = uni.$UIKitStore.teamStore.teams.get(props.teamId)
if (team) {
team.value = _team
teamExt.value = _team?.serverExtension || ''
}
}
})
onUnmounted(() => {
/** 移除监听 */
teamMemberWatch()
})
</script>
<style scoped lang="scss">
@import '@/styles/common.scss';
.mention-member-list-wrapper {
z-index: 9999999;
touch-action: none;
}
.title {
text-align: center;
font-weight: 500;
margin: 0 auto;
}
.header {
display: flex;
align-items: center;
height: 60px;
}
.close {
transform: rotate(90deg);
margin-left: 15px;
}
.member-list-content {
height: 70vh;
box-sizing: border-box;
overflow-y: auto;
}
.member-item {
display: flex;
align-items: center;
height: 50px;
padding: 8px 20px;
}
.member-item-block {
height: 100px;
}
.member-name {
margin-left: 10px;
font-size: 16px;
padding-right: 20px;
color: #333333;
flex: 1;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
}
.contact-item-icon {
height: 42px;
width: 42px;
border-radius: 50%;
text-align: center;
line-height: 39px;
font-size: 20px;
color: #fff;
background-color: #53c3f4;
}
.owner,
.manager {
color: rgb(6, 155, 235);
background-color: rgb(210, 229, 246);
height: 20px;
line-height: 20px;
border-radius: 4px;
font-size: 14px;
text-align: center;
padding: 2px 4px;
position: relative;
right: 10px;
}
</style>

View File

@ -0,0 +1,242 @@
<template>
<div
:class="!msg.isSelf || mode === 'audio-in' ? 'audio-in' : 'audio-out'"
:style="{ width: audioContainerWidth + 'px' }"
@tap="handlePlayAudio"
>
<div class="audio-dur">{{ duration }}s</div>
<div class="audio-icon-wrapper">
<Icon :size="24" :key="audioIconType" :type="audioIconType" />
</div>
</div>
</template>
<script lang="ts" setup>
/** 音频消息组件 */
import { ref, onUnmounted, computed, watch, withDefaults } from 'vue'
import Icon from '@/components/Icon.vue'
import { events } from '@/utils/im/constants'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMMessageAudioAttachment } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMMessageService'
import { onHide, onUnload } from '@dcloudio/uni-app'
import { isHarmonyOs } from '@/utils/im/index'
const props = withDefaults(
defineProps<{
msg: V2NIMMessageForUI
mode?: 'audio-in' | 'audio-out'
broadcastNewAudioSrc?: string
}>(),
{}
)
const audioIconType = ref('icon-yuyin3')
const animationFlag = ref(false)
const isAudioPlaying = ref<boolean>(false)
const audioMap = new Map<string, any>()
const emits = defineEmits(['getGlobalAudioContext'])
/** 格式化音频时长 */
const formatDuration = (duration: number) => {
return Math.round(duration / 1000) || 1
}
/** 音频消息宽度 */
const audioContainerWidth = computed(() => {
//@ts-ignore
const duration = formatDuration(props.msg.attachment?.duration)
const maxWidth = 180
return 50 + 8 * (duration - 1) > maxWidth ? maxWidth : 50 + 8 * (duration - 1)
})
/** 音频时长 */
const duration = computed(() => {
return formatDuration(
(props.msg.attachment as V2NIMMessageAudioAttachment)?.duration
)
})
/**播放音频 */
const handlePlayAudio = () => {
//@ts-ignore
uni.$emit(events.AUDIO_URL_CHANGE, props.msg?.attachment?.url)
const audioContext = getAudio()
if (!audioContext) {
const globalAudioContext = uni.createInnerAudioContext()
audioMap.set('audio', globalAudioContext)
initAudioSrc()
}
toggleAudioPlayState()
}
/** 监听当前的音频播放 是不是当前点击url如果不是就停止 */
watch(
() => props.broadcastNewAudioSrc,
(newSrc: string) => {
//@ts-ignore
if (newSrc !== props.msg?.attachment?.url && isAudioPlaying.value) {
stopAudio()
isAudioPlaying.value = false
}
}
)
/** 播放 */
const toggleAudioPlayState = () => {
if (!isAudioPlaying.value) {
playAudio()
} else {
stopAudio()
}
}
/**停止播放音频 */
const stopAudio = () => {
const audioContext = getAudio()
if (!audioContext) {
return
}
try {
audioContext.stop()
isAudioPlaying.value = false
} catch {
console.log('stop audio error')
}
}
/** 初始化音频实例 */
function initAudioSrc() {
const audioContext = getAudio()
if (!audioContext) {
return
}
//@ts-ignore
audioContext.src = props.msg?.attachment?.url
isAudioPlaying.value = false
audioContext.onPlay(onAudioPlay)
audioContext.onStop(onAudioStop)
audioContext.onEnded(onAudioEnded)
audioContext.onError(onAudioError)
}
/**播放音频 */
function playAudio() {
const audioContext = getAudio()
console.log('audio played', audioContext)
if (!audioContext) {
return
}
try {
audioContext.play()
} catch (error) {
console.log('audio played error', error)
}
}
/** 音频开始播放 */
function onAudioPlay() {
isAudioPlaying.value = true
playAudioAnimation()
}
/** 音频停止播放 */
function onAudioStop() {
animationFlag.value = false
isAudioPlaying.value = false
if (isHarmonyOs) {
const audioContext = getAudio()
audioContext?.destroy?.()
audioMap.delete('audio')
}
}
/** 音频播放结束 */
function onAudioEnded() {
animationFlag.value = false
isAudioPlaying.value = false
}
/** 音频播放失败 */
function onAudioError(error: any) {
animationFlag.value = false
console.warn('audio played error', error)
}
/**获取音频实例 */
const getAudio = () => {
return audioMap.get('audio')
}
/** 播放音频动画 */
const playAudioAnimation = () => {
try {
animationFlag.value = true
let audioIcons = ['icon-yuyin1', 'icon-yuyin2', 'icon-yuyin3']
const handler = () => {
const icon = audioIcons.shift()
if (icon) {
audioIconType.value = icon
if (!audioIcons.length && animationFlag.value) {
audioIcons = ['icon-yuyin1', 'icon-yuyin2', 'icon-yuyin3']
}
if (audioIcons.length) {
setTimeout(handler, 300)
}
}
}
handler()
} catch (error) {
console.log('playAudioAnimation error', error)
}
}
/** 离开当前页面时,停止播放音频 */
const stopAudioOnHide = () => {
const audioContext = getAudio()
if (isAudioPlaying.value) {
stopAudio()
}
audioContext?.destroy?.()
animationFlag.value = false
audioMap.delete('audio')
}
onUnmounted(() => {
stopAudioOnHide()
})
onHide(() => {
stopAudioOnHide()
})
onUnload(() => {
stopAudioOnHide()
})
</script>
<style scoped lang="scss">
.audio-dur {
height: 24px;
line-height: 24px;
}
.audio-in,
.audio-out {
width: 50px;
display: flex;
cursor: pointer;
justify-content: flex-end;
align-items: center;
}
.audio-in {
flex-direction: row-reverse;
.audio-icon-wrapper {
transform: rotate(180deg);
}
}
.audio-icon-wrapper {
height: 24px;
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<Avatar
:account="account"
:teamId="teamId"
:size="size"
:gotoUserCard="gotoUserCard"
:onLongpress="handleAitTeamMember"
/>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
import { events } from '@/utils/im/constants'
import Avatar from '@/components/Avatar.vue'
const props = defineProps<{
account: string
teamId?: string
avatar?: string
size?: string
gotoUserCard?: boolean
fontSize?: string
}>()
const store = uni.$UIKitStore
const handleAitTeamMember = () => {
const isSelf = props.account === store.userStore.myUserInfo.account
if (props.teamId && props.account && !isSelf) {
uni.$emit(events.AIT_TEAM_MEMBER, {
account: props.account,
appellation: store.uiStore.getAppellation({
account: props.account,
teamId: props.teamId,
ignoreAlias: true,
}),
})
}
}
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,883 @@
<template>
<Tooltip
v-if="!props.msg.isSelf"
:placement="placement"
ref="tooltipRef"
color="white"
>
<template #content>
<div class="msg-action-groups" v-if="!isUnknownMsg">
<div
class="msg-action-btn"
v-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
@tap="handleCopy"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-fuzhi1"
></Icon>
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleReplyMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-huifu"
></Icon>
<text class="msg-action-btn-text">{{ t('replyText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO &&
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleForwardMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-zhuanfa"
></Icon>
<text class="msg-action-btn-text">{{ t('forwardText') }}</text>
</div>
<div
class="msg-action-btn"
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
@tap="handlePinMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-pin"
></Icon>
<!-- pinState 0 或者 undefined显示标记其他显示取消标记 -->
<text class="msg-action-btn-text">{{
props.msg.pinState ? t('unpinText') : t('pinText')
}}</text>
</div>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
<div
class="msg-action-btn"
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
@tap="handleCollectionMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-collection"
></Icon>
<text class="msg-action-btn-text">{{ t('collectionText') }}</text>
</div>
</div>
<!-- 未知消息体 -->
<div class="msg-action-groups-unknown" v-else>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
</div>
</template>
<div v-if="bgVisible" class="msg-bg msg-bg-in">
<slot></slot>
</div>
<slot v-else></slot>
</Tooltip>
<div
v-else-if="
props.msg.sendingState ===
V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SENDING
"
class="msg-status-wrapper"
>
<Icon
:size="21"
color="#337EFF"
class="msg-status-icon icon-loading"
type="icon-a-Frame8"
></Icon>
<Tooltip
:placement="placement"
ref="tooltipRef"
color="white"
:align="props.msg.isSelf"
>
<template #content>
<div class="msg-action-groups">
<div
class="msg-action-btn"
v-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
@tap="handleCopy"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-fuzhi1"
></Icon>
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
</div>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
</div>
</template>
<div v-if="bgVisible" class="msg-bg msg-bg-out">
<slot></slot>
</div>
<slot v-else></slot>
</Tooltip>
</div>
<div
v-else-if="
props.msg.sendingState ===
V2NIMConst.V2NIMMessageSendingState
.V2NIM_MESSAGE_SENDING_STATE_FAILED ||
props.msg.messageStatus.errorCode === 102426 ||
props.msg.messageStatus.errorCode === 104404
"
class="msg-failed-wrapper"
>
<div class="msg-failed">
<div class="msg-status-wrapper" @tap="handleResendMsg">
<div class="icon-fail">!</div>
</div>
<Tooltip
:placement="placement"
ref="tooltipRef"
color="white"
:align="props.msg.isSelf"
>
<template #content>
<div
class="msg-action-groups"
:style="{
width:
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
? '112px'
: '56px',
}"
>
<div
class="msg-action-btn"
v-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
@tap="handleCopy"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-fuzhi1"
></Icon>
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
</div>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
</div>
</template>
<div v-if="bgVisible" class="msg-bg msg-bg-out">
<slot></slot>
</div>
<slot v-else></slot>
</Tooltip>
</div>
<div
class="in-blacklist"
v-if="props.msg.messageStatus.errorCode === 102426"
>
{{ t('sendFailWithInBlackText') }}
</div>
<div
class="friend-delete"
v-else-if="props.msg.messageStatus.errorCode === 104404"
>
{{ t('sendFailWithDeleteText') }}
<span @tap="addFriend" class="friend-verification">{{
t('friendVerificationText')
}}</span>
</div>
</div>
<Tooltip
v-else-if="tooltipVisible"
:placement="placement"
ref="tooltipRef"
color="white"
:align="props.msg.isSelf"
>
<template #content>
<div class="msg-action-groups" v-if="!isUnknownMsg">
<div
class="msg-action-btn"
v-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
@tap="handleCopy"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-fuzhi1"
></Icon>
<text class="msg-action-btn-text">{{ t('copyText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleReplyMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-huifu"
></Icon>
<text class="msg-action-btn-text">{{ t('replyText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO &&
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleForwardMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-zhuanfa"
></Icon>
<text class="msg-action-btn-text">{{ t('forwardText') }}</text>
</div>
<div
class="msg-action-btn"
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
@tap="handlePinMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-pin"
></Icon>
<!-- pinState 0 或者 undefined显示标记其他显示取消标记 -->
<text class="msg-action-btn-text">{{
props.msg.pinState ? t('unpinText') : t('pinText')
}}</text>
</div>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
<div
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
class="msg-action-btn"
@tap="handleRecallMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-chehui"
></Icon>
<text class="msg-action-btn-text">{{ t('recallText') }}</text>
</div>
<div
class="msg-action-btn"
v-if="
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
@tap="handleCollectionMsg"
>
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-collection"
></Icon>
<text class="msg-action-btn-text">{{ t('collectionText') }}</text>
</div>
</div>
<!-- 未知消息体 -->
<div class="msg-action-groups-unknown" v-else>
<div class="msg-action-btn" @tap="handleDeleteMsg">
<Icon
:size="18"
color="#656A72"
class="msg-action-btn-icon"
type="icon-shanchu"
></Icon>
<text class="msg-action-btn-text">{{ t('deleteText') }}</text>
</div>
</div>
</template>
<div v-if="bgVisible" class="msg-bg msg-bg-out">
<slot></slot>
</div>
<slot v-else></slot>
</Tooltip>
<div v-else-if="bgVisible" class="msg-bg msg-bg-out">
<slot></slot>
</div>
<div v-else>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
/** 消息操作组件 */
import { onMounted, onUnmounted, ref } from 'vue'
// @ts-ignore
import Tooltip from '@/components/Tooltip.vue'
import Icon from '@/components/Icon.vue'
import { customNavigateTo } from '@/utils/im/customNavigate'
import { events } from '@/utils/im/constants'
import { autorun } from 'mobx'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
//@ts-ignore
import { V2NIMConst } from '@/utils/im/nim'
import { msgRecallTime } from '@/utils/im/constants'
import { t } from '@/utils/im/i18n'
import { V2NIMMessage } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
const tooltipRef = ref(null)
const props = withDefaults(
defineProps<{
msg: V2NIMMessageForUI
tooltipVisible?: boolean
bgVisible?: boolean
placement?: string
}>(),
{}
)
/**会话类型 */
const conversationType =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
props.msg.conversationId
) as unknown as V2NIMConst.V2NIMConversationType
onMounted(() => {
/** 当前版本仅支持文本、图片、文件、语音、视频 话单消息,其他消息类型统一为未知消息 */
isUnknownMsg.value = !(
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT ||
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE ||
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE ||
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO ||
props.msg.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO ||
props.msg.messageType == V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
)
})
/** 是否是好友 */
const isFriend = ref(true)
/** 未知消息 */
const isUnknownMsg = ref(false)
const closeTooltip = () => {
// @ts-ignore
tooltipRef.value.close()
}
/** 复制消息 */
const handleCopy = () => {
uni.setClipboardData({
data: props.msg.text || '',
showToast: false,
success: () => {
uni.showToast({
title: t('copySuccessText'),
icon: 'none',
})
},
fail: () => {
uni.showToast({
title: t('copyFailText'),
icon: 'none',
})
},
complete() {
closeTooltip()
},
})
}
/** 页面滚动到底部 */
const scrollBottom = () => {
setTimeout(() => {
uni.$emit(events.ON_SCROLL_BOTTOM)
}, 100)
}
/** 重发消息 */
const handleResendMsg = async () => {
const _msg = props.msg as V2NIMMessageForUI
uni.$UIKitStore.msgStore.removeMsg(_msg.conversationId, [
_msg.messageClientId,
])
try {
if (_msg.threadReply) {
const beReplyMsg =
await uni.$UIKitNIM.V2NIMMessageService.getMessageListByRefers([
//@ts-ignore
_msg.threadReply,
])
if (beReplyMsg.length > 0) {
//@ts-ignore
uni.$UIKitStore.msgStore.replyMsgActive(beReplyMsg[0])
}
}
uni.$UIKitStore.msgStore.sendMessageActive({
msg: _msg,
conversationId: _msg.conversationId,
progress: () => true,
sendBefore: () => {
scrollBottom()
},
})
scrollBottom()
} catch (error) {
console.log(error)
}
}
/** 转发消息 */
const handleForwardMsg = () => {
uni.showActionSheet({
itemList: [t('forwardToTeamText'), t('forwardToFriendText')],
success(data) {
if (data.tapIndex === 0) {
customNavigateTo({
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM}&msgIdClient=${props.msg.messageClientId}`,
})
} else if (data.tapIndex === 1) {
customNavigateTo({
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P}&msgIdClient=${props.msg.messageClientId}`,
})
}
},
complete() {
closeTooltip()
},
})
}
/** pin消息 */
const handlePinMsg = () => {
const _msg = props.msg
if (_msg.pinState) {
//
uni.$UIKitStore.msgStore.unpinMessageActive(_msg).catch((err: any) => {
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
uni.showToast({
title: t(`${err.code}`),
icon: 'error',
duration: 1000,
})
} else {
uni.showToast({
title: t('unpinFailedText'),
icon: 'error',
duration: 1000,
})
}
})
} else {
/** 显示标记 */
uni.$UIKitStore.msgStore.pinMessageActive(_msg).catch((err: any) => {
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
uni.showToast({
title: t(`${err.code}`),
icon: 'error',
duration: 1000,
})
} else {
uni.showToast({
title: t('pinFailedText'),
icon: 'error',
duration: 1000,
})
}
})
}
closeTooltip()
}
/**是否是云端会话 */
const enableV2CloudConversation =
uni.$UIKitStore?.sdkOptions?.enableV2CloudConversation
/** 收藏消息 */
const handleCollectionMsg = () => {
const _msg = props.msg
const conversation = enableV2CloudConversation
? uni.$UIKitStore.conversationStore?.conversations.get(_msg.conversationId)
: uni.$UIKitStore.localConversationStore?.conversations.get(
_msg.conversationId
)
const collectionDataObj = {
//@ts-expect-error
message: uni.$UIKitNIM.V2NIMMessageConverter.messageSerialization(_msg), //
avatar: uni.$UIKitStore.userStore.users.get(_msg.senderId)?.avatar,
conversationName: conversation?.name,
senderName: uni.$UIKitStore.uiStore.getAppellation({
account: _msg.senderId,
teamId:
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? _msg.receiverId
: '',
}),
}
const addCollectionParams = {
collectionType: 1000 + _msg.messageType, //
collectionData: JSON.stringify(collectionDataObj),
uniqueId: _msg.messageServerId,
}
uni.$UIKitStore.msgStore
.addCollectionActive(addCollectionParams)
.then(() => {
uni.showToast({
title: t('addCollectionSuccessText'),
icon: 'none',
})
})
.catch((err: any) => {
if (err?.code && typeof t(`${err.code}`) !== 'undefined') {
uni.showToast({
title: t(`${err.code}`),
icon: 'error',
duration: 1000,
})
} else {
uni.showToast({
title: t('addCollectionFailedText'),
icon: 'error',
duration: 1000,
})
}
})
closeTooltip()
}
/** 回复消息 */
const handleReplyMsg = async () => {
const _msg = props.msg
uni.$UIKitStore.msgStore.replyMsgActive(_msg)
closeTooltip()
uni.$emit(events.REPLY_MSG, props.msg)
// @
if (
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM &&
!props.msg.isSelf
) {
uni.$emit(events.AIT_TEAM_MEMBER, {
accountId: props.msg.senderId,
appellation: uni.$UIKitStore.uiStore.getAppellation({
account: props.msg.senderId,
teamId: props.msg.receiverId,
ignoreAlias: true,
}),
})
}
}
/** 撤回消息 */
const handleRecallMsg = () => {
const diff = Date.now() - props.msg.createTime
if (diff > msgRecallTime) {
uni.showToast({
title: t('msgRecallTimeErrorText'),
icon: 'none',
})
closeTooltip()
return
}
uni.showModal({
title: t('recallText'),
content: t('recall3'),
showCancel: true,
confirmText: t('recallText'),
confirmColor: '#1861df',
success(data) {
if (data.confirm) {
const _msg = props.msg
uni.$UIKitStore.msgStore.reCallMsgActive(_msg).catch(() => {
uni.showToast({
title: t('recallMsgFailText'),
icon: 'error',
})
})
}
},
complete() {
closeTooltip()
},
})
}
/** 删除消息 */
const handleDeleteMsg = () => {
const _msg = props.msg
uni.showModal({
title: t('deleteText'),
content: t('delete'),
showCancel: true,
confirmText: t('deleteText'),
confirmColor: '#1861df',
success(data) {
if (data.confirm) {
uni.$UIKitStore.msgStore
.deleteMsgActive([_msg])
.then(() => {
uni.showToast({
title: t('deleteMsgSuccessText'),
icon: 'none',
})
})
.catch((error: any) => {
uni.showToast({
title: t('deleteMsgFailText'),
icon: 'error',
})
})
}
},
complete() {
closeTooltip()
},
})
}
/** 添加好友 */
const addFriend = () => {
customNavigateTo({
url: `/pages/User/friend/index?account=${props.msg.receiverId}`,
})
}
/** 监听好友列表 */
const friendsWatch = autorun(() => {
const _isFriend = uni.$UIKitStore.uiStore.friends
.filter(
(item) =>
!uni.$UIKitStore.relationStore.blacklist.includes(item.accountId)
)
.map((item) => item.accountId)
.some((item: any) => item.account === props.msg.receiverId)
isFriend.value = _isFriend
})
/** 卸载监听 */
onUnmounted(() => {
friendsWatch()
})
</script>
<style scoped lang="scss">
@import '@/styles/common.scss';
.msg-bg {
max-width: 360rpx;
overflow: hidden;
padding: 12px 16px;
&-in {
border-radius: 0 8px 8px 8px;
background-color: #e8eaed;
margin-left: 8px;
}
&-out {
border-radius: 8px 0 8px 8px;
background-color: #d6e5f6;
margin-right: 8px;
}
}
.msg-action-groups {
display: flex;
flex-direction: row;
flex-wrap: wrap;
max-width: 224px;
width: max-content;
}
.msg-action-btn {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 10px;
width: 56px;
&-icon {
color: #656a72;
font-size: 18px;
}
&-text {
color: #333;
font-size: 14px;
word-break: keep-all;
}
}
.msg-failed-wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
width: 100%;
.in-blacklist {
color: #b3b7bc;
font-size: 14px;
position: relative;
right: 20%;
margin: 10px 0;
}
.friend-delete {
color: #b3b7bc;
font-size: 14px;
margin: 10px 0;
.friend-verification {
color: #337eff;
font-size: 14px;
}
}
}
.msg-status-wrapper {
// max-width: 450rpx;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
margin-right: 8px;
box-sizing: border-box;
.msg-bg-out {
margin-right: 0;
flex: 1;
}
}
.msg-status-icon {
margin-right: 8px;
font-size: 21px;
position: absolute;
bottom: 2px;
left: -30px;
&.icon-loading {
color: #337eff;
animation: loadingCircle 1s infinite linear;
}
}
.icon-fail {
background: #fc596a;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
margin-right: 5px;
font-size: 15px;
}
.msg-failed {
display: flex;
flex-direction: row;
align-items: center;
}
</style>

View File

@ -0,0 +1,167 @@
<template>
<uni-link
v-if="!isWxApp"
class="msg-file-wrapper"
:href="downloadUrl"
:download="name"
:showUnderLine="false"
>
<div
:class="!msg.isSelf ? 'msg-file msg-file-in' : 'msg-file msg-file-out'"
@click="() => openInBrowser(downloadUrl)"
>
<Icon :type="iconType" :size="32"></Icon>
<div class="msg-file-content">
<div class="msg-file-title">
<div class="msg-file-title-prefix">{{ prefixName }}</div>
<div class="msg-file-title-suffix">{{ suffixName }}</div>
</div>
<div class="msg-file-size">{{ parseFileSize(size) }}</div>
</div>
</div>
</uni-link>
<div v-else @click="mpDownload">
<div
:class="!msg.isSelf ? 'msg-file msg-file-in' : 'msg-file msg-file-out'"
>
<Icon :type="iconType" :size="32"></Icon>
<div class="msg-file-content">
<div class="msg-file-title">
<div class="msg-file-title-prefix">{{ prefixName }}</div>
<div class="msg-file-title-suffix">{{ suffixName }}</div>
<!-- <text class="msg-file-name" v-text="name"></text> -->
</div>
<div class="msg-file-size">{{ parseFileSize(size) }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
/** 文件消息组件 */
import { getFileType, parseFileSize } from '@xkit-yx/utils'
import Icon from '@/components/Icon.vue'
// @ts-ignore
import UniLink from '@/components/uni-components/uni-link/components/uni-link/uni-link.vue'
import { isWxApp } from '@/utils/im/index'
import { t } from '@/utils/im/i18n'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMMessageFileAttachment } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMMessageService'
const props = withDefaults(defineProps<{ msg: V2NIMMessageForUI }>(), {})
/** 文件图标映射 */
const fileIconMap = {
pdf: 'icon-PPT',
word: 'icon-Word',
excel: 'icon-Excel',
ppt: 'icon-PPT',
zip: 'icon-RAR1',
txt: 'icon-qita',
img: 'icon-tupian2',
audio: 'icon-yinle',
video: 'icon-shipin',
}
const {
name = '',
url = '',
ext = '',
size = 0,
} = (props.msg.attachment as V2NIMMessageFileAttachment) || {}
//@ts-ignore
const iconType = fileIconMap[getFileType(ext)] || 'icon-weizhiwenjian'
const index = name.lastIndexOf('.') > -1 ? name.lastIndexOf('.') : name.length
/** 文件名前缀 */
const prefixName = name.slice(0, Math.max(index - 5, 0))
/** 文件名后缀 */
const suffixName = name.slice(Math.max(index - 5, 0))
/** 下载地址 */
const downloadUrl =
url + ((url as string).includes('?') ? '&' : '?') + `download=${name}`
/** 小程序不支持直接下载文件,复制链接到剪切板,浏览器打开 */
const mpDownload = () => {
uni.setClipboardData({
data: downloadUrl,
})
uni.showModal({
content: t('wxAppFileCopyText'),
showCancel: false,
})
}
/** 打开浏览器 */
const openInBrowser = (url: string) => {
uni.setClipboardData({
data: url,
showToast: false,
success: () => {
uni.showToast({
title: t('openUrlText'),
icon: 'none',
})
},
})
}
</script>
<style scoped lang="scss">
.msg-file {
height: 56px;
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 15px;
border-radius: 8px;
border: 1px solid #dee0e2;
&-in {
margin-left: 8px;
}
&-out {
margin-right: 8px;
}
&-content {
margin-left: 15px;
max-width: 300rpx;
min-width: 0;
}
&-title {
color: #333;
font-size: 14px;
font-weight: 400;
display: flex;
&-prefix {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-suffix {
white-space: nowrap;
}
}
&-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-size {
color: #999;
font-size: 10px;
margin-top: 4px;
}
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<Modal
:title="t('sendToText')"
:visible="forwardModalVisible"
:confirmText="t('sendText')"
:cancelText="t('cancelText')"
@cancel="handleCancel"
@confirm="handleConfirm"
>
<div
v-if="
props.forwardConversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
"
class="avatar-wrapper"
>
<Avatar
:account="
(props.forwardToTeamInfo && props.forwardToTeamInfo.teamId) || ''
"
:avatar="props.forwardToTeamInfo && props.forwardToTeamInfo.avatar"
size="36"
/>
<div class="name">
{{ (props.forwardToTeamInfo && props.forwardToTeamInfo.name) || '' }}
</div>
</div>
<div v-else class="avatar-wrapper">
<Avatar :account="forwardTo" size="36" />
<div class="name">
<span>{{ forwardToNick }}</span>
</div>
</div>
<div class="description">
{{ '[' + t('forwardText') + ']' }}
{{ forwardFromNick }}
{{ t('sessionRecordText') }}
</div>
<input
class="forward-input"
@input="handleForwardInputChange"
:placeholder="t('forwardComment')"
/>
</Modal>
</template>
<script lang="ts" setup>
/** 消息转发弹窗组件 */
import { t } from '@/utils/im/i18n'
import { ref, computed } from 'vue'
import Modal from '@/components/Modal.vue'
import Avatar from '@/components/Avatar.vue'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMConst } from '@/utils/im/nim'
interface ForwardToTeamInfo {
teamId: string
name: string
avatar: string
}
const props = withDefaults(
defineProps<{
forwardModalVisible: boolean
forwardTo: string
forwardMsg: V2NIMMessageForUI
forwardConversationType: V2NIMConst.V2NIMConversationType
forwardToTeamInfo?: ForwardToTeamInfo
}>(),
{}
)
const emit = defineEmits(['confirm', 'cancel'])
/** 留言 */
const forwardComment = ref('')
/** 转发弹窗 Input */
const handleForwardInputChange = (event: any) => {
forwardComment.value = event.detail.value
}
/** 取消转发 */
const handleCancel = () => {
emit('cancel')
}
/** 确认转发 */
const handleConfirm = () => {
emit('confirm', forwardComment.value)
}
/** 转发消息的接收方昵称 */
const forwardToNick = computed(() => {
return uni.$UIKitStore.uiStore.getAppellation({
account: props.forwardTo,
})
})
/** 转发消息的发送方昵称 */
const forwardFromNick = computed(() => {
return uni.$UIKitStore.uiStore.getAppellation({
account: props.forwardMsg?.senderId,
})
})
</script>
<style lang="scss" scoped>
.description {
font-size: 14px;
height: 32px;
color: #000000;
background-color: #f2f4f5;
margin: 16px;
padding: 0 16px;
line-height: 32px;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.forward-input {
height: 32px;
border: 1px solid #e1e6e8;
border-radius: 4px;
margin: 10px 16px 0 16px;
padding: 5px 8px;
box-sizing: border-box;
}
.avatar-wrapper {
display: flex;
align-items: center;
height: 36px;
margin: 13px 16px;
.name {
margin-left: 10px;
font-size: 14px;
color: #333333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div class="g2-message-wrapper" @click="handleCall">
<Icon :type="iconType" :size="28"></Icon>
<div class="g2-message-status">{{ status }}</div>
<div v-if="duration" class="g2-message-duration">{{ duration }}</div>
</div>
</template>
<script lang="ts" setup>
/** 音视频消息组件 */
import Icon from '@/components/Icon.vue'
import { t } from '@/utils/im/i18n'
import { convertSecondsToTime, startCall, isApp } from '@/utils/im/index'
import { g2StatusMap } from '@/utils/im/constants'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
const props = withDefaults(defineProps<{ msg: V2NIMMessageForUI }>(), {})
/** 通话时长 */
const duration = convertSecondsToTime(
//@ts-ignore
props.msg.attachment?.durations[0]?.duration
)
/** 通话状态 */
//@ts-expect-error
const status = g2StatusMap[props.msg.attachment?.status]
const iconType =
//@ts-expect-error
props.msg.attachment?.type == 1 ? 'icon-yuyin8' : 'icon-shipin8'
/** 发起呼叫 */
const handleCall = () => {
if (isApp) {
//@ts-ignore
const callType = props.msg.attachment?.type
const myAccount = uni.$UIKitStore.userStore.myUserInfo.accountId
const isSelfMsg = props.msg.senderId === myAccount
if (isSelfMsg) {
const remoteShowName = uni.$UIKitStore.uiStore.getAppellation({
account: props.msg.receiverId,
})
startCall({
remoteUserAccid: props.msg.receiverId,
currentUserAccid: myAccount,
type: callType,
remoteShowName: remoteShowName,
})
} else {
const remoteShowName = uni.$UIKitStore.uiStore.getAppellation({
account: props.msg.senderId,
})
startCall({
remoteUserAccid: props.msg.senderId,
currentUserAccid: myAccount,
type: callType,
remoteShowName: remoteShowName,
})
}
} else {
uni.showToast({
title: t('callFailedText'),
icon: 'none',
})
}
}
</script>
<style scoped lang="scss">
.g2-message-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.g2-message-status {
margin: 0 7px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,664 @@
<template>
<div
:class="`msg-item-wrapper ${
props.msg.pinState &&
!(
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
props.msg.timeValue !== undefined
) &&
!props.msg.recallType
? 'msg-pin'
: ''
}`"
:id="MSG_ID_FLAG + props.msg.messageClientId"
:key="props.msg.createTime"
>
<!-- 消息时间 -->
<div
class="msg-time"
v-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
props.msg.timeValue !== undefined
"
>
{{ props.msg.timeValue }}
</div>
<!-- 撤回消息 可重新编辑 -->
<div
class="msg-common"
:style="{
flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse',
}"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
props.msg.recallType === 'reCallMsg' &&
props.msg.canEdit
"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<MessageBubble :msg="props.msg" :bg-visible="true">
{{ t('recall2') }}
<text
class="msg-recall-btn"
@tap="
() => {
handleReeditMsg(props.msg)
}
"
>
{{ t('reeditText') }}
</text>
</MessageBubble>
</div>
<!-- 撤回消息 不可重新编辑 主动撤回 -->
<div
class="msg-common"
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
props.msg.recallType === 'reCallMsg' &&
!props.msg.canEdit
"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<MessageBubble :msg="props.msg" :bg-visible="true">
<div class="recall-text">{{ t('you') + t('recall') }}</div>
</MessageBubble>
</div>
<!-- 撤回消息 对方撤回-->
<div
class="msg-common"
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
props.msg.recallType === 'beReCallMsg'
"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<div class="msg-content">
<div class="msg-name" v-if="!props.msg.isSelf">
{{ appellation }}
</div>
<div :class="props.msg.isSelf ? 'self-msg-recall' : 'msg-recall'">
<text class="msg-recall2">
{{ !props.msg.isSelf ? t('recall2') : `${t('you') + t('recall')}` }}
</text>
</div>
</div>
</div>
<!-- 文本消息-->
<div
class="msg-common"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<div class="msg-content">
<div class="msg-name" v-if="!props.msg.isSelf">
{{ appellation }}
</div>
<MessageBubble
:msg="props.msg"
:tooltip-visible="true"
:bg-visible="true"
>
<ReplyMessage v-if="!!replyMsg" :replyMsg="replyMsg"></ReplyMessage>
<MessageText :msg="props.msg"></MessageText>
</MessageBubble>
</div>
<MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead>
</div>
<!-- 图片消息-->
<div
class="msg-common"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE
"
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<div class="msg-content">
<div class="msg-name" v-if="!props.msg.isSelf">
{{ appellation }}
</div>
<MessageBubble
:msg="props.msg"
:tooltip-visible="true"
:bg-visible="true"
style="cursor: pointer"
>
<div
@tap="
() => {
//@ts-ignore
handleImageTouch(props.msg.attachment?.url)
}
"
>
<image
class="msg-image"
:lazy-load="true"
mode="aspectFill"
:src="imageUrl"
></image>
</div>
</MessageBubble>
</div>
<MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead>
</div>
<!-- 视频消息-->
<div
class="msg-common"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO
"
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<div class="msg-content">
<div class="msg-name" v-if="!props.msg.isSelf">
{{ appellation }}
</div>
<MessageBubble
:msg="props.msg"
:tooltip-visible="true"
:bg-visible="true"
style="cursor: pointer"
>
<div
class="video-msg-wrapper"
@tap="() => handleVideoTouch(props.msg)"
>
<div class="video-play-button">
<div class="video-play-icon"></div>
</div>
<image
class="msg-image"
:lazy-load="true"
mode="aspectFill"
:src="videoFirstFrameDataUrl"
></image>
</div>
</MessageBubble>
</div>
<MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead>
</div>
<!-- 音视频消息-->
<div
class="msg-common"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<div class="msg-content">
<div class="msg-name" v-if="!props.msg.isSelf">
{{ appellation }}
</div>
<MessageBubble
:msg="props.msg"
:tooltip-visible="true"
:bg-visible="true"
>
<MessageG2 :msg="props.msg" />
</MessageBubble>
</div>
</div>
<!-- 文件消息-->
<div
class="msg-common"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE
"
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<div class="msg-content">
<div class="msg-name" v-if="!props.msg.isSelf">
{{ appellation }}
</div>
<MessageBubble
:msg="props.msg"
:tooltip-visible="true"
:bg-visible="false"
>
<MessageFile :msg="props.msg" />
</MessageBubble>
</div>
<MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead>
</div>
<!-- 语音消息-->
<div
class="msg-common"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
"
:style="{
flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse',
}"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<div class="msg-content">
<div class="msg-name" v-if="!props.msg.isSelf">
{{ appellation }}
</div>
<MessageBubble
:msg="props.msg"
:tooltip-visible="true"
:bg-visible="true"
style="cursor: pointer"
>
<MessageAudio
:msg="props.msg"
:broadcastNewAudioSrc="broadcastNewAudioSrc"
/>
</MessageBubble>
</div>
<!-- <MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead> -->
</div>
<!-- 通知消息-->
<MessageNotification
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION
"
:msg="props.msg"
/>
<div
class="msg-common"
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
v-else
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<div class="msg-content">
<div class="msg-name" v-if="!props.msg.isSelf">
{{ appellation }}
</div>
<MessageBubble
:msg="props.msg"
:tooltip-visible="true"
:bg-visible="true"
>
[{{ t('unknowMsgText') }}]
</MessageBubble>
</div>
</div>
<!-- 消息标记 不展示 pinState 0 时间消息以及撤回消息的标记样式 -->
<div
class="msg-pin-tip"
v-if="
props.msg.pinState &&
!(
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
props.msg.timeValue !== undefined
) &&
!props.msg.recallType
"
:style="{ justifyContent: !props.msg.isSelf ? 'flex-start' : 'flex-end' }"
>
<Icon :size="11" type="icon-green-pin"></Icon>&nbsp;<span
v-if="props.msg.operatorId === accountId"
>{{ `${t('you')}` }}</span
>
<Appellation
v-else
:account="props.msg.operatorId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
color="#3EAF96"
fontSize="11"
></Appellation
>&nbsp;{{ `${t('pinThisText')}` }}
</div>
</div>
</template>
<script lang="ts" setup>
/** 消息组件 */
import { ref, computed, onUnmounted } from 'vue'
import Avatar from '@/components/Avatar.vue'
import MessageBubble from './message-bubble.vue'
import ReplyMessage from './message-reply.vue'
import MessageFile from './message-file.vue'
import MessageText from './message-text.vue'
import MessageAudio from './message-audio.vue'
import MessageNotification from './message-notification.vue'
import MessageG2 from './message-g2.vue'
import { customNavigateTo } from '@/utils/im/customNavigate'
import MessageIsRead from './message-read.vue'
import Icon from '@/components/Icon.vue'
import Appellation from '@/components/Appellation.vue'
import { events, MSG_ID_FLAG } from '@/utils/im/constants'
import { autorun } from 'mobx'
import { stopAllAudio } from '@/utils/im/index'
import { t } from '@/utils/im/i18n'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMConst } from '@/utils/im/nim'
const props = withDefaults(
defineProps<{
msg: V2NIMMessageForUI & { timeValue?: number }
index: number
replyMsgsMap?: {
[key: string]: V2NIMMessageForUI
}
broadcastNewAudioSrc: string
}>(),
{}
)
/** 回复消息 */
const replyMsg = computed(() => {
return props.replyMsgsMap && props.replyMsgsMap[props.msg.messageClientId]
})
/** 昵称 */
const appellation = ref('')
/** 当前用户账号 */
const accountId = uni.$UIKitStore?.userStore?.myUserInfo.accountId
/** 会话类型 */
const conversationType =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
props.msg.conversationId
) as unknown as V2NIMConst.V2NIMConversationType
/** 会话对象 */
const to = uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
props.msg.conversationId
)
/** 获取视频首帧 */
const videoFirstFrameDataUrl = computed(() => {
//@ts-ignore
const url = props.msg.attachment?.url
return url ? `${url}${url.includes('?') ? '&' : '?'}vframe&offset=1` : ''
})
/** 图片地址 */
const imageUrl = computed(() => {
/** 被拉黑 */
if (props.msg.messageStatus.errorCode == 102426) {
return 'https://yx-web-nosdn.netease.im/common/c1f278b963b18667ecba4ee9a6e68047/img-fail.png'
}
/** 非好友关系 */
if (props.msg.messageStatus.errorCode == 104404) {
return 'https://yx-web-nosdn.netease.im/common/c1f278b963b18667ecba4ee9a6e68047/img-fail.png'
}
//@ts-ignore
return props.msg?.attachment?.url || props.msg.attachment?.file
})
/** 点击图片预览 */
const handleImageTouch = (url: string) => {
if (url) {
uni.previewImage({
urls: [url],
})
}
}
/** 点击视频播放 */
const handleVideoTouch = (msg: V2NIMMessageForUI) => {
stopAllAudio()
//@ts-ignore
const url = msg.attachment?.url
if (url) {
customNavigateTo({
url: `/pages/Chat/video-play?videoUrl=${encodeURIComponent(url)}`,
})
}
}
/** 重新编辑消息 */
const handleReeditMsg = (msg: V2NIMMessageForUI) => {
uni.$emit(events.ON_REEDIT_MSG, msg)
}
/** 监听昵称变化 */
const appellationWatch = autorun(() => {
/** 昵称展示顺序 群昵称 > 备注 > 个人昵称 > 帐号 */
appellation.value = uni.$UIKitStore.uiStore.getAppellation({
account: props.msg.senderId,
teamId:
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: '',
})
})
onUnmounted(() => {
appellationWatch()
})
</script>
<style scoped lang="scss">
.msg-item-wrapper {
padding: 0 15px 15px;
}
.msg-common {
padding-top: 8px;
display: flex;
align-items: flex-start;
font-size: 16px;
message-is-read {
align-self: flex-end;
}
}
.msg-pin {
opacity: 1;
background: #fffbea;
}
.msg-pin-tip {
font-size: 11px;
font-weight: normal;
color: #3eaf96;
margin: 8px 50px 0 50px;
display: flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
}
.msg-content {
display: flex;
flex-direction: column;
}
.msg-name {
font-size: 12px;
color: #999;
text-align: left;
margin-bottom: 4px;
max-width: 300rpx;
padding-left: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.msg-image {
max-width: 100%;
}
.msg-time {
margin-top: 8px;
text-align: center;
color: #b3b7bc;
font-size: 12px;
}
.msg-recall-btn {
margin-left: 5px;
color: #1861df;
}
.msg-recall2 {
font-size: 16px;
}
.self-msg-recall {
max-width: 360rpx;
overflow: hidden;
padding: 12px 16px;
border-radius: 8px 0px 8px 8px;
margin-right: 8px;
background-color: #d6e5f6;
color: #666666;
}
.msg-recall {
max-width: 360rpx;
overflow: hidden;
padding: 12px 16px;
border-radius: 0px 8px 8px 8px;
margin-left: 8px;
background-color: #e8eaed;
color: #666666;
}
.recall-text {
color: #666666;
}
.video-play-button {
width: 50px;
height: 50px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
z-index: 9;
}
.video-play-icon {
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 18px solid #fff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-40%, -50%);
}
.video-msg-wrapper {
box-sizing: border-box;
max-width: 360rpx;
}
</style>

View File

@ -0,0 +1,222 @@
<template>
<div class="msg-list-wrapper" @touchstart="handleTapMessageList">
<scroll-view
id="message-scroll-list"
scroll-y="true"
:scroll-top="scrollTop"
class="message-scroll-list"
>
<!-- 查看更多 -->
<div v-show="!noMore" @click="onLoadMore" class="view-more-text">
{{ t('viewMoreText') }}
</div>
<view class="msg-tip" v-show="noMore">{{ t('noMoreText') }}</view>
<div v-for="(item, index) in finalMsgs" :key="item.renderKey">
<MessageItem
:msg="item"
:index="index"
:key="item.renderKey"
:reply-msgs-map="replyMsgsMap"
:broadcastNewAudioSrc="broadcastNewAudioSrc"
>
</MessageItem>
</div>
</scroll-view>
</div>
</template>
<script lang="ts" setup>
/** 消息列表组件 */
import { ref, computed, onBeforeMount, onUnmounted, withDefaults } from 'vue'
import MessageItem from './message-item.vue'
import { events } from '@/utils/im/constants'
import { caculateTimeago } from '@/utils/im/date'
import { t } from '@/utils/im/i18n'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMConst } from '@/utils/im/nim'
import { V2NIMTeam } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMTeamService'
import { autorun } from 'mobx'
const props = withDefaults(
defineProps<{
msgs: V2NIMMessageForUI[]
conversationType: V2NIMConst.V2NIMConversationType
to: string
loadingMore?: boolean
noMore?: boolean
replyMsgsMap?: {
[key: string]: V2NIMMessageForUI
}
}>(),
{}
)
/** 群信息监听 */
let teamWatch = () => {}
onBeforeMount(() => {
let team: V2NIMTeam | undefined = undefined
/** 群监听 */
teamWatch = autorun(() => {
team = uni.$UIKitStore.teamStore.teams.get(props.to) as unknown as V2NIMTeam
})
if (
props.conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
uni.$UIKitStore.teamMemberStore.getTeamMemberActive({
teamId: props.to,
queryOption: {
limit: Math.max((team as unknown as V2NIMTeam)?.memberLimit || 0, 200),
roleQueryType: 0,
},
})
}
/** 全局播放音频url */
uni.$on(events.AUDIO_URL_CHANGE, (url) => {
broadcastNewAudioSrc.value = url
})
/** 滚动到底部 */
uni.$on(events.ON_SCROLL_BOTTOM, () => {
scrollToBottom()
})
/** 加载更多 */
uni.$on(events.ON_LOAD_MORE, () => {
const msg = finalMsgs.value.filter(
(item) =>
!(
item.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
)
)[0]
if (msg) {
uni.$emit(events.GET_HISTORY_MSG, msg)
}
})
})
/** 滚动条位置距离 */
const scrollTop = ref(99999)
/** 消息列表 */
const finalMsgs = computed(() => {
const res: (V2NIMMessageForUI & { renderKey: string })[] = []
props.msgs.forEach((item, index) => {
// 5
if (
index > 0 &&
item.createTime - props.msgs[index - 1].createTime > 5 * 60 * 1000
) {
res.push({
...item,
messageClientId: 'time-' + item.createTime,
messageType: V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM,
sendingState:
V2NIMConst.V2NIMMessageSendingState
.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED,
// @ts-ignore
timeValue: caculateTimeago(item.createTime),
renderKey: `${item.createTime + 1}`,
})
}
res.push({
...item,
// @ts-ignore
renderKey: `${item.createTime}`,
})
})
return res
})
/** 全局播放音频url */
const broadcastNewAudioSrc = ref<string>('')
/**
* 不建议查询当前的消息列表dom高度进行滚动在个别机型会不生效并有卡顿问题使用极大值进行滚动无该问题
*/
const scrollToBottom = () => {
scrollTop.value += 9999999
const timer = setTimeout(() => {
scrollTop.value += 1
clearTimeout(timer)
}, 300)
}
/** 加载更多消息 */
const onLoadMore = () => {
const msg = finalMsgs.value.filter(
(item) =>
!(
item.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM &&
['beReCallMsg', 'reCallMsg'].includes(item.recallType || '')
)
)[0]
uni.$emit(events.GET_HISTORY_MSG, msg)
}
/** 点击消息列表 */
const handleTapMessageList = () => {
uni.$emit(events.CLOSE_PANEL)
setTimeout(() => {
uni.$emit(events.CLOSE_PANEL)
}, 300)
}
onUnmounted(() => {
uni.$off(events.ON_SCROLL_BOTTOM)
uni.$off(events.ON_LOAD_MORE)
uni.$off(events.AUDIO_URL_CHANGE)
teamWatch()
})
</script>
<style scoped lang="scss">
.msg-list-wrapper {
flex: 1;
overflow: hidden;
display: flex;
height: 100%;
box-sizing: border-box;
padding: 16px 0;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.msg-tip {
text-align: center;
color: #b3b7bc;
font-size: 12px;
margin-top: 10px;
}
.block {
width: 100%;
height: 40px;
}
.message-scroll-list {
height: 100%;
box-sizing: border-box;
padding-bottom: 1px;
}
.view-more-text {
text-align: center;
color: #b3b7bc;
font-size: 15px;
margin-top: 20px;
}
page > view > message > view > message-list {
height: 100%;
}
</style>

View File

@ -0,0 +1,223 @@
<template>
<div v-if="notificationContent" class="msg-noti">
{{ notificationContent }}
</div>
</template>
<script lang="ts" setup>
/** 通知消息组件 */
import { ALLOW_AT } from '@/utils/im/constants'
import { t } from '@/utils/im/i18n'
import { V2NIMConst } from '@/utils/im/nim'
import { V2NIMTeam } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMTeamService'
import {
V2NIMMessageForUI,
YxServerExt,
} from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMMessageNotificationAttachment } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
import { onUnmounted, ref } from 'vue'
import { autorun } from 'mobx'
const props = withDefaults(defineProps<{ msg: V2NIMMessageForUI }>(), {})
/** 群ID */
const teamId =
props.msg.conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? props.msg.receiverId
: ''
/** 通知消息内容 */
const notificationContent = ref('')
/** 通知消息监听 */
const notificationContentWatch = autorun(() => {
const getNotificationContent = () => {
const attachment = props.msg
.attachment as V2NIMMessageNotificationAttachment
switch (attachment?.type) {
case V2NIMConst.V2NIMMessageNotificationType
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_UPDATE_TINFO: {
const team = (attachment?.updatedTeamInfo || {}) as V2NIMTeam
const content: string[] = []
if (team.avatar !== undefined) {
content.push(t('updateTeamAvatar'))
}
if (team.name !== undefined) {
content.push(`${t('updateTeamName')}${team.name}`)
}
if (team.intro !== undefined) {
content.push(t('updateTeamIntro'))
}
if (team.inviteMode !== undefined) {
content.push(
`${t('updateTeamInviteMode')}${
team.inviteMode ===
V2NIMConst.V2NIMTeamInviteMode.V2NIM_TEAM_INVITE_MODE_ALL
? t('teamAll')
: t('teamOwnerAndManagerText')
}`
)
}
if (team.updateInfoMode !== undefined) {
content.push(
`${t('updateTeamUpdateTeamMode')}${
team.updateInfoMode ===
V2NIMConst.V2NIMTeamUpdateInfoMode.V2NIM_TEAM_UPDATE_INFO_MODE_ALL
? t('teamAll')
: t('teamOwnerAndManagerText')
}`
)
}
if (team.chatBannedMode !== void 0) {
content.push(
`${t('updateTeamMute')}${
team.chatBannedMode ===
V2NIMConst.V2NIMTeamChatBannedMode
.V2NIM_TEAM_CHAT_BANNED_MODE_UNBAN
? t('closeText')
: t('openText')
}`
)
}
if (team.serverExtension) {
let ext: YxServerExt = {}
try {
ext = JSON.parse(team.serverExtension)
} catch (error) {
//
}
if (ext[ALLOW_AT] !== undefined) {
content.push(
`${t('updateAllowAt')}${
ext[ALLOW_AT] === 'manager'
? t('teamOwnerAndManagerText')
: t('teamAll')
}`
)
}
}
return content.length
? `${uni.$UIKitStore.uiStore.getAppellation({
account: props.msg.senderId,
teamId,
})} ${content.join('、')}`
: ''
}
case V2NIMConst.V2NIMMessageNotificationType
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_APPLY_PASS:
case V2NIMConst.V2NIMMessageNotificationType
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE_ACCEPT: {
return `${uni.$UIKitStore.uiStore.getAppellation({
account: props.msg.senderId,
teamId,
})} ${t('joinTeamText')}`
}
case V2NIMConst.V2NIMMessageNotificationType
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE: {
const accounts: string[] = attachment?.targetIds || []
accounts.map(async (item) => {
await uni.$UIKitStore.userStore.getUserActive(item)
})
const nicks = accounts
.map((item) => {
return uni.$UIKitStore.uiStore.getAppellation({
account: item,
teamId,
})
})
.filter((item) => !!item)
.join('、')
return `${nicks} ${t('joinTeamText')}`
}
case V2NIMConst.V2NIMMessageNotificationType
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_KICK: {
const accounts: string[] = attachment?.targetIds || []
accounts.map(async (item) => {
await uni.$UIKitStore.userStore.getUserActive(item)
})
const nicks = accounts
.map((item) => {
return uni.$UIKitStore.uiStore.getAppellation({
account: item,
teamId,
})
})
.filter((item) => !!item)
.join('、')
return `${nicks} ${t('beRemoveTeamText')}`
}
case V2NIMConst.V2NIMMessageNotificationType
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_ADD_MANAGER: {
const accounts: string[] = attachment?.targetIds || []
accounts.map(async (item) => {
await uni.$UIKitStore.userStore.getUserActive(item)
})
const nicks = accounts
.map((item) => {
return uni.$UIKitStore.uiStore.getAppellation({
account: item,
teamId,
})
})
.filter((item) => !!item)
.join('、')
return `${nicks} ${t('beAddTeamManagersText')}`
}
case V2NIMConst.V2NIMMessageNotificationType
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_REMOVE_MANAGER: {
const accounts: string[] = attachment?.targetIds || []
accounts.map(async (item) => {
await uni.$UIKitStore.userStore.getUserActive(item)
})
const nicks = accounts
.map((item) => {
return uni.$UIKitStore.uiStore.getAppellation({
account: item,
teamId,
})
})
.filter((item) => !!item)
.join('、')
return `${nicks} ${t('beRemoveTeamManagersText')}`
}
case V2NIMConst.V2NIMMessageNotificationType
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE: {
return `${uni.$UIKitStore.uiStore.getAppellation({
account: props.msg.senderId,
teamId,
})} ${t('leaveTeamText')}`
}
case V2NIMConst.V2NIMMessageNotificationType
.V2NIM_MESSAGE_NOTIFICATION_TYPE_TEAM_OWNER_TRANSFER: {
return `${uni.$UIKitStore.uiStore.getAppellation({
account: (attachment?.targetIds || [])[0],
teamId,
})} ${t('newGroupOwnerText')}`
}
default:
return ''
}
}
notificationContent.value = getNotificationContent()
})
onUnmounted(() => {
notificationContentWatch()
})
</script>
<style scoped lang="scss">
.msg-noti {
margin: 8px auto 0;
text-align: center;
font-size: 14px;
color: #b3b7bc;
max-width: 70%;
}
</style>

View File

@ -0,0 +1,382 @@
<template>
<div class="pincard-wrapper">
<div class="info-wrapper">
<div class="info" @tap="gotoChat">
<div class="info-left">
<Avatar size="32" :account="props.msg.senderId"></Avatar>
</div>
<div class="info-right">
<div class="name">
<Appellation
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
></Appellation>
</div>
<div class="createtime">
{{ timeFormat() }}
</div>
</div>
</div>
<Icon type="icon-More" @tap="handlePinMsg" />
</div>
<div class="content-wrapper">
<div
class="msg-text"
v-if="
msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
>
<MessageText :msg="props.msg"></MessageText>
</div>
<div class="file-wrapper">
<div
v-if="
msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE
"
>
<div
@tap="
() => {
handleImageTouch(props.msg.attachment.url)
}
"
>
<image
class="msg-image"
:lazy-load="true"
mode="aspectFill"
:src="imageUrl"
></image>
</div>
</div>
<div
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO
"
>
<div
class="video-msg-wrapper"
@tap="() => handleVideoTouch(props.msg)"
>
<div class="video-play-button">
<div class="video-play-icon"></div>
</div>
<image
class="msg-image"
:lazy-load="true"
mode="aspectFill"
:src="videoFirstFrameDataUrl"
></image>
</div>
</div>
<div
class="extra"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE
"
>
<MessageFile :msg="props.msg" />
</div>
<div
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
"
>
<div class="audio-wrapper">
<MessageAudio :msg="props.msg" mode="audio-in" />
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, withDefaults } from 'vue'
import { stopAllAudio } from '@/utils/index'
import Avatar from '@/components/Avatar.vue'
import Icon from '@/components/Icon.vue'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMConst } from '@/utils/im/nim'
import Appellation from '@/components/Appellation.vue'
import dayjs from 'dayjs'
import { customNavigateTo } from '@/utils/im/customNavigate'
import MessageFile from './message-file.vue'
import MessageAudio from './message-audio.vue'
import { t } from '@/utils/im/i18n'
import { customRedirectTo } from '@/utils/im/customNavigate'
import MessageText from './message-text.vue'
const props = withDefaults(
defineProps<{
msg: V2NIMMessageForUI
handleUnPinMsg: any
}>(),
{}
)
/** 会话类型 */
const conversationType =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
props.msg.conversationId
)
/** 会话对象 */
const to = uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
props.msg.conversationId
)
/** 调整至聊天页面 */
const gotoChat = async () => {
await uni.$UIKitStore.uiStore.selectConversation(props.msg.conversationId)
customRedirectTo({
url: '/pages/Chat/index',
})
}
/** 复制 */
const handleCopy = () => {
uni.setClipboardData({
data: props.msg.text || '',
showToast: false,
success: () => {
uni.showToast({
title: t('copySuccessText'),
icon: 'none',
})
},
fail: () => {
uni.showToast({
title: t('copyFailText'),
icon: 'none',
})
},
})
}
// pin
const handlePinMsg = () => {
let itemList = [t('unpinText')]
if (
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
) {
itemList = itemList.concat([
t('copyText'),
t('forwardToTeamText'),
t('forwardToFriendText'),
])
} else if (
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
) {
itemList = itemList.concat([
t('forwardToTeamText'),
t('forwardToFriendText'),
])
}
uni.showActionSheet({
itemList,
success(data) {
if (
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
) {
if (data.tapIndex === 0) {
props.handleUnPinMsg()
} else if (data.tapIndex === 1) {
handleCopy()
} else if (data.tapIndex === 2) {
customNavigateTo({
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM}&msgIdClient=${props.msg.messageClientId}&origin=pin`,
})
} else if (data.tapIndex === 3) {
customNavigateTo({
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P}&msgIdClient=${props.msg.messageClientId}&origin=pin`,
})
}
} else if (
props.msg.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
) {
if (data.tapIndex === 0) {
props.handleUnPinMsg()
} else if (data.tapIndex === 1) {
customNavigateTo({
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM}&msgIdClient=${props.msg.messageClientId}&origin=pin`,
})
} else if (data.tapIndex === 2) {
customNavigateTo({
url: `/pages/Chat/forward?forwardConversationType=${V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P}&msgIdClient=${props.msg.messageClientId}&origin=pin`,
})
}
} else {
if (data.tapIndex === 0) {
props.handleUnPinMsg()
}
}
},
complete() {},
})
}
/** 图片Url */
const imageUrl = computed(() => {
//@ts-ignore
return props.msg?.attachment?.url || props.msg.attachment?.file
})
//
const videoFirstFrameDataUrl = computed(() => {
//@ts-ignore
const url = props.msg.attachment?.url
return url ? `${url}${url.includes('?') ? '&' : '?'}vframe&offset=1` : ''
})
//
const handleImageTouch = (url: string) => {
if (url) {
uni.previewImage({
urls: [url],
})
}
}
//
const handleVideoTouch = (msg: V2NIMMessageForUI) => {
stopAllAudio()
//@ts-ignore
const url = msg.attachment?.url
if (url) {
customNavigateTo({
url: `/pages/Chat/video-play?videoUrl=${encodeURIComponent(url)}`,
})
}
}
/** 时间格式化 */
const isToday = (time: number) => {
const createTime = new Date(time)
const now = new Date()
return (
createTime.getFullYear() === now.getFullYear() &&
createTime.getMonth() === now.getMonth() &&
createTime.getDate() === now.getDate()
)
}
/** 时间格式化 */
const isThisYear = (time: number) => {
const createTime = new Date(time)
const now = new Date()
return createTime.getFullYear() === now.getFullYear()
}
/** 时间格式化 */
const timeFormat = () => {
const createTime = props.msg.createTime
if (isToday(createTime)) {
return dayjs(createTime).format('HH:mm')
} else if (isThisYear(createTime)) {
return dayjs(createTime).format('MM月DD日 HH:mm')
} else {
return dayjs(createTime).format('YYYY年MM月DD日 HH:mm')
}
}
</script>
<style lang="scss" scoped>
.pincard-wrapper {
background-color: #fff;
margin: 0 20px;
margin-top: 12px;
overflow: hidden;
border-radius: 8px;
.info-wrapper {
display: flex;
margin: 0 16px;
padding: 16px 0 12px 0;
border-bottom: 1px solid #e4e9f2;
justify-content: space-between;
align-items: center;
.info {
display: flex;
flex-grow: 1;
.info-left {
margin-right: 8px;
}
.info-right {
font-size: 12px;
.createtime {
color: #999999;
}
}
}
}
.content-wrapper {
padding: 12px 16px 16px 16px;
word-break: break-all;
word-wrap: break-word;
white-space: break-spaces;
.file-wrapper {
position: relative;
width: 50%;
.extra {
width: 200%;
}
.msg-image {
width: 100%;
}
.video-msg-wrapper {
box-sizing: border-box;
max-width: 360rpx;
}
.video-play-button {
width: 50px;
height: 50px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
z-index: 9;
}
.video-play-icon {
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 18px solid #fff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-40%, -50%);
}
.audio-wrapper {
width: fit-content;
opacity: 1;
background: #d6e5f6;
overflow: hidden;
padding: 10px 12px;
border-radius: 0 8px 8px 8px;
background-color: #e8eaed;
}
}
.msg-text {
word-break: break-all;
word-wrap: break-word;
white-space: break-spaces;
}
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<div
v-if="
props.msg.sendingState ==
V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
"
class="msg-read-wrapper"
>
<div
v-if="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
p2pMsgReceiptVisible
"
>
<div v-if="p2pMsgRotateDeg == 360" class="icon-read-wrapper">
<Icon type="icon-read" :size="18"></Icon>
</div>
<div v-else class="sector">
<span
class="cover-1"
:style="`transform: rotate(${p2pMsgRotateDeg}deg)`"
></span>
<span
:class="p2pMsgRotateDeg >= 180 ? 'cover-2 cover-3' : 'cover-2'"
></span>
</div>
</div>
<div
v-if="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM &&
teamManagerVisible
"
>
<div class="icon-read-wrapper" v-if="teamMsgRotateDeg == 360">
<Icon type="icon-read" :size="18"></Icon>
</div>
<div v-else class="sector" @click="jumpToTeamMsgReadInfo">
<span
class="cover-1"
:style="`transform: rotate(${teamMsgRotateDeg}deg)`"
></span>
<span
:class="teamMsgRotateDeg >= 180 ? 'cover-2 cover-3' : 'cover-2'"
></span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
/** 消息已读未读组件 */
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import Icon from '@/components/Icon.vue'
import { V2NIMConst } from '@/utils/im/nim'
import { customNavigateTo } from '@/utils/im/customNavigate'
import { t } from '@/utils/im/i18n'
import { autorun } from 'mobx'
const props = withDefaults(
defineProps<{
msg: V2NIMMessageForUI
}>(),
{}
)
/** 是否需要显示群组消息已读未读,默认 false */
const teamManagerVisible = uni.$UIKitStore.localOptions.teamMsgReceiptVisible
/** 是否需要显示 p2p 消息、p2p会话列表消息已读未读默认 false */
const p2pMsgReceiptVisible = uni.$UIKitStore.localOptions.p2pMsgReceiptVisible
/** 会话类型 */
const conversationType =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
props.msg.conversationId
) as unknown as V2NIMConst.V2NIMConversationType
/** 单聊消息已读未读用于UI变更 */
const p2pMsgRotateDeg = ref(0)
/**是否是云端会话 */
const enableV2CloudConversation =
uni.$UIKitStore?.sdkOptions?.enableV2CloudConversation
/** 设置单聊消息已读未读 */
const setP2pMsgRotateDeg = () => {
/**如果是单聊 */
if (
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
const conversation = enableV2CloudConversation
? uni.$UIKitStore.conversationStore?.conversations.get(
props.msg.conversationId
)
: uni.$UIKitStore.localConversationStore?.conversations.get(
props.msg.conversationId
)
p2pMsgRotateDeg.value =
props?.msg?.createTime <= (conversation?.msgReceiptTime || 0) ? 360 : 0
}
}
/** 监听单聊消息已读未读 */
const p2pMsgReadWatch = autorun(() => {
setP2pMsgRotateDeg()
})
/** 跳转到已读未读详情 */
const jumpToTeamMsgReadInfo = () => {
if (
uni.$UIKitStore.connectStore.connectStatus !==
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
) {
uni.showToast({
title: t('offlineText'),
icon: 'none',
})
return
}
//
if (props?.msg?.messageClientId && props?.msg?.conversationId) {
customNavigateTo({
url: `/pages/Chat/message-read-info?messageClientId=${props.msg.messageClientId}&conversationId=${props.msg.conversationId}`,
})
}
}
/** 群消息已读未读用于UI变更 */
const teamMsgRotateDeg = computed(() => {
if (
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
const percentage =
(props?.msg?.yxRead || 0) /
((props?.msg?.yxUnread || 0) + (props?.msg?.yxRead || 0)) || 0
return percentage * 360
}
return 0
})
onMounted(() => {
setP2pMsgRotateDeg()
})
onUnmounted(() => {
p2pMsgReadWatch()
})
</script>
<style scoped lang="scss">
.msg-read-wrapper {
align-self: flex-end;
display: none;
}
.icon-read-wrapper {
margin: 0px 10px 5px 0;
}
.sector {
display: inline-block;
position: relative;
overflow: hidden;
border: 2px solid #4c84ff;
width: 14px;
height: 14px;
background-color: #eeeeee;
border-radius: 50%;
margin: 0px 10px 0 0;
.cover-1,
.cover-2 {
position: absolute;
top: 0;
width: 50%;
height: 100%;
background-color: #eeeeee;
}
.cover-1 {
background-color: #4c84ff;
transform-origin: right;
}
.cover-3 {
right: 0;
background-color: #4c84ff;
}
}
</style>

View File

@ -0,0 +1,334 @@
<template>
<div>
<div v-if="props.replyMsg?.messageClientId" class="reply-msg-wrapper">
<!-- replyMsg 不存在 说明回复的消息被删除或者撤回 -->
<div v-if="!isReplyMsgExist">
<span>{{ t('replyNotFindText') }}</span>
</div>
<div v-else class="reply-msg" @tap="showFullReplyMsg">
<div class="reply-msg-name-wrapper">
<div class="reply-msg-name-line">|</div>
<div class="reply-msg-name-content">
<Appellation
:account="props.replyMsg?.senderId"
:teamId="props.replyMsg?.receiverId"
color="#929299"
:fontSize="13"
></Appellation>
</div>
<div class="reply-msg-name-to">:</div>
</div>
<message-one-line
v-if="
props.replyMsg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
:text="props.replyMsg.text"
></message-one-line>
<div
v-else-if="
props.replyMsg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE
"
class="other-msg-wrapper"
>
<uni-link
v-if="!isHarmonyOs"
:href="downloadUrl"
:download="name"
:showUnderLine="false"
>
{{ t('fileMsgTitleText') }}
</uni-link>
<span
v-else
class="other-msg-wrapper"
@click="() => openInBrowser(downloadUrl)"
>
{{ t('fileMsgTitleText') }}
</span>
</div>
<div class="other-msg-wrapper" v-else>
{{ '[' + REPLY_MSG_TYPE_MAP[props.replyMsg.messageType] + ']' }}
</div>
</div>
</div>
<!-- 点击被回复的消息需要全屏显示 -->
<div v-if="isFullScreen" class="reply-full-screen" @tap="closeFullReplyMsg">
<!-- #ifdef MP -->
<div class="reply-message-close-mp" @tap="closeFullReplyMsg">
<Icon
color="#929299"
:iconStyle="{ fontWeight: '200' }"
:size="18"
type="icon-guanbi"
/>
</div>
<!-- #endif -->
<!-- #ifndef MP -->
<div class="reply-message-close" @tap="closeFullReplyMsg">
<Icon
color="#929299"
:iconStyle="{ fontWeight: '200' }"
:size="18"
type="icon-guanbi"
/>
</div>
<!-- #endif -->
<div
v-if="
props.replyMsg?.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
class="reply-message-content"
>
<message-text :msg="replyMsg" :fontSize="22"></message-text>
</div>
<div
v-else-if="
props.replyMsg?.messageType ==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
"
class="msg-common"
:style="{
flexDirection: props.replyMsg?.isSelf ? 'row-reverse' : 'row',
backgroundColor: props.replyMsg?.isSelf ? '#d6e5f6' : '#e8eaed',
borderRadius: props.replyMsg?.isSelf
? '8px 0px 8px 8px'
: '0 8px 8px',
}"
@click.stop="() => {}"
>
<MessageAudio :msg="replyMsg" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
/** 回复消息组件 */
import { t } from '@/utils/im/i18n'
import MessageOneLine from '@/components/MessageOneLine.vue'
import { ref, onMounted, computed, onUnmounted } from 'vue'
import MessageText from './message-text.vue'
// @ts-ignore
import UniLink from '@/components/uni-components/uni-link/components/uni-link/uni-link.vue'
import { REPLY_MSG_TYPE_MAP } from '@/utils/im/constants'
import { events } from '@/utils/im/constants'
import { isHarmonyOs, stopAllAudio } from '@/utils/im/index'
import { autorun } from 'mobx'
import { customNavigateTo } from '@/utils/im/customNavigate'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMConst } from '@/utils/im/nim'
import MessageAudio from './message-audio.vue'
import Icon from '@/components/Icon.vue'
import Appellation from '@/components/Appellation.vue'
const props = withDefaults(
defineProps<{ replyMsg: V2NIMMessageForUI | undefined }>(),
{}
)
/**是否全屏展示 */
const isFullScreen = ref(false)
/** 回复对象 */
const repliedTo = ref('')
//@ts-ignore
const { name = '', url = '' } = props.replyMsg?.attachment || {}
/**下载地址 */
const downloadUrl = computed(() => {
//@ts-ignore
const { name = '', url = '' } = props.replyMsg?.attachment || {}
return url + ((url as string).includes('?') ? '&' : '?') + `download=${name}`
})
/**被回复消息是否存在 */
const isReplyMsgExist = computed(() => {
return props.replyMsg?.messageClientId !== 'noFind'
})
/**回复消息昵称 */
const repliedToWatch = autorun(() => {
repliedTo.value = uni.$UIKitStore.uiStore.getAppellation({
account: props.replyMsg?.senderId as string,
teamId: props.replyMsg?.receiverId,
})
})
/**全屏展示回复消息 */
const showFullReplyMsg = () => {
if (
props.replyMsg?.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE
) {
uni.previewImage({
//@ts-ignore
urls: [props.replyMsg?.attachment?.url as string],
})
} else if (
props.replyMsg?.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
) {
isFullScreen.value = true
uni.$emit(events.HANDLE_MOVE_THROUGH, true)
} else if (
props.replyMsg?.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO
) {
//@ts-ignore
const url = props.replyMsg?.attachment?.url
stopAllAudio()
if (url) {
customNavigateTo({
url: `/pages/Chat/video-play?videoUrl=${encodeURIComponent(url)}`,
})
}
} else if (
props.replyMsg?.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
) {
isFullScreen.value = true
}
}
/**点击全屏的回复消息,关闭全屏 */
const closeFullReplyMsg = () => {
isFullScreen.value = false
stopAllAudio()
uni.$emit(events.HANDLE_MOVE_THROUGH, false)
}
/**复制下载链接 */
const openInBrowser = (url: string) => {
uni.setClipboardData({
data: url,
showToast: false,
success: () => {
uni.showToast({
title: t('openUrlText'),
icon: 'none',
})
},
})
}
onMounted(() => {
repliedTo.value = uni.$UIKitStore.uiStore.getAppellation({
account: props.replyMsg?.senderId as string,
teamId: props.replyMsg?.receiverId,
})
})
onUnmounted(() => {
repliedToWatch()
})
</script>
<style lang="scss" scoped>
.reply-msg-wrapper {
display: flex;
align-items: center;
color: #929299;
font-size: 13px;
white-space: nowrap;
.reply-msg {
display: flex;
align-items: center;
width: 100%;
message-one-line {
flex: 1;
font-size: 13px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.reply-msg-name-wrapper {
margin-right: 5px;
max-width: 125px;
flex: 0 0 auto;
display: flex;
white-space: nowrap;
}
.reply-msg-name-line {
flex-basis: 0 0 3px;
margin-right: 2px;
}
.reply-msg-name-to {
flex-basis: 0 0 3px;
}
.reply-msg-name-content {
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.reply-msg-content {
// display: flex;
// align-items: center;
flex: 1;
}
}
.reply-full-screen {
position: fixed;
right: 0;
bottom: 0;
top: 0;
left: 0;
height: 100vh;
overflow: hidden;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
font-size: 24px;
justify-content: center;
touch-action: none;
z-index: 999999999;
box-sizing: border-box;
}
.reply-message-content {
height: 85vh;
overflow-y: auto;
box-sizing: border-box;
padding: 30px 30px 100px 30px;
touch-action: none;
display: flex;
align-items: center;
}
.other-msg-wrapper {
flex-wrap: nowrap;
}
.reply-message-close {
position: fixed;
right: 20px;
z-index: 999999;
top: 60px;
}
.reply-message-close-mp {
position: fixed;
right: 20px;
top: 100px;
z-index: 999999;
}
.msg-common {
display: flex;
align-items: flex-start;
font-size: 16px;
max-width: 360rpx;
overflow: hidden;
padding: 16px 20px;
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div v-for="(item, index) in sliceMsgs">
<text>{{ start + index }}</text>
<messageItem
:id="MSG_ID_FLAG + item.idClient"
:scene="scene"
:to="to"
:msg="item"
:key="item.idClient"
:msg-index="start + index"
>
</messageItem>
</div>
</template>
<script lang="ts" setup>
import messageItem from './message-item.vue'
import { computed } from '../../../utils/transformVue'
import { MSG_ID_FLAG } from '../../../utils/constants'
import { caculateTimeago } from '../../../utils/date'
import { V2NIMMessage } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMMessageService'
import { V2NIMConst } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/index'
const props = defineProps({
msgs: {
type: Array,
required: true,
},
scene: {
type: Object, // Assuming TMsgScene is a custom object type
required: true,
},
to: {
type: String,
required: true,
},
start: {
type: Number,
required: true,
},
end: {
type: Number,
required: true,
},
})
const sliceMsgs = computed(() => {
const res: V2NIMMessage[] = []
const msgs = props.msgs as V2NIMMessage[]
const _slice = msgs.slice(props.start, props.end)
_slice.forEach((item, index) => {
const msgIndex = props.start + index
// 5
if (
msgIndex > 0 &&
item.createTime - msgs[msgIndex - 1].createTime > 5 * 60 * 1000
) {
res.push({
messageClientId: 'time-' + item.createTime,
messageType: V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM,
attachment: {
type: 'time',
value: caculateTimeago(item.createTime),
},
sendingState:
V2NIMConst.V2NIMMessageSendingState
.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED,
})
}
res.push(item)
})
return res.filter((item) => item.type !== 'notification')
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,86 @@
<template>
<div class="msg-text" :style="{ fontSize: (fontSize || 16) + 'px' }">
<template v-for="item in textArr" :key="item.key">
<template v-if="item.type === 'text'">
<span class="msg-text">{{ item.value }}</span>
</template>
<template v-else-if="item.type === 'Ait'">
<text class="msg-text" :style="{ color: '#1861df' }">
{{ ' ' + item.value + ' ' }}
</text>
</template>
<template v-else-if="item.type === 'emoji'">
<Icon
:type="EMOJI_ICON_MAP_CONFIG[item.value]"
:size="fontSize || 22"
:style="{ margin: '0 2px 2px 2px', verticalAlign: 'bottom' }"
/>
</template>
<template v-else-if="item.type === 'link'">
<UniLink
v-if="!isHarmonyOs"
:href="item.value"
:style="{ color: '#1861df', fontSize: (fontSize || 16) + 'px' }"
:showUnderLine="false"
>
{{ item.value }}
</UniLink>
<span
v-else
:style="{ color: '#1861df', fontSize: (fontSize || 16) + 'px' }"
@click="() => openInBrowser(item.value)"
>
{{ item.value }}
</span>
</template>
</template>
</div>
</template>
<script lang="ts" setup>
/**文本消息组件 */
import Icon from '@/components/Icon.vue'
// @ts-ignore
import UniLink from '@/components/uni-components/uni-link/components/uni-link/uni-link.vue'
import { parseText } from '@/utils/im/parseText'
import { EMOJI_ICON_MAP_CONFIG } from '@/utils/im/emoji'
import { V2NIMMessageForUI } from '@xkit-yx/im-store-v2/dist/types/types'
import { t } from '@/utils/im/i18n'
import { isHarmonyOs } from '@/utils/im/index'
const props = withDefaults(
defineProps<{
msg: V2NIMMessageForUI
fontSize?: number
}>(),
{}
)
/**解析文本 */
const textArr = parseText(props.msg?.text || '', props.msg?.serverExtension)
/**unilink 不支持鸿蒙 故提示在浏览器打开链接 */
const openInBrowser = (url: string) => {
uni.setClipboardData({
data: url,
showToast: false,
success: () => {
uni.showToast({
title: t('openUrlText'),
icon: 'none',
})
},
})
}
</script>
<style lang="scss" scoped>
.msg-text {
color: #333;
text-align: left;
overflow-y: auto;
word-break: break-all;
word-wrap: break-word;
white-space: break-spaces;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<!-- 样式兼容微信小程序 -->
<div>
<div
class="nav-bar-wrapper"
:style="{
backgroundColor: backgroundColor || '#ffffff',
backgroundImage: `url(${title})`,
height: isWxApp ? '55px' : '40px',
alignItems: isWxApp ? 'flex-end' : 'center',
}"
>
<slot v-if="showLeft" name="left"></slot>
<div v-else @tap="back">
<Icon type="icon-zuojiantou" :size="22"></Icon>
</div>
<div class="title-container">
<div class="title">{{ title }}</div>
<div class="subTitle" v-if="subTitle">{{ subTitle }}</div>
<slot name="icon"></slot>
</div>
<div>
<slot name="right"></slot>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { isWxApp } from '@/utils/im/index'
import Icon from '@/components/Icon.vue'
withDefaults(
defineProps<{
title: string
subTitle?: string
backgroundColor?: string
showLeft?: boolean
}>(),
{
subTitle: '',
backgroundColor: '',
showLeft: true,
}
)
const back = () => {
uni.navigateBack({
delta: 1,
})
}
</script>
<style lang="scss" scoped>
@import '@/styles/common.scss';
.nav-bar-wrapper {
display: flex;
justify-content: space-between;
padding: var(--status-bar-height) 10px 5px 10px;
z-index: 9999;
.title-container {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 300px;
display: flex;
justify-content: center;
align-items: center;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
white-space: nowrap;
font-weight: 500;
max-width: 230px;
}
.subTitle {
white-space: nowrap;
font-weight: 500;
}
}
</style>

View File

@ -0,0 +1,279 @@
<template>
<div>
<NavBar :title="t('setText')" />
<div class="p2p-set-container">
<div class="p2p-set-card">
<div class="p2p-set-item">
<div class="p2p-set-my-info">
<Avatar :account="account" />
<div class="p2p-set-my-nick">{{ myNick }}</div>
</div>
<div class="member-add" @tap="addTeamMember">
<Icon type="icon-tianjiaanniu" />
</div>
</div>
</div>
<div class="p2p-set-card">
<div class="p2p-set-item p2p-set-item-flex-sb" @tap="goPinInP2p">
<div>{{ t('pinText') }}</div>
<Icon iconClassName="more-icon" color="#999" type="icon-jiantou" />
</div>
<div class="p2p-set-item p2p-set-item-flex-sb">
<div>{{ t('sessionMuteText') }}</div>
<switch :checked="!isMute" @change="changeSessionMute" />
</div>
<div class="p2p-set-item p2p-set-item-flex-sb">
<div>{{ t('stickTopText') }}</div>
<switch :checked="isStickTop" @change="changeStickTopInfo" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
/**单聊设置组件 */
import NavBar from '@/components/NavBar.vue'
import Avatar from '@/components/Avatar.vue'
import Icon from '@/components/Icon.vue'
import { onLoad } from '@dcloudio/uni-app'
import { onUnmounted, ref } from 'vue'
import { autorun } from 'mobx'
import { t } from '@/utils/im/i18n'
import { customNavigateTo } from '@/utils/im/customNavigate'
import { V2NIMConst } from '@/utils/im/nim'
const myNick = ref('')
const conversation = ref()
const isMute = ref(false)
const isStickTop = ref(false)
const account = ref('')
const conversationId = ref('')
let p2pSetWatch: () => void
/**是否是云端会话 */
const enableV2CloudConversation =
uni.$UIKitStore?.sdkOptions?.enableV2CloudConversation
onLoad((option) => {
const _account = option?.id
account.value = _account
const _conversationId =
uni.$UIKitNIM.V2NIMConversationIdUtil.p2pConversationId(_account)
conversationId.value = _conversationId
p2pSetWatch = autorun(() => {
conversation.value = enableV2CloudConversation
? uni.$UIKitStore.conversationStore?.conversations.get(_conversationId)
: uni.$UIKitStore.localConversationStore?.conversations.get(
_conversationId
)
myNick.value = uni.$UIKitStore.uiStore.getAppellation({ account: _account })
isMute.value = uni.$UIKitStore.relationStore.mutes.includes(_account)
isStickTop.value = !!conversation.value?.stickTop
})
})
/**添加群成员 */
const addTeamMember = () => {
const to = uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
conversationId.value
)
customNavigateTo({
url: `/pages/Team/team-create/index?p2pConversationId=${to}`,
})
}
/**跳转至pin页面 */
const goPinInP2p = () => {
customNavigateTo({
url: `/pages/Chat/message/pin-list?conversationId=${conversationId.value}`,
})
}
/**修改会话免打扰 */
const changeSessionMute = async (e: any) => {
const checked = !e.detail.value
try {
await uni.$UIKitStore.relationStore.setP2PMessageMuteModeActive(
account.value,
checked
? V2NIMConst.V2NIMP2PMessageMuteMode.V2NIM_P2P_MESSAGE_MUTE_MODE_ON
: V2NIMConst.V2NIMP2PMessageMuteMode.V2NIM_P2P_MESSAGE_MUTE_MODE_OFF
)
} catch (error) {
uni.showToast({
title: checked ? t('sessionMuteFailText') : t('sessionUnMuteFailText'),
icon: 'error',
})
}
}
/**修改置顶 */
const changeStickTopInfo = async (e: any) => {
const checked = e.detail.value
try {
if (enableV2CloudConversation) {
await uni.$UIKitStore.conversationStore?.stickTopConversationActive(
conversationId.value,
checked
)
} else {
await uni.$UIKitStore.localConversationStore?.stickTopConversationActive(
conversationId.value,
checked
)
}
} catch (error) {
uni.showToast({
title: checked ? t('addStickTopFailText') : t('deleteStickTopFailText'),
icon: 'error',
})
}
}
onUnmounted(() => {
p2pSetWatch()
})
</script>
<style lang="scss" scoped>
@import '../../styles/common.scss';
page {
padding-top: var(--status-bar-height);
height: 100vh;
overflow: hidden;
}
.p2p-set-container {
height: 100vh;
box-sizing: border-box;
background-color: #eff1f4;
padding: 10px 20px;
}
.p2p-set-card {
background: #ffffff;
border-radius: 8px;
padding-left: 16px;
margin-bottom: 10px;
}
.p2p-set-button {
text-align: center;
background: #ffffff;
border-radius: 8px;
color: #e6605c;
height: 40px;
line-height: 40px;
}
.p2p-set-item {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 10px 0;
&:not(:last-child) {
border-bottom: 1rpx solid #f5f8fc;
}
}
.p2p-set-item-flex-sb {
justify-content: space-between;
}
.p2p-set-my-info {
margin-right: 12px;
display: flex;
flex-direction: column;
align-items: center;
}
.p2p-set-my-nick {
margin-top: 5px;
color: #333;
font-size: 12px;
max-width: 70px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.more-icon {
margin: 0 16px;
color: #999999;
}
.p2p-info-item {
height: 70px;
display: flex;
align-items: center;
.p2p-info-title {
font-size: 16px;
margin-left: 10px;
width: 0;
flex: 1;
overflow: hidden; //
text-overflow: ellipsis; //
white-space: nowrap; //
}
}
.p2p-members-item {
height: 90px;
}
.p2p-members-info-item {
display: flex;
align-items: center;
}
.p2p-members-info {
height: 40px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
.p2p-info-subtitle {
color: #999999;
}
}
.member-list {
white-space: nowrap;
overflow-x: hidden;
margin-right: 30px;
height: 50px;
display: flex;
align-items: center;
}
.member-add {
width: 40px;
height: 40px;
border-radius: 100%;
border: 1px dashed #999999;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.member-item {
margin-right: 10px;
display: inline-block;
flex-shrink: 0;
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<div :class="`wrapper ${pinInfos.length === 0 ? 'bg-white' : ''}`">
<NavBar :title="t('pinText')">
<template v-slot:left>
<div class="nav-bar-text" @tap="back">{{ t('pinText') }}</div>
</template>
</NavBar>
<div class="pinCard-item-wrapper">
<Empty v-if="pinInfos.length === 0" :text="t('noPinListText')" />
<div
v-else
v-for="(item, index) in pinInfos"
:key="item.message.messageClientId"
>
<PinCard
:msg="item.message"
:index="index"
:key="item.message.messageClientId"
:handleUnPinMsg="handleUnPinMsg(item.message)"
>
</PinCard>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app'
import { autorun } from 'mobx'
import { onUnmounted, ref } from '@/utils/im/transformVue'
import Icon from '@/components/Icon.vue'
import NavBar from '@/components/NavBar.vue'
import PinCard from '@/components/PinCard.vue'
import { t } from '@/utils/im/i18n'
import { getUniPlatform } from '@/utils/index'
import { deepClone } from '@/utils/index'
import { V2NIMTeamMember } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMTeamService'
import Empty from '@/components/Empty.vue'
const inputValue = ref('')
const showClearIcon = ref(false)
const myMemberInfo = ref<V2NIMTeamMember>()
let teamId = ''
let conversationId = ''
let pinInfos = ref([])
let uninstallTeamMemberWatch = () => {}
onLoad((option) => {
conversationId = option?.conversationId
getPinnedMessageList()
})
const getPinnedMessageList = () => {
uni.$UIKitStore.msgStore
.getPinnedMessageListActive(conversationId)
.then((data) => {
pinInfos.value = data
})
}
const handleUnPinMsg = (msg) => {
return () => {
// catch store pin
return uni.$UIKitStore.msgStore
.unpinMessageActive(msg)
.then(() => {
return getPinnedMessageList()
})
.catch(() => {
uni.showToast({
title: t('unpinFailedText'),
icon: 'none',
duration: 1000,
})
})
}
}
onUnmounted(() => {
uninstallTeamMemberWatch()
})
</script>
<style lang="scss" scoped>
@import '../../styles/common.scss';
page {
padding-top: var(--status-bar-height);
height: 100vh;
background-color: rgb(245, 246, 247);
}
.wrapper {
width: 100%;
height: 100vh;
box-sizing: border-box;
background-color: rgb(245, 246, 247);
.nav-bar-text {
color: rgb(20, 146, 209);
}
}
.bg-white {
background: #fff;
}
</style>

Some files were not shown because too many files have changed in this diff Show More