Compare commits

..

No commits in common. "main" and "wyfDev-0313-mention" have entirely different histories.

104 changed files with 6815 additions and 13492 deletions

2
auto-imports.d.ts vendored
View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

8
components.d.ts vendored
View File

@ -8,8 +8,6 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AsyncError: typeof import('./src/components/async-error/index.vue')['default']
AsyncLoading: typeof import('./src/components/async-loading/index.vue')['default']
AudioMessage: typeof import('./src/components/talk/message/AudioMessage.vue')['default']
Avatar: typeof import('./src/components/base/Avatar.vue')['default']
AvatarCropper: typeof import('./src/components/base/AvatarCropper.vue')['default']
@ -32,23 +30,17 @@ declare module 'vue' {
LoginMessage: typeof import('./src/components/talk/message/LoginMessage.vue')['default']
Message: typeof import('./src/components/x-message/message/index.vue')['default']
MixedMessage: typeof import('./src/components/talk/message/MixedMessage.vue')['default']
NButton: typeof import('naive-ui')['NButton']
NIcon: typeof import('naive-ui')['NIcon']
PageAnimation: typeof import('./src/components/page-animation/index.vue')['default']
RevokeMessage: typeof import('./src/components/talk/message/RevokeMessage.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SysGroupAdminMessage: typeof import('./src/components/talk/message/system/SysGroupAdminMessage.vue')['default']
SysGroupCancelMutedMessage: typeof import('./src/components/talk/message/system/SysGroupCancelMutedMessage.vue')['default']
SysGroupCreateMessage: typeof import('./src/components/talk/message/system/SysGroupCreateMessage.vue')['default']
SysGroupDismissed: typeof import('./src/components/talk/message/system/SysGroupDismissed.vue')['default']
SysGroupInfoChangeMessage: typeof import('./src/components/talk/message/system/SysGroupInfoChangeMessage.vue')['default']
SysGroupJoinMessage: typeof import('./src/components/talk/message/system/SysGroupJoinMessage.vue')['default']
SysGroupMemberCancelMutedMessage: typeof import('./src/components/talk/message/system/SysGroupMemberCancelMutedMessage.vue')['default']
SysGroupMemberKickedMessage: typeof import('./src/components/talk/message/system/SysGroupMemberKickedMessage.vue')['default']
SysGroupMemberMutedMessage: typeof import('./src/components/talk/message/system/SysGroupMemberMutedMessage.vue')['default']
SysGroupMemberQuitMessage: typeof import('./src/components/talk/message/system/SysGroupMemberQuitMessage.vue')['default']
SysGroupMemberRemovedMessage: typeof import('./src/components/talk/message/system/SysGroupMemberRemovedMessage.vue')['default']
SysGroupMutedMessage: typeof import('./src/components/talk/message/system/SysGroupMutedMessage.vue')['default']
SysGroupTransferMessage: typeof import('./src/components/talk/message/system/SysGroupTransferMessage.vue')['default']
SysTextMessage: typeof import('./src/components/talk/message/system/SysTextMessage.vue')['default']

15
env/.env.prod vendored
View File

@ -4,18 +4,5 @@ NODE_ENV = 'prod'
VITE_SHOW_CONSOLE = false
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = false
# baseUrl
VITE_BASEURL = 'https://chat-out.szjixun.cn' #体制外
#VITE_SOCKET_API
VITE_SOCKET_API = 'wss://chat-out.szjixun.cn' #体制外
# EPRAPI baseUrl
VITE_EPR_BASEURL = 'https://erpapi-out.szjixun.cn' #体制外
# # baseUrl
# VITE_BASEURL = 'https://chat.szjixun.cn' #体制内
# #VITE_SOCKET_API
# VITE_SOCKET_API = 'wss://chat.szjixun.cn' #体制内
# # EPRAPI baseUrl
# VITE_EPR_BASEURL = 'https://erpapi.fontree.cn' #体制内
VITE_BASEURL = 'https://oa-a.szjixun.cn/api'

11
env/.env.test vendored
View File

@ -4,16 +4,9 @@ NODE_ENV = 'test'
VITE_SHOW_CONSOLE = true
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = true
# # baseUrl
VITE_BASEURL = 'http://172.16.100.93:8503'
# #VITE_SOCKET_API
VITE_SOCKET_API = 'ws://172.16.100.93:8504'
# baseUrl
# VITE_BASEURL = 'http://192.168.88.21:9503'
VITE_BASEURL = 'http://172.16.100.93:8503'
#VITE_SOCKET_API
# VITE_SOCKET_API = 'ws://192.168.88.21:9504'
VITE_SOCKET_API = 'ws://172.16.100.93:8504'
# EPRAPI baseUrl
VITE_EPR_BASEURL = 'http://114.218.158.24:9020'

View File

@ -39,9 +39,7 @@
"lodash": "^4.17.21",
"nzh": "^1.0.13",
"pinia-plugin-persistedstate": "^4.1.3",
"quill": "^1.3.7",
"quill-mention": "^4.1.0",
"recorder-core": "^1.3.25011100",
"quill-mention": "^6.0.2",
"vconsole": "^3.15.1",
"vue": "^3.3.8",
"vue-i18n": "11.0.0-rc.1"
@ -65,7 +63,7 @@
"lint-staged": "^15.2.0",
"naive-ui": "^2.41.0",
"pinia": "2.0.36",
"sass": "1.62.1",
"sass": "^1.77.8",
"simple-git-hooks": "^2.9.0",
"typescript": "^5.3.3",
"unocss": "^0.58.9",

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,28 @@
<script setup>
import { useStatus } from '@/store/status'
import { useUserStore, useDialogueListStore } from '@/store'
import { useUserStore } from '@/store'
import { useProvideUserModal } from '@/hooks'
import {useAuth} from "@/store/auth";
const {token} = useAuth()
import ws from '@/connect'
import {uniStorage} from "@/utils/uniStorage.js"
const { statusBarHeight } = useStatus()
const { uid, isShow } = useProvideUserModal()
const userStore = useUserStore()
const root = document.documentElement
root.style.setProperty('--statusBarHeight', `${statusBarHeight.value}px`)
const handleWebview = () => {
let statusBarHeight_ = window?.plus?.navigator?.getStatusbarHeight()
let statusBarHeight = window?.plus?.navigator?.getStatusbarHeight()
const webview = plus.webview.currentWebview()
// webview.setStyle({
// top: statusBarHeight_,
// bottom: 0,
// })
console.log("webview", webview)
webview.setStyle({
top: statusBarHeight,
bottom: 0,
})
// console.log(webview)
token.value = webview.token
if(webview?.doClearDialogueList){
useDialogueListStore().dialogueList.value = []
uniStorage.removeItem('dialogueList')
}
userStore.loadSetting()
ws.connect()
}
const init = () => {
userStore.loadSetting()
ws.connect()
if (typeof plus !== 'undefined') {
handleWebview()
} else {

View File

@ -1,6 +1,5 @@
import request from '@/service/index.js'
import qs from 'qs'
import { useTalkStore, useDialogueStore } from '@/store'
// 获取聊天列表服务接口
export const ServeGetTalkList = (data) => {
@ -39,31 +38,7 @@ export const ServeTopTalkList = (data) => {
}
// 清除聊天消息未读数服务接口
export const ServeClearTalkUnreadNum = (data, unReadNum) => {
console.log('=======chatApp==UnreadNum', unReadNum)
if (
!useTalkStore().items[
useTalkStore().findTalkIndex(useDialogueStore().index_name)
]?.is_disturb
) {
if (typeof plus !== 'undefined') {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`updateUnreadMsgNumReduce('${unReadNum}')`)
}
})
} else {
document.addEventListener('plusready', () => {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`updateUnreadMsgNumReduce('${unReadNum}')`)
}
})
})
}
}
export const ServeClearTalkUnreadNum = (data) => {
return request({
url: '/api/v1/talk/unread/clear',
method: 'POST',
@ -194,49 +169,13 @@ export const ServeConfirmVoteHandle = (data) => {
})
}
export const uploadImg = (data, onProgressFn) => {
export const uploadImg = (data,onProgressFn) => {
return request({
url: '/upload/img',
method: 'POST',
data: data,
baseURL: import.meta.env.VITE_EPR_BASEURL,
isFormData: true,
onUploadProgress: (progressEvent) =>
onProgressFn(progressEvent, data.get('file')),
})
}
// 根据msg_id获取消息
export const detailGetRecordsContext = (data) => {
return request({
url: '/api/v1/talk/record/detail',
method: 'GET',
data,
})
}
// 获取自己消息已读回执列表
export const ServeReadConditionList = (data) => {
return request({
url: '/api/v1/talk/my-records/read/condition',
method: 'POST',
data,
})
}
// 获取消息已读未读详情
export const ServeMessageReadDetail = (data) => {
return request({
url: '/api/v1/talk/my-records/read/condition',
method: 'POST',
data,
})
}
// 语音转文字
export const ServeConvertText = (data) => {
return request({
url: '/api/v1/talk/message/voice-to-text',
method: 'POST',
data,
data:data,
baseURL:import.meta.env.VITE_EPR_BASEURL,
isFormData:true,
onUploadProgress:(progressEvent)=>onProgressFn(progressEvent,data.get('file'))
})
}

View File

@ -26,22 +26,6 @@ export const departmentV2TreeAll = (data) => {
data,
})
}
// 通讯录过滤测试部门
export const departmentV2TreeAll2 = (data) => {
return request({
url: '/api/v1/contact/department/v2/tree/all',
method: 'POST',
data,
})
}
// 查询是否有权限
export const userHasPermission = (data) => {
return request({
url: '/api/v1/contact/check/erp/rule',
method: 'POST',
data,
})
}
//获取指定部门下的所有岗位
export const v2TreePositionByDepartment = (data) => {
@ -61,14 +45,6 @@ export const userV2List = (data) => {
data,
})
}
// 过滤测试数据的erp用户接口
export const userV2List2 = (data) => {
return request({
url: '/api/v1/group/erp/users',
method: 'POST',
data,
})
}
export const groupCreateDept = (data) => {
return request({

View File

@ -45,12 +45,3 @@ export const ServeTalkDate = (data) => {
data,
})
}
//获取会话Id
export const ServeGetSessionId = (data) => {
return request({
url: '/api/v1/talk/session/getId',
method: 'POST',
data,
})
}

View File

@ -10,9 +10,10 @@ export const ServeGetUserSetting = (data) => {
export const userInfoApi = (data) => {
return request({
url: '/api/v1/users/erp/info',
url: '/user/info',
method: 'POST',
data,
baseURL:import.meta.env.VITE_EPR_BASEURL,
})
}

View File

@ -1,89 +0,0 @@
<template>
<div class="chat-app-error-page">
<div class="error-container">
<div class="error-icon">
<i class="iconfont icon-wifi"></i>
</div>
<div class="error-message">
<span>您的网络好像波动了一下~</span>
</div>
<div class="reload-button" @click="handleReload">
<span>重新加载</span>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, computed, nextTick } from 'vue'
const handleReload = () => {
location.reload(true);
}
onMounted(() => {
if (typeof plus !== 'undefined') {
} else {
document.addEventListener('plusready', () => {
})
}
})
</script>
<style scoped lang="scss">
.chat-app-error-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-image: url('@/static/image/clockIn/z3280@3x.png');
background-size: cover;
background-position: bottom center;
.error-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 32px;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
max-width: 80%;
width: 320px;
.error-icon {
font-size: 48px;
color: #ff6b6b;
margin-bottom: 16px;
}
.error-message {
font-size: 16px;
color: #333333;
margin-bottom: 24px;
text-align: center;
}
.reload-button {
padding: 12px 32px;
background: $theme-primary;
color: #ffffff;
border-radius: 24px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
opacity: 0.9;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
}
}
</style>

View File

@ -1,81 +0,0 @@
<script setup>
import { computed } from 'vue';
</script>
<template>
<div class="loader">
<p class="heading">加载中</p>
<div class="loading">
<div class="load"></div>
<div class="load"></div>
<div class="load"></div>
<div class="load"></div>
</div>
</div>
</template>
<style scoped lang="scss">
.loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-image: url('@/static/image/clockIn/z3280@3x.png');
background-size: cover;
background-position: bottom center;
}
.heading {
color: black;
letter-spacing: 0.2em;
margin-bottom: 1em;
}
.loading {
display: flex;
width: 5em;
align-items: center;
justify-content: center;
}
.load {
width: 23px;
height: 3px;
background-color: limegreen;
animation: 1s move_5011 infinite;
border-radius: 5px;
margin: 0.1em;
}
.load:nth-child(1) {
animation-delay: 0.2s;
}
.load:nth-child(2) {
animation-delay: 0.4s;
}
.load:nth-child(3) {
animation-delay: 0.6s;
}
@keyframes move_5011 {
0% {
width: 0.2em;
}
25% {
width: 0.7em;
}
50% {
width: 1.5em;
}
100% {
width: 0.2em;
}
}
</style>

View File

@ -89,7 +89,6 @@ const text_avatar = computed(() => {
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
</style>

View File

@ -90,6 +90,8 @@ const onSubmit = () => {
ServeUploadAvatar(form).then((res) => {
if (res.code == 200) {
emit('success', res.data.avatar)
} else {
message.warning(res.message)
}
})
})

View File

@ -6,11 +6,7 @@
props.subBtnText ? 'apposition-btn-style' : '',
]"
>
<wd-button
custom-class="custom-sub-btn-class"
v-if="props.subBtnText"
@click="clickSubBtn"
>
<wd-button custom-class="custom-sub-btn-class" v-if="props.subBtnText">
{{ props.subBtnText }}
</wd-button>
<wd-button
@ -19,7 +15,6 @@
:disabled="props?.disabled"
:class="[props?.disabled ? 'custom-btn-class-disabled' : '']"
:plain="props?.plain"
:loading="props?.isLoading"
>
{{ props.btnText }}
</wd-button>
@ -29,22 +24,16 @@
import { reactive } from 'vue'
import { defineProps, defineEmits } from 'vue'
const state = reactive({})
const emits = defineEmits(['clickSubBtn', 'clickBtn'])
const emits = defineEmits(['clickBtn'])
const props = defineProps({
isBottom: false, //
btnText: '', //
subBtnText: '', //
disabled: false, //
plain: false, //
isLoading: false, //
})
//
const clickSubBtn = () => {
emits('clickSubBtn')
}
//
//
const clickBtn = () => {
emits('clickBtn')
}

View File

@ -21,20 +21,9 @@
<script setup>
import { defineProps, defineEmits, reactive, watch } from 'vue'
const props = defineProps({
searchText: {
type: String,
default: ''
},
first_talk_record_infos: {
type: Object,
default(){
return {}
}
},
disabled: {
type: Boolean,
default: false
},
searchText: String,
first_talk_record_infos: Object,
disabled: Boolean,
})
const state = reactive({
searchText: '', //

View File

@ -1,7 +1,7 @@
<template>
<tm-navbar
:hideBack="props.hideBack"
:hideHome="props.hideHome"
hideHome
:title="props.title"
:shadow="props.shadowNum"
:fontSize="34"
@ -38,10 +38,6 @@ const props = defineProps({
type: Number,
default: 1,
},
hideHome: {
type: Boolean,
default: false,
},
})
</script>

View File

@ -25,15 +25,7 @@
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="copy07"></tm-image>
<div class="mt-1">复制</div>
</div>
<div
v-if="props.isShowConvertText"
@click="() => itemClick('convertText')"
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="copy07"></tm-image>
<div class="mt-1">转文字</div>
<div>复制</div>
</div>
<div
@click="() => itemClick('multipleChoose')"
@ -44,7 +36,7 @@
:height="40"
:src="multipleChoices"
></tm-image>
<div class="mt-1">多选</div>
<div>多选</div>
</div>
<div
v-if="props.isShowCite"
@ -52,7 +44,7 @@
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="cite"></tm-image>
<div class="mt-1">引用</div>
<div>引用</div>
</div>
<div
v-if="props.isShowWithdraw"
@ -60,14 +52,14 @@
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="withdraw"></tm-image>
<div class="mt-1">撤回</div>
<div>撤回</div>
</div>
<div
@click="() => itemClick('actionDelete')"
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="delete07"></tm-image>
<div class="mt-1">删除</div>
<div>删除</div>
</div>
</div>
<div :style="data.iconStyle" class="icon"></div>
@ -86,8 +78,8 @@
//
// uniapp & vue
import { onLoad, onReady } from '@dcloudio/uni-app'
import { defineEmits, defineProps } from 'vue'
import { onLoad, onReady } from "@dcloudio/uni-app";
import { defineEmits, defineProps } from "vue";
import {
reactive,
ref,
@ -95,18 +87,18 @@ import {
computed,
nextTick,
getCurrentInstance,
onMounted,
onBeforeUnmount,
} from 'vue'
import copy07 from '@/static/image/chatList/copy07@2x.png'
import multipleChoices from '@/static/image/chatList/multipleChoices@2x.png'
import cite from '@/static/image/chatList/cite@2x.png'
import withdraw from '@/static/image/chatList/withdraw@2x.png'
import delete07 from '@/static/image/chatList/delete@2x.png'
onMounted,
onBeforeUnmount
} from "vue";
import copy07 from "@/static/image/chatList/copy07@2x.png";
import multipleChoices from "@/static/image/chatList/multipleChoices@2x.png";
import cite from "@/static/image/chatList/cite@2x.png";
import withdraw from "@/static/image/chatList/withdraw@2x.png";
import delete07 from "@/static/image/chatList/delete@2x.png";
// pinia
const systemInfo = uni.getSystemInfoSync()
const bubbleRef = ref(null)
const systemInfo = uni.getSystemInfoSync();
const bubbleRef = ref(null);
const props = defineProps({
isShowCopy: {
@ -121,114 +113,109 @@ const props = defineProps({
type: Boolean,
default: true,
},
isShowConvertText: {
//
type: Boolean,
default: false,
},
})
});
const emits = defineEmits(['clickMenu'])
const emits = defineEmits(["clickMenu"]);
/**
* @name 生成UUID
*/
const uuid = () => {
const reg = /[xy]/g
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
const reg = /[xy]/g;
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
.replace(reg, function (c) {
var r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
})
.replace(/-/g, '')
}
.replace(/-/g, "");
};
const popoverBoxId = `ID${uuid()}`
const popoverContentId = `ID${uuid()}`
const instance = getCurrentInstance()
const popoverBoxId = `ID${uuid()}`;
const popoverContentId = `ID${uuid()}`;
const instance = getCurrentInstance();
const data = reactive({
popoverShow: false,
defaultStyle: {},
showStyle: {
left: 0,
right: '',
transform: '',
right: "",
transform: "",
},
iconStyle: {
left: '',
right: '',
transform: '',
left: "",
right: "",
transform: "",
},
})
});
/**
* @name 获取DOM
*/
const getDom = (dom) => {
return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(instance)
let select = query.select(dom)
const query = uni.createSelectorQuery().in(instance);
let select = query.select(dom);
const boundingClientRect = select.boundingClientRect((data) => {
resolve(data)
})
boundingClientRect.exec()
})
}
resolve(data);
});
boundingClientRect.exec();
});
};
const itemClick = (item) => {
emits('clickMenu', item)
}
emits("clickMenu", item);
};
// 5
let pressDownTime = 0
let time = null
let pressDownTime = 0;
let time = null;
const onTouchstart = () => {
time && clearTimeout(time)
time = setTimeout(open, 500)
}
time && clearTimeout(time);
time = setTimeout(open, 500);
};
const onTouchend = () => {
time && clearTimeout(time)
}
time && clearTimeout(time);
};
const open = async () => {
let popoverContent = await getDom(`#${popoverContentId}`)
let popoverBox = await getDom(`#${popoverBoxId}`)
let popoverContent = await getDom(`#${popoverContentId}`);
let popoverBox = await getDom(`#${popoverBoxId}`);
//
let originX = popoverBox.width / 2
let originX = popoverBox.width / 2;
//
let isTop = popoverBox.top - 50 > popoverContent.height
let isTop = popoverBox.top - 50 > popoverContent.height;
//
data.defaultStyle = {
top: isTop ? '60rpx' : 'auto',
bottom: !isTop ? '-20rpx' : 'auto',
transform: `translateY(${isTop ? '-100%' : '100%'}) scale(.8)`,
}
top: isTop ? "60rpx" : "auto",
bottom: !isTop ? "-20rpx" : "auto",
transform: `translateY(${isTop ? "-100%" : "100%"}) scale(.8)`,
};
//
if (popoverBox.left > systemInfo.windowWidth - popoverBox.right) {
data.defaultStyle.right = 0
data.defaultStyle.right = 0;
//
data.defaultStyle['transform-origin'] = `${
data.defaultStyle["transform-origin"] = `${
popoverContent.width - originX
}px ${isTop ? '100%' : '0%'}`
}px ${isTop ? "100%" : "0%"}`;
} else {
data.defaultStyle.left = 0
data.defaultStyle.left = 0;
//
data.defaultStyle['transform-origin'] = `${originX}px ${
isTop ? '100%' : '0%'
}`
data.defaultStyle["transform-origin"] = `${originX}px ${
isTop ? "100%" : "0%"
}`;
}
data.showStyle = { ...data.defaultStyle }
data.showStyle = { ...data.defaultStyle };
// icon
let iconDefsultStyle = {
transform: `translate(0%, ${isTop ? '20%' : '-20%'})`,
'border-top-color': isTop ? '#333333' : '',
'border-bottom-color': !isTop ? '#333333' : '',
top: !isTop ? '-20rpx' : 'auto',
bottom: isTop ? '-20rpx' : 'auto',
}
transform: `translate(0%, ${isTop ? "20%" : "-20%"})`,
"border-top-color": isTop ? "#333333" : "",
"border-bottom-color": !isTop ? "#333333" : "",
top: !isTop ? "-20rpx" : "auto",
bottom: isTop ? "-20rpx" : "auto",
};
setTimeout(() => {
if (popoverBox.left > systemInfo.windowWidth - popoverBox.right) {
@ -237,55 +224,55 @@ const open = async () => {
...data.defaultStyle,
//
opacity: 1,
transform: `translateY(${isTop ? '-100%' : '100%'}) scale(1)`,
'pointer-events': 'auto',
}
transform: `translateY(${isTop ? "-100%" : "100%"}) scale(1)`,
"pointer-events": "auto",
};
data.iconStyle = {
right: `${originX}px`,
left: 'auto',
left: "auto",
...iconDefsultStyle,
}
};
} else {
data.showStyle = {
//
...data.defaultStyle,
//
opacity: 1,
transform: `translateY(${isTop ? '-100%' : '100%'}) scale(1)`,
'pointer-events': 'auto',
}
transform: `translateY(${isTop ? "-100%" : "100%"}) scale(1)`,
"pointer-events": "auto",
};
data.iconStyle = {
left: `${originX}px`,
right: 'auto',
right: "auto",
...iconDefsultStyle,
}
};
}
if (!data.popoverShow) data.popoverShow = true
}, 200)
}
if (!data.popoverShow) data.popoverShow = true;
}, 200);
};
const close = (time) => {
setTimeout(() => {
data.popoverShow = false
data.showStyle = data.defaultStyle
}, time || 0)
}
data.popoverShow = false;
data.showStyle = data.defaultStyle;
}, time || 0);
};
const handleClickOutside = (event) => {
if ((data.popoverShow = false)) {
if (data.popoverShow = false) {
return false
}
if (bubbleRef.value && !bubbleRef.value.contains(event.target)) {
close()
close();
}
}
};
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
document.addEventListener('click', handleClickOutside);
});
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
})
document.removeEventListener('click', handleClickOutside);
});
</script>
<style lang="scss" scoped>

View File

@ -25,130 +25,69 @@ const getFileTypeIMG = computed(() => {
let objT = {
finishedImg: '',
blankImg: '',
progressColor: '',
}
progressColor: ''
};
switch (suffix) {
case 'pdf':
objT.finishedImg = filePaperPDF
objT.blankImg = filePaperPDFBlank
objT.progressColor = '#DE4E4E'
break
break;
case 'doc':
case 'docx':
objT.finishedImg = filePaperWord
objT.blankImg = filePaperWordBlank
objT.progressColor = '#2750B2'
break
break;
case 'xls':
case 'xlsx':
objT.finishedImg = filePaperExcel
objT.blankImg = filePaperExcelBlank
objT.progressColor = '#3C7F4B'
break
break;
case 'ppt':
case 'pptx':
objT.finishedImg = filePaperPPT
objT.blankImg = filePaperPPTBlank
objT.progressColor = '#B74B2B'
break
break;
default:
objT.finishedImg = filePaperOther
objT.blankImg = filePaperOtherBlank
objT.progressColor = '#46299d'
objT.progressColor = '#747474'
}
return objT
})
const previewPDF = () => {
if (typeof plus !== 'undefined') {
downloadAndOpenFile()
} else {
document.addEventListener('plusready', () => {
downloadAndOpenFile()
})
}
}
const downloadAndOpenFile = () => {
uni.showLoading({ title: '加载中...', mask: true })
const downloadUrl = props?.extra?.path
if (!downloadUrl) {
uni.hideLoading()
uni.showToast({ title: '文件路径无效', icon: 'none' })
return
}
const options = {
filename: '_doc/downloads/', //
}
const dtask = plus.downloader.createDownload(downloadUrl, options, function (
d,
status,
) {
if (status === 200) {
uni.hideLoading()
const filePath = d.filename
if (filePath) {
plus.runtime.openFile(filePath, {}, function () {})
} else {
uni.showToast({ title: '文件路径无效', icon: 'none' })
}
} else {
uni.hideLoading()
}
})
dtask.start()
}
</script>
<template>
<section
class="file-message"
@click="previewPDF"
:class="{ left: data.float === 'left', right: data.float === 'right' }"
>
<div class="flex justify-between">
<div
class="w-[228rpx] text-[32rpx] text-[#1A1A1A] h-[88rpx] leading-[44rpx] textEllipsis file_name"
>
<div class="w-[228rpx] text-[32rpx] text-[#1A1A1A] h-[88rpx] leading-[44rpx] textEllipsis file_name">
{{ extra.name }}
</div>
<div
v-if="data.uploadStatus === 2 || !data.uploadStatus"
class="w-[95rpx]"
>
<tm-image
:width="95"
:height="95"
:src="getFileTypeIMG.finishedImg"
></tm-image>
<div v-if="data.uploadStatus === 2 || !data.uploadStatus" class="w-[95rpx]">
<tm-image :width="95" :height="95" :src="getFileTypeIMG.finishedImg"></tm-image>
</div>
<div
v-if="data.uploadStatus === 1 || data.uploadStatus === 3"
class="w-[95rpx]"
>
<tm-image
:width="95"
:height="95"
:src="getFileTypeIMG.blankImg"
></tm-image>
<wd-circle v-if="data.uploadStatus === 1"
customClass="circleProgress"
:modelValue="data.uploadCurrent"
layerColor="#E3E3E3"
:color="getFileTypeIMG.progressColor"
:strokeWidth="3"
:size="20"
></wd-circle>
<div class="upload-failed" v-if="data.uploadStatus === 3">
<tm-icon :font-size="20" name="tmicon-times" color="#fff"></tm-icon>
</div>
<div v-if="data.uploadStatus === 1 || data.uploadStatus === 3" class="w-[95rpx]">
<tm-image :width="95" :height="95" :src="getFileTypeIMG.blankImg"></tm-image>
<wd-circle
customClass="circleProgress"
:modelValue="data.uploadCurrent"
layerColor="#E3E3E3"
:color="getFileTypeIMG.progressColor"
:strokeWidth="3"
:size="20"
></wd-circle>
</div>
</div>
<div class="divider mt-[28rpx]"></div>
<div class="text-[24rpx] text-[#747474] mt-[10rpx]">
{{ fileFormatSize(extra.size) }}
</div>
<div class="text-[24rpx] text-[#747474] mt-[10rpx]">{{ fileFormatSize(extra.size) }}</div>
<!-- <div class="main">
<div class="ext">{{ getFileNameSuffix(extra.name) }}</div>
<div class="file-box">
@ -185,11 +124,11 @@ const downloadAndOpenFile = () => {
border-radius: 16rpx 0 16rpx 16rpx;
}
.file_name {
.file_name{
word-break: break-all; /* 在任意字符间断行 */
word-wrap: break-word; /* 允许长单词换行到下一行 */
}
.main {
height: 45px;
display: flex;
@ -278,7 +217,7 @@ const downloadAndOpenFile = () => {
}
.divider {
background-color: #e7e7e7;
background-color: #E7E7E7;
height: 1rpx;
}
@ -291,20 +230,4 @@ const downloadAndOpenFile = () => {
width: 40rpx !important;
height: 40rpx !important;
}
.upload-failed {
position: absolute;
top: 120rpx;
right: 52rpx;
transform: translate(-50%, -50%);
z-index: 1;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
color: #ff4d4f;
background: #ff4d4f;
border-radius: 50%;
}
</style>

View File

@ -12,15 +12,9 @@ const props = defineProps<{
const isShowRecord = ref(false)
const title = computed(() => {
const uniqueNames = [...new Set(props.extra.records.map(v => v.nickname))];
if (uniqueNames.length <= 2) {
return uniqueNames.join('和');
} else {
return uniqueNames.slice(0, 2).join('和') + '等';
}
// return [...new Set(props.extra.records.map((v) => v.nickname))].join('')
return [...new Set(props.extra.records.map((v) => v.nickname))].join('、')
})
console.log(props.extra.records)
const onClick = () => {
// isShowRecord.value = true
uni.navigateTo({
@ -30,7 +24,7 @@ const onClick = () => {
</script>
<template>
<section class="im-message-forward pointer" :class="{ left: data.float === 'left' }" @click="onClick">
<div class="title">{{ extra.forward_name || title}}的会话记录</div>
<div class="title">{{ title }} 的会话记录</div>
<div class="list" v-for="(record, index) in extra.records" :key="index">
<p>
<span>{{ record.nickname }}: </span>
@ -65,14 +59,13 @@ const onClick = () => {
}
.title {
max-height: 88rpx;
height: 44rpx;
line-height: 44rpx;
font-size: 32rpx;
color: #1A1A1A;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 400;
margin-bottom: 8rpx;
}

View File

@ -12,40 +12,40 @@ const img = computed(() => {
// console.log(props.extra);
let info = {
width: 0,
height: 0,
height: 0
}
if (props.extra.url.includes('blob:http://')) {
info = {
width: props.extra.width,
height: props.extra.height,
height: props.extra.height
}
} else {
}else {
info = getImageInfo(props.extra.url)
}
if (info.width == 0 || info.height == 0) {
return {
width: 450,
height: 298,
height: 298
}
}
if (info.width < 300) {
if(info.width<300){
return {
width: 300,
height: info.height / (info.width / 300),
height: info.height / (info.width / 300)
}
}
if (info.width < 350) {
return {
width: info.width,
height: info.height,
height: info.height
}
}
return {
width: 350,
height: info.height / (info.width / 350),
height: info.height / (info.width / 350)
}
})
</script>
@ -54,30 +54,13 @@ const img = computed(() => {
class="im-message-image"
:class="{
left: data.float === 'left',
right: data.float === 'right',
right: data.float === 'right'
}"
>
<div class="image-container">
<div class="relative">
<tm-image
preview
:width="img.width"
:height="img.height"
:src="extra.url"
model="aspectFill"
/>
<wd-circle
custom-class="circleProgress"
v-if="data.uploadStatus === 1"
v-model="props.data.uploadCurrent"
color="#46299d"
layer-color="#E3E3E3"
></wd-circle>
<div class="upload-failed" v-if="data.uploadStatus === 3">
<tm-icon :font-size="20" name="tmicon-times" color="#fff"></tm-icon>
</div>
</div>
</div>
<div class="image-container">
<tm-image preview :width="img.width" :height="img.height" :src="extra.url" />
<wd-circle custom-class="circleProgress" v-if="props.data.uploadCurrent && props.data.uploadCurrent<100" v-model="props.data.uploadCurrent" color="#ffffff" layer-color="#E3E3E3"></wd-circle>
</div>
</section>
</template>
<style lang="less" scoped>
@ -96,12 +79,12 @@ const img = computed(() => {
}
&.right {
background-color: #46299d;
background-color: #46299D;
border-radius: 16rpx 0 16rpx 16rpx;
}
}
.image-container {
position: relative;
position: relative;
.circleProgress {
position: absolute;
@ -111,18 +94,4 @@ const img = computed(() => {
z-index: 1;
}
}
.upload-failed {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ff4d4f;
border-radius: 50%;
}
</style>

View File

@ -4,71 +4,33 @@ import { formatTime } from '@/utils/datetime'
defineProps({
login_uid: {
type: Number,
default: 0,
default: 0
},
user_id: {
type: Number,
default: 0,
default: 0
},
talk_type: {
type: Number,
default: 0,
default: 0
},
nickname: {
type: String,
default: '',
default: ''
},
datetime: {
type: String,
default: '',
},
msg_id: {
type: String,
default: '',
},
revokeInfo: {
type: Object,
default() {
return {}
},
},
extra:{
type: String,
default: '',
default: ''
}
})
</script>
<template>
<div class="im-message-revoke">
<div class="content" v-if="JSON.stringify(revokeInfo) !== '{}'">
<span v-if="talk_type === 1 && login_uid === revokeInfo.withdraw_id">
你撤回了一条消息 | {{ formatTime(datetime) }}
<slot></slot>
<!-- 添加插槽用于放置重新编辑按钮 -->
</span>
<span v-if="talk_type === 1 && login_uid !== revokeInfo.withdraw_id">
{{revokeInfo.withdraw_name}}撤回了一条消息 | {{ formatTime(datetime) }}
</span>
<span v-if="talk_type === 2 && login_uid === revokeInfo.withdraw_id && login_uid === revokeInfo.retracted_id">
你撤回了一条消息 |
{{ formatTime(datetime) }}
<slot></slot>
</span>
<span v-if="talk_type === 2 && login_uid === revokeInfo.withdraw_id && login_uid !== revokeInfo.retracted_id">
你撤回了{{revokeInfo.retracted_name}}一条消息 |
{{ formatTime(datetime) }}
</span>
<span v-if="talk_type === 2 && login_uid !== revokeInfo.withdraw_id && revokeInfo.withdraw_id === revokeInfo.retracted_id">
{{revokeInfo.withdraw_name}}撤回了一条消息 |
{{ formatTime(datetime) }}
</span>
<span v-if="talk_type === 2 && login_uid !== revokeInfo.withdraw_id && login_uid === revokeInfo.retracted_id && revokeInfo.withdraw_id !== revokeInfo.retracted_id">
{{revokeInfo.withdraw_name}}撤回了你一条消息 |
{{ formatTime(datetime) }}
</span>
<span v-if="talk_type === 2 && login_uid !== revokeInfo.withdraw_id && login_uid !== revokeInfo.retracted_id && revokeInfo.withdraw_id !== revokeInfo.retracted_id">
{{revokeInfo.withdraw_name}}撤回了{{revokeInfo.retracted_name}}一条消息 |
<div class="content">
<span v-if="login_uid == user_id"> 你撤回了一条消息 | {{ formatTime(datetime) }} </span>
<span v-else-if="talk_type == 1"> 对方撤回了一条消息 | {{ formatTime(datetime) }} </span>
<span v-else>
"{{ nickname }}" 撤回了一条消息 |
{{ formatTime(datetime) }}
</span>
@ -76,27 +38,6 @@ defineProps({
<span v-else-if="login_uid == user_idB"> A撤回你了一条消息 | {{ formatTime(datetime) }} </span>
<span v-else> A撤回B了一条消息 | {{ formatTime(datetime) }} </span> -->
</div>
<div class="content" v-if="JSON.stringify(revokeInfo) === '{}'">
<span v-if="talk_type === 1 && login_uid === user_id">
你撤回了一条消息 | {{ formatTime(datetime) }}
<slot></slot>
<!-- 添加插槽用于放置重新编辑按钮 -->
</span>
<span v-if="talk_type === 1 && login_uid !== user_id">
{{nickname}}撤回了一条消息 | {{ formatTime(datetime) }}
</span>
<span v-if="talk_type === 2 && !extra && login_uid === user_id">
你撤回了一条消息 | {{ formatTime(datetime) }}
<slot></slot>
<!-- 添加插槽用于放置重新编辑按钮 -->
</span>
<span v-if="talk_type === 2 && !extra && login_uid !== user_id">
{{nickname}}撤回了一条消息 | {{ formatTime(datetime) }}
</span>
<span v-if="talk_type === 2 && extra">
{{extra}} | {{ formatTime(datetime) }}
</span>
</div>
</div>
</template>
<style lang="less" scoped>

View File

@ -2,7 +2,6 @@
import { textReplaceEmoji } from '@/utils/emojis'
import { textReplaceLink, textReplaceMention } from '@/utils/strings'
import { ITalkRecordExtraText, ITalkRecord } from '@/types/chat'
import { computed } from 'vue'
const props = defineProps<{
extra: ITalkRecordExtraText
@ -13,14 +12,15 @@ const props = defineProps<{
const float = props.data.float
const textContent = computed(() => {
let text = props.extra?.content || ''
// text = textReplaceLink(text)
if (props.data.talk_type == 2) {
text = textReplaceMention(text, '#1890ff')
}
return textReplaceEmoji(text)
})
let textContent = props.extra?.content || ''
textContent = textReplaceLink(textContent)
if (props.data.talk_type == 2) {
textContent = textReplaceMention(textContent, '#1890ff')
}
textContent = textReplaceEmoji(textContent)
</script>
<template>

View File

@ -1,8 +1,8 @@
<script lang="ts" setup>
import { ref, nextTick, getCurrentInstance, computed, onMounted } from 'vue'
import { getImageInfo } from '@/utils/functions'
import playCircle from '@/static/image/chatList/playCircle@2x.png'
import { useStatus } from '@/store/status'
import playCircle from "@/static/image/chatList/playCircle@2x.png";
import { useStatus } from "@/store/status";
const { statusBarHeight } = useStatus()
const instance = getCurrentInstance()
@ -20,12 +20,12 @@ const open = ref(false)
const img = computed(() => {
let info = {
width: 0,
height: 0,
height: 0
}
if (props.extra.url.includes('blob:http://')) {
info = {
width: props.extra.width,
height: props.extra.height,
height: props.extra.height
}
} else {
info = getImageInfo(props.extra.url)
@ -34,26 +34,26 @@ const img = computed(() => {
if (info.width == 0 || info.height == 0) {
return {
width: 450,
height: 298,
height: 298
}
}
if (info.width < 300) {
return {
width: 300,
height: info.height / (info.width / 300),
height: info.height / (info.width / 300)
}
}
if (info.width < 350) {
return {
width: info.width,
height: info.height,
height: info.height
}
}
return {
width: 350,
height: info.height / (info.width / 350),
height: info.height / (info.width / 350)
}
})
@ -67,98 +67,57 @@ const fullscreenchange = (e) => {
/* 视频播放 获取第一帧 */
const canplay = (e) => {
console.log('Video can play:', e)
console.log('Video can play:', e);
if (e.target) {
setTimeout(() => {
e.target.pause()
}, 200)
e.target.pause();
}, 200);
}
}
};
async function onPlay() {
videoContext.value = uni.createVideoContext(props.extra.url, instance);
videoContext.value.requestFullScreen({ direction: 2 });
videoContext.value.play()
open.value = true
await nextTick()
videoContext.value = uni.createVideoContext(props.extra.url, instance)
setTimeout(() => {
//
videoContext.value.requestFullScreen({ direction: 2 })
//
setTimeout(() => {
videoContext.value.play()
}, 100)
}, 200)
}
onMounted(() => {
videoRef.value = uni.createVideoContext(props.data.msg_id)
videoRef.value = uni.createVideoContext(props.data.msg_id);
videoRef.value.play()
setTimeout(() => {
videoRef.value.pause()
}, 200)
}, 200);
})
</script>
<template>
<section
class="im-message-video"
:class="{ left: data.float === 'left' }"
@click="onPlay"
>
<div
class="coverVideo"
:style="{
width: img.width + 'rpx',
height: img.height + 'rpx',
}"
v-if="props.extra.url.includes('blob:http://')"
>
<video
:id="data.msg_id"
:autoplay="false"
disablepictureinpicture
muted
:src="props.extra.url"
width="100%"
height="100%"
playsinline
preload="auto"
controls="false"
x5-playsinline
webkit-playsinline
style="object-fit: cover; pointer-events: none;"
></video>
<section class="im-message-video" :class="{ left: data.float === 'left' }" @click="onPlay">
<div class="coverVideo" :style="{
width: img.width + 'rpx',
height: img.height + 'rpx'
}" v-if="props.extra.url.includes('blob:http://')">
<video :id="data.msg_id" :autoplay="false" disablepictureinpicture muted :src="props.extra.url" width="100%"
height="100%" playsinline preload="auto" controls="false" x5-playsinline
webkit-playsinline style="object-fit: cover; pointer-events: none;">
</video>
</div>
<wd-img
v-else
:width="`${img.width}rpx`"
:height="`${img.height}rpx`"
:src="data.extra.cover"
/>
<div
v-if="data.uploadStatus === 2 || !data.uploadStatus"
class="btn-video"
:style="{
width: img.width + 'rpx',
height: img.height + 'rpx',
}"
>
<wd-img v-else :width="`${img.width}rpx`" :height="`${img.height}rpx`" :src="data.extra.cover" />
<div v-if="data.uploadStatus === 2 || !data.uploadStatus" class="btn-video" :style="{
width: img.width + 'rpx',
height: img.height + 'rpx'
}">
<tm-image :src="playCircle" :width="80" :height="80" />
</div>
<div
v-else
class="btn-video"
:style="{
width: img.width + 'rpx',
height: img.height + 'rpx',
}"
>
<div v-else class="btn-video" :style="{
width: img.width + 'rpx',
height: img.height + 'rpx'
}" >
<wd-circle
v-if="data.uploadStatus === 1"
v-model="props.data.uploadCurrent"
customClass="circleProgress"
color="#46299d"
layer-color="#E3E3E3"
layerColor="#E3E3E3"
color="#FFFFFF"
:strokeWidth="6"
:size="40"
></wd-circle>
@ -172,32 +131,18 @@ onMounted(() => {
:width="70"
:percent="props.data.uploadCurrent">
</tm-progress> -->
<div class="upload-failed" v-if="data.uploadStatus === 3">
<tm-icon :font-size="20" name="tmicon-times" color="#fff"></tm-icon>
</div>
</div>
<div v-show="open">
<video :src="props.extra.url" controls @fullscreenchange="fullscreenchange" :id="props.extra.url">
</video>
</div>
</section>
<teleport to="body">
<div v-show="open" class="video-container">
<video
:src="props.extra.url"
controls
@fullscreenchange="fullscreenchange"
:id="props.extra.url"
playsinline
webkit-playsinline
x5-playsinline
class="fullscreen-video"
></video>
</div>
</teleport>
</template>
<style lang="less" scoped>
.im-message-video {
overflow: hidden;
padding: 20rpx 18rpx;
background: #46299d;
background: #46299D;
min-width: 30rpx;
min-height: 30rpx;
display: inline-flex;
@ -255,43 +200,10 @@ onMounted(() => {
:deep(.uni-video-bar) {
display: none;
}
}
.circleProgress {
width: 80rpx !important;
height: 80rpx !important;
}
.video-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #000;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.fullscreen-video {
width: 100%;
height: 100%;
object-fit: contain;
}
.upload-failed {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ff4d4f;
border-radius: 50%;
}
</style>

View File

@ -1,25 +0,0 @@
<script setup>
import './sys-message.less'
import { useInject } from '@/hooks'
defineProps({
extra: Object,
data: Object
})
const { showUserInfoModal } = useInject()
</script>
<template>
<div class="im-message-sys-text">
<div class="sys-text">
<template v-for="(user, index) in extra.members" :key="index">
<a @click="showUserInfoModal(user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em>
</template>
<span>已成为管理员</span>
</div>
</div>
</template>

View File

@ -1,19 +0,0 @@
<script setup>
import './sys-message.less'
import { useInject } from '@/hooks'
const { showUserInfoModal } = useInject()
defineProps({
extra: Object,
data: Object
})
</script>
<template>
<div class="im-message-sys-text">
<div class="sys-text">
<span>{{ extra.content }}</span>
</div>
</div>
</template>

View File

@ -1,25 +0,0 @@
<script setup>
import './sys-message.less'
import { useInject } from '@/hooks'
const { showUserInfoModal } = useInject()
defineProps({
extra: Object,
data: Object
})
</script>
<template>
<div class="im-message-sys-text">
<div class="sys-text">
<a @click="showUserInfoModal(data.user_id)">
<!-- {{ data.nickname }} -->
管理员
</a>
<!-- <span>修改群名为</span>
<span>"{{ extra.group_name }}"</span> -->
<span>修改了群信息</span>
</div>
</div>
</template>

View File

@ -24,7 +24,7 @@ const { showUserInfoModal } = useInject()
<em v-show="index < extra.members.length - 1"></em>
</template>
<span>出群聊</span>
<span>出群聊</span>
</div>
</div>
</template>

View File

@ -1,23 +0,0 @@
<script setup>
import './sys-message.less'
import { useInject } from '@/hooks'
const { showUserInfoModal } = useInject()
defineProps({
extra: Object,
data: Object,
})
</script>
<template>
<div class="im-message-sys-text">
<div class="sys-text">
<template v-for="(user, index) in extra?.members" :key="index">
<a @click="showUserInfoModal(user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em>
</template>
<span>已离开此群</span>
</div>
</div>
</template>

View File

@ -17,7 +17,7 @@ export const ChatMsgSysText = 1000 // 系统文本消息
export const ChatMsgSysGroupCreate = 1101 // 创建群聊消息
export const ChatMsgSysGroupMemberJoin = 1102 // 加入群聊消息
export const ChatMsgSysGroupMemberQuit = 1103 // 群成员退出群消息
export const ChatMsgSysGroupMemberKicked = 1104 // 移出群成员消息(普通群、项目群被踢)
export const ChatMsgSysGroupMemberKicked = 1104 // 踢出群成员消息
export const ChatMsgSysGroupMessageRevoke = 1105 // 管理员撤回成员消息
export const ChatMsgSysGroupDismissed = 1106 // 群解散
export const ChatMsgSysGroupMuted = 1107 // 群禁言
@ -26,9 +26,6 @@ export const ChatMsgSysGroupMemberMuted = 1109 // 群成员禁言
export const ChatMsgSysGroupMemberCancelMuted = 1110 // 群成员解除禁言
export const ChatMsgSysGroupNotice = 1111 // 编辑群公告
export const ChatMsgSysGroupTransfer = 1113 // 变更群主
export const ChatMsgSysGroupAdmin = 1114 // 设置管理员
export const ChatMsgSysGroupMemberRemoved = 1115 // 移出群成员消息(部门群、公司群自动移出)
export const ChatMsgSysGroupInfoChange = 1116 // 管理员更新了群信息
export const ChatMsgTypeMapping = {
[ChatMsgTypeText]: '[文本消息]',
@ -49,18 +46,14 @@ export const ChatMsgTypeMapping = {
[ChatMsgSysGroupCreate]: '[创建群消息]',
[ChatMsgSysGroupMemberJoin]: '[加入群消息]',
[ChatMsgSysGroupMemberQuit]: '[退出群消息]',
[ChatMsgSysGroupMemberKicked]: '[出群消息]',
[ChatMsgSysGroupMemberKicked]: '[出群消息]',
[ChatMsgSysGroupMessageRevoke]: '[撤回消息]',
[ChatMsgSysGroupDismissed]: '[群解散消息]',
[ChatMsgSysGroupMuted]: '[群禁言消息]',
[ChatMsgSysGroupCancelMuted]: '[群解除禁言消息]',
[ChatMsgSysGroupMemberMuted]: '[群成员禁言消息]',
[ChatMsgSysGroupMemberCancelMuted]: '[群成员解除禁言消息]',
[ChatMsgSysGroupNotice]: '[群公告]',
[ChatMsgSysGroupTransfer]: '[转让群主]',
[ChatMsgSysGroupAdmin]: '[设置管理员]',
[ChatMsgSysGroupMemberRemoved]: '[移出群成员消息]',
[ChatMsgSysGroupInfoChange]: '[群信息更新]'
[ChatMsgSysGroupNotice]: '[群公告]'
}
// 消息类型 - 消息组件 映射关系
@ -85,15 +78,12 @@ export const MessageComponents = {
[ChatMsgSysGroupMemberQuit]: 'sys-group-member-quit-message',
[ChatMsgSysGroupMemberKicked]: 'sys-group-member-kicked-message',
// [ChatMsgSysGroupMessageRevoke]: '[撤回消息]',
[ChatMsgSysGroupDismissed]: 'sys-group-dismissed',
// [ChatMsgSysGroupDismissed]: '[群解散消息]',
[ChatMsgSysGroupMuted]: 'sys-group-muted-message',
[ChatMsgSysGroupCancelMuted]: 'sys-group-cancel-muted-message',
[ChatMsgSysGroupMemberMuted]: 'sys-group-member-muted-message',
[ChatMsgSysGroupMemberCancelMuted]: 'sys-group-member-cancel-muted-message',
[ChatMsgSysGroupTransfer]: 'sys-group-transfer-message',
[ChatMsgSysGroupAdmin]:'sys-group-admin-message',
[ChatMsgSysGroupMemberRemoved]:'sys-group-member-removed-message',
[ChatMsgSysGroupInfoChange]:'sys-group-info-change-message'
[ChatMsgSysGroupTransfer]: 'sys-group-transfer-message'
}
// 可转发的消息类型

View File

@ -68,38 +68,20 @@ class Revoke extends Base {
}
handle() {
const { updateDialogueRecord } = useDialogueListStore()
const {updateDialogueRecord} = useDialogueListStore()
useTalkStore().updateItem({
index_name: this.getIndexName(),
msg_text: this.resource.text,
revokeInfo: {
retracted_id: this.resource.retracted_id,
retracted_name: this.resource.retracted_name,
withdraw_id: this.resource.withdraw_id,
withdraw_name: this.resource.withdraw_name,
},
updated_at: parseTime(new Date()),
updated_at: parseTime(new Date())
})
useDialogueStore().updateDialogueRecord({
msg_id: this.msg_id,
revokeInfo: {
retracted_id: this.resource.retracted_id,
retracted_name: this.resource.retracted_name,
withdraw_id: this.resource.withdraw_id,
withdraw_name: this.resource.withdraw_name,
},
is_revoke: 1,
is_revoke: 1
})
updateDialogueRecord({
msg_id: this.msg_id,
revokeInfo: {
retracted_id: this.resource.retracted_id,
retracted_name: this.resource.retracted_name,
withdraw_id: this.resource.withdraw_id,
withdraw_name: this.resource.withdraw_name,
},
is_revoke: 1,
is_revoke: 1
})
}
}

View File

@ -5,16 +5,8 @@ import { parseTime } from '@/utils/datetime'
import * as message from '@/constant/message'
import { formatTalkItem, palyMusic, formatTalkRecord } from '@/utils/talk'
// import { isElectronMode } from '@/utils/common'
import {
ServeClearTalkUnreadNum,
ServeCreateTalkList,
} from '@/api/chat/index.js'
import {
useTalkStore,
useDialogueStore,
useDialogueListStore,
useGroupStore,
} from '@/store'
import { ServeClearTalkUnreadNum, ServeCreateTalkList } from '@/api/chat/index.js'
import { useTalkStore, useDialogueStore,useDialogueListStore } from '@/store'
/**
* 好友状态事件
@ -40,11 +32,6 @@ class Talk extends Base {
*/
talk_type = 0
/**
* 文件上传唯一随机值
*/
fileNum = ''
/**
* 初始化构造方法
*
@ -56,10 +43,6 @@ class Talk extends Base {
this.sender_id = resource.sender_id
this.receiver_id = resource.receiver_id
this.talk_type = resource.talk_type
// this.fileNum = resource.file_num
if (resource.file_num) {
resource.data.file_num = resource.file_num
}
this.resource = resource.data
this.handle()
@ -106,10 +89,11 @@ class Talk extends Base {
play() {
// 客户端有消息提示
// if (isElectronMode()) return
// useSettingsStore().isPromptTone && palyMusic()
}
async handle() {
handle() {
// 不是自己发送的消息则需要播放提示音
if (!this.isCurrSender()) {
this.play()
@ -117,21 +101,7 @@ class Talk extends Base {
// 判断会话列表是否存在,不存在则创建
if (useTalkStore().findTalkIndex(this.getIndexName()) == -1) {
if (this.resource.msg_type == 1102) {
//被邀请进入群聊时,需要热更新会话列表
await useTalkStore().loadTalkList()
} else if (this.resource.msg_type == 1106) {
//群解散时,需要热更新会话列表
await useTalkStore().loadTalkList()
} else if (
this.resource.msg_type == 1104 ||
this.resource.msg_type == 1115
) {
//群成员被移出时,需要热更新会话列表
await useTalkStore().loadTalkList()
} else {
return this.addTalkItem()
}
return this.addTalkItem()
}
// 判断当前是否正在和好友对话
@ -139,37 +109,6 @@ class Talk extends Base {
this.insertTalkRecord()
} else {
this.updateTalkItem()
if (
!useTalkStore().items[useTalkStore().findTalkIndex(this.getIndexName())]
?.is_disturb &&
!(
useTalkStore().findTalkIndex(this.getIndexName()) == -1 &&
(this.resource.msg_type == 1104 || this.resource.msg_type == 1115)
)
) {
this.updateUnreadMsgNumAdd()
}
}
}
//更新未读数量+1
updateUnreadMsgNumAdd() {
if (typeof plus !== 'undefined') {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`updateUnreadMsgNumAdd()`)
}
})
} else {
document.addEventListener('plusready', () => {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`updateUnreadMsgNumAdd()`)
}
})
})
}
}
@ -184,6 +123,7 @@ class Talk extends Base {
// lang: 'zh-CN',
// body: '您有新的消息请注意查收'
// })
// notification.onclick = () => {
// notification.close()
// }
@ -211,16 +151,12 @@ class Talk extends Base {
ServeCreateTalkList({
talk_type,
receiver_id,
}).then(async ({ code, data }) => {
receiver_id
}).then(({ code, data }) => {
if (code == 200) {
let item = formatTalkItem(data)
if (!item?.is_disturb) {
item.unread_num = 1
this.updateUnreadMsgNumAdd()
}
item.unread_num = 1
useTalkStore().addItem(item)
await useTalkStore().loadTalkList()
}
})
}
@ -230,76 +166,24 @@ class Talk extends Base {
*/
insertTalkRecord() {
let record = this.resource
let newRecord = formatTalkRecord(this.getAccountId(), this.resource)
const { addDialogueRecord, addChatRecord } = useDialogueListStore()
let newRecord = formatTalkRecord(this.getAccountId(), this.resource);
const {addDialogueRecord,addChatRecord} = useDialogueListStore()
// 群成员变化的消息,需要更新群成员列表
if ([1102, 1103, 1104, 1115].includes(record.msg_type)) {
if ([1102, 1103, 1104].includes(record.msg_type)) {
useDialogueStore().updateGroupMembers()
}
//群解散时,需要更新群成员权限
if ([1106].includes(record.msg_type)) {
useDialogueStore().updateDismiss()
}
//群成员被移出时,需要更新群成员权限
if ([1104, 1115].includes(record.msg_type)) {
console.error(this.resource.extra.members, 'this.resource.extra.members')
if (this.resource?.extra?.members?.length > 0) {
const isMeQuit = this.resource.extra.members.find(
(item) => item.user_id === this.getAccountId(),
)
if (isMeQuit) {
useDialogueStore().updateQuit()
}
}
}
//群禁言变化时,需要更新群禁言状态——即更新群成员列表
if ([1107, 1108, 1109, 1110].includes(record.msg_type)) {
useDialogueStore().updateGroupMembers()
}
//群公告变化时,需要更新群公告(新增和修改有热更新,删除没有)
if ([13].includes(record.msg_type)) {
useGroupStore().ServeGetGroupNotices()
}
//群管理员变化时,需要更新群管理员列表——即更新群成员列表,同时更新群信息
if ([1114].includes(record.msg_type)) {
useDialogueStore().updateGroupMembers()
useGroupStore().ServeGroupDetail()
}
if ([1116].includes(record.msg_type)) {
// 更新会话信息
useDialogueStore().setDialogue({
name: record.extra.group_name,
talk_type: record.talk_type,
receiver_id: record.receiver_id,
})
// 更新群聊信息
useGroupStore().updateGroupInfo({
group_name: record.extra.group_name,
avatar: record.extra.group_avatar,
})
// 更新会话列表中的会话信息
const dialogue = useDialogueListStore().getDialogueList(
`${record.talk_type}_${record.receiver_id}`,
)
if (dialogue) {
dialogue.talk.username = record.extra.group_name
}
}
addDialogueRecord([newRecord], 'add')
addChatRecord(this.getIndexName(), newRecord)
addDialogueRecord([newRecord],'add')
addChatRecord(this.getIndexName(),newRecord)
useDialogueStore().addDialogueRecord(newRecord)
if (!this.isCurrSender()) {
// 推送已读消息
// setTimeout(() => {
// ws.emit('im.message.read', {
// receiver_id: this.sender_id,
// msg_ids: [this.resource.msg_id],
// })
// }, 1000)
setTimeout(() => {
ws.emit('im.message.read', {
receiver_id: this.sender_id,
msg_ids: [this.resource.msg_id]
})
}, 1000)
}
// 获取聊天面板元素节点
@ -307,8 +191,7 @@ class Talk extends Base {
if (!el) return
// 判断的滚动条是否在底部
const isBottom =
Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight
const isBottom = Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight
if (isBottom || record.user_id == this.getAccountId()) {
nextTick(() => {
@ -321,15 +204,14 @@ class Talk extends Base {
useTalkStore().updateItem({
index_name: this.getIndexName(),
msg_text: this.getTalkText(),
updated_at: parseTime(new Date()),
updated_at: parseTime(new Date())
})
if (this.talk_type == 1 && this.getAccountId() !== this.sender_id) {
//不在此处维护未读消息数量
// ServeClearTalkUnreadNum({
// talk_type: 1,
// receiver_id: this.sender_id,
// })
ServeClearTalkUnreadNum({
talk_type: 1,
receiver_id: this.sender_id
})
}
}
@ -340,31 +222,8 @@ class Talk extends Base {
useTalkStore().updateMessage({
index_name: this.getIndexName(),
msg_text: this.getTalkText(),
updated_at: parseTime(new Date()),
updated_at: parseTime(new Date())
})
//收到新消息时,同时判断是否有人@我
if (
this.resource.msg_type === 1 &&
this.resource?.extra?.mentions?.length > 0
) {
const findMention = this.resource?.extra?.mentions?.find(
(mention) => mention === this.getAccountId(),
)
//有人@我或者@所有人,则更新会话列表
if (findMention || this.resource?.extra?.mentions?.includes(0)) {
useTalkStore().loadTalkList()
}
}
if (this.resource.msg_type == 1116) {
// 更新会话列表中的会话信息
const dialogue = useDialogueListStore().getDialogueList(
`${this.resource.talk_type}_${this.resource.receiver_id}`,
)
if (dialogue) {
dialogue.talk.username = this.resource.extra.group_name
}
useTalkStore().loadTalkList()
}
}
}

View File

@ -11,12 +11,7 @@ import { reactive, nextTick, computed, h, inject } from 'vue'
// EditTwo,
// IdCard
// } from '@icon-park/vue-next'
import {
ServeTopTalkList,
ServeDeleteTalkList,
ServeSetNotDisturb,
ServeClearTalkUnreadNum,
} from '@/api/chat'
import { ServeTopTalkList, ServeDeleteTalkList, ServeSetNotDisturb } from '@/api/chat'
import { useDialogueStore, useTalkStore, useDialogueListStore } from '@/store'
import { ServeSecedeGroup } from '@/api/group'
// import { ServeDeleteContact, ServeEditContactRemark } from '@/api/contact'
@ -28,7 +23,7 @@ export function useSessionMenu() {
show: false,
x: 0,
y: 0,
item: {},
item: {}
})
const dialogueStore = useDialogueStore()
@ -123,22 +118,10 @@ export function useSessionMenu() {
// 移除会话
const onRemoveTalk = (item) => {
ServeDeleteTalkList({
list_id: item.id,
list_id: item.id
}).then(({ code }) => {
if (code == 200) {
onDeleteTalk(item.index_name)
console.error(item, 'item')
if (item.unread_num > 0) {
//同时已读
ServeClearTalkUnreadNum(
{
talk_type: item.talk_type,
receiver_id: item.receiver_id,
},
item.unread_num,
).then(() => {
})
}
}
})
}
@ -148,13 +131,13 @@ export function useSessionMenu() {
ServeSetNotDisturb({
talk_type: item.talk_type,
receiver_id: item.receiver_id,
is_disturb: item.is_disturb == 0 ? 1 : 0,
is_disturb: item.is_disturb == 0 ? 1 : 0
}).then(({ code, message }) => {
if (code == 200) {
message.success('设置成功!')
talkStore.updateItem({
index_name: item.index_name,
is_disturb: item.is_disturb == 0 ? 1 : 0,
is_disturb: item.is_disturb == 0 ? 1 : 0
})
} else {
message.error(message)
@ -170,12 +153,12 @@ export function useSessionMenu() {
ServeTopTalkList({
list_id: item.id,
type: item.is_top == 0 ? 1 : 2,
type: item.is_top == 0 ? 1 : 2
}).then(({ code, message }) => {
if (code == 200) {
talkStore.updateItem({
index_name: item.index_name,
is_top: item.is_top == 0 ? 1 : 0,
is_top: item.is_top == 0 ? 1 : 0
})
} else {
message.error(message)
@ -218,7 +201,7 @@ export function useSessionMenu() {
negativeText: '取消',
onPositiveClick: () => {
ServeSecedeGroup({
group_id: item.receiver_id,
group_id: item.receiver_id
}).then(({ code, message }) => {
if (code == 200) {
message.success('已退出群聊')
@ -227,7 +210,7 @@ export function useSessionMenu() {
message.error(message)
}
})
},
}
})
}
@ -276,18 +259,12 @@ export function useSessionMenu() {
disturb: onSetDisturb,
signout_group: onSignOutGroup,
delete_contact: onDeleteContact,
remark: onChangeRemark,
remark: onChangeRemark
}
dropdown.show = false
evnets[key] && evnets[key](dropdown.item)
}
return {
dropdown,
onCloseContextMenu,
onContextMenuTalkHandle,
onToTopTalk,
onRemoveTalk,
}
return { dropdown, onCloseContextMenu, onContextMenuTalkHandle, onToTopTalk, onRemoveTalk }
}

View File

@ -10,15 +10,10 @@ import tmui from '@/uni_modules/tmui'
import { config } from '@/config/tmui/index.js'
import 'dayjs/locale/zh-cn'
import xLoaderror from '@/components/x-loaderror/index.vue'
import asyncLoading from '@/components/async-loading/index.vue'
import asyncError from '@/components/async-error/index.vue'
import { vLoading } from '@/components/x-loading/index.js'
import messagePopup from '@/components/x-message/useMessagePopup'
import pageAnimation from '@/components/page-animation/index.vue'
import * as plugins from './plugins'
import { useDialogueStore, useTalkStore, useUserStore, useDialogueListStore } from '@/store'
import {uniStorage} from "@/utils/uniStorage.js"
const { showMessage } = messagePopup()
dayjs.locale('zh-cn')
if (import.meta.env.VITE_SHOW_CONSOLE === 'true') {
@ -33,8 +28,6 @@ export function createApp() {
app.mixin(pageAnimation)
app.component('customNavbar', customNavbar)
app.component('x-loaderror', xLoaderror)
app.component('AsyncLoading', asyncLoading)
app.component('AsyncError', asyncError)
app.directive('no-space', {
mounted(el) {
el.addEventListener('input', (e) => {
@ -47,86 +40,6 @@ export function createApp() {
})
},
})
//获取当前聊天页面所在页面并通过当前的receiver_id判断是否要创建本地通知栏消息
window.getCurrentChatRoute = (msg) => {
let pushMsg = JSON.parse(decodeURIComponent(msg))
//当前所在聊天会话的receiver_id
const receiver_id = pushMsg?.payload?.receiver_id
// 获取当前页面路径
const pages = getCurrentPages()
const page = pages[pages.length - 1]
console.log(page.route)
const dialogueStore = useDialogueStore()
console.log(dialogueStore?.talk?.receiver_id)
if (
page?.route === 'pages/dialog/index' &&
receiver_id === dialogueStore?.talk?.receiver_id
) {
return
}
console.log('===准备创建本地通知栏消息')
let OAWebView = plus.webview.all()
OAWebView.forEach((webview, index) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`doCreatePushMessage('${msg}')`)
}
})
}
//处理聊天推送弹窗点开
window.openUniPushMsg = (msg) => {
console.log('=====点击通知栏消息')
let pushMsg = JSON.parse(decodeURIComponent(msg))
console.log('=====pushMsg', pushMsg)
//由于弹窗前处理了不该弹窗的场景,因此这里弹窗可以一并处理
//也就是都跳转到聊天页面
const talkStore = useTalkStore()
talkStore.toTalk(pushMsg?.payload?.talk_type, pushMsg?.payload?.receiver_id)
}
//处理当用户信息发生变化时,更新用户信息
window.updateUserInfo = () => {
useUserStore().loadSetting()
}
// 通讯录跳转
window.handleContacts = () => {
uni.navigateTo({
url: '/pages/chooseByDeps/index?chooseMode=3&type=true'
});
};
//处理OA、墨册强制刷新时聊天同步强制刷新
window.doLocationRefresh = () => {
uniStorage.removeItem('dialogueList')
uniStorage.removeItem('dialogue')
useUserStore().loadSetting()
useDialogueListStore().dialogueList.value = []
// location.reload(true)
}
//检查聊天页面是否可用
window.checkChatWebviewAvailable = () => {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview, index) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`doneCheckChatWebviewAvailable()`)
}
})
}
//获取从base传来的多选视频列表
window.getBaseMulVideo = (videoList) => {
const videos = JSON.parse(decodeURIComponent(videoList))
console.error('=====videos', videos)
if(videos.length > 0){
const videoUri = videos[0]
console.error('=====videoUri', videoUri)
}
}
window.message = ['success', 'error', 'warning'].reduce((acc, type) => {
acc[type] = (message) => {
if (typeof message === 'string') {

View File

@ -5,7 +5,8 @@
"^tm-(.*)": "@/tmui/components/tm-$1/tm-$1.vue"
}
},
"pages": [{
"pages": [
{
"path": "pages/index/index",
"type": "page",
"style": {
@ -177,22 +178,6 @@
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{
"path": "pages/chatSettings/groupManage/manageGroupDeps",
"type": "page",
"style": {
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{
"path": "pages/complaintReport/index",
"type": "page",
"style": {
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
}
],
"globalStyle": {

View File

@ -9,7 +9,7 @@
? ''
: '0',
}"
v-for="(memberItem, memberIndex) in sortedMemberList"
v-for="(memberItem, memberIndex) in props?.memberList"
@click="toUserDetailPage(memberItem)"
>
<div
@ -53,7 +53,7 @@
>
<div class="group-member-each">
<div class="group-member-avatar" :style="{ background: 'unset' }">
<img src="@/static/image/chatSettings/add-member.png" />
<img src="/src/static/image/chatSettings/add-member.png" />
</div>
<div class="group-member-name">
<span class="text-[24rpx] font-regular">添加</span>
@ -71,7 +71,7 @@
>
<div class="group-member-each">
<div class="group-member-avatar" :style="{ background: 'unset' }">
<img src="@/static/image/chatSettings/remove-member.png" />
<img src="/src/static/image/chatSettings/remove-member.png" />
</div>
<div class="group-member-name">
<span class="text-[24rpx] font-regular">移除</span>
@ -81,7 +81,7 @@
</div>
</template>
<script setup>
import { defineProps, computed } from 'vue'
import { defineProps } from 'vue'
const props = defineProps({
memberList: Array, //
memberListsLimit: Number, //
@ -90,36 +90,6 @@ const props = defineProps({
hideAddRemoveBtns: Boolean, //
})
//
const sortedMemberList = computed(() => {
if (!props.memberList || props.memberList.length === 0) return [];
//
const indexedList = props.memberList.map((item, index) => ({ item, index }));
// leader
return indexedList.sort((a, b) => {
const leaderA = a.item.leader || 0;
const leaderB = b.item.leader || 0;
// leaderleader
if ((leaderA === 1 || leaderA === 2) && (leaderB !== 1 && leaderB !== 2)) {
return -1; // a
}
if ((leaderB === 1 || leaderB === 2) && (leaderA !== 1 && leaderA !== 2)) {
return 1; // b
}
// leader
if ((leaderA === 1 || leaderA === 2) && (leaderB === 1 || leaderB === 2)) {
return leaderB - leaderA; // leader
}
//
return a.index - b.index;
}).map(item => item.item); //
})
//
const toUserDetailPage = (userItem) => {
console.log(userItem)

View File

@ -49,7 +49,7 @@
props?.memberItem?.is_mine && props?.manageType === 'removeMembers'
"
>
<img src="@/static/image/chatSettings/is-mine.png" />
<img src="/src/static/image/chatSettings/is-mine.png" />
</div>
<div
class="is-admin-tag"

File diff suppressed because it is too large Load Diff

View File

@ -16,10 +16,9 @@
props?.item?.hasPointer &&
(props?.isManager ||
(!props?.isManager &&
(props?.item?.label !== $t('chat.settings.groupName') ||
props?.item?.label !== $t('chat.settings.groupType'))))
props?.item?.label !== $t('chat.settings.groupName')))
"
src="@/static/image/chatSettings/pointer.png"
src="/src/static/image/chatSettings/pointer.png"
/>
<tm-switch
:width="88"

View File

@ -22,7 +22,6 @@
<customBtn
:btnText="$t('button.text.edit')"
@click="editAvatar"
:isLoading="state.isLoading"
></customBtn>
</ZPaging>
</div>
@ -50,10 +49,6 @@ const dialogueParams = reactive({
receiver_id: computed(() => dialogueStore.talk.receiver_id),
})
const state = reactive({
isLoading: false, //
})
const onProgressFn = (progress, id) => {
console.log((progress.loaded / progress.total) * 100, 'progress')
@ -74,7 +69,6 @@ const editAvatar = () => {
count: 1,
success: async (res) => {
console.log(res, 'res')
state.isLoading = true
res.tempFiles.forEach(async (file) => {
console.log(file)
let image = new Image()
@ -99,7 +93,6 @@ const editAvatar = () => {
const resp = ServeEditGroup(params)
resp.then(({ code }) => {
if (code == 200) {
state.isLoading = false
groupStore.updateGroupInfo({
avatar: data.ori_url,
})
@ -107,18 +100,13 @@ const editAvatar = () => {
// delta: 1,
// })
} else {
state.isLoading = false
}
})
resp.catch(() => {
state.isLoading = false})
resp.catch(() => {})
} else {
state.isLoading = false
}
},
).catch(() => {
state.isLoading = false
})
)
}
})
},

View File

@ -29,13 +29,10 @@
:placeholder="$t('edit.groupName.placeholder')"
placeholder-style="color:#B4B4B4;font-size:28rpx;font-weight:500;line-height:40rpx;"
v-model="state.groupName"
@input="handleGroupNameInput"
maxlength="20"
/>
<img
v-if="state.groupName"
class="groupName-input-clearBtn"
src="@/static/image/chatSettings/clear-btn.png"
src="/src/static/image/chatSettings/clear-btn.png"
@click="clearGroupNameInput"
/>
</div>
@ -85,12 +82,7 @@ onLoad((options) => {
const clearGroupNameInput = () => {
state.groupName = ''
}
const handleGroupNameInput = (e) => {
if (state.groupName.length > 20) {
// 20
state.groupName = state.groupName.slice(0, 20)
}
}
//
const confirmEdit = () => {
console.log(state.groupName)
@ -154,7 +146,7 @@ const confirmEdit = () => {
height: 110rpx;
box-shadow: 0 6px 12px 2px rgba(188, 188, 188, 0.08);
padding: 0 74rpx 0 32rpx;
color: #747474;
color: #B747474;
font-size: 28rpx;
font-weight: 500;
line-height: 40rpx;

View File

@ -46,7 +46,7 @@
<div class="group-admin-list-each-btns">
<img
v-if="item.is_mine"
src="@/static/image/chatSettings/is-mine.png"
src="/src/static/image/chatSettings/is-mine.png"
/>
<div
class="group-admin-list-each-btns-each"
@ -67,7 +67,7 @@
margin: state?.groupAdminList.length == 0 ? '20rpx 0 0' : '',
}"
>
<img src="@/static/image/chatSettings/add-btn.png" />
<img src="/src/static/image/chatSettings/add-btn.png" />
<span class="text-[28rpx] font-medium">
{{ $t('chat.manage.addAdmin') }}
</span>

View File

@ -1,76 +0,0 @@
<template>
<div class="outer-layer manage-group-deps-page">
<div class="root">
<ZPaging
ref="zPaging"
:show-scrollbar="false"
:use-virtual-list="true"
:virtual-list-col="5"
:auto="false"
:refresher-enabled="false"
:loading-more-enabled="false"
>
<template #top>
<customNavbar :title="$t('pageTitle.view.deps')"></customNavbar>
</template>
<div class="group-deps-list">
<div
class="group-deps-list-each"
v-for="item in groupParams.groupInfo.deptInfos"
:key="item.dept_id"
>
<span>{{ item.dept_name }}</span>
</div>
</div>
</ZPaging>
</div>
</div>
</template>
<script setup>
import settingFormItem from '../components/settingFormItem.vue'
import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import { onLoad } from '@dcloudio/uni-app'
import { computed, onMounted, reactive } from 'vue'
import { useGroupStore } from '@/store'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const groupStore = useGroupStore()
const groupParams = reactive({
groupInfo: computed(() => groupStore.groupInfo),
})
const state = reactive({})
onLoad((options) => {
console.log(options)
})
onMounted(() => {
console.log(groupParams.groupInfo.deptInfos)
})
</script>
<style scoped lang="scss">
.outer-layer {
flex: 1;
background-image: url('@/static/image/clockIn/z3280@3x.png');
background-size: cover;
background-repeat: no-repeat;
}
.group-deps-list {
margin: 20rpx 32rpx;
background-color: #fff;
.group-deps-list-each {
padding: 34rpx 32rpx;
border-bottom: 1px solid $theme-border-color;
span {
font-size: 28rpx;
font-weight: 500;
color: #333;
line-height: 40rpx;
}
}
}
</style>

View File

@ -42,7 +42,7 @@
class="add-silence-member-btn chat-settings-card"
@click="toSelectMembersPage"
>
<img src="@/static/image/chatSettings/add-btn.png" />
<img src="/src/static/image/chatSettings/add-btn.png" />
<span class="text-[28rpx] font-medium">
{{ $t('chat.manage.addSilenceMember') }}
</span>

View File

@ -14,7 +14,7 @@
</span>
</div>
</template>
<template #right v-if="state.isManager === 'true'">
<template #right>
<div
v-if="state.editMode !== 3"
class="nav-bar-done-btn"
@ -39,7 +39,7 @@
</template>
<div class="notice-text-area">
<div class="notice-view-area">
<div class="notice-view-info" v-if="state.editMode !== 1 && state?.groupNoticeObj?.content">
<div class="notice-view-info" v-if="state.editMode !== 1">
<div class="notice-creater-avatar">
<img :src="state?.groupNoticeObj?.avatar" />
</div>
@ -53,12 +53,9 @@
</div>
</div>
<div class="notice-view-content" v-if="state.editMode === 3">
<span class="text-[32rpx] font-regular" v-if="state?.groupNoticeObj?.content">
<span class="text-[32rpx] font-regular">
{{ state?.groupNoticeObj?.content }}
</span>
<span class="text-[32rpx] font-regular color-[#898989]" v-else>
{{ $t('groupNotice.notice.empty') }}
</span>
</div>
<wd-textarea
size="large"
@ -80,7 +77,6 @@ import useConfirm from '@/components/x-confirm/useConfirm.js'
import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import { reactive, computed, onMounted } from 'vue'
import { useGroupStore, useDialogueStore } from '@/store'
import { onLoad } from '@dcloudio/uni-app'
import {
ServeEditGroupNotice,
ServeDeleteGroupNotice,
@ -102,8 +98,7 @@ const state = reactive({
groupNoticeObj: null, //
groupNotice: '', //
canDoComplete: false, //
editMode: 3, // 123
isManager: 'false', //
editMode: 1, // 123
})
onMounted(() => {
@ -112,14 +107,6 @@ onMounted(() => {
state.editMode = 3
state.groupNoticeObj = groupParams.groupNotice[0]
inputGroupNotice(groupParams?.groupNotice[0]?.content)
} else if( state.isManager === 'true'){
state.editMode = 1
}
})
onLoad((options) => {
console.log(options)
if (options?.is_manager) {
state.isManager = options?.is_manager
}
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -17,17 +17,10 @@
</div>
<div class="avatarImg">
<avatarModule
:mode="props?.data?.group_type === 0 ? 1 : 2"
:mode="2"
:avatar="props?.data?.avatar"
:groupType="props?.data?.group_type"
:userName="props?.data?.name"
:customStyle="{ width: '96rpx', height: '96rpx' }"
:customTextStyle="{
fontSize: '32rpx',
fontWeight: 'bold',
color: '#fff',
lineHeight: '44rpx',
}"
></avatarModule>
</div>
<div class="chatInfo">
@ -37,18 +30,7 @@
class="text-[#171717] text-[32rpx] font-medium leading-[44rpx]"
>
<span>{{ props.data.name }}</span>
<span v-if="props.data.talk_type === 2">
{{ props.data.group_member_num }}
</span>
<span v-if="props.data.group_type === 2" class="depTag tag">
部门
</span>
<span v-if="props.data.group_type === 3" class="projectTag tag">
项目
</span>
<span v-if="props.data.group_type === 4" class="companyTag tag">
公司
</span>
<span>{{ props.data.group_member_num }}</span>
</div>
</div>
</div>
@ -136,31 +118,16 @@ const cellClick = () => {
opacity: 40%;
}
.tag {
display: inline-flex;
align-items: center;
text-align: center;
// margin-left: 10rpx;
margin-top: 4rpx;
vertical-align: top;
height: 38rpx;
line-height: 38rpx;
padding: 0 10rpx;
font-size: 24rpx;
border-radius: 6rpx;
font-weight: bold;
}
.companyTag {
width: 76rpx;
height: 38rpx;
border: 1px solid #7a58de;
font-size: 24rpx;
text-align: center;
border-radius: 6rpx;
color: #7a58de;
}
.depTag {
border: 1px solid #377ec6;
color: #377ec6;
}
.projectTag {
border: 1px solid #c1681c;
color: #c1681c;
font-weight: bold;
margin-left: 12rpx;
}
.textEllipsis {

View File

@ -58,7 +58,6 @@
okColor="#FFFFFF"
@ok="handleOk"
@cancel="handleCancel"
:okText="'发送'"
>
<template v-slot:title>
<div
@ -87,7 +86,7 @@
<div
class="mt-[8rpx] text-[#666666] text-[24rpx] w-[94rpx] truncate"
>
<span>{{ item.name }}</span>
{{ item.name }}
</div>
</div>
<div v-else class="w-full flex items-center justify-start">
@ -99,16 +98,6 @@
></avatarModule>
<div class="ml-[16rpx] text-[#161616] text-[32rpx] font-bold">
{{ selectItemsModal[0].name }}
<span v-if="selectItemsModal[0].talk_type === 2">{{ selectItemsModal[0].group_member_num }}</span>
<span v-if="selectItemsModal[0].group_type === 2" class="depTag tag">
部门
</span>
<span v-if="selectItemsModal[0].group_type === 3" class="projectTag tag">
项目
</span>
<span v-if="selectItemsModal[0].group_type === 4" class="companyTag tag">
公司
</span>
</div>
</div>
</div>
@ -219,7 +208,7 @@ watch(
onMounted(() => {
talkStore.loadTalkList()
console.log(talkStore.talkItems)
items.value = lodash.cloneDeep(talkStore.talkItems).filter(item=>item.is_dismiss === 0 && item.is_quit === 0)
items.value = lodash.cloneDeep(talkStore.talkItems)
})
onUnmounted(() => {
dialogueStore.setForwardType('')
@ -252,32 +241,7 @@ onUnmounted(() => {
margin-top: 20rpx;
background-color: #fff;
}
.tag{
display: inline-flex;
align-items: center;
text-align: center;
// margin-left: 10rpx;
margin-top: 4rpx;
vertical-align: top;
height: 38rpx;
line-height: 38rpx;
padding: 0 10rpx;
font-size: 24rpx;
border-radius: 6rpx;
font-weight: bold;
}
.companyTag {
border: 1px solid #7a58de;
color: #7a58de;
}
.depTag {
border: 1px solid #377ec6;
color: #377ec6;
}
.projectTag {
border: 1px solid #c1681c;
color: #c1681c;
}
.btnBox {
::v-deep .custom-btn-class {
padding: 6rpx 24rpx !important;

View File

@ -15,7 +15,7 @@
</div>
<div class="mt-[54rpx] w-full h-[872rpx]">
<div
@click="groupActiveIndex = 0;depCheckedKeys = []"
@click="groupActiveIndex = 0"
class="groupCard firstPanel"
:class="groupActiveIndex === 0 ? 'activePanel' : ''"
>
@ -33,7 +33,6 @@
</div>
</div>
<div
v-if="isHasPermission"
@click="groupActiveIndex = 1"
class="groupCard secondPanel"
:class="groupActiveIndex === 1 ? 'activePanel' : ''"
@ -96,8 +95,7 @@
</div>
</div>
<div
v-if="isCreateProjecy"
@click="groupActiveIndex = 2;depCheckedKeys = [];"
@click="groupActiveIndex = 2"
class="groupCard thirdPanel"
:class="groupActiveIndex === 2 ? 'activePanel' : ''"
>
@ -133,36 +131,13 @@
import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import customBtn from '@/components/custom-btn/custom-btn.vue'
import { ref, watch, computed } from 'vue'
import { onShow, onLoad, onUnload } from '@dcloudio/uni-app'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { useChatList } from '@/store/chatList/index.js'
import { useAuth } from '@/store/auth'
import { useTalkStore, useUserStore } from '@/store'
import { useGroupTypeStore } from '@/store/groupType'
import { userHasPermission } from '@/api/deps/index.js'
const { groupActiveIndex, depCheckedKeys } = useGroupTypeStore()
const { userInfo } = useAuth()
onUnload(()=> {
})
const isHasPermission = ref(false)
const isCreateProjecy = ref(false)
onShow( async() =>{
const isHasRes = await userHasPermission({
erpUserId: userInfo?.value?.ID,
ruleUrl: [
"auth_chat_app_create_all_dept",
"auth_chat_app_create_limit_dept"
]
})
if (isHasRes.code === 200) {
if (isHasRes.data.auth_chat_app_create_all_dept || isHasRes.data.auth_chat_app_create_limit_dept) {
isHasPermission.value = true
} else{
isHasPermission.value = false
}
isCreateProjecy.value = isHasRes.data.btn_rule_create_project_group
}
})
const { groupActiveIndex, depCheckedKeys } = useGroupTypeStore()
const confirmBtnStatus = computed(() => {
let disabledT = false
@ -233,10 +208,10 @@ const handleConfirm = () => {
border-radius: 12rpx;
&.firstPanel {
background-image: url('@/static/image/chatList/zu6033@2x.png');
margin-bottom: 28rpx;
}
&.secondPanel {
background-image: url('@/static/image/chatList/zu6031@2x.png');
margin-top: 28rpx;
margin-bottom: 28rpx;
}
&.thirdPanel {

View File

@ -404,7 +404,7 @@ const getCurrentMembers = async (depItem) => {
departmentId: depItem.ID,
status: 'notactive',
})
if (res.code === 200) {
if (res.status === 0) {
currentMembers.value = res.data.data.length
? res.data.data.map((v) => {
return {
@ -628,7 +628,7 @@ const handleConfirm = async () => {
departmentIds: allCheckedList.value.map((v) => v.ID),
status: 'notactive',
})
if (res.code == 200 && res.data?.data?.length) {
if (res.status == 0 && res.data?.data?.length) {
res.data?.data.forEach((v) => {
listT.push(v)
})

View File

@ -1,366 +0,0 @@
<template>
<div class="outer-layer user-detail-page">
<div class="root">
<ZPaging ref="zPaging" :show-scrollbar="false">
<template #top>
<customNavbar :title="$t('complaint.title')"></customNavbar>
</template>
<!-- 投诉主体内容 -->
<view class="complaint-container">
<!-- 投诉类型选择 -->
<view class="form-item">
<text class="form-label">{{ $t('complaint.selectType') }}</text>
<picker mode="selector" :range="complaintTypes" range-key="label" @change="handleTypeChange">
<view class="picker">
{{ selectedType.label || $t('complaint.selectPlaceholder') }}
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</picker>
</view>
<!-- 图片证据上传 -->
<view class="form-item">
<text class="form-label">{{ $t('complaint.imageEvidence') }}</text>
<view class="upload-area">
<view v-for="(img, index) in imageList" :key="index" class="image-wrapper">
<image :src="img" mode="aspectFill" class="uploaded-image" @click="previewImage(index)" />
<uni-icons type="close" size="18" color="#fff" class="delete-icon"
@click="removeImage(index)"></uni-icons>
</view>
<view v-if="imageList.length < 9" class="upload-btn" @click="chooseImage">
<uni-icons type="plusempty" size="28" color="#999"></uni-icons>
<text class="upload-text">{{ $t('complaint.addImage') }}</text>
</view>
</view>
</view>
<!-- 投诉内容 -->
<view class="form-item">
<text class="form-label">{{ $t('complaint.complaintContent') }}</text>
<textarea v-model="complaintContent" :placeholder="$t('complaint.contentPlaceholder')"
class="content-textarea"></textarea>
</view>
<!-- 投诉须知 -->
<view class="notice-box">
<view class="notice-header" @click="toggleNotice">
<text class="notice-title">{{ $t('complaint.noticeTitle') }}</text>
<text class="toggle-btn">
{{ isNoticeExpanded ? $t('complaint.collapse') : $t('complaint.expand') }}
</text>
</view>
<!-- 折叠状态只显示前两项 -->
<view class="notice-content" v-if="!isNoticeExpanded">
<view class="notice-item">
<text>{{ $t('complaint.noticeone') }}</text>
</view>
<view class="notice-item">
<text>{{ $t('complaint.noticetwo') }}</text>
</view>
</view>
<!-- 展开状态显示全部 -->
<view class="notice-content" v-else>
<view class="notice-item">
<text>{{ $t('complaint.noticeone') }}</text>
</view>
<view class="notice-item">
<text>{{ $t('complaint.noticetwo') }}</text>
</view>
<view class="notice-item">
<text>{{ $t('complaint.noticethree') }}</text>
</view>
<view class="notice-item">
<text>{{ $t('complaint.noticefour') }}</text>
</view>
<view class="notice-item">
<text>{{ $t('complaint.noticefive') }}</text>
</view>
<view class="notice-item">
<text>{{ $t('complaint.noticenoticeContent') }}</text>
</view>
</view>
</view>
<button class="submit-btn" :disabled="!selectedType.value" @click="handleSubmit">
{{ $t('complaint.submit') }}
</button>
</view>
</ZPaging>
<!-- 固定在底部的提交按钮 -->
</div>
</div>
</template>
<script setup>
import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import {
ref
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import {
useI18n
} from 'vue-i18n';
const {
t
} = useI18n();
//
const complaintTypes = computed(() => [{
label: t('complaint.typeOptions.porn'),
value: 'porn'
},
{
label: t('complaint.typeOptions.illegal'),
value: 'illegal'
},
{
label: t('complaint.typeOptions.gambling'),
value: 'gambling'
},
{
label: t('complaint.typeOptions.violence'),
value: 'violence'
},
{
label: t('complaint.typeOptions.selfHarm'),
value: 'selfHarm'
},
{
label: t('complaint.typeOptions.other'),
value: 'other'
}
]);
//
const selectedType = ref({});
const imageList = ref([]);
const complaintContent = ref('');
const isNoticeExpanded = ref(false);
//
const toggleNotice = () => {
isNoticeExpanded.value = !isNoticeExpanded.value;
};
//
const handleTypeChange = (e) => {
selectedType.value = complaintTypes.value[e.detail.value];
};
//
const chooseImage = () => {
uni.chooseImage({
count: 9 - imageList.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
imageList.value = [...imageList.value, ...res.tempFilePaths];
if (imageList.value.length > 9) {
uni.showToast({
title: `最多只能选择9张图片`,
icon: 'none'
});
}
}
});
};
//
const removeImage = (index) => {
imageList.value.splice(index, 1);
};
//
const previewImage = (index) => {
uni.previewImage({
current: index,
urls: imageList.value
});
};
//
const handleSubmit = () => {
const formData = {
type: selectedType.value,
images: imageList.value,
content: complaintContent.value
};
uni.showLoading({
title: t('complaint.toast.submitting') // 使
});
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: t('complaint.toast.success'),
icon: 'success'
});
selectedType.value = {};
imageList.value = [];
complaintContent.value = '';
//
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 2000);
};
onLoad(() => {
//
});
</script>
<style scoped lang="scss">
::v-deep .uni-picker-action-confirm {
color: #452aa1 !important;
}
.outer-layer {
flex: 1;
background-image: url('@/static/image/mine/1111.png');
background-size: cover;
background-repeat: no-repeat;
}
.complaint-container {
padding: 20rpx 30rpx;
background-color: rgba(255, 255, 255, 0.9);
margin: 20rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.form-item {
margin-bottom: 40rpx;
}
.form-label {
display: block;
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 20rpx;
}
.picker {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
background-color: #f7f7f7;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
}
.upload-area {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.upload-btn {
width: 160rpx;
height: 160rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f7f7f7;
border-radius: 12rpx;
border: 1rpx dashed #ddd;
}
.upload-text {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.image-wrapper {
width: 160rpx;
height: 160rpx;
position: relative;
border-radius: 12rpx;
overflow: hidden;
}
.uploaded-image {
width: 100%;
height: 100%;
}
.delete-icon {
position: absolute;
top: 8rpx;
right: 8rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
padding: 4rpx;
}
.content-textarea {
width: 100%;
height: 200rpx;
padding: 20rpx;
background-color: #f7f7f7;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
}
.notice-box {
padding: 20rpx;
background-color: #f0f7ff;
border-radius: 12rpx;
margin: 40rpx 0;
}
.notice-title {
font-size: 28rpx;
color: #452aa1;
font-weight: bold;
display: block;
margin-bottom: 10rpx;
}
.notice-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
}
.toggle-btn {
color: #452aa1;
font-size: 24rpx;
}
.notice-content {
font-size: 24rpx;
color: #666;
line-height: 1.6;
padding-bottom: 20rpx;
}
.submit-btn {
margin-top: 40rpx;
background-color: #452aa1;
color: white;
border-radius: 8rpx;
height: 90rpx;
line-height: 90rpx;
font-size: 32rpx;
&[disabled] {
background-color: #f0f0f0;
}
}
</style>

View File

@ -8,8 +8,12 @@
<div class="group-avatar flex items-center justify-center">
<div class="avatar-placeholder" v-if="groupActiveIndex === -1"></div>
<div v-else>
<avatarModule :mode="2" :avatar="avatarImg" :groupType="groupType"
:customStyle="{ width: '192rpx', height: '192rpx' }"></avatarModule>
<avatarModule
:mode="2"
:avatar="avatarImg"
:groupType="groupType"
:customStyle="{ width: '192rpx', height: '192rpx' }"
></avatarModule>
</div>
</div>
<div class="input-group flex items-center justify-between">
@ -17,12 +21,25 @@
群名称
</div>
<div class="input-box">
<tm-input v-model="groupName" :followTheme="false" fontColor="#747474" placeholderStyle="color: #B4B4B4"
focusColor="#FFF" :fontSize="28" :maxlength="20" :height="40" :transprent="true"
placeholder="请输入群名称1~20个字" :padding="[0, 0]" align="right"></tm-input>
<tm-input
v-model="groupName"
:followTheme="false"
fontColor="#747474"
placeholderStyle="color: #B4B4B4"
focusColor="#FFF"
:fontSize="28"
:maxlength="20"
:height="40"
:transprent="true"
placeholder="请输入群名称1~20个字"
:padding="[0, 0]"
align="right"
></tm-input>
</div>
</div>
<div class="input-group w-full flex flex-col mt-[20rpx] leading-[40rpx]">
<div
class="input-group w-full flex flex-col mt-[20rpx] leading-[40rpx]"
>
<div class="flex items-center justify-between">
<div class="input-item">
群类型
@ -35,310 +52,350 @@
<span v-else-if="groupActiveIndex === 2">项目群</span>
</div>
<div class="ml-[32rpx]">
<tm-icon :font-size="22" color="#747474" name="tmicon-angle-right"></tm-icon>
<tm-icon
:font-size="22"
color="#747474"
name="tmicon-angle-right"
></tm-icon>
</div>
</div>
</div>
<div v-if="depCheckedKeys.length && groupActiveIndex === 1" class="mt-[32rpx]">
<div v-for="(item, index) in depCheckedKeys" class="text-[#747474] text-[28rpx] leading-[40rpx] font-bold"
<div
v-if="depCheckedKeys.length && groupActiveIndex === 1"
class="mt-[32rpx]"
>
<div
v-for="(item, index) in depCheckedKeys"
class="text-[#747474] text-[28rpx] leading-[40rpx] font-bold"
:class="[
index !== 0 ? 'mt-[10rpx]' : '',
depsNoExpanded_1 && index > 4 ? 'hidden' : '',
]">
depsNoExpanded && index > 4 ? 'hidden' : '',
]"
>
{{ item.name }}
</div>
<div class="text-[#46299D] text-[28rpx] mt-[20rpx] font-bold flex justify-center">
<div v-if="depCheckedKeys.length > 5" @click="depsNoExpanded_1 = !depsNoExpanded_1" class="w-[100rpx]">
{{ depsNoExpanded_1 ? '展开' : '收起' }}
<div
class="text-[#46299D] text-[28rpx] mt-[20rpx] font-bold flex justify-center"
>
<div
v-if="depCheckedKeys.length > 5"
@click="depsNoExpanded = !depsNoExpanded"
class="w-[100rpx]"
>
{{ depsNoExpanded ? '展开' : '收起' }}
</div>
</div>
</div>
</div>
<div v-if="groupActiveIndex === 0 || groupActiveIndex === 2"
class="input-group w-full flex flex-col mt-[20rpx] leading-[40rpx]">
<div
v-if="groupActiveIndex === 0 || groupActiveIndex === 2"
class="input-group w-full flex flex-col mt-[20rpx] leading-[40rpx]"
>
<div class="flex items-center justify-between">
<div class="input-item">
群成员
</div>
<div @click="chooseMembers" class="left-box">
<div class="ml-[32rpx] flex items-center">
<div class="text-[#B4B4B4] text-[28rpx] font-bold mr-[32rpx]">
<div
class="text-[#B4B4B4] text-[28rpx] font-bold mr-[32rpx]"
>
全部({{ allChooseMembers?.length || 0 }})
</div>
<tm-icon :font-size="22" color="#747474" name="tmicon-angle-right"></tm-icon>
<tm-icon
:font-size="22"
color="#747474"
name="tmicon-angle-right"
></tm-icon>
</div>
</div>
</div>
<groupMemberList :groupType="3" :is_manager="true" :memberList="allChooseMembers" :memberListsLimit="15"
:hideAddRemoveBtns="true"></groupMemberList>
<groupMemberList
:groupType="3"
:is_manager="true"
:memberList="allChooseMembers"
:memberListsLimit="15"
:hideAddRemoveBtns="true"
></groupMemberList>
</div>
<div v-if="groupActiveIndex === 1" class="input-group w-full flex flex-col mt-[20rpx] leading-[40rpx]">
<div
v-if="groupActiveIndex === 1"
class="input-group w-full flex flex-col mt-[20rpx] leading-[40rpx]"
>
<div class="flex items-center justify-between">
<div class="input-item">
群管理员
</div>
<div @click="chooseGroupAdmin" class="left-box">
<div class="ml-[32rpx] flex items-center">
<div v-if="!groupAdmins.length" class="text-[#B4B4B4] text-[28rpx] font-bold mr-[32rpx]">
<div
v-if="!groupAdmins.length"
class="text-[#B4B4B4] text-[28rpx] font-bold mr-[32rpx]"
>
请选择群管理员
</div>
<tm-icon :font-size="22" color="#747474" name="tmicon-angle-right"></tm-icon>
<tm-icon
:font-size="22"
color="#747474"
name="tmicon-angle-right"
></tm-icon>
</div>
</div>
</div>
<div v-if="groupAdmins.length" class="mt-[32rpx]">
<div v-for="(item, index) in groupAdmins" class="text-[#747474] text-[28rpx] leading-[40rpx] font-bold"
<div
v-for="(item, index) in groupAdmins"
class="text-[#747474] text-[28rpx] leading-[40rpx] font-bold"
:class="[
index !== 0 ? 'mt-[10rpx]' : '',
depsNoExpanded_2 && index > 4 ? 'hidden' : '',
]">
depsNoExpanded && index > 4 ? 'hidden' : '',
]"
>
{{ item.name }}
</div>
<div class="text-[#46299D] text-[28rpx] mt-[20rpx] font-bold flex justify-center">
<div v-if="groupAdmins.length > 5" @click="depsNoExpanded_2 = !depsNoExpanded_2" class="w-[100rpx]">
{{ depsNoExpanded_2 ? '展开' : '收起' }}
<div
class="text-[#46299D] text-[28rpx] mt-[20rpx] font-bold flex justify-center"
>
<div
v-if="groupAdmins.length > 5"
@click="depsNoExpanded = !depsNoExpanded"
class="w-[100rpx]"
>
{{ depsNoExpanded ? '展开' : '收起' }}
</div>
</div>
</div>
</div>
</div>
<template #bottom>
<customBtn :isBottom="true" :btnText="$t('pageTitle.create.group')" @click="handleConfirm"
:isLoading="isLoading" :disabled="confirmBtnStatus || isLoading"></customBtn>
<customBtn
:isBottom="true"
:btnText="$t('pageTitle.create.group')"
@click="handleConfirm"
:disabled="confirmBtnStatus"
></customBtn>
</template>
</zPaging>
</div>
</template>
<script setup>
import zPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import customBtn from '@/components/custom-btn/custom-btn.vue'
import groupMemberList from '../chatSettings/components/groupMembersList.vue'
import avatarModule from '@/components/avatar-module/index.vue'
import zPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import customBtn from '@/components/custom-btn/custom-btn.vue'
import groupMemberList from '../chatSettings/components/groupMembersList.vue'
import avatarModule from '@/components/avatar-module/index.vue'
import {
ref,
watch,
computed,
onMounted
} from 'vue'
import {
onShow,
onLoad,
onUnload
} from '@dcloudio/uni-app'
import {
useChatList
} from '@/store/chatList/index.js'
import {
useAuth
} from '@/store/auth'
import {
useTalkStore,
useUserStore,
useGroupStore
} from '@/store'
import addCircle from '@/static/image/chatList/addCircle.png'
import cahtPopover from '@/static/image/chatList/cahtPopover.png'
import {
ServeCreateGroup
} from '@/api/group/index'
import {
useGroupTypeStore
} from '@/store/groupType'
import {
handleSetWebviewStyle
} from '@/utils/common'
import { ref, watch, computed } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { useChatList } from '@/store/chatList/index.js'
import { useAuth } from '@/store/auth'
import { useTalkStore, useUserStore, useGroupStore } from '@/store'
import addCircle from '@/static/image/chatList/addCircle.png'
import cahtPopover from '@/static/image/chatList/cahtPopover.png'
import { ServeCreateGroup } from '@/api/group/index'
import { useGroupTypeStore } from '@/store/groupType'
const {
groupName,
groupActiveIndex,
depCheckedKeys,
groupAdmins,
createDepGroup,
resetGroupInfo,
allChooseMembers,
} = useGroupTypeStore()
const talkStore = useTalkStore()
const userStore = useUserStore()
const groupStore = useGroupStore()
const {
userInfo
} = useAuth()
const {
groupName,
groupActiveIndex,
depCheckedKeys,
groupAdmins,
createDepGroup,
resetGroupInfo,
allChooseMembers,
} = useGroupTypeStore()
const talkStore = useTalkStore()
const userStore = useUserStore()
const groupStore = useGroupStore()
const { userInfo } = useAuth()
const groupChatType = ref('')
const depsNoExpanded_1 = ref(true)
const depsNoExpanded_2 = ref(true)
const groupChatType = ref('')
const depsNoExpanded = ref(true)
onLoad(()=> {
groupStore.$reset()
})
onLoad(() => {
groupStore.$reset()
})
onUnload(() => {
resetGroupInfo();
})
onMounted(() => {
handleSetWebviewStyle()
//
const groupType = computed(() => {
let group_type = ''
switch (groupActiveIndex.value) {
case 0:
group_type = 1
break
case 1:
group_type = 2
break
case 2:
group_type = 3
break
default:
group_type = ''
}
return group_type
})
//
const chooseGroupType = () => {
uni.navigateTo({
url: '/pages/chooseGroupType/index',
})
}
//
const groupType = computed(() => {
let group_type = ''
switch (groupActiveIndex.value) {
case 0:
group_type = 1
break
case 1:
group_type = 2
break
case 2:
group_type = 3
break
default:
group_type = ''
const chooseGroupAdmin = () => {
uni.navigateTo({
url:
'/pages/chatSettings/groupManage/selectMembers?manageType=admin&isCreateDepGroup=1',
})
}
const chooseMembers = () => {
uni.navigateTo({
url: '/pages/chooseByDeps/index?chooseMode=2',
})
}
//
const handleConfirm = async () => {
// console.log(allChooseMembers.value)
let erp_ids = ''
if (allChooseMembers?.value?.length > 0) {
allChooseMembers?.value?.forEach((ele) => {
if (!erp_ids) {
erp_ids = String(ele.ID)
} else {
erp_ids += ',' + ele.ID
}
})
}
if (groupActiveIndex.value === 0) {
//
let params = {
avatar: '',
name: groupName.value,
erp_ids: erp_ids,
type: 1,
profile: '',
}
return group_type
})
//
const chooseGroupType = () => {
uni.navigateTo({
url: '/pages/chooseGroupType/index',
})
}
const chooseGroupAdmin = () => {
uni.navigateTo({
url: '/pages/chatSettings/groupManage/selectMembers?manageType=admin&isCreateDepGroup=1',
})
}
const chooseMembers = () => {
uni.navigateTo({
url: '/pages/chooseByDeps/index?chooseMode=2',
})
}
const isLoading = ref(false)
//
const handleConfirm = async () => {
if (isLoading.value) return
isLoading.value = true
try {
let erp_ids = ''
if (allChooseMembers?.value?.length > 0) {
allChooseMembers.value.forEach((ele) => {
if (!erp_ids) {
erp_ids = String(ele.ID)
} else {
erp_ids += ',' + ele.ID
}
})
}
let res = null
if (groupActiveIndex.value === 0) {
//
const params = {
avatar: '',
name: groupName.value,
erp_ids: erp_ids,
type: 1,
profile: '',
}
console.log('普通群参数:', params)
res = await ServeCreateGroup(params)
} else if (groupActiveIndex.value === 1) {
//
res = await createDepGroup()
} else if (groupActiveIndex.value === 2) {
//
const params = {
avatar: '',
name: groupName.value,
erp_ids: erp_ids,
type: 3,
profile: '',
}
console.log('项目群参数:', params)
res = await ServeCreateGroup(params)
}
if (res?.code === 200) {
resetGroupInfo()
uni.navigateBack()
}
} catch (err) {
console.error(err)
} finally {
isLoading.value = false
console.log(params)
const res = await ServeCreateGroup(params)
if (res.code === 200) {
resetGroupInfo()
uni.navigateBack()
}
} else if (groupActiveIndex.value === 1) {
//
const res = await createDepGroup()
if (res.code === 200) {
resetGroupInfo()
uni.navigateBack()
}
} else if (groupActiveIndex.value === 2) {
//
let params = {
avatar: '',
name: groupName.value,
erp_ids: erp_ids,
type: 3,
profile: '',
}
console.log(params)
const res = await ServeCreateGroup(params)
if (res.code === 200) {
resetGroupInfo()
uni.navigateBack()
}
} else {
}
//
const confirmBtnStatus = computed(() => {
return groupActiveIndex.value === -1;
});
}
onShow(() => {
depsNoExpanded_1.value = true;
depsNoExpanded_2.value = true;
})
//
const confirmBtnStatus = computed(() => {
let disabledT = false
console.log(groupActiveIndex.value !== -1)
if (
groupName.value === '' ||
(groupActiveIndex.value && groupActiveIndex.value === -1) ||
(!groupActiveIndex.value && groupActiveIndex.value !== 0)
) {
return true
}
switch (groupActiveIndex.value) {
case 0:
if(allChooseMembers?.value?.length < 3) {
disabledT = true
}
break
case 1:
if (!depCheckedKeys.value.length) {
disabledT = true
}
if (!groupAdmins.value.length) {
disabledT = true
}
break
case 2:
if(allChooseMembers?.value?.length < 3) {
disabledT = true
}
break
default:
break
}
return disabledT
})
onShow(() => {
depsNoExpanded.value = true
})
</script>
<style scoped lang="scss">
::v-deep .zp-paging-container-content {
height: 100%;
display: flex;
}
::v-deep .zp-paging-container-content {
height: 100%;
display: flex;
}
.create-group-chat {
background-image: url('@/static/image/clockIn/z3280@3x.png');
background-size: cover;
background-position: center bottom;
width: 100%;
padding: 0 32rpx 20rpx;
.group-avatar {
padding: 60rpx 0;
.avatar-placeholder {
width: 192rpx;
height: 192rpx;
background-color: #e0e0e0;
border-radius: 50%;
}
.create-group-chat {
background-image: url('@/static/image/clockIn/z3280@3x.png');
background-size: cover;
background-position: center bottom;
width: 100%;
padding: 0 32rpx 20rpx;
.group-avatar {
padding: 60rpx 0;
.avatar-placeholder {
width: 192rpx;
height: 192rpx;
background-color: #e0e0e0;
border-radius: 50%;
}
}
}
.divider {
height: 1rpx;
background-color: #7c7c7c;
opacity: 0.6;
}
.divider {
height: 1rpx;
background-color: #7c7c7c;
opacity: 0.6;
}
.input-group {
background-color: #fff;
padding: 38rpx 40rpx 32rpx 32rpx;
}
.input-item {
line-height: 40rpx;
font-size: 28rpx;
color: #000;
font-weight: bold;
}
.input-box {
margin-left: 84rpx;
line-height: 40rpx;
width: 404rpx;
font-weight: bold;
}
.left-box {
display: flex;
align-items: center;
}
.input-group {
background-color: #fff;
padding: 38rpx 40rpx 32rpx 32rpx;
}
.input-item {
line-height: 40rpx;
font-size: 28rpx;
color: #000;
font-weight: bold;
}
.input-box {
margin-left: 84rpx;
line-height: 40rpx;
width: 404rpx;
font-weight: bold;
}
.left-box {
display: flex;
align-items: center;
}
</style>

View File

@ -1,34 +1,25 @@
<template>
<div>
<zPaging :fixed="false" :height="'210px'" :show-scrollbar="false">
<div class="emojiRoot">
<div
v-for="(img, key) in emojis"
v-html="img"
:key="key"
@click="onSendEmoticon(1, key, img)"
class="option pointer flex-center"
/>
</div>
</zPaging>
<div class="emojiRoot">
<div v-for="(img, key) in emojis" v-html="img" :key="key" @click="onSendEmoticon(1, key, img)"
class="option pointer flex-center" />
</div>
</template>
<script setup>
import zPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import { ref, reactive, defineProps, defineEmits } from 'vue'
import dayjs from 'dayjs'
import { ref, reactive, defineProps,defineEmits } from "vue"
import dayjs from "dayjs";
import { beautifyTime } from '@/utils/datetime'
import { useTalkStore } from '@/store'
import { useSessionMenu } from '@/hooks'
import { emojis } from '@/utils/emojis'
const props = defineProps({
data: {
type: Object,
default: {},
required: false,
},
})
});
const emit = defineEmits(['on-select'])
const onSendEmoticon = (type, value, img = '') => {
@ -42,15 +33,21 @@ const onSendEmoticon = (type, value, img = '') => {
emit('on-select', { type, value, img })
}
}
</script>
<style lang="scss" scoped>
.emojiRoot {
width: 100%;
height: 420rpx;
padding: 5rpx 32rpx;
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
overflow: auto;
}
.option {

View File

@ -1,39 +1,25 @@
<template>
<div class="emojiRoot">
<div
@click="() => photoActionsSelect(0)"
class="flex flex-col items-center"
>
<div
class="w-[106rpx] h-[106rpx] bg-[#F9F9F9] rounded-[60rpx] flex-center"
>
<div @click="()=>photoActionsSelect(0)" class="flex flex-col items-center">
<div class="w-[106rpx] h-[106rpx] bg-[#F9F9F9] rounded-[60rpx] flex-center">
<tm-image :width="53" :height="44" :src="photoAlbum"></tm-image>
</div>
<div class="mt-[6rpx] text-[#666666] text-[24rpx]">照片</div>
</div>
<div
@click="() => photoActionsSelect(1)"
class="flex flex-col items-center"
>
<div
class="w-[106rpx] h-[106rpx] bg-[#F9F9F9] rounded-[60rpx] flex-center"
>
<div @click="()=>photoActionsSelect(1)" class="flex flex-col items-center">
<div class="w-[106rpx] h-[106rpx] bg-[#F9F9F9] rounded-[60rpx] flex-center">
<tm-image :width="53" :height="44" :src="videoImg"></tm-image>
</div>
<div class="mt-[6rpx] text-[#666666] text-[24rpx]">视频</div>
</div>
<div @click="takePhoto" class="flex flex-col items-center">
<div
class="w-[106rpx] h-[106rpx] bg-[#F9F9F9] rounded-[60rpx] flex-center"
>
<div class="w-[106rpx] h-[106rpx] bg-[#F9F9F9] rounded-[60rpx] flex-center">
<tm-image :width="53" :height="44" :src="photoGraph"></tm-image>
</div>
<div class="mt-[6rpx] text-[#666666] text-[24rpx]">拍摄</div>
</div>
<div @click="chooseFile" class="flex flex-col items-center">
<div
class="w-[106rpx] h-[106rpx] bg-[#F9F9F9] rounded-[60rpx] flex-center"
>
<div class="w-[106rpx] h-[106rpx] bg-[#F9F9F9] rounded-[60rpx] flex-center">
<tm-image :width="53" :height="44" :src="folder"></tm-image>
</div>
<div class="mt-[6rpx] text-[#666666] text-[24rpx]">文件</div>
@ -41,15 +27,10 @@
</div>
</template>
<script setup>
import { ref, reactive, defineProps, defineEmits } from 'vue'
import dayjs from 'dayjs'
import { ref, reactive, defineProps, defineEmits } from "vue"
import dayjs from "dayjs";
import { beautifyTime } from '@/utils/datetime'
import {
useDialogueListStore,
useDialogueStore,
useUserStore,
useUploadsStore,
} from '@/store'
import { useDialogueListStore, useDialogueStore, useUserStore,useUploadsStore } from '@/store'
import { useSessionMenu } from '@/hooks'
import photoAlbum from '@/static/image/chatList/photoAlbum.png'
import photoGraph from '@/static/image/chatList/photoGraph.png'
@ -62,167 +43,82 @@ const props = defineProps({
sendUserInfo: {
type: Object,
default: {},
required: true,
required: true
},
talkParams: {
type: Object,
default: {},
required: true,
},
})
const state = reactive({
base64Url: '',
})
required: true
}
});
const uploadsStore = useUploadsStore()
const {
addDialogueRecord,
virtualList,
updateUploadProgress,
} = useDialogueListStore()
const { addDialogueRecord, virtualList, updateUploadProgress } = useDialogueListStore()
const dialogueStore = useDialogueStore()
const userStore = useUserStore()
const emit = defineEmits(['selectImg'])
const onProgressFn = (progress, id) => {
console.log((progress.loaded / progress.total) * 100, 'progress')
console.log(progress.loaded / progress.total * 100, 'progress');
updateUploadProgress(id, (progress.loaded / progress.total) * 100)
updateUploadProgress(id, progress.loaded / progress.total * 100)
}
const photoActionsSelect = (index) => {
if (index === 0) {
if (typeof plus === 'undefined') {
uni.chooseImage({
sourceType: ['album'],
count: 9,
success: async (res) => {
console.log(res, 'res')
res.tempFiles.forEach(async (file) => {
const fileSizeInMB = (file.size / (1024 * 1024)).toFixed(2)
if (fileSizeInMB > 100) {
plus.nativeUI.toast('图片大小不能超过100MB')
return
}
const result = await onUploadImageVideo(file, 'image')
if (result) {
emit('selectImg', result, result.file_num)
}
})
},
})
} else {
plus?.gallery.pick(
(res) => {
console.log(res, 'res')
res.files.reverse()
res.files.forEach(async (filePath) => {
plus?.io?.resolveLocalFileSystemURL(
filePath,
async (entry) => {
entry.file((file) => {
const fileReader = new plus.io.FileReader()
fileReader.readAsDataURL(file)
fileReader.onloadend = async (e) => {
const base64Url = e.target.result
const fileObj = base64ToFile(base64Url)
const fileSizeInMB = (fileObj.size / (1024 * 1024)).toFixed(
2,
)
if (fileSizeInMB > 100) {
plus.nativeUI.toast('图片大小不能超过100MB')
return
}
let data = await onUploadImageVideo(fileObj, 'image')
if (data) {
emit('selectImg', data, data.file_num)
}
}
})
},
(err) => {
console.log(err)
},
)
})
},
(err) => {
console.log(err)
},
{
filter: 'image',
maximum: 9,
multiple: true,
onmaxed: () => {
plus.nativeUI.toast('最多只能选择9张图片')
},
},
)
}
} else {
// let OAWebView = plus.webview.all()
// OAWebView.forEach((webview, index) => {
// if (webview.id === 'webviewId1') {
// webview.evalJS(`getPlusVideoPicker()`)
// }
// })
// return
uni.chooseImage({
sourceType: ['album'],
count: 9,
success: async (res) => {
console.log(res,'res');
res.tempFiles.forEach(async (file) => {
let data = await onUploadImageVideo(file, 'image')
emit('selectImg', data)
})
}
})
}else{
uni.chooseVideo({
sourceType: ['album'],
compressed: true,
maxDuration: 60,
success: async (res) => {
console.log(res, 'res')
const fileSizeInMB = (res.tempFile.size / (1024 * 1024)).toFixed(2)
if (fileSizeInMB > 100) {
plus.nativeUI.toast('视频大小不能超过100MB')
return
}
let data = await onUploadImageVideo(
res.tempFile,
'video',
res.tempFilePath,
)
if (data) {
emit('selectImg', data, data.file_num)
}
},
console.log(res,'res');
let data = await onUploadImageVideo(res.tempFile, 'video',res.tempFilePath)
emit('selectImg', data)
}
})
}
}
const onUploadImageVideo = async (file, type = 'image', fileUrl) => {
console.log('开始上传文件:', file.name)
uploadsStore.updateUploadStatus(true)
const onUploadImageVideo = async (file, type = 'image',fileUrl) => {
console.log(file, 'file');
return new Promise(async (resolve) => {
if (type === 'image') {
let image = new Image()
image.src = URL.createObjectURL(file)
image.onload = async () => {
image.onload = () => {
const form = new FormData()
form.append('file', file)
form.append('source', 'fonchain-chat')
form.append('urlParam', `width=${image.width}&height=${image.height}`)
form.append("source", "fonchain-chat");
form.append("urlParam", `width=${image.width}&height=${image.height}`);
let randomId = uniqueId()
let newItem = {
avatar: userStore.avatar,
created_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
extra: {
height: image.height,
name: '',
name: "",
size: 0,
url: image.src,
width: image.width,
width: image.width
},
float: 'right',
float: "right",
isCheck: false,
is_mark: 0,
is_read: 0,
is_revoke: 0,
msg_id: randomId,
file_num: randomId,
msg_type: 3,
nickname: userStore.nickname,
receiver_id: dialogueStore.talk.receiver_id,
@ -230,372 +126,210 @@ const onUploadImageVideo = async (file, type = 'image', fileUrl) => {
talk_type: dialogueStore.talk.talk_type,
user_id: userStore.uid,
uploadCurrent: 0,
uploadStatus: 1, // 1 2 3
uploadStatus: 1, // 1 2 3
}
virtualList.value.unshift(newItem)
try {
const result = await uploadImg(form, (e) => onProgressFn(e, randomId))
console.log('上传完成,结果:', result)
if (result.status === 0) {
//
const index = virtualList.value.findIndex(
(item) => item.file_num === randomId,
)
if (index !== -1) {
virtualList.value[index].uploadStatus = 2
virtualList.value[index].uploadCurrent = 100
}
//
uploadImg(form, (e) => onProgressFn(e, randomId)).then(({ status, data, msg }) => {
if (status == 0) {
resolve({
type: 'image',
url: result.data.ori_url,
url: data.ori_url,
size: file.size,
width: image.width,
height: image.height,
file_num: randomId,
height: image.height
})
} else {
uploadsStore.updateUploadStatus(false)
//
const index = virtualList.value.findIndex(
(item) => item.file_num === randomId,
)
if (index !== -1) {
virtualList.value[index].uploadStatus = 3
}
message.error(result.msg)
resolve('')
message.error(msg)
}
} catch (error) {
console.error('上传出错:', error)
uploadsStore.updateUploadStatus(false)
//
const index = virtualList.value.findIndex(
(item) => item.file_num === randomId,
)
if (index !== -1) {
virtualList.value[index].uploadStatus = 3
}
message.error('上传失败')
resolve('')
}
})
}
} else {
uni.getVideoInfo({
src: fileUrl,
success: async (resp) => {
console.log('视频信息:', resp)
const form = new FormData()
form.append('file', file)
form.append('source', 'fonchain-chat')
form.append('type', 'video')
form.append('urlParam', `width=${resp.width}&height=${resp.height}`)
let randomId = uniqueId()
let newItem = {
avatar: userStore.avatar,
created_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
extra: {
src:fileUrl,
success:(resp)=>{
console.log(resp);
form.append('file', file)
form.append("source", "fonchain-chat");
form.append("type", "video");
form.append("urlParam", `width=${resp.width}&height=${resp.height}`);
let randomId = uniqueId()
let newItem = {
avatar: userStore.avatar,
created_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
extra: {
duration: parseInt(resp.duration),
height: resp.height,
name: "",
url: fileUrl,
width: resp.width
},
float: "right",
isCheck: false,
is_mark: 0,
is_read: 0,
is_revoke: 0,
msg_id: randomId,
msg_type: 5,
nickname: userStore.nickname,
receiver_id: dialogueStore.talk.receiver_id,
sequence: -1,
talk_type: dialogueStore.talk.talk_type,
user_id: userStore.uid,
uploadCurrent: 0,
uploadStatus: 1, // 1 2 3
}
virtualList.value.unshift(newItem)
uploadImg(form, (e) => onProgressFn(e, randomId)).then(({ status, data, msg }) => {
if (status == 0) {
console.log(data);
resolve({
type: 'video',
url: data.ori_url,
cover: data.cover_url,
duration: parseInt(resp.duration),
height: resp.height,
name: '',
url: fileUrl,
width: resp.width,
},
float: 'right',
isCheck: false,
is_mark: 0,
is_read: 0,
is_revoke: 0,
msg_id: randomId,
file_num: randomId,
msg_type: 5,
nickname: userStore.nickname,
receiver_id: dialogueStore.talk.receiver_id,
sequence: -1,
talk_type: dialogueStore.talk.talk_type,
user_id: userStore.uid,
uploadCurrent: 0,
uploadStatus: 1,
size: file.size
})
} else {
// resolve('')
// message.error(msg)
}
virtualList.value.unshift(newItem)
try {
const result = await uploadImg(form, (e) =>
onProgressFn(e, randomId),
)
console.log('视频上传完成,结果:', result)
if (result.status === 0) {
//
const index = virtualList.value.findIndex(
(item) => item.file_num === randomId,
)
if (index !== -1) {
virtualList.value[index].uploadStatus = 2
virtualList.value[index].uploadCurrent = 100
}
resolve({
type: 'video',
url: result.data.ori_url,
cover: result.data.cover_url,
duration: parseInt(resp.duration),
size: file.size,
file_num: randomId,
})
} else {
uploadsStore.updateUploadStatus(false)
//
const index = virtualList.value.findIndex(
(item) => item.file_num === randomId,
)
if (index !== -1) {
virtualList.value[index].uploadStatus = 3
}
message.error(result.msg)
resolve('')
}
} catch (error) {
console.error('视频上传出错:', error)
uploadsStore.updateUploadStatus(false)
//
const index = virtualList.value.findIndex(
(item) => item.file_num === randomId,
)
if (index !== -1) {
virtualList.value[index].uploadStatus = 3
}
message.error('上传失败')
resolve('')
}
},
fail: (error) => {
console.error('获取视频信息失败:', error)
uploadsStore.updateUploadStatus(false)
message.error('获取视频信息失败')
resolve('')
},
})
}
})
const form = new FormData()
}
})
}
const base64ToFile = (base64) => {
if (!base64) {
message.warning('您的系统暂不支持发送原图哦')
}
// base64file
const [header, base64String] = base64.split(';base64,')
const imageType = header.split(':')[1]
const byteCharacters = atob(base64String)
const [header, base64String] = base64.split(";base64,");
const imageType = header.split(":")[1];
const byteCharacters = atob(base64String);
const byteArray = new Uint8Array(
Array.from(byteCharacters, (char) => char.charCodeAt(0)),
)
return new File([new Blob([byteArray], { type: imageType })], 'example.png', {
type: imageType,
})
Array.from(byteCharacters, (char) => char.charCodeAt(0))
);
return new File(
[new Blob([byteArray], { type: imageType })],
"example.png",
{ type: imageType }
);
}
const choosePhoto = (filter = 'none', maximum = 9, multiple = true) => {
window.plus?.gallery.pick(
(res) => {
console.log(res)
res.files.reverse()
res.files.forEach(async (filePath) => {
const suffix = filePath.split('.').pop()?.toLowerCase() || ''
if (['jpg', 'png'].includes(suffix)) {
console.log('进入图片')
window.plus?.io?.resolveLocalFileSystemURL(
filePath,
async (entry) => {
entry.file((file) => {
const fileReader = new plus.io.FileReader()
fileReader.readAsDataURL(file)
fileReader.onloadend = async (e) => {
const base64Url = e.target.result
const fileObj = base64ToFile(base64Url)
let data = await onUploadImageVideo(fileObj, 'image')
emit('selectImg', data)
}
})
},
(err) => {
console.log(err)
},
)
}
if (['mp4', 'flv'].includes(suffix)) {
console.log(filePath, '进入视频')
// const localUrl = plus.io.convertLocalFileSystemURL(filePath)
// console.log(localUrl);
plus.io.getVideoInfo({
filePath: filePath,
success: (event) => {
console.log(event)
},
fail: (err) => {
console.log(err)
},
window.plus?.gallery.pick((res) => {
console.log(res);
res.files.reverse()
res.files.forEach(async (filePath) => {
const suffix = filePath.split('.').pop()?.toLowerCase() || ''
if (['jpg', 'png'].includes(suffix)) {
console.log("进入图片")
window.plus?.io?.resolveLocalFileSystemURL(filePath, async (entry) => {
entry.file((file) => {
const fileReader = new plus.io.FileReader();
fileReader.readAsDataURL(file);
fileReader.onloadend = async (e) => {
const base64Url = e.target.result;
const fileObj = base64ToFile(base64Url);
let data = await onUploadImageVideo(fileObj, 'image')
emit('selectImg', data)
};
})
// window.plus?.io?.resolveLocalFileSystemURL(localUrl, async (entry) => {
// entry.file((file) => {
// console.log(file,'file');
// const fileReader = new plus.io.FileReader();
// fileReader.readAsDataURL(file);
// fileReader.onloadend = async (e) => {
// const base64Url = e.target.result;
// const fileObj = base64ToFile(base64Url);
// let data = await onUploadImageVideo(fileObj, 'video')
// emit('selectImg', data)
// };
// })
// },
// (err) => {
// console.log(err);
// }
// )
},
(err) => {
console.log(err);
}
)
}
if (['mp4', 'flv'].includes(suffix)) {
console.log(filePath,"进入视频")
// const localUrl = plus.io.convertLocalFileSystemURL(filePath)
// console.log(localUrl);
plus.io.getVideoInfo({
filePath:filePath,
success:(event)=>{
console.log(event);
},
fail:(err)=>{
console.log(err);
}
})
},
(err) => {
console.log(err)
},
});
// window.plus?.io?.resolveLocalFileSystemURL(localUrl, async (entry) => {
// entry.file((file) => {
// console.log(file,'file');
// const fileReader = new plus.io.FileReader();
// fileReader.readAsDataURL(file);
// fileReader.onloadend = async (e) => {
// const base64Url = e.target.result;
// const fileObj = base64ToFile(base64Url);
// let data = await onUploadImageVideo(fileObj, 'video')
// emit('selectImg', data)
// };
// })
// },
// (err) => {
// console.log(err);
// }
// )
}
})
}, (err) => {
console.log(err);
},
{
filter: filter,
maximum: maximum,
multiple: multiple,
},
}
)
}
const takePhoto = () => {
if (typeof plus !== 'undefined') {
getCamera()
} else {
document.addEventListener('plusready', () => {
getCamera()
})
}
}
const getCamera = () => {
const cmr = plus.camera.getCamera()
cmr.captureImage(
(p) => {
plus.io.resolveLocalFileSystemURL(
p,
(entry) => {
compressAndShowImage(entry.toLocalURL(), entry.name)
},
(err) => {
console.log(err)
},
)
},
() => {},
{ index: '2' },
)
}
const compressAndShowImage = (url, filename) => {
const dst = `_doc/upload/${filename}`
plus.zip.compressImage(
{ src: url, dst, quality: 10, overwrite: true },
(zip) => displayImage(zip.target),
(err) => {
console.log(err)
},
)
}
const displayImage = (url) => {
plus.io.resolveLocalFileSystemURL(url, (entry) => {
entry.file((file) => {
const fileReader = new plus.io.FileReader()
fileReader.readAsDataURL(file)
fileReader.onloadend = async (e) => {
state.base64Url = e.target.result
const imageFile = base64ToFile(state.base64Url)
let data = await onUploadImageVideo(imageFile, 'image')
emit('selectImg', data, data.file_num)
}
})
})
}
const chooseFile = () => {
uni.chooseFile({
count: 1,
extension: [''],
extension:[''],
success: (res) => {
const fileSizeInMB = (res.tempFiles[0].size / (1024 * 1024)).toFixed(2)
if (fileSizeInMB > 100) {
plus.nativeUI.toast('文件大小不能超过100MB')
return
}
let randomId = uniqueId()
let newItem = {
avatar: userStore.avatar,
created_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
extra: {
drive: 3,
name: res.tempFiles[0].name,
size: res.tempFiles[0].size,
path: res.tempFilePaths[0],
},
float: 'right',
isCheck: false,
is_mark: 0,
is_read: 0,
is_revoke: 0,
msg_id: randomId,
file_num: randomId,
msg_type: 6,
nickname: userStore.nickname,
receiver_id: dialogueStore.talk.receiver_id,
sequence: -1,
talk_type: dialogueStore.talk.talk_type,
user_id: userStore.uid,
uploadCurrent: 0,
uploadStatus: 1, // 1 2 3
}
let newItem = {
avatar: userStore.avatar,
created_at: dayjs().format('YYYY-MM-DD HH:mm:ss'),
extra: {
drive: 3,
name: res.tempFiles[0].name,
size: res.tempFiles[0].size,
path: res.tempFilePaths[0],
},
float: "right",
isCheck: false,
is_mark: 0,
is_read: 0,
is_revoke: 0,
msg_id: randomId,
msg_type: 6,
nickname: userStore.nickname,
receiver_id: dialogueStore.talk.receiver_id,
sequence: -1,
talk_type: dialogueStore.talk.talk_type,
user_id: userStore.uid,
uploadCurrent: 0,
uploadStatus: 1, // 1 2 3
}
virtualList.value.unshift(newItem)
uploadsStore.updateUploadStatus(true)
uploadsStore.initUploadFile(
res.tempFiles[0],
props.talkParams,
randomId,
(status, data, msg) => {
if (status === 0) {
//
const index = virtualList.value.findIndex(
(item) => item.file_num === randomId,
)
if (index !== -1) {
virtualList.value[index].uploadStatus = 2
virtualList.value[index].uploadCurrent = 100
}
} else {
uploadsStore.updateUploadStatus(false)
//
const index = virtualList.value.findIndex(
(item) => item.file_num === randomId,
)
if (index !== -1) {
virtualList.value[index].uploadStatus = 3
}
message.error(msg)
}
},
)
},
uploadsStore.initUploadFile(res.tempFiles[0], props.talkParams,randomId)
}
})
}
</script>
<style lang="scss" scoped>
.emojiRoot {

View File

@ -37,7 +37,7 @@
</div>
<div class="user-info-main user-info-card">
<div class="user-info-main-title">
<img src="@/static/image/mine/ming001@3x.png" />
<img src="/src/static/image/mine/ming001@3x.png" />
<span class="text-[28rpx] font-medium">
{{ $t('index.mine.basic') }}
</span>
@ -62,43 +62,12 @@
<customBtn
:isBottom="true"
:btnText="$t('user.detail.sendMsg')"
:subBtnText="
state.userInfo.sys_id === state.uid
? ''
: $t('user.detail.ringBell')
"
:subBtnText="$t('user.detail.ringBell')"
@clickBtn="toTalkUser"
@clickSubBtn="handleCall"
></customBtn>
</template>
</ZPaging>
</div>
<tm-drawer
placement="bottom"
v-model:show="state.isShowPhoneCall"
:hideHeader="true"
:height="416"
:round="6"
>
<div class="do-phone-call">
<div class="do-phone-call-header">
<span>{{ $t('popup.title.phone') }}</span>
<img
src="@/static/image/login/check-circle-filled@3x.png"
@click="hidePhoneCallPopup"
/>
</div>
<div class="do-phone-call-number">
<span>{{ state.phoneNumber }}</span>
</div>
<div class="do-phone-call-btn">
<customBtn
:btnText="$t('do.phone.call')"
@clickBtn="doPhoneCall"
></customBtn>
</div>
</div>
</tm-drawer>
</div>
</template>
<script setup>
@ -107,9 +76,8 @@ import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import { onLoad } from '@dcloudio/uni-app'
import { reactive } from 'vue'
import { useTalkStore, useUserStore } from '@/store'
import { useTalkStore } from '@/store'
const talkStore = useTalkStore()
const userStore = useUserStore()
import { getUserInfoByClickAvatar } from '@/api/user/index'
@ -120,9 +88,6 @@ const state = reactive({
erpUserId: '', //erpid
userInfo: null, //
userBasicInfos: [], //
isShowPhoneCall: false, //
phoneNumber: '', //
uid: computed(() => userStore.uid), //id
})
onLoad((options) => {
@ -202,7 +167,6 @@ const getUserInfo = () => {
value: data.enter_date,
},
]
state.phoneNumber = data.tel_num
} else {
}
})
@ -210,33 +174,10 @@ const getUserInfo = () => {
resp.catch(() => {})
}
//
const handleCall = () => {
state.isShowPhoneCall = true
}
//
const hidePhoneCallPopup = () => {
state.isShowPhoneCall = false
}
//
const toTalkUser = () => {
talkStore.toTalk(1, state.userInfo.sys_id, state.erpUserId)
}
//
const doPhoneCall = () => {
uni.makePhoneCall({
phoneNumber: state.phoneNumber,
success: () => {
console.log('拨号成功')
},
fail: (err) => {
console.error('失败:', err)
},
})
}
</script>
<style scoped lang="scss">
.outer-layer {
@ -349,58 +290,4 @@ const doPhoneCall = () => {
}
}
}
.do-phone-call {
.do-phone-call-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 42rpx 0;
position: relative;
span {
font-size: 28rpx;
line-height: 40rpx;
color: #747474;
font-weight: 400;
}
img {
position: absolute;
top: 44rpx;
right: 30rpx;
width: 36rpx;
height: 36rpx;
}
}
.do-phone-call-number {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 32rpx 0;
border-top: 2rpx solid #e7e7e7;
border-bottom: 2rpx solid #e7e7e7;
span {
font-size: 32rpx;
line-height: 44rpx;
color: #1a1a1a;
font-weight: 400;
}
}
.do-phone-call-btn {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 50rpx 0;
:deep(.custom-btn-class) {
width: 690rpx;
font-size: 32rpx;
line-height: 44rpx;
padding: 18rpx 0;
height: 80rpx;
background: linear-gradient(to right, #674bbc, #46299d);
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
<script lang="ts" setup>
import zPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import { ref, onMounted } from 'vue'
import { ServeGetForwardRecords } from '@/api/chat'
import { MessageComponents } from '@/constant/message'
@ -25,13 +24,8 @@ const onLoadData = () => {
}).then((res) => {
if (res.code == 200) {
items.value = res.data.items || []
//
const uniqueNames = [...new Set(items.value.map(v => v.nickname))];
if (uniqueNames.length <= 2) {
title.value = uniqueNames.join('和');
} else {
title.value = uniqueNames.slice(0, 2).join('和') + '等';
}
title.value = [...new Set(items.value.map((v) => v.nickname))].join('、')
}
})
}
@ -48,12 +42,12 @@ onMounted(() => {
</script>
<template>
<div class="forward-record-page">
<zPaging ref="zPaging" :show-scrollbar="false">
<template #top>
<customNavbar :title="`${title}的会话记录`"></customNavbar>
</template>
<div class="main-box">
<div class="outer-layer">
<div>
<tm-navbar :hideBack="false" hideHome :title="`${title}的会话记录`" >
</tm-navbar>
</div>
<div class="main-box">
<div v-if="items.length === 0" class="flex justify-center items-center w-full mt-[200rpx]">
<wd-loading />
</div>
@ -77,15 +71,16 @@ onMounted(() => {
</div>
</div>
</div>
</zPaging>
</div>
</template>
<style lang="less" scoped>
.forward-record-page {
.outer-layer {
overflow-y: auto;
flex: 1;
background-image: url("@/static/image/clockIn/z3280@3x.png");
background-size: cover;
padding: 0 66rpx 20rpx 50rpx;
display: flex;
flex-direction: column;
}
@ -96,7 +91,7 @@ onMounted(() => {
display: flex;
flex-direction: column;
overflow: auto;
padding: 28rpx 66rpx 20rpx 50rpx;
padding-top: 28rpx;
}
.message-item {

View File

@ -1,50 +1,40 @@
<template>
<div>
<wd-swipe-action class="swipe_action">
<div
@click="cellClick"
:class="['chatItem', props.data.is_top === 1 ? 'isTop' : '']"
>
<div class="avatarImg">
<tm-badge
:count="props.data.is_disturb === 0 ? props.data.unread_num : ''"
:dot="
props.data.is_disturb === 1 && props.data.unread_num
? true
: false
"
:maxCount="99"
class="badge"
color="#D03050"
>
<avatarModule
:mode="props?.data?.group_type === 0 ? 1 : 2"
:avatar="props?.data?.avatar"
:groupType="props?.data?.group_type"
:userName="props?.data?.name"
:customStyle="{
width: '96rpx',
height: '96rpx',
}"
:customTextStyle="{
fontSize: '32rpx',
fontWeight: 'bold',
color: '#fff',
lineHeight: '44rpx',
}"
></avatarModule>
</tm-badge>
</div>
<div class="chatInfo">
<div class="chatInfo_1">
<div class="name_center">
<div
class="text-[#000000] text-[32rpx] font-bold opacity-90 name_text"
>
<div>
<wd-swipe-action class="swipe_action">
<div
@click="cellClick"
:class="['chatItem', props.data.is_top === 1 ? 'isTop' : '']"
>
<div class="avatarImg">
<tm-badge
:count="props.data.unread_num"
:maxCount="99"
color="#D03050"
>
<avatarModule
:mode="props?.data?.group_type === 0 ? 1 : 2"
:avatar="props?.data?.avatar"
:groupType="props?.data?.group_type"
:userName="props?.data?.name"
:customStyle="{
width: '96rpx',
height: '96rpx',
}"
:customTextStyle="{
fontSize: '32rpx',
fontWeight: 'bold',
color: '#fff',
lineHeight: '44rpx',
}"
></avatarModule>
</tm-badge>
</div>
<div class="chatInfo">
<div class="chatInfo_1">
<div class="name_center">
<div class="text-[#000000] text-[32rpx]
font-bold opacity-90 name_text">
{{ formatNameText(props.data.name) }}
<span v-if="props.data.talk_type === 2">
{{ props.data.group_member_num }}
</span>
<span v-if="props.data.group_type === 2" class="depTag tag">
部门
</span>
@ -55,167 +45,153 @@
公司
</span>
</div>
</div>
<div
style="flex-shrink: 0;"
class="text-[#000000] text-[28rpx] font-medium opacity-26 ml-[24rpx] time_right"
>
{{ beautifyTime(props.data.updated_at) }}
</div>
</div>
<div class="chatInfo_2 w-full mr-[6rpx]">
<div class="w-full chatInfo_2_1 textEllipsis">
<span v-if="props.data.atsign_num" style="color: red;">
[有人@]
</span>
{{ props.data.msg_text }}
</div>
</div>
</div>
</div>
<template #right>
<div class="flex flex-row flex-row-center-end">
<!-- 样式占位 -->
<div style="width: 1px;"></div>
<div
@click="handleTop"
class="w-[156rpx] h-[154rpx] text-[#ffffff] bg-[#F09F1F] flex items-center justify-center"
>
{{ props.data.is_top === 1 ? '取消置顶' : '置顶' }}
</div>
<div
@click="handleDelete"
class="w-[156rpx] h-[154rpx] text-[#ffffff] bg-[#CF3050] flex items-center justify-center"
>
删除
</div>
</div>
</template>
</wd-swipe-action>
<div
v-if="props.index !== talkStore.talkItems.length - 1"
class="divider"
></div>
</div>
</div>
<div style="flex-shrink: 0;"
class="text-[#000000] text-[28rpx] font-medium opacity-26 ml-[24rpx] time_right"
>
{{ beautifyTime(props.data.updated_at) }}
</div>
</div>
<div class="chatInfo_2 w-full mr-[6rpx]">
<div class="w-full chatInfo_2_1 textEllipsis">
{{ props.data.msg_text }}
</div>
</div>
</div>
</div>
<template #right>
<div class="flex flex-row flex-row-center-end">
<!-- 样式占位 -->
<div style="width: 1px"></div>
<div
@click="handleTop"
class="w-[156rpx] h-[154rpx] text-[#ffffff] bg-[#F09F1F] flex items-center justify-center"
>
{{ props.data.is_top === 1 ? "取消置顶" : "置顶" }}
</div>
<div
@click="handleDelete"
class="w-[156rpx] h-[154rpx] text-[#ffffff] bg-[#CF3050] flex items-center justify-center"
>
删除
</div>
</div>
</template>
</wd-swipe-action>
<div
v-if="props.index !== talkStore.talkItems.length - 1"
class="divider"
></div>
</div>
</template>
<script setup>
import avatarModule from '@/components/avatar-module/index.vue'
import { ref, reactive, defineProps, computed } from 'vue'
import dayjs from 'dayjs'
import { beautifyTime } from '@/utils/datetime'
import { ServeClearTalkUnreadNum } from '@/api/chat'
import { useTalkStore, useDialogueStore } from '@/store'
import { useSessionMenu } from '@/hooks'
import avatarModule from "@/components/avatar-module/index.vue";
import { ref, reactive, defineProps, computed } from "vue";
import dayjs from "dayjs";
import { beautifyTime } from "@/utils/datetime";
import { ServeClearTalkUnreadNum } from "@/api/chat";
import { useTalkStore, useDialogueStore } from "@/store";
import { useSessionMenu } from "@/hooks";
const talkStore = useTalkStore()
const { onToTopTalk, onRemoveTalk } = useSessionMenu()
const dialogueStore = useDialogueStore()
const dialogueParams = reactive({
unReadNum: computed(() => dialogueStore.unreadNum),
})
const talkStore = useTalkStore();
const { onToTopTalk, onRemoveTalk } = useSessionMenu();
const dialogueStore = useDialogueStore();
const props = defineProps({
data: {
type: Object,
default: {},
required: true,
},
index: {
type: Number,
default: -1,
required: true,
},
})
data: {
type: Object,
default: {},
required: true,
},
index: {
type: Number,
default: -1,
required: true,
},
});
//
const formatNameText = (text, maxLength = 16) => {
return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text
}
const formatNameText = (text, maxLength = 19) => {
return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text;
};
const cellClick = () => {
console.log(props.data)
//
dialogueStore.setDialogue(props.data)
console.log(props.data);
//
dialogueStore.setDialogue(props.data);
//
if (props.data.unread_num > 0) {
ServeClearTalkUnreadNum(
{
talk_type: props.data.talk_type,
receiver_id: props.data.receiver_id,
},
dialogueParams.unReadNum,
).then(() => {
talkStore.updateItem({
index_name: props.data.index_name,
unread_num: 0,
})
dialogueStore.clearUnreadNum()
})
}
uni.navigateTo({
url: `/pages/dialog/index?sessionId=${props.data.id}`,
})
}
//
if (props.data.unread_num > 0) {
ServeClearTalkUnreadNum({
talk_type: props.data.talk_type,
receiver_id: props.data.receiver_id,
}).then(() => {
talkStore.updateItem({
index_name: props.data.index_name,
unread_num: 0,
});
});
}
uni.navigateTo({
url: "/pages/dialog/index?sessionId=" + props.data.id,
});
};
const handleTop = () => {
console.log(props.data, 1)
onToTopTalk(props.data)
}
console.log(props.data, 1);
onToTopTalk(props.data);
};
//
const handleDelete = () => {
console.log(props.data)
onRemoveTalk(props.data)
}
console.log(props.data);
onRemoveTalk(props.data);
};
</script>
<style lang="scss" scoped>
::v-deep .swipe_action {
// border: 1px solid #fff;
// transform: translate3d(1px, 0px, 0px) !important;
}
::v-deep .badge .round-6 {
min-width: 22rpx;
min-height: 22rpx;
// border: 1px solid #fff;
// transform: translate3d(1px, 0px, 0px) !important;
}
.chatItem {
width: 100%;
height: 154rpx;
padding: 30rpx 16rpx;
display: flex;
align-items: center;
width: 100%;
height: 154rpx;
padding: 30rpx 16rpx;
display: flex;
align-items: center;
&.isTop {
background-color: #f3f3f3;
}
&.isTop {
background-color: #f3f3f3;
}
}
.chatInfo {
flex: 1;
margin-left: 20rpx;
flex: 1;
margin-left: 20rpx;
}
.chatInfo_1 {
display: flex;
align-items: center;
justify-content: space-between;
display: flex;
align-items: center;
justify-content: space-between;
}
.chatInfo_2 {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 6rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 6rpx;
}
.chatInfo_2_1 {
font-size: 28rpx;
color: rgba($color: #000000, $alpha: 0.4);
font-size: 28rpx;
color: rgba($color: #000000, $alpha: 0.4);
}
.tag {
.tag{
display: inline-flex;
align-items: center;
text-align: center;
// margin-left: 10rpx;
margin-left: 10rpx;
margin-top: 4rpx;
vertical-align: top;
height: 38rpx;
@ -226,22 +202,22 @@ const handleDelete = () => {
font-weight: bold;
}
.companyTag {
border: 1px solid #7a58de;
color: #7a58de;
border: 1px solid #7a58de;
color: #7a58de;
}
.depTag {
border: 1px solid #377ec6;
color: #377ec6;
border: 1px solid #377ec6;
color: #377ec6;
}
.projectTag {
border: 1px solid #c1681c;
color: #c1681c;
border: 1px solid #c1681c;
color: #c1681c;
}
.name_center {
.name_center{
flex: 1;
min-width: 0;
}
.name_text {
.name_text{
display: inline-block;
max-height: 88rpx; //
line-height: 44rpx;
@ -253,24 +229,23 @@ const handleDelete = () => {
word-break: break-all;
}
.time_right {
/* white-space: nowrap;
/* white-space: nowrap;
max-width: 146rpx; */
flex: 0 0 auto; /* 不伸缩,保持内容宽度 */
white-space: nowrap;
}
.textEllipsis {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.divider {
background-color: rgba(243, 243, 243, 1);
height: 1rpx;
margin: 0 18rpx;
background-color: rgba(243, 243, 243, 1);
height: 1rpx;
margin: 0 18rpx;
}
</style>

View File

@ -31,25 +31,12 @@
>
<template v-slot:left>
<div class="flex items-center ml-[48rpx]">
<!-- <image
<image
class="w-[72rpx] h-[72rpx]"
style="border-radius: 50%;"
:src="userStore.avatar"
mode="scaleToFill"
/> -->
<avatarModule
:mode="1"
:avatar="userStore.avatar"
:groupType="0"
:userName="userStore.nickname"
:customStyle="{ width: '72rpx', height: '72rpx' }"
:customTextStyle="{
fontSize: '32rpx',
fontWeight: 'bold',
color: '#fff',
lineHeight: '44rpx',
}"
></avatarModule>
/>
<div class="ml-[24rpx] text-[36rpx] font-bold">
{{ userStore.nickname }}
</div>
@ -138,22 +125,17 @@ import { ref, watch, computed } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { useChatList } from '@/store/chatList/index.js'
import { useAuth } from '@/store/auth'
import { ServeClearTalkUnreadNum } from '@/api/chat'
import { useTalkStore, useUserStore, useDialogueStore } from '@/store'
import chatItem from './components/chatItem.vue'
import addCircle from '@/static/image/chatList/addCircle.png'
import cahtPopover from '@/static/image/chatList/cahtPopover.png'
import zu3289 from '@/static/image/chatList/zu3289@2x.png'
import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import { handleSetWebviewStyle } from '@/utils/common'
const paging = ref()
const isEmptyViewShow = ref(false)
const talkStore = useTalkStore()
const userStore = useUserStore()
const dialogueStore = useDialogueStore()
const dialogueParams = reactive({
unReadNum: computed(() => dialogueStore.unreadNum),
})
const { userInfo } = useAuth()
const topItems = computed(() => talkStore.topItems)
@ -232,7 +214,6 @@ const toAddressBookPage = () => {
); */
onShow(() => {
handleSetWebviewStyle(true)
//
talkStore
.loadTalkList()
@ -254,27 +235,21 @@ onLoad((options) => {
if (items?.value?.length > 0) {
items.value.forEach((openSession) => {
if (openSession.index_name === options?.openSessionIndexName) {
setTimeout(() => {
dialogueStore.setDialogue(openSession)
if (openSession.unread_num > 0) {
ServeClearTalkUnreadNum(
{
talk_type: openSession.talk_type,
receiver_id: openSession.receiver_id,
},
dialogueParams.unReadNum,
).then(() => {
talkStore.updateItem({
index_name: openSession.index_name,
unread_num: 0,
})
dialogueStore.clearUnreadNum()
dialogueStore.setDialogue(openSession)
if (openSession.unread_num > 0) {
ServeClearTalkUnreadNum({
talk_type: openSession.talk_type,
receiver_id: openSession.receiver_id,
}).then(() => {
talkStore.updateItem({
index_name: openSession.index_name,
unread_num: 0,
})
}
uni.navigateTo({
url: `/pages/dialog/index?sessionId=${openSession.id}`,
})
}, 500)
}
uni.navigateTo({
url: '/pages/dialog/index?sessionId=' + openSession.id,
})
}
})
}

View File

@ -96,30 +96,14 @@ import {
import HighlightText from './highLightText.vue'
import { useI18n } from 'vue-i18n'
import { beautifyTime } from '@/utils/datetime'
import { ChatMsgTypeMapping } from '@/constant/message'
const { t } = useI18n()
const props = defineProps({
searchItem: Object | Number,
searchResultKey: {
type: String,
default: '',
},
searchText: {
type: String,
default: '',
}, //
searchRecordDetail: {
type: Boolean,
default: false,
}, //
pointerIconSrc: {
type: String,
default: '',
}, //
conditionType: {
type: Number,
default: 0,
}, //
searchResultKey: String,
searchText: String, //
searchRecordDetail: Boolean, //
pointerIconSrc: String, //
conditionType: Number, //
})
// -
const keyMapping = {
@ -176,13 +160,6 @@ const keyMapping = {
name: 'receiver_name',
group_num: 'group_num',
},
search_by_member_condition: {
avatar: 'avatar',
name: 'nickname',
created_at: 'created_at',
msg_type: 'msg_type',
detailKey: 'chatMessageType',
},
}
//key
const getKeyValue = (keys) => {
@ -216,8 +193,10 @@ const imgText = computed(() => {
})
// -groupType
const groupTypeMapping = {
0: {},
1: {},
0: {
},
1: {
},
2: {
result_type: t('index.mine.department'),
result_type_color: '#377EC6',
@ -262,12 +241,6 @@ const resultDetail = computed(() => {
case 'extra':
result_detail = props.searchItem?.extra
break
case 'chatMessageType':
result_detail =
props.searchItem?.msg_type === 1
? props.searchItem?.extra?.content
: ChatMsgTypeMapping[props.searchItem?.msg_type]
break
default:
result_detail = ''
}
@ -302,7 +275,6 @@ const resultDetail = computed(() => {
padding: 2rpx 14rpx;
border: 2rpx solid #000;
border-radius: 6rpx;
flex-shrink: 0;
span {
line-height: 34rpx;
}
@ -327,7 +299,6 @@ const resultDetail = computed(() => {
.info-detail-searchRecordDetail {
span {
color: $theme-text;
word-break: break-all;
}
}
}

View File

@ -9,7 +9,7 @@
:default-page-size="props.searchResultPageSize"
:loading-more-default-as-loading="true"
:inside-more="true"
:empty-view-img="searchNoData"
:empty-view-img="'/src/static//image/search/search-no-data.png'"
:empty-view-text="$t('search.hint')"
:empty-view-img-style="{ width: '476rpx', height: '261rpx' }"
:empty-view-title-style="{
@ -19,7 +19,6 @@
'font-size': '28rpx',
'font-weight': 400,
}"
:refresher-enabled="false"
>
<template #top>
<div class="searchRoot">
@ -36,10 +35,7 @@
</span>
</div>
</template>
<div
class="search-record-detail"
v-if="props.searchRecordDetail && !props?.hideFirstRecord"
>
<div class="search-record-detail" v-if="props.searchRecordDetail">
<searchItem
@click="
clickSearchItem(
@ -113,7 +109,6 @@
</div>
</template>
<script setup>
import searchNoData from '@/static/image/search/search-no-data.png'
import customInput from '@/components/custom-input/custom-input.vue'
import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import useZPaging from '@/uni_modules/z-paging/components/z-paging/js/hooks/useZPaging.js'
@ -122,10 +117,6 @@ import { useI18n } from 'vue-i18n'
import { ref, reactive, defineEmits, defineProps, onMounted } from 'vue'
import pointerIconSrc from '@/static/image/search/search-item-pointer.png'
import lodash from 'lodash'
import { useUserStore } from '@/store'
const userStore = useUserStore()
const zPaging = ref()
useZPaging(zPaging)
@ -140,45 +131,17 @@ const state = reactive({
searchResultList: [], //
searchResult: null, //
pageNum: 1, //
uid: computed(() => userStore.uid), //id
})
const props = defineProps({
searchResultPageSize: {
type: Number,
default: 0,
}, //
listLimit: {
type: Boolean,
default: false,
}, //
apiParams: {
type: String,
default: '',
}, //
searchResultPageSize: Number, //
listLimit: Boolean, //
apiParams: String, //
apiRequest: Function, //
searchText: {
type: String,
default: '',
}, //
isPagination: {
type: Boolean,
default: false,
}, //
searchRecordDetail: {
type: Boolean,
default: false,
}, //
first_talk_record_infos: {
type: Object,
default() {
return {}
},
}, //
hideFirstRecord: {
type: Boolean,
default: false,
}, ///
searchText: String, //
isPagination: Boolean, //
searchRecordDetail: Boolean, //
first_talk_record_infos: Object, //
})
const { t } = useI18n()
@ -191,14 +154,13 @@ onMounted(() => {
//
const inputSearchText = (e) => {
// console.log(e)
if (e.trim() != state.searchText.trim()) {
state.pageNum = 1
state.searchResult = null //
emits('lastIdChange', 0, 0, 0)
}
state.searchText = e.trim()
if (!e.trim()) {
state.searchResult = null //
emits('lastIdChange', 0, 0, 0)
}
zPaging.value?.reload()
@ -235,35 +197,15 @@ const queryAllSearch = (pageNum, searchResultPageSize) => {
})
}
if ((data.talk_record_infos || []).length > 0) {
let receiverInfo = lodash.cloneDeep(data.talk_record_infos[0])
if (receiverInfo.talk_type === 1) {
//
if (receiverInfo.user_id === state.uid) {
//
}
if (receiverInfo.receiver_id === state.uid) {
//
let temp_id = receiverInfo.receiver_id
let temp_name = receiverInfo.receiver_name
let temp_avatar = receiverInfo.receiver_avatar
receiverInfo.receiver_id = receiverInfo.user_id
receiverInfo.receiver_name = receiverInfo.user_name
receiverInfo.receiver_avatar = receiverInfo.user_avatar
receiverInfo.user_id = temp_id
receiverInfo.user_name = temp_name
receiverInfo.user_avatar = temp_avatar
}
}
state.first_talk_record_infos = Object.assign(
{},
state.first_talk_record_infos,
receiverInfo,
data.talk_record_infos[0],
)
;(data.talk_record_infos || []).forEach((item) => {
item.group_type = 0
})
}
let tempGeneral_infos = Array.isArray(data.general_infos)
? [...data.general_infos]
: data.general_infos
@ -272,8 +214,6 @@ const queryAllSearch = (pageNum, searchResultPageSize) => {
data.group_member_infos || [],
)
data.general_infos = tempGeneral_infos
//
let isEmpty = true
let dataKeys = Object.keys(data)
let paginationKey = ''
@ -283,94 +223,58 @@ const queryAllSearch = (pageNum, searchResultPageSize) => {
isEmpty = false
}
})
if (isEmpty) {
if (pageNum === 1) {
//
state.searchResult = null
if (pageNum == 1) {
zPaging.value?.complete([])
} else {
//
zPaging.value?.complete(state.searchResult ? [state.searchResult] : [])
data = state.searchResult
zPaging.value?.complete([data])
}
} else {
if (props.isPagination) {
if (pageNum === 1) {
//
state.searchResult = data
} else {
//
if (
paginationKey &&
Array.isArray(
(state?.searchResult && state?.searchResult[paginationKey]) || [],
)
) {
data[paginationKey] = state.searchResult[paginationKey].concat(
data[paginationKey],
)
}
state.searchResult = data
if (
paginationKey &&
Array.isArray(
(state?.searchResult && state?.searchResult[paginationKey]) || [],
) &&
((state?.searchResult && state?.searchResult[paginationKey]) || [])
.length > 0
) {
data[paginationKey] = state.searchResult[paginationKey].concat(
data[paginationKey],
)
}
emits(
'lastIdChange',
data.last_id,
data.last_group_id,
data.last_member_id,
data.last_receiver_user_name,
data.last_receiver_group_name,
)
let total = data.count
if (props.searchRecordDetail) {
if (state?.first_talk_record_infos?.talk_type === 1) {
total = data.user_record_count
} else if (state?.first_talk_record_infos?.talk_type === 2) {
total = data.group_record_count
}
total = data.group_record_count
}
zPaging.value?.completeByTotal([data], total)
} else {
state.searchResult = data
zPaging.value?.complete([data])
}
}
state.searchResult = data
} else {
if (pageNum === 1) {
//
state.searchResult = null
zPaging.value?.complete([])
} else {
//
zPaging.value?.complete(state.searchResult ? [state.searchResult] : [])
}
zPaging.value?.complete([])
}
})
resp.catch(() => {
if (pageNum === 1) {
//
state.searchResult = null
zPaging.value?.complete([])
} else {
//
zPaging.value?.complete(state.searchResult ? [state.searchResult] : [])
}
zPaging.value?.complete([])
})
}
//
const cancelSearch = () => {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack({
delta: 1,
})
} else {
uni.reLaunch({
url: '/pages/index/index'
})
}
uni.navigateBack({
delta: 1,
})
}
//key
@ -438,12 +342,6 @@ const getHasMoreResult = (searchResultKey) => {
}
break
case 'general_infos':
if (
state.searchResult['record_count'] &&
state.searchResult['record_count'] >= 3
) {
has_more_result = t('has_more') + t('chat.type.record')
}
break
default:
}
@ -457,41 +355,12 @@ const toMoreResultPage = (searchResultKey) => {
//
const clickSearchItem = (searchResultKey, searchItem) => {
console.log(searchResultKey, searchItem)
let talk_type = searchItem.talk_type
let receiver_id = searchItem.receiver_id
if (searchResultKey === 'user_infos') {
talk_type = 1
receiver_id = searchItem.id
} else if (searchResultKey === 'combinedGroup') {
talk_type = searchItem.type || 2
receiver_id = searchItem.group_id || searchItem.id
} else if (searchResultKey === 'general_infos') {
if (searchItem.talk_type === 1) {
if (searchItem.user_id === state.uid) {
//
}
if (searchItem.receiver_id === state.uid) {
//
let temp_id = searchItem.receiver_id
let temp_name = searchItem.receiver_name
let temp_avatar = searchItem.receiver_avatar
searchItem.receiver_id = searchItem.user_id
searchItem.receiver_name = searchItem.user_name
searchItem.receiver_avatar = searchItem.user_avatar
searchItem.user_id = temp_id
searchItem.user_name = temp_name
searchItem.user_avatar = temp_avatar
}
}
}
emits(
'clickSearchItem',
state.searchText,
searchResultKey,
talk_type,
receiver_id,
encodeURIComponent(JSON.stringify(searchItem)),
searchItem.talk_type,
searchItem.receiver_id,
)
}
</script>

View File

@ -13,19 +13,7 @@
</template>
<script setup>
import searchList from './components/searchList.vue'
import { ServeSeachQueryAll, ServeGetSessionId } from '@/api/search/index'
import { onMounted } from 'vue'
import { handleSetWebviewStyle } from '@/utils/common'
import { useDialogueStore, useTalkStore } from '@/store'
import { ServeCreateTalkList } from '@/api/chat/index.js'
import { formatTalkItem } from '@/utils/talk'
const dialogueStore = useDialogueStore()
onMounted(() => {
handleSetWebviewStyle()
})
import { ServeSeachQueryAll } from '@/api/search/index'
//
const toMoreResultPage = (searchResultKey, searchText) => {
@ -39,49 +27,14 @@ const toMoreResultPage = (searchResultKey, searchText) => {
}
//
const clickSearchItem = async (
const clickSearchItem = (
searchText,
searchResultKey,
talk_type,
receiver_id,
res,
) => {
console.log(searchResultKey)
const result = JSON.parse(decodeURIComponent(res))
console.log(result)
console.log(talk_type, receiver_id)
const sessionId = await getSessionId(talk_type, receiver_id)
if (searchResultKey === 'user_infos') {
if (useTalkStore().findTalkIndex(`${talk_type}_${receiver_id}`) === -1) {
ServeCreateTalkList({
talk_type,
receiver_id,
erp_user_id: result.erp_user_id,
}).then(async ({ code, data }) => {
if (code == 200) {
let item = formatTalkItem(data)
useTalkStore().addItem(item)
}
})
}
dialogueStore.setDialogue({
name: result.nickname,
talk_type: 1,
receiver_id: receiver_id,
})
uni.navigateTo({
url: '/pages/dialog/index?sessionId=' + sessionId,
})
} else if (searchResultKey === 'combinedGroup') {
dialogueStore.setDialogue({
name: result.name || result.group_name,
talk_type: result.type || 2,
receiver_id: result.group_id || result.id,
})
uni.navigateTo({
url: '/pages/dialog/index?sessionId=' + sessionId,
})
} else if (searchResultKey === 'general_infos') {
if (searchResultKey === 'general_infos') {
uni.navigateTo({
url:
'/pages/search/moreResult/moreResultDetail?searchText=' +
@ -93,25 +46,5 @@ const clickSearchItem = async (
})
}
}
//Id
const getSessionId = (talk_type, receiver_id) => {
return new Promise((resolve, reject) => {
let params = {
talkType: talk_type,
receiverId: receiver_id,
}
const resp = ServeGetSessionId(params)
console.log(resp)
resp.then(({ code, data }) => {
console.log(data)
if (code == 200) {
resolve(data?.sessionId)
} else {
}
})
resp.catch(() => {})
})
}
</script>
<style scoped lang="scss"></style>

View File

@ -21,21 +21,14 @@ import {
ServeQueryUser,
ServeQueryGroup,
ServeTalkRecord,
ServeGetSessionId,
} from '@/api/search/index'
import { reactive } from 'vue'
import { useDialogueStore, useTalkStore } from '@/store'
import { ServeCreateTalkList } from '@/api/chat/index.js'
import { formatTalkItem } from '@/utils/talk'
const dialogueStore = useDialogueStore()
const state = reactive({
apiRequest: Function,
apiParams: '',
searchText: '',
searchResultKey: '',
apiParams: String,
searchText: String,
searchResultKey: String,
})
onLoad((options) => {
@ -64,8 +57,6 @@ onLoad((options) => {
receiver_id: 0, //
last_group_id: 0, //id
last_member_id: 0, //id
last_receiver_user_name: '', //
last_receiver_group_name: '', //
}),
)
state.apiRequest = ServeTalkRecord
@ -77,19 +68,11 @@ onLoad((options) => {
})
//id
const lastIdChange = (
last_id,
last_group_id,
last_member_id,
last_receiver_user_name,
last_receiver_group_name,
) => {
const lastIdChange = (last_id, last_group_id, last_member_id) => {
let idChanges = {
last_id,
last_group_id,
last_member_id,
last_receiver_user_name,
last_receiver_group_name,
}
state.apiParams = encodeURIComponent(
JSON.stringify(
@ -103,79 +86,12 @@ const lastIdChange = (
}
//
const clickSearchItem = async (
searchText,
searchResultKey,
talk_type,
receiver_id,
res,
) => {
console.log(state.searchResultKey)
const result = JSON.parse(decodeURIComponent(res))
console.log(result)
console.log(talk_type, receiver_id)
const sessionId = await getSessionId(talk_type, receiver_id)
if (state.searchResultKey === 'user_infos') {
if (useTalkStore().findTalkIndex(`${talk_type}_${receiver_id}`) === -1) {
ServeCreateTalkList({
talk_type,
receiver_id,
erp_user_id: result.erp_user_id,
}).then(async ({ code, data }) => {
if (code == 200) {
let item = formatTalkItem(data)
useTalkStore().addItem(item)
}
})
}
dialogueStore.setDialogue({
name: result.nickname,
talk_type: 1,
receiver_id: receiver_id,
})
const clickSearchItem = (searchText) => {
if (state.searchResultKey === 'general_infos') {
uni.navigateTo({
url: '/pages/dialog/index?sessionId=' + sessionId,
})
} else if (state.searchResultKey === 'combinedGroup') {
dialogueStore.setDialogue({
name: result.name || result.group_name,
talk_type: result.type || 2,
receiver_id: result.group_id || result.id,
})
uni.navigateTo({
url: '/pages/dialog/index?sessionId=' + sessionId,
})
} else if (state.searchResultKey === 'general_infos') {
uni.navigateTo({
url:
'/pages/search/moreResult/moreResultDetail?searchText=' +
searchText +
'&talk_type=' +
talk_type +
'&receiver_id=' +
receiver_id,
url: '/pages/search/moreResult/moreResultDetail?searchText=' + searchText,
})
}
}
//Id
const getSessionId = (talk_type, receiver_id) => {
return new Promise((resolve, reject) => {
let params = {
talkType: talk_type,
receiverId: receiver_id,
}
const resp = ServeGetSessionId(params)
console.log(resp)
resp.then(({ code, data }) => {
console.log(data)
if (code == 200) {
resolve(data?.sessionId)
} else {
}
})
resp.catch(() => {})
})
}
</script>
<style scoped lang="scss"></style>

View File

@ -7,11 +7,9 @@
:apiRequest="ServeTalkRecord"
:apiParams="state.apiParams"
:searchText="state.searchText"
:hideFirstRecord="state.hideFirstRecord"
:isPagination="true"
:searchRecordDetail="true"
@lastIdChange="lastIdChange"
@clickSearchItem="clickSearchItem"
></searchList>
</div>
</div>
@ -21,17 +19,10 @@ import searchList from '../components/searchList.vue'
import { onLoad } from '@dcloudio/uni-app'
import { ServeTalkRecord } from '@/api/search/index'
import { reactive } from 'vue'
import { useDialogueStore, useUserStore } from '@/store'
import lodash from 'lodash'
const dialogueStore = useDialogueStore()
const userStore = useUserStore()
const state = reactive({
apiParams: '',
searchText: '',
uid: computed(() => userStore.uid), //id
hideFirstRecord: false, ///
apiParams: String,
searchText: String,
})
onLoad((options) => {
@ -57,10 +48,6 @@ onLoad((options) => {
}
console.log(JSON.parse(decodeURIComponent(state.apiParams)))
if (options.hideFirstRecord) {
state.hideFirstRecord = options.hideFirstRecord === '1' ? true : false
}
})
//id
@ -80,51 +67,5 @@ const lastIdChange = (last_id, last_group_id, last_member_id) => {
),
)
}
//
const clickSearchItem = (
searchText,
searchResultKey,
talk_type,
receiver_id,
res,
) => {
console.log(searchResultKey)
let result = JSON.parse(decodeURIComponent(res))
console.log(result)
let receiverInfo = lodash.cloneDeep(result)
if (receiverInfo.talk_type === 1) {
//
if (receiverInfo.user_id === state.uid) {
//
}
if (receiverInfo.receiver_id === state.uid) {
//
let temp_id = receiverInfo.receiver_id
let temp_name = receiverInfo.receiver_name
let temp_avatar = receiverInfo.receiver_avatar
receiverInfo.receiver_id = receiverInfo.user_id
receiverInfo.receiver_name = receiverInfo.user_name
receiverInfo.receiver_avatar = receiverInfo.user_avatar
receiverInfo.user_id = temp_id
receiverInfo.user_name = temp_name
receiverInfo.user_avatar = temp_avatar
}
}
dialogueStore.setDialogue({
name: receiverInfo.receiver_name,
talk_type: talk_type,
receiver_id: receiverInfo.receiver_id,
})
if (searchResultKey === 'talk_record_infos_receiver') {
uni.navigateTo({
url: '/pages/dialog/index',
})
} else {
uni.navigateTo({
url: '/pages/dialog/index?msgInfo=' + res + '&keepDialogInfo=1',
})
}
}
</script>
<style scoped lang="scss"></style>

View File

@ -9,7 +9,6 @@
:auto="false"
:loading-more-default-as-loading="true"
:inside-more="true"
v-model="state.flatList"
>
<template #top v-if="state.showPageTitle">
<customNavbar :title="state.pageTitle"></customNavbar>
@ -42,7 +41,7 @@
<span class="text-[28rpx] font-regular">
{{ state.selectedMonth }}
</span>
<img src="@/static/image/search/down-pointer.png" />
<img src="/src/static/image/search/down-pointer.png" />
</div>
</tm-time-picker>
<tm-calendar-view
@ -65,8 +64,7 @@
v-if="
state.condition === 'imgAndVideo' ||
state.condition === 'file' ||
state.condition === 'link' ||
state.condition === 'member'
state.condition === 'link'
"
:style="{
padding: state.condition === 'imgAndVideo' ? '0 27rpx' : '',
@ -114,27 +112,11 @@
v-for="(item, index) in conditionItem.monthResultList"
:key="index"
:style="{
border:
state.condition === 'imgAndVideo' ||
state.condition === 'member'
? '0'
: '',
border: state.condition === 'imgAndVideo' ? '0' : '',
padding:
state.condition === 'imgAndVideo' ? '0 0 10rpx' : '',
}"
>
<div
class="condition-result-member"
v-if="state.condition === 'member'"
>
<searchItem
@click="toDialogueByMember(item)"
:searchResultKey="'search_by_member_condition'"
:searchItem="item"
:searchText="state.searchText"
:searchRecordDetail="true"
></searchItem>
</div>
<div
class="condition-result-imgAndVideo"
v-if="state.condition === 'imgAndVideo'"
@ -160,27 +142,17 @@
class="condition-result-imgAndVideo-area"
v-if="item?.extra?.url"
>
<template v-if="item?.msg_type === 3">
<tm-image
preview
:src="item?.extra?.url"
model="aspectFill"
/>
</template>
<template v-else-if="item?.msg_type === 5">
<div
class="video-preview"
@click="onPlay(item?.extra?.url)"
>
<tm-image
:src="item?.extra?.cover"
model="aspectFill"
/>
<div class="play-icon">
<img :src="playCircle" />
</div>
</div>
</template>
<tm-image
preview
:src="
item?.msg_type === 3
? item?.extra?.url
: item?.msg_type === 5
? item?.extra?.cover
: ''
"
model="aspectFill"
/>
</div>
</div>
<div
@ -201,7 +173,6 @@
</div>
<div
class="condition-each-result-attachments"
@click="previewPDF(item)"
v-if="
state.condition === 'file' || state.condition === 'link'
"
@ -212,7 +183,7 @@
v-if="state.condition === 'file'"
/>
<img
src="@/static/image/search/result-link-icon.png"
src="/src/static/image/search/result-link-icon.png"
v-if="state.condition === 'link'"
/>
</div>
@ -267,20 +238,6 @@
</div>
</div>
</ZPaging>
<teleport to="body">
<div v-show="open" class="video-container">
<video
:src="currentVideoUrl"
controls
@fullscreenchange="fullscreenchange"
:id="currentVideoUrl"
playsinline
webkit-playsinline
x5-playsinline
class="fullscreen-video"
></video>
</div>
</teleport>
</div>
</div>
</template>
@ -290,16 +247,15 @@ import fileType_EXCEL from '@/static/image/search/fileType_EXCEL.png'
import fileType_WORD from '@/static/image/search/fileType_WORD.png'
import fileType_PDF from '@/static/image/search/fileType_PDF.png'
import fileType_Files from '@/static/image/search/fileType_Files.png'
import playCircle from '@/static/image/chatList/playCircle@2x.png'
import { fileFormatSize, fileSuffix } from '@/utils/strings'
import searchItem from '../components/searchItem.vue'
import customInput from '@/components/custom-input/custom-input.vue'
import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue'
import useZPaging from '@/uni_modules/z-paging/components/z-paging/js/hooks/useZPaging.js'
import { parseTime } from '@/utils/datetime'
import { onMounted, reactive, computed, ref, nextTick } from 'vue'
import { onMounted, reactive, computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { ServeTalkDate, ServeGetSessionId } from '@/api/search/index'
import { ServeTalkDate } from '@/api/search/index'
import { ServeFindTalkRecords } from '@/api/chat/index'
import { useDialogueStore } from '@/store'
import { useI18n } from 'vue-i18n'
@ -317,6 +273,7 @@ const dialogueParams = reactive({
let nowDay = new Date().setHours(0, 0, 0, 0)
const state = reactive({
receiver_id: '', //id
pageTitle: '', //
dateStyle: [], //
nowDate: new Date(nowDay), //
@ -332,53 +289,16 @@ const state = reactive({
searchResultList: [], //
cursor: 0, //
msg_type: 0, //
group_member_id: 0, //id
flatList: [], //
})
const videoContext = ref()
const open = ref(false)
const currentVideoUrl = ref('')
const fullscreenchange = (e) => {
if (!e.detail.fullScreen) {
videoContext.value.stop()
videoContext.value.seek(0)
open.value = false
}
}
async function onPlay(url) {
currentVideoUrl.value = url
open.value = true
// DOM
await nextTick()
//
videoContext.value = uni.createVideoContext(url, getCurrentInstance())
setTimeout(() => {
//
videoContext.value.requestFullScreen({ direction: 2 })
//
setTimeout(() => {
videoContext.value.play()
}, 100)
}, 200)
}
onLoad((options) => {
console.log(options)
if (options.receiver_id) {
state.receiver_id = Number(options.receiver_id)
}
if (options.condition) {
state.condition = options.condition
if (options.condition === 'member') {
state.showPageTitle = true
state.pageTitle = t('search.condition.member')
state.group_member_id = options.groupMemberId
queryAllSearch()
} else if (options.condition === 'date') {
if (options.condition === 'date') {
state.showPageTitle = true
state.pageTitle = t('search.condition.date')
ServeQueryTalkDate(parseTime(state.nowDate, '{y}{m}'))
@ -431,8 +351,8 @@ onMounted(() => {
const ServeQueryTalkDate = (month) => {
let params = {
month: month,
talk_type: dialogueParams.talk_type, //12
receiver_id: dialogueParams.receiver_id, //id
talk_type: 2, //12
receiver_id: state.receiver_id, //id
}
const resp = ServeTalkDate(params)
console.log(resp)
@ -467,7 +387,7 @@ const ServeQueryTalkDate = (month) => {
}
//
const selectDate = async (e) => {
const selectDate = (e) => {
if (e == parseTime(state.nowDate, '{y}/{m}/{d}')) {
console.log('==今日')
state.dateStyle = [
@ -493,38 +413,6 @@ const selectDate = async (e) => {
},
]
}
const sessionId = await getSessionId(
dialogueParams.talk_type,
dialogueParams.receiver_id,
)
uni.navigateTo({
url:
'/pages/dialog/index?sessionId=' +
sessionId +
'&keepDialogInfo=1' +
'&recordDate=' +
parseTime(e, '{y}-{m}-{d}'),
})
}
//Id
const getSessionId = (talk_type, receiver_id) => {
return new Promise((resolve, reject) => {
let params = {
talkType: talk_type,
receiverId: receiver_id,
}
const resp = ServeGetSessionId(params)
console.log(resp)
resp.then(({ code, data }) => {
console.log(data)
if (code == 200) {
resolve(data?.sessionId)
} else {
}
})
resp.catch(() => {})
})
}
//
@ -555,22 +443,13 @@ const getDArray = (dArray) => {
//
const inputSearchText = (e) => {
state.searchText = e
state.cursor = 0
queryAllSearch()
}
//
const cancelSearch = () => {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack({
delta: 1,
})
} else {
uni.reLaunch({
url: '/pages/index/index',
})
}
uni.navigateBack({
delta: 1,
})
}
//
@ -585,8 +464,7 @@ const queryAllSearch = () => {
direction: 'up', //downup
start_time: '',
end_time: '',
group_member_user_id: state.group_member_id, //id
file_name: state.msg_type === 6 ? state.searchText : '',
group_member_user_id: 0, //id
}
console.log(params)
const resp = ServeFindTalkRecords(params)
@ -594,20 +472,17 @@ const queryAllSearch = () => {
resp.then(({ code, data }) => {
console.log(data)
if (code == 200) {
// cursor0searchResultList
let dateList = state.cursor === 0 ? [] : state.searchResultList
let dateList = state.searchResultList
let noMore = false
if (data?.items?.length > 0) {
data.items.forEach((item) => {
item.dateTime = parseTime(item?.created_at, '{m}/{d}')
if (item?.extra) {
item.extra.fileSize = fileFormatSize(item?.extra?.size)
item.extra.typeText = item?.extra?.name
? fileSuffix(item?.extra?.name)
: ''
item.extra.file_avatar = fileTypeAvatar(item?.extra?.typeText)
console.log(item.extra.type)
}
item.extra.fileSize = fileFormatSize(item?.extra?.size)
item.extra.typeText = item?.extra?.name
? fileSuffix(item?.extra?.name)
: ''
item.extra.file_avatar = fileTypeAvatar(item?.extra?.typeText)
console.log(item.extra.type)
let year = new Date(item.created_at).getFullYear()
let month = new Date(item.created_at).getMonth() + 1
let dateMonth =
@ -640,35 +515,15 @@ const queryAllSearch = () => {
} else {
noMore = true
}
//
state.searchResultList = dateList
// z-paging
state.flatList = dateList.reduce((acc, group) => {
return acc.concat(group.monthResultList)
}, [])
if (state.cursor === 0) {
zPaging.value?.complete(state.flatList)
} else {
zPaging.value?.completeByNoMore(state.flatList, noMore)
}
console.log(dateList)
state.cursor = data?.cursor
zPaging.value?.completeByNoMore(dateList, noMore)
} else {
if (state.cursor === 0) {
state.searchResultList = []
state.flatList = []
}
zPaging.value?.complete([])
}
})
resp.catch(() => {
if (state.cursor === 0) {
state.searchResultList = []
state.flatList = []
}
zPaging.value?.complete([])
})
}
@ -691,59 +546,6 @@ const fileTypeAvatar = (fileType) => {
}
return file_type_avatar
}
const previewPDF = (item) => {
console.log(item)
if (typeof plus !== 'undefined') {
downloadAndOpenFile(item)
} else {
document.addEventListener('plusready', () => {
downloadAndOpenFile(item)
})
}
}
const downloadAndOpenFile = (item) => {
uni.showLoading({ title: '加载中...', mask: true })
const downloadUrl = item?.extra?.path
const options = {
filename: '_doc/downloads/', //
}
const dtask = plus.downloader.createDownload(downloadUrl, options, function (
d,
status,
) {
if (status === 200) {
uni.hideLoading()
const filePath = d.filename
plus.runtime.openFile(
filePath,
{},
function () {},
function (error) {},
)
} else {
uni.hideLoading()
}
})
dtask.start()
}
//
const toDialogueByMember = async (msgInfo) => {
const sessionId = await getSessionId(
dialogueParams.talk_type,
dialogueParams.receiver_id,
)
uni.navigateTo({
url:
'/pages/dialog/index?sessionId=' +
sessionId +
'&keepDialogInfo=1' +
'&msgInfo=' +
encodeURIComponent(JSON.stringify(msgInfo)),
})
}
</script>
<style scoped lang="scss">
.search-by-date {
@ -850,7 +652,6 @@ body::v-deep .round-3 {
span {
line-height: 40rpx;
color: $theme-text;
word-break: break-all;
}
}
.attachment-sub-info {
@ -897,26 +698,6 @@ body::v-deep .round-3 {
width: 164rpx !important;
height: 164rpx !important;
}
.video-preview {
position: relative;
width: 164rpx;
height: 164rpx;
cursor: pointer;
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
img {
width: 80rpx !important;
height: 80rpx !important;
}
}
}
}
}
}
@ -925,23 +706,4 @@ body::v-deep .round-3 {
}
}
}
.video-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #000;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.fullscreen-video {
width: 100%;
height: 100%;
object-fit: contain;
}
</style>

View File

@ -23,7 +23,7 @@ class WsSocket {
lockReconnect: false,
setTimeout: null, // 计时器对象
time: 3000, // 重连间隔时间
number: 20 // 重连次数
number: 10000000 // 重连次数
}
}

View File

@ -1,21 +1,5 @@
import Request from '@/service/request/index.js'
import {useAuth} from "@/store/auth";
import { createApp } from 'vue';
import XMessage from '@/components/x-message/index.vue';
// 创建消息提示实例
const messageInstance = (() => {
const messageNode = document.createElement('div');
document.body.appendChild(messageNode);
const app = createApp(XMessage);
const instance = app.mount(messageNode);
return {
warning: (msg) => instance.showMessage({ type: 'warning', message: msg }),
error: (msg) => instance.showMessage({ type: 'error', message: msg }),
success: (msg) => instance.showMessage({ type: 'success', message: msg }),
info: (msg) => instance.showMessage({ type: 'info', message: msg })
};
})();
const { token ,refreshToken,userInfo}=useAuth()
let isRefreshing = false;
let refreshSubscribers = [];
@ -40,25 +24,19 @@ const request = new Request({
},
responseInterceptors: async (res) => {
if(res.data.status===1){
// message.warning(res.data.msg)
messageInstance.warning(res.data.msg)
message.warning(res.data.msg)
}
if (res.data.status === 401) {
return
// return getRefreshToken(res);
return getRefreshToken(res);
// uni.navigateTo({
// url:'/pages/login/index'
// })
}
if ([200, 201, 204].includes(res.status)) {
if(res.data.code !== 200 && res.data.code!== 0) {
messageInstance.error(res.data.message || res.data.msg || 'An error occurred.');
}
return res.config.responseType === 'blob' ? res : res;
} else {
/* message.error(res.data.msg || 'An error occurred.');*/
messageInstance.error(res.data.message || res.data.msg || 'An error occurred.');
return Promise.reject(new Error(res.data.message || res.data.msg || 'An error occurred.'));
return Promise.reject(new Error(res.data.msg || 'An error occurred.'));
}
}
}
@ -81,14 +59,13 @@ async function getRefreshToken(response) {
})
return request.request(response.config);
} else {
messageInstance.error(res.message || res.msg);
message.error(res.message || res.msg);
throw new Error(res.message || res.msg);
}
} catch (error) {
// uni.navigateTo({
// url:'/pages/login/index'
// })
uni.navigateTo({
url:'/pages/login/index'
})
throw error
} finally {
isRefreshing = false;
@ -96,9 +73,9 @@ async function getRefreshToken(response) {
refreshSubscribers = [];
}
} else {
// uni.navigateTo({
// url:'/pages/login/index'
// })
uni.navigateTo({
url:'/pages/login/index'
})
throw new Error('No refresh token available.');
}
} else {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -4,8 +4,8 @@ import { userInfoApi } from "@/api/user";
import {ref} from 'vue'
export const useAuth = createGlobalState(() => {
const token = useStorage('token', '', uniStorage)
// const token = ref("2046c3941ed4959f6d988d3d4a0fe40d4b52f33f3f5fc1001406064554641d9406bb13cacb92939b0ca223c17e2c2f2fe70212ef017dbae8965d5cf86bad48ce4316e605ca187bd9ffd4aa6b56865be4ad4e422701d330b52d60cfe649cd48cf3a21a2a6e9a9cabafff364ee9c311ec634b0afc09db0d3215bedce561e9d50e5a8da6092062e2ebe35f747d77d72a68ad492a4ab218c07887c9cd4867f2c2d28e4ae1fd671144cc20ef0632f9ce067289004d67f6adf41b20d6ef5cdbfb74aadc2d2736ececf07254f1a76552bde4f1161a0fca7bfe32a29685ce1e76366116b81ae2195b3713dbb04285e5ddfd36184fe671c5524d20b4fe74a555db755f8d939b0bc46fb0cb998323d54c9925729d7ca835b7925999a677faa0cbe1cbc67b5203d85317653883aec81d3e71d865b326376bea726cc66d9f7f5a160d43f671c")
// const token = useStorage('token', '', uniStorage)
const token = ref("79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caba0afc18e7cb819d125e8c04282beb5d4dd454307169f7eb51cd3dbbee20c1a17784f78ad30c3b3a102bd03c3218080beedae501a95c365d360a7a0d682cba2eca94a2915c13f0bdcb42f2eab9458cb4bd89ae6f3200fe4893d64caf1bce02894aab876f94f7825d23be507733468deba21734b6d666b75fd0bee74753fdd6ca5def7ec42cd7dbb47750e61cac6965f34fd71b52be66a472d3ee5b1e4661e59db28983527a8af787c5212a06f9b116a37a6679c7ce23b7307a093ea17841ab84695183fbec1f0e683a48ed05133f80a21c30d1cdd7faf3123e3cd69a8da6ae9240e08767898cf1c5f156be8c175066f9")
const refreshToken = useStorage('refreshToken', '', uniStorage)
const userInfo = useStorage('userInfo', {}, uniStorage)
const leaderList = useStorage('leaderList', [], uniStorage)

View File

@ -10,14 +10,10 @@ import {
departmentV2AllPosition,
groupCreateDept,
departmentV2TreeAll,
departmentV2TreeAll2,
userHasPermission,
userV2List,
userV2List2,
v2TreePositionByDepartment,
} from '@/api/deps/index.js'
import { useAuth } from '@/store/auth'
const { userInfo } = useAuth()
export const useGroupTypeStore = createGlobalState(() => {
const groupName = ref('')
const groupActiveIndex = ref(-1) // 当前激活的分组索引
@ -38,40 +34,6 @@ export const useGroupTypeStore = createGlobalState(() => {
depTreeMyList.value = res.data.nodes
}
}
// userInfo?.value?.ID
const getDepsTreeMy2 = async (chooseMode) => {
let params = { nowUserId: 0 }
if (chooseMode === 1) {
const isHasRes = await userHasPermission({
erpUserId: userInfo?.value?.ID,
ruleUrl: [
'auth_chat_app_create_all_dept',
'auth_chat_app_create_limit_dept',
],
})
if (isHasRes.code === 200) {
if (isHasRes.data.auth_chat_app_create_all_dept) {
params = {
nowUserId: 0,
}
} else {
if (isHasRes.data.auth_chat_app_create_limit_dept) {
params = {
nowUserId: userInfo?.value?.ID,
}
} else {
params = {
nowUserId: 0,
}
}
}
}
}
const res = await departmentV2TreeAll2(params)
if (res.status === 0) {
depTreeMyList.value = res.data.nodes
}
}
//获取指定部门下的所有岗位
const getPositionByDepartment = async (params) => {
@ -89,7 +51,7 @@ export const useGroupTypeStore = createGlobalState(() => {
}
const getDepMembers = async (param) => {
const res = await userV2List2(param)
const res = await userV2List(param)
return res
}
@ -162,7 +124,6 @@ export const useGroupTypeStore = createGlobalState(() => {
postTreeList,
departmentAllPositions,
getDepsTreeMy,
getDepsTreeMy2,
getPositionByDepartment,
getPositionsTree,
crumbs,

View File

@ -15,10 +15,6 @@ import { useAuth } from '../auth/index'
// let keyboardTimeout = null
export const useDialogueStore = defineStore('dialogue', {
// 添加持久化配置
persist: {
paths: ['talk.talk_type', 'talk.receiver_id']
},
state: () => {
return {
// 对话索引(聊天对话的唯一索引)
@ -52,15 +48,6 @@ export const useDialogueStore = defineStore('dialogue', {
// 是否显示会话列表
isShowSessionList: true,
//是否已被解散
isDismiss: false,
//是否退群/移出群
isQuit: false,
//未读消息数量
unreadNum:0,
// 群成员列表
members: [],
@ -97,21 +84,6 @@ export const useDialogueStore = defineStore('dialogue', {
this.online = status
},
// 更新未读消息数量-清空未读
clearUnreadNum() {
this.unreadNum = 0
},
// 更新群解散状态
updateDismiss() {
this.isDismiss = true
},
// 更新群成员退出状态
updateQuit() {
this.isQuit = true
},
// 更新对话信息
setDialogue(data = {}) {
this.online = data.is_online == 1
@ -126,13 +98,8 @@ export const useDialogueStore = defineStore('dialogue', {
this.unreadBubble = 0
this.isShowEditor = data?.is_robot === 0
this.isDismiss = data?.is_dismiss === 1 ? true : false
this.isQuit = data?.is_quit === 1 ? true : false
this.unreadNum = data?.unread_num || 0
this.members = []
if (data.talk_type == 2 && !this.isDismiss && !this.isQuit) {
if (data.talk_type == 2) {
this.updateGroupMembers()
}
},
@ -242,6 +209,8 @@ export const useDialogueStore = defineStore('dialogue', {
if (res.code == 200) {
this.batchDelDialogueRecord(msgIds)
batchDelDialogueRecord(msgIds)
} else {
message.warning(res.message)
}
})
},
@ -265,6 +234,8 @@ export const useDialogueStore = defineStore('dialogue', {
ServeRevokeRecords({ msg_id }).then((res) => {
if (res.code == 200) {
this.updateDialogueRecord({ msg_id, is_revoke: 1 })
} else {
message.warning(res.message)
}
})
},

View File

@ -7,105 +7,7 @@ import { createGlobalState, useStorage } from '@vueuse/core'
import { uniStorage } from '@/utils/uniStorage.js'
export const useDialogueListStore = createGlobalState(() => {
const testDatabase = async () => {
// 初始化数据库
let chatDatabase = {
eventType: 'openDatabase',
eventParams: {
name: 'chat',
path: '_doc/chat.db',
},
}
let chatDBexecuteSql = {
eventType: 'executeSql',
eventParams: {
name: 'chat',
sql: `CREATE TABLE IF NOT EXISTS talk_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
msg_id TEXT NOT NULL,
sequence INTEGER NOT NULL,
talk_type INTEGER NOT NULL DEFAULT 1,
msg_type INTEGER NOT NULL DEFAULT 1,
user_id INTEGER NOT NULL DEFAULT 0,
receiver_id INTEGER NOT NULL DEFAULT 0,
is_revoke INTEGER NOT NULL DEFAULT 0,
is_mark INTEGER NOT NULL DEFAULT 0,
quote_id TEXT NOT NULL,
extra TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
biz_date TEXT
)`,
},
}
const content = {
content: "我试试传送文件和图片是不是一个接口",
name: "测试excel1.xlsx",
path: "https://cdn-test.szjixun.cn/fonchain-chat/chat/file/multipart/20250307/727a2371-ffc4-46da-b953-a7d449ff82ff-测试excel1.xlsx",
size: 9909,
drive: 3
};
const extra = JSON.stringify(content);
let chatDBexecuteSql2 = {
eventType: 'executeSql',
eventParams: {
name: 'chat',
sql: 'INSERT INTO talk_records (msg_id, sequence, talk_type, msg_type, user_id, receiver_id, is_revoke, is_mark, quote_id, extra, created_at, updated_at, biz_date) VALUES ("'+'77b715fb30f54f739a255a915ef72445'+'", 166, 2, 1, 1774, 888890, 0, 0, "'+''+'", "'+extra+'", "'+'2025-03-06T15:57:07.000Z'+'", "'+'2025-03-06T15:57:07.000Z'+'", "'+'20250306'+'")',
},
}
let chatDBSelectSql = {
eventType: 'selectSql',
eventParams: {
name: 'chat',
sql: `SELECT * FROM talk_records ORDER BY sequence DESC LIMIT 20`,
},
}
let chatDBIsOpenDatabase = {
eventType: 'isOpenDatabase',
eventParams: {
name: 'chat',
path: '_doc/chat.db',
},
}
document.addEventListener('plusready', () => {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview, index) => {
if (webview.id === 'webviewId1') {
webview.evalJS(
`operateSQLite('${encodeURIComponent(
JSON.stringify(chatDatabase),
)}')`,
)
webview.evalJS(
`operateSQLite('${encodeURIComponent(
JSON.stringify(chatDBexecuteSql),
)}')`,
)
webview.evalJS(
`operateSQLite('${encodeURIComponent(
JSON.stringify(chatDBexecuteSql2),
)}')`,
)
webview.evalJS(
`operateSQLite('${encodeURIComponent(
JSON.stringify(chatDBSelectSql),
)}')`,
)
webview.evalJS(
`operateSQLite('${encodeURIComponent(
JSON.stringify(chatDBIsOpenDatabase),
)}')`,
)
}
})
})
}
// testDatabase()
const dialogueList = useStorage('dialogueList', [], uniStorage)
// const dialogueList = ref([])
const dialogueList = ref([])
const zpagingRef = ref()
const virtualList = ref([])
@ -116,49 +18,18 @@ export const useDialogueListStore = createGlobalState(() => {
const addDialogueRecord = (newRecords, type = 'add') => {
console.log(newRecords)
const dialogue = useDialogueStore()
const dialogue = lodash.cloneDeep(useDialogueStore())
if (!dialogue || typeof dialogue !== 'object') return
// 检查是否已存在相同 index_name 的对话
const existingIndex = dialogueList.value.findIndex(
(item) => item.index_name === dialogue.index_name,
)
if (existingIndex === -1) {
// 如果不存在,创建新对话,只保存需要的属性
const newDialogue = {
index_name: dialogue.index_name,
talk: {
username: dialogue.talk.username,
talk_type: dialogue.talk.talk_type,
receiver_id: dialogue.talk.receiver_id,
},
online: dialogue.online,
records: dialogue.records || [],
unreadBubble: dialogue.unreadBubble,
isOpenMultiSelect: dialogue.isOpenMultiSelect,
isShowEditor: dialogue.isShowEditor,
isShowSessionList: dialogue.isShowSessionList,
isDismiss: dialogue.isDismiss,
isQuit: dialogue.isQuit,
unreadNum: dialogue.unreadNum,
members: dialogue.members.map((member) => ({
id: member.id,
nickname: member.nickname,
avatar: member.avatar,
gender: member.gender,
leader: member.leader,
remark: member.remark,
online: member.online,
value: member.value,
key: member.key,
erp_user_id: member.erp_user_id,
is_mute: member.is_mute,
is_mine: member.is_mine,
})),
forwardType: dialogue.forwardType,
}
dialogueList.value.push(newDialogue)
// 如果不存在,直接添加
dialogueList.value.push(dialogue)
} else {
// 如果对话存在,处理 records 数组
const { records = [] } = dialogue
newRecords.forEach((newRecord) => {
const recordIndex = dialogueList.value[existingIndex].records.findIndex(
(record) => record.msg_id === newRecord.msg_id,
@ -173,11 +44,18 @@ export const useDialogueListStore = createGlobalState(() => {
}
}
})
// 更新除 records 和 index_name 外的其他属性
const { index_name, records: _, ...updateProps } = dialogue
dialogueList.value[existingIndex] = {
...dialogueList.value[existingIndex],
...updateProps,
}
}
}
const updateDialogueRecord = (record) => {
const dialogue = useDialogueStore()
const dialogue = lodash.cloneDeep(useDialogueStore())
const item = getDialogueList(dialogue.index_name)
const recordIndex = item.records.findIndex(
(item) => item.msg_id === record.msg_id,
@ -200,7 +78,7 @@ export const useDialogueListStore = createGlobalState(() => {
}
const deleteDialogueRecord = (record) => {
const dialogue = useDialogueStore()
const dialogue = lodash.cloneDeep(useDialogueStore())
const item = getDialogueList(dialogue.index_name)
const recordIndex = item.records.findIndex(
(item) => item.msg_id === record.msg_id,
@ -226,29 +104,14 @@ export const useDialogueListStore = createGlobalState(() => {
}
const addChatRecord = (indexName, item) => {
const dialogue = useDialogueStore()
const dialogue = lodash.cloneDeep(useDialogueStore())
if (dialogue?.index_name === indexName) {
if (item?.file_num) {
const index = virtualList.value.findIndex(
(v) => v?.file_num === item?.file_num,
)
if (index > -1) {
// 保持响应性的同时替换整个对象
virtualList.value.splice(index, 1, {
...virtualList.value[index], // 保留原有不需要修改的字段
...item, // 覆盖需要更新的字段
})
} else {
zpagingRef.value?.addChatRecordData(item, false, false)
}
} else {
zpagingRef.value?.addChatRecordData(item, false, false)
}
zpagingRef.value?.addChatRecordData(item, false, false)
}
}
const batchDelDialogueRecord = (msgIds) => {
const dialogue = useDialogueStore()
const dialogue = lodash.cloneDeep(useDialogueStore())
const item = getDialogueList(dialogue.index_name)
item.records = item.records.filter((item) => !msgIds.includes(item.msg_id))
}
@ -266,7 +129,7 @@ export const useDialogueListStore = createGlobalState(() => {
//清空聊天记录时,同时清空本地保存的聊天记录
const clearDialogueRecord = () => {
const dialogue = useDialogueStore()
const dialogue = lodash.cloneDeep(useDialogueStore())
const item = getDialogueList(dialogue.index_name)
item.records = []
virtualList.value = []

View File

@ -41,6 +41,8 @@ export const useEditorStore = defineStore('editor', {
}).then((res) => {
if (res.code == 200) {
this.loadUserEmoticon()
} else {
message.warning(res.message)
}
})
},
@ -53,6 +55,8 @@ export const useEditorStore = defineStore('editor', {
ServeUploadEmoticon(data).then((res) => {
if (res.code == 200) {
this.emoticon.items[1].children.unshift(res.data)
} else {
message.warning(res.message)
}
})
},
@ -65,7 +69,9 @@ export const useEditorStore = defineStore('editor', {
if (res.code == 200) {
this.emoticon.items[1].children.splice(resoure.index, 1)
message.success('删除成功')
}
} else {
message.warning(res.message)
}
})
}
}

View File

@ -102,24 +102,6 @@ export const useTalkStore = defineStore('talk', {
// 返回 Promise 对象,使调用方可以使用 then/catch
return resp.then(({ code, data }) => {
if (code == 200) {
//向OA的webview通信改变未读消息数量
if (typeof plus !== 'undefined') {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`doUpdateUnreadNum('${data.unread_num}')`)
}
})
} else {
document.addEventListener('plusready', () => {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`doUpdateUnreadNum('${data.unread_num}')`)
}
})
})
}
this.items = data.items.map((item) => {
const value = formatTalkItem(item)
@ -162,7 +144,6 @@ export const useTalkStore = defineStore('talk', {
ServeCreateTalkList({
talk_type,
receiver_id,
erp_user_id,
}).then(({ code, data, message }) => {
if (code == 200) {

View File

@ -33,9 +33,7 @@ export const useUploadsStore = defineStore('uploads', {
state: () => {
return {
isShow: false,
items: [],
isUploading: false,//当前是否正在上传
uploadingNum: 0//当前正在上传数量
items: []
}
},
getters: {
@ -77,10 +75,8 @@ export const useUploadsStore = defineStore('uploads', {
this.triggerUpload(upload_id,msgId)
this.isShow = true
} else {
this.updateUploadStatus(false)
message.error(res.message)
}
}).catch(()=> {
this.updateUploadStatus(false)
})
},
@ -97,19 +93,7 @@ export const useUploadsStore = defineStore('uploads', {
item.status = 1
// 开始上传时就更新进度
const currentPercentage = (item.uploadIndex / item.files.length) * 100
item.percentage = currentPercentage.toFixed(1)
updateUploadProgress(msgId, currentPercentage)
ServeFileSubareaUpload(form, (progressEvent) => {
// 计算当前分片的进度
const currentChunkProgress = (progressEvent.loaded / progressEvent.total) * 100
// 计算总体进度:已完成分片 + 当前分片的进度
const totalProgress = ((item.uploadIndex + currentChunkProgress / 100) / item.files.length) * 100
item.percentage = totalProgress.toFixed(1)
updateUploadProgress(msgId, totalProgress)
})
ServeFileSubareaUpload(form)
.then((res) => {
if (res.code == 200) {
item.uploadIndex++
@ -120,81 +104,32 @@ export const useUploadsStore = defineStore('uploads', {
console.log(msgId,'msgId');
updateUploadProgress(msgId,100)
this.sendUploadMessage(item, msgId)
// 更新虚拟列表中的状态
const { virtualList } = useDialogueListStore()
const index = virtualList.value.findIndex(item => item.file_num === msgId)
if (index !== -1) {
virtualList.value[index].uploadStatus = 2
virtualList.value[index].uploadCurrent = 100
}
this.sendUploadMessage(item)
} else {
// 继续上传下一个分片
this.triggerUpload(uploadId, msgId)
const percentage = (item.uploadIndex / item.files.length) * 100
item.percentage = percentage.toFixed(1)
console.log(msgId,'msgId');
console.log(percentage,'percentage');
updateUploadProgress(msgId,percentage)
this.triggerUpload(uploadId,msgId)
}
} else {
this.updateUploadStatus(false)
item.status = 3
// 更新虚拟列表中的状态为失败
const { virtualList } = useDialogueListStore()
const index = virtualList.value.findIndex(item => item.file_num === msgId)
if (index !== -1) {
virtualList.value[index].uploadStatus = 3
}
}
})
.catch(() => {
this.updateUploadStatus(false)
item.status = 3
// 更新虚拟列表中的状态为失败
const { virtualList } = useDialogueListStore()
const index = virtualList.value.findIndex(item => item.file_num === msgId)
if (index !== -1) {
virtualList.value[index].uploadStatus = 3
}
})
},
// 发送上传消息
sendUploadMessage(item: any, file_num: String) {
sendUploadMessage(item: any) {
ServeSendTalkFile({
upload_id: item.upload_id,
receiver_id: item.receiver_id,
talk_type: item.talk_type,
file_num: file_num
}).then((res) => {
console.log(res, 'res')
if(res.code == 200){
this.updateUploadStatus(false)
}else{
this.updateUploadStatus(false)
}
}).catch(() => {
this.updateUploadStatus(false)
talk_type: item.talk_type
})
},
//更新资源上传状态
updateUploadStatus(isUploading: boolean){
if(isUploading){
this.uploadingNum++
this.isUploading = true
}else{
this.uploadingNum--
if(this.uploadingNum < 0){
this.uploadingNum = 0
}
if(this.uploadingNum === 0){
this.isUploading = false
}
}
},
// 清除上传
clearUpload(){
this.isUploading = false
this.uploadingNum = 0
}
}
})

File diff suppressed because one or more lines are too long

View File

@ -1,19 +0,0 @@
## 1.0.2503312025-03-31
增加RecordApp.UniNativeUtsPlugin_OnJsCall接口App端搭配原生插件使用时可绑定接收配套原生录音插件事件原生插件新增PcmPlayer播放器支持流式播放、完整播放App端边录音边播放更流畅
## 1.0.2501112025-01-11
修复vue3 Fragments(multi-root 多个根节点)的兼容性问题修复uniapp Android自带的XXPermissions库在后台无法请求权限的问题仅限搭配原生录音插件可用
## 1.0.2410202024-10-20
适配HBuilder4.28 vue3 setup编译环境下$root.$scope无法读取的bugHBuilder4.29已修复此编译bug但似乎还是有不能使用的问题。如果setup内不能使用可尝试新建个vue组件然后使用选项式api来调用录音功能页面的setup内使用此vue组件
## 1.0.2409102024-09-10
- 新增RecordApp.UniMainCallBack_Register接口允许App renderjs层多次回调数据给逻辑层
- iOS App请求权限时会预先检查NSMicrophoneUsageDescription是否声明避免无声明时调用录音会崩溃
- 新增appNativePlugin_sampleRate原生插件录音选项
- Android App已提供后台录音保活功能启用后App在后台或锁屏后可继续正常录音
## 1.0.2406252024-06-25
调整UniWebViewCallAsync调用失败时返回更详细信息。android_audioSource默认值由1改成0新增ios_categoryOptions原生插件录音选项
## 1.0.2404092024-04-09
增加功能调用完善demo项目
## 1.0.2312082023-12-08
完善文档增加asr语音识别示例
## 1.0.2312012023-12-04
第一次发布

View File

@ -1,6 +0,0 @@
<template>
<view>
<view style="font-weight: bold;">Recorder-UniCore Vue Component</view>
<view style="font-size:14px; color:#f60">无需手动显示本UI组件只需在script中正常引入 RecordApp + app-uni-support.js 即可实现 H5iOS Android App微信小程序 多端录音</view>
</view>
</template>

View File

@ -1,495 +0,0 @@
/*
Recorder ../app-support-sample/demo_UniApp/uni_modules/Recorder-UniCore/i18n/Template.js
https://github.com/xiangyuecn/Recorder
Usage: Recorder.i18n.lang="Your-Language-Name" or "your-language"
Desc: This file is a language translation template file. After copying and renaming, translate the text into the corresponding language. 此文件为语言翻译模板文件复制并改名后将文本翻译成对应语言即可
注意请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成只允许在字符串中填写翻译后的文本请勿改变代码结构翻译的文本如果需要明确的空值请填写"=Empty";文本中的变量用{n}表示n代表第几个变量所有变量必须都出现至少一次如果不要某变量用{n!}表示
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
factory(win.Recorder,browser);
}(function(Recorder,isBrowser){
"use strict";
var i18n=Recorder.i18n;
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-1 End @@
//@@Exec i18n.lang="Your-Language-Name";
Recorder.CLog('Import Page[Recorder_UniCore] lang="Your-Language-Name"');
//@@Exec i18n.alias["Your-Language-Name"]="your-language";
var putSet={lang:"your-language"};
//@@Exec i18n.data["rtl$your-language"]=false;
i18n.data["desc-page-Recorder_UniCore$your-language"]="This file is a language translation template file. After copying and renaming, translate the text into the corresponding language. 此文件为语言翻译模板文件,复制并改名后,将文本翻译成对应语言即可。";
//@@Exec i18n.GenerateDisplayEnglish=true;
//*************** Begin srcFile=../app-support-sample/demo_UniApp/uni_modules/Recorder-UniCore/app-uni-support.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="微信小程序中需要:{1}"
//@@en="WeChat miniProgram requires: {1}"
//@@Put0
"RXs7:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="Recorder-UniCore目前只支持H5、APP(Android iOS)、MP-WEIXIN其他平台环境需要自行编写适配文件实现接入"
//@@en="Recorder-UniCore currently only supports: H5, APP (Android iOS), MP-WEIXIN, other platform environments need to write their own adaptation files to achieve access"
,"4ATo:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="RecordApp.UniWebViewActivate 需要传入当前页面或组件的this对象作为参数"
//@@en="RecordApp.UniWebViewActivate needs to pass in the this object of the current page or component as a parameter"
,"GwCz:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="RecordApp.UniWebViewActivate 发生不应该出现的错误(可能需要升级插件代码):"
//@@en="An error occurred in RecordApp.UniWebViewActivate that should not occur (the plug-in code may need to be upgraded): "
,"ipB3:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="RecordApp.UniWebViewActivate 已切换当前页面或组件的renderjs所在的WebView"
//@@en="RecordApp.UniWebViewActivate has switched the WebView where the renderjs of the current page or component is located"
,"WpKg:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="RecordApp.UniRenderjsRegister 发生不应该出现的错误(可能需要升级插件代码):"
//@@en="An error occurred in RecordApp.UniRenderjsRegister that should not occur (the plugin code may need to be upgraded): "
,"Uc9E:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="RecordApp.UniRenderjsRegister 重复注册当前页面renderjs模块一个组件内只允许一个renderjs模块进行注册"
//@@en="RecordApp.UniRenderjsRegister repeatedly registers the renderjs module of the current page. Only one renderjs module is allowed to be registered in a component"
,"mzKj:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="RecordApp.UniRenderjsRegister 已注册当前页面renderjs模块"
//@@en="RecordApp.UniRenderjsRegister has registered the renderjs module of the current page"
,"7kJS:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="严重兼容性问题无法获取页面或组件this.$root.$scope或.$page"
//@@en="Serious compatibility issue: Unable to get page or component this.$root.$scope or .$page"
,"KpY6:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="需要先调用RecordApp.UniWebViewActivate方法"
//@@en="You need to call the RecordApp.UniWebViewActivate method first"
,"AGd7:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="需先调用RecordApp.RequestPermission方法"
//@@en="You need to call the RecordApp.RequestPermission method first"
,"7ot0:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="需重新调用RecordApp.RequestPermission方法"
//@@en="The RecordApp.RequestPermission method needs to be called again"
,"VsdN:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="检测到有其他页面或组件调用了RecordApp.UniPageOnShowWvCid={1}但未调用过RecordApp.UniWebViewActivate当前WvCid={2}部分功能会继续使用之前Activate的WebView和组件请确保这是符合你的业务逻辑不是因为忘记了调用UniWebViewActivate"
//@@en="It is detected that another page or component has called RecordApp.UniPageOnShow (WvCid={1}), but RecordApp.UniWebViewActivate (current WvCid={2}) has not been called. Some functions will continue to use the previously Activated WebView and components. Please make sure This is in line with your business logic, not because you forgot to call UniWebViewActivate"
,"SWsy:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="{1}未正确查询到节点将使用传入的当前页面或组件this的$el.parentNode作为组件根节点。如果template下存在多个根节点(vue3 multi-root)尽量在最外面再套一层view来避免兼容性问题"
//@@en="{1} does not query the node correctly, and will use the current page or component this's $el.parentNode as the component root node. If there are multiple root nodes under the template (vue3 multi-root), try to add another layer of view on the outermost to avoid compatibility issues"
,"dX7B:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="{1}需在renderjs中调用并且传入当前模块的this"
//@@en="{1} needs to be called in renderjs and pass in this of the current module"
,"dX5B:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="{1}需要传入当前页面或组件的this对象作为参数"
//@@en="{1} needs to pass in the this object of the current page or component as a parameter"
,"dX6B:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="当前不是App逻辑层"
//@@en="Currently it is not the App logic layer"
,"TfJX:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前还未调用过RecordApp.UniWebViewActivate"
//@@en="RecordApp.UniWebViewActivate has not been called yet"
,"peIm:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未找到此页面renderjs所在的WebView"
//@@en="The WebView where renderjs for this page is not found"
,"qDo1:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不可以调用RecordApp.UniWebViewEval"
//@@en=", RecordApp.UniWebViewEval cannot be called"
,"igw2:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前不是App逻辑层"
//@@en="Currently it is not the App logic layer"
,"lU1W:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前还未调用过RecordApp.UniWebViewActivate"
//@@en="RecordApp.UniWebViewActivate has not been called yet"
,"mSbR:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未找到此页面renderjs所在的WebView Cid"
//@@en="The WebView Cid where renderjs for this page is not found"
,"6Iql:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不可以调用RecordApp.UniWebViewVueCall"
//@@en=", RecordApp.UniWebViewVueCall cannot be called"
,"TtoS:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="renderjs中未import导入RecordApp"
//@@en="RecordApp is not imported in renderjs"
,"U1Be:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="renderjs中的mounted内需要调用RecordApp.UniRenderjsRegister"
//@@en="RecordApp.UniRenderjsRegister needs to be called in mounted in renderjs"
,"Bcgi:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="没有找到组件的renderjs模块"
//@@en="The renderjs module for the component was not found"
,"URyD:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="{1}连接renderjs超时"
//@@en="{1} connection renderjs timeout"
,"KQhJ:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="{1}处理超时"
//@@en="{1} processing timeout"
,"RDcZ:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="需要在页面中提供一个renderjs在里面import导入RecordApp、录音格式编码器、可视化插件等"
//@@en="You need to provide a renderjs in the page, and import RecordApp, recording format encoder, visualization plug-in, etc."
,"TSmQ:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="需在renderjs中import {1}"
//@@en="Need to import {1} in renderjs"
,"AN0e:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="不应该出现的MainReceiveBind重复绑定"
//@@en="MainReceiveBind duplicate binding that should not occur"
,"vEgr:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="从renderjs发回数据但UniMainCallBack回调不存在"
//@@en="Sending data back from renderjs but UniMainCallBack callback does not exist: "
,"kZx6:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="[MainReceive]从renderjs发回未知数据"
//@@en="[MainReceive] Unknown data sent back from renderjs: "
,"ZHwv:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="只允许在renderjs中调用RecordApp.UniWebViewSendBigBytesToMain"
//@@en="Only allowed to call RecordApp.UniWebViewSendBigBytesToMain in renderjs"
,"MujG:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="renderjs中的mounted内需要调用RecordApp.UniRenderjsRegister才能调用RecordApp.UniWebViewSendBigBytesToMain"
//@@en="RecordApp.UniRenderjsRegister needs to be called in mounted in renderjs to call RecordApp.UniWebViewSendBigBytesToMain"
,"kE91:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="无效的BigBytes回传数据"
//@@en="Invalid BigBytes return data"
,"CjMb:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="保存文件{1}失败:"
//@@en="Failed to save file {1}: "
,"UqfI:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="当前环境未支持保存本地文件"
//@@en="The current environment does not support saving local files"
,"kxOd:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh=" | RecordApp的uni-app支持文档和示例{1} "
//@@en=" | RecordApps uni-app support documentation and examples: {1}"
,"1f2V:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="当前录音由原生录音插件提供支持"
//@@en="Current recording is powered by native recording plug-in"
,"XSYY:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前录音由uts插件提供支持"
//@@en="Current recording is powered by uts plugin"
,"nnM6:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前已配置RecordApp.UniWithoutAppRenderjs必须提供原生录音插件或uts插件才能录音请参考RecordApp.UniNativeUtsPlugin配置"
//@@en="RecordApp.UniWithoutAppRenderjs is currently configured. A native recording plug-in or uts plug-in must be provided to record. Please refer to the RecordApp.UniNativeUtsPlugin configuration"
,"fqhr:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前RecordApp运行在逻辑层中性能会略低一些可视化等插件不可用"
//@@en="Currently RecordApp runs in the logical layer (performance will be slightly lower, and plug-ins such as visualization are not available) "
,"xYRb:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未找到当前页面renderjs所在的WebView"
//@@en="The WebView where renderjs of the current page is located is not found"
,"S3eF:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前RecordApp运行在renderjs所在的WebView中逻辑层中只能做有限的实时处理可视化等插件均需要在renderjs中进行调用"
//@@en="The current RecordApp runs in the WebView where renderjs is located (only limited real-time processing can be done in the logic layer, and visualization and other plug-ins need to be called in renderjs) "
,"0hyi:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="请检查此页面代码中是否编写了lang=renderjs的module并且调用了RecordApp.UniRenderjsRegister如果确实没有renderjs比如nvue页面请设置RecordApp.UniWithoutAppRenderjs=true并且搭配配套的原生插件在逻辑层中直接录音"
//@@en=", please check whether the module with lang=renderjs is written in the code of this page and RecordApp.UniRenderjsRegister is called; if there is indeed no renderjs, such as nvue page, please set RecordApp.UniWithoutAppRenderjs=true and use the matching native plug-in to record directly in the logic layer"
,"e6Mo:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="【在App内使用{1}的授权许可】"
//@@en="[License for use of {1} within the App] "
,"FabE:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="已购买原生录音插件,获得授权许可"
//@@en="Purchased the native recording plug-in and obtained the license"
,"w37G:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="已购买uts插件获得授权许可"
//@@en="Purchased uts plug-in and obtained license"
,"e71S:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="UniAppUseLicense填写无效如果已获取到了商用授权请填写{1},否则请使用空字符串"
//@@en="UniAppUseLicense is invalid. If you have obtained a commercial license, please fill in: {1}, otherwise please use an empty string"
,"aPoj:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="未找到Canvas{1}请确保此DOM已挂载可尝试用$nextTick等待DOM更新"
//@@en="Canvas not found: {1}, please make sure this DOM is mounted (try $nextTick to wait for DOM update) "
,"k7im:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="RecordApp.UniFindCanvas未适配当前环境"
//@@en="RecordApp.UniFindCanvas does not adapt to the current environment"
,"yI24:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未配置RecordApp.UniNativeUtsPlugin原生录音插件"
//@@en="RecordApp.UniNativeUtsPlugin native recording plug-in is not configured"
,"H753:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="renderjs中不支持设置RecordApp.UniNativeUtsPlugin"
//@@en="Setting RecordApp.UniNativeUtsPlugin is not supported in renderjs"
,"l6sY:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="当前App未打包进双端原生插件[{1}],尝试加载单端[{2}]"
//@@en="The current App is not packaged into the dual-end native plug-in [{1}], try to load the single-end [{2}]"
,"kSjQ:"+ //args: {1}-{2}
"" /** TODO: translate to your-language **/
//@@zh="已加载原生录音插件[{1}]"
//@@en="Native recording plugin loaded [{1}]"
,"Xh1W:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="配置了RecordApp.UniNativeUtsPlugin但当前App未打包进原生录音插件[{1}]"
//@@en="RecordApp.UniNativeUtsPlugin is configured, but the current App is not packaged with the native recording plug-in [{1}]"
,"SCW9:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="提供的RecordApp.UniNativeUtsPlugin值不是RecordApp的uts原生录音插件"
//@@en="The provided RecordApp.UniNativeUtsPlugin value is not RecordApps uts native recording plug-in"
,"TGMm:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="需在App逻辑层中调用原生插件功能"
//@@en="The native plug-in function needs to be called in the App logic layer"
,"MrBx:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未开始录音,不可以调用{1}"
//@@en="Recording has not started and {1} cannot be called"
,"0FGq:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="需先调用RecordApp.UniWebViewActivate然后才可以调用RequestPermission"
//@@en="RecordApp.UniWebViewActivate needs to be called first, and then RequestPermission can be called"
,"PkQ2:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不应当出现的非H5权限请求"
//@@en="Non-H5 permission requests that should not appear"
,"Jk72:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="正在调用plus.ios@AVAudioSession请求iOS原生录音权限"
//@@en="Calling plus.ios@AVAudioSession to request iOS native recording permissions"
,"Y3rC:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="项目配置中未声明iOS录音权限{1}"
//@@en="iOS recording permission {1} is not declared in the project configuration"
,"9xoE:"+ //args: {1}
"" /** TODO: translate to your-language **/
//@@zh="已获得iOS原生录音权限"
//@@en="Obtained iOS native recording permissions"
,"j15C:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="plus.ios请求录音权限状态值: "
//@@en="plus.ios requests recording permission, status value: "
,"iKhe:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="正在调用plus.android.requestPermissions请求Android原生录音权限"
//@@en="Calling plus.android.requestPermissions to request Android native recording permissions"
,"7Noe:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="已获得Android原生录音权限"
//@@en="Obtained Android native recording permission: "
,"Bgls:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="plus.android请求录音权限无权限"
//@@en="plus.android requests recording permission: No permission"
,"Ruxl:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="plus.android请求录音权限出错"
//@@en="plus.android error in requesting recording permission: "
,"0JQw:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="调用plus的权限请求出错"
//@@en="An error occurred in the permission request to call plus: "
,"Mvl7:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="用户拒绝了录音权限"
//@@en="User denied recording permission"
,"0caE:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="正在调用原生插件请求录音权限"
//@@en="Calling the native plug-in to request recording permission"
,"Lx5r:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="已获得录音权限"
//@@en="Recording permission obtained"
,"Lx6r:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="无录音权限"
//@@en="No recording permission"
,"Lx7r:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="无法调用RequestPermission"
//@@en="Unable to call RequestPermission: "
,"ksoA:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="无法连接到renderjs"
//@@en="Unable to connect to renderjs"
,"KnF0:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="需先调用RecordApp.UniWebViewActivate然后才可以调用Start"
//@@en="RecordApp.UniWebViewActivate needs to be called first, and then Start can be called"
,"XCMU:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不应当出现的非H5录音Start"
//@@en="Start of non-H5 recordings that should not appear"
,"rSLO:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="无法调用Start"
//@@en="Unable to call Start: "
,"Bjx9:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未开始录音但收到renderjs回传的onRecEncodeChunk"
//@@en="Recording did not start, but onRecEncodeChunk returned by renderjs was received"
,"MTdp:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未开始录音但收到Uni Native PCM数据"
//@@en="Recording did not start, but Uni Native PCM data was received"
,"BjGP:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未开始录音但收到UniNativeUtsPlugin PCM数据"
//@@en="Recording did not start, but UniNativeUtsPlugin PCM data was received"
,"byzO:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未开始录音"
//@@en="Recording not started"
,"YP4V:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不应当出现的非H5录音Stop"
//@@en="Stop non-H5 recordings that should not appear"
,"TPhg:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="未开始录音"
//@@en="Recording not started"
,"pP4O:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="无法调用Stop"
//@@en="Unable to call Stop: "
,"H6cq:"+ //no args
"" /** TODO: translate to your-language **/
//@@zh="不应该出现的renderjs发回的文件数据丢失"
//@@en="The file data sent back by renderjs should not be lost"
,"gomD:"+ //no args
"" /** TODO: translate to your-language **/
]);
//*************** End srcFile=../app-support-sample/demo_UniApp/uni_modules/Recorder-UniCore/app-uni-support.js ***************
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-2 End @@
}));

View File

@ -1,406 +0,0 @@
/*
Recorder ../app-support-sample/demo_UniApp/uni_modules/Recorder-UniCore/i18n/en-US.js
https://github.com/xiangyuecn/Recorder
Usage: Recorder.i18n.lang="en-US" or "en"
Desc: English, 英语This translation mainly comes from: google translation + Baidu translation, translated from Chinese to English. 此翻译主要来自google翻译+百度翻译由中文翻译成英文
注意请勿修改//@@打头的文本行;以下代码结构由/src/package-i18n.js自动生成只允许在字符串中填写翻译后的文本请勿改变代码结构翻译的文本如果需要明确的空值请填写"=Empty";文本中的变量用{n}表示n代表第几个变量所有变量必须都出现至少一次如果不要某变量用{n!}表示
Note: Do not modify the text lines starting with //@@; The following code structure is automatically generated by /src/package-i18n.js, only the translated text is allowed to be filled in the string, please do not change the code structure; If the translated text requires an explicit empty value, please fill in "=Empty"; Variables in the text are represented by {n} (n represents the number of variables), all variables must appear at least once, if a variable is not required, it is represented by {n!}
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
factory(win.Recorder,browser);
}(function(Recorder,isBrowser){
"use strict";
var i18n=Recorder.i18n;
//@@User Code-1 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-1 End @@
//@@Exec i18n.lang="en-US";
Recorder.CLog('Import Page[Recorder_UniCore] lang="en-US"');
//@@Exec i18n.alias["en-US"]="en";
var putSet={lang:"en"};
//@@Exec i18n.data["rtl$en"]=false;
i18n.data["desc-page-Recorder_UniCore$en"]="English, 英语。This translation mainly comes from: google translation + Baidu translation, translated from Chinese to English. 此翻译主要来自google翻译+百度翻译,由中文翻译成英文。";
//@@Exec i18n.GenerateDisplayEnglish=false;
//*************** Begin srcFile=../app-support-sample/demo_UniApp/uni_modules/Recorder-UniCore/app-uni-support.js ***************
i18n.put(putSet,
[ //@@PutList
//@@zh="微信小程序中需要:{1}"
//@@Put0
"RXs7:"+ //args: {1}
"WeChat miniProgram requires: {1}"
//@@zh="Recorder-UniCore目前只支持H5、APP(Android iOS)、MP-WEIXIN其他平台环境需要自行编写适配文件实现接入"
,"4ATo:"+ //no args
"Recorder-UniCore currently only supports: H5, APP (Android iOS), MP-WEIXIN, other platform environments need to write their own adaptation files to achieve access"
//@@zh="RecordApp.UniWebViewActivate 需要传入当前页面或组件的this对象作为参数"
,"GwCz:"+ //no args
"RecordApp.UniWebViewActivate needs to pass in the this object of the current page or component as a parameter"
//@@zh="RecordApp.UniWebViewActivate 发生不应该出现的错误(可能需要升级插件代码):"
,"ipB3:"+ //no args
"An error occurred in RecordApp.UniWebViewActivate that should not occur (the plug-in code may need to be upgraded): "
//@@zh="RecordApp.UniWebViewActivate 已切换当前页面或组件的renderjs所在的WebView"
,"WpKg:"+ //no args
"RecordApp.UniWebViewActivate has switched the WebView where the renderjs of the current page or component is located"
//@@zh="RecordApp.UniRenderjsRegister 发生不应该出现的错误(可能需要升级插件代码):"
,"Uc9E:"+ //no args
"An error occurred in RecordApp.UniRenderjsRegister that should not occur (the plugin code may need to be upgraded): "
//@@zh="RecordApp.UniRenderjsRegister 重复注册当前页面renderjs模块一个组件内只允许一个renderjs模块进行注册"
,"mzKj:"+ //no args
"RecordApp.UniRenderjsRegister repeatedly registers the renderjs module of the current page. Only one renderjs module is allowed to be registered in a component"
//@@zh="RecordApp.UniRenderjsRegister 已注册当前页面renderjs模块"
,"7kJS:"+ //no args
"RecordApp.UniRenderjsRegister has registered the renderjs module of the current page"
//@@zh="严重兼容性问题无法获取页面或组件this.$root.$scope或.$page"
,"KpY6:"+ //no args
"Serious compatibility issue: Unable to get page or component this.$root.$scope or .$page"
//@@zh="需要先调用RecordApp.UniWebViewActivate方法"
,"AGd7:"+ //no args
"You need to call the RecordApp.UniWebViewActivate method first"
//@@zh="需先调用RecordApp.RequestPermission方法"
,"7ot0:"+ //no args
"You need to call the RecordApp.RequestPermission method first"
//@@zh="需重新调用RecordApp.RequestPermission方法"
,"VsdN:"+ //no args
"The RecordApp.RequestPermission method needs to be called again"
//@@zh="检测到有其他页面或组件调用了RecordApp.UniPageOnShowWvCid={1}但未调用过RecordApp.UniWebViewActivate当前WvCid={2}部分功能会继续使用之前Activate的WebView和组件请确保这是符合你的业务逻辑不是因为忘记了调用UniWebViewActivate"
,"SWsy:"+ //args: {1}-{2}
"It is detected that another page or component has called RecordApp.UniPageOnShow (WvCid={1}), but RecordApp.UniWebViewActivate (current WvCid={2}) has not been called. Some functions will continue to use the previously Activated WebView and components. Please make sure This is in line with your business logic, not because you forgot to call UniWebViewActivate"
//@@zh="{1}未正确查询到节点将使用传入的当前页面或组件this的$el.parentNode作为组件根节点。如果template下存在多个根节点(vue3 multi-root)尽量在最外面再套一层view来避免兼容性问题"
,"dX7B:"+ //args: {1}
"{1} does not query the node correctly, and will use the current page or component this's $el.parentNode as the component root node. If there are multiple root nodes under the template (vue3 multi-root), try to add another layer of view on the outermost to avoid compatibility issues"
//@@zh="{1}需在renderjs中调用并且传入当前模块的this"
,"dX5B:"+ //args: {1}
"{1} needs to be called in renderjs and pass in this of the current module"
//@@zh="{1}需要传入当前页面或组件的this对象作为参数"
,"dX6B:"+ //args: {1}
"{1} needs to pass in the this object of the current page or component as a parameter"
//@@zh="当前不是App逻辑层"
,"TfJX:"+ //no args
"Currently it is not the App logic layer"
//@@zh="当前还未调用过RecordApp.UniWebViewActivate"
,"peIm:"+ //no args
"RecordApp.UniWebViewActivate has not been called yet"
//@@zh="未找到此页面renderjs所在的WebView"
,"qDo1:"+ //no args
"The WebView where renderjs for this page is not found"
//@@zh="不可以调用RecordApp.UniWebViewEval"
,"igw2:"+ //no args
", RecordApp.UniWebViewEval cannot be called"
//@@zh="当前不是App逻辑层"
,"lU1W:"+ //no args
"Currently it is not the App logic layer"
//@@zh="当前还未调用过RecordApp.UniWebViewActivate"
,"mSbR:"+ //no args
"RecordApp.UniWebViewActivate has not been called yet"
//@@zh="未找到此页面renderjs所在的WebView Cid"
,"6Iql:"+ //no args
"The WebView Cid where renderjs for this page is not found"
//@@zh="不可以调用RecordApp.UniWebViewVueCall"
,"TtoS:"+ //no args
", RecordApp.UniWebViewVueCall cannot be called"
//@@zh="renderjs中未import导入RecordApp"
,"U1Be:"+ //no args
"RecordApp is not imported in renderjs"
//@@zh="renderjs中的mounted内需要调用RecordApp.UniRenderjsRegister"
,"Bcgi:"+ //no args
"RecordApp.UniRenderjsRegister needs to be called in mounted in renderjs"
//@@zh="没有找到组件的renderjs模块"
,"URyD:"+ //no args
"The renderjs module for the component was not found"
//@@zh="{1}连接renderjs超时"
,"KQhJ:"+ //args: {1}
"{1} connection renderjs timeout"
//@@zh="{1}处理超时"
,"RDcZ:"+ //args: {1}
"{1} processing timeout"
//@@zh="需要在页面中提供一个renderjs在里面import导入RecordApp、录音格式编码器、可视化插件等"
,"TSmQ:"+ //no args
"You need to provide a renderjs in the page, and import RecordApp, recording format encoder, visualization plug-in, etc."
//@@zh="需在renderjs中import {1}"
,"AN0e:"+ //args: {1}
"Need to import {1} in renderjs"
//@@zh="不应该出现的MainReceiveBind重复绑定"
,"vEgr:"+ //no args
"MainReceiveBind duplicate binding that should not occur"
//@@zh="从renderjs发回数据但UniMainCallBack回调不存在"
,"kZx6:"+ //no args
"Sending data back from renderjs but UniMainCallBack callback does not exist: "
//@@zh="[MainReceive]从renderjs发回未知数据"
,"ZHwv:"+ //no args
"[MainReceive] Unknown data sent back from renderjs: "
//@@zh="只允许在renderjs中调用RecordApp.UniWebViewSendBigBytesToMain"
,"MujG:"+ //no args
"Only allowed to call RecordApp.UniWebViewSendBigBytesToMain in renderjs"
//@@zh="renderjs中的mounted内需要调用RecordApp.UniRenderjsRegister才能调用RecordApp.UniWebViewSendBigBytesToMain"
,"kE91:"+ //no args
"RecordApp.UniRenderjsRegister needs to be called in mounted in renderjs to call RecordApp.UniWebViewSendBigBytesToMain"
//@@zh="无效的BigBytes回传数据"
,"CjMb:"+ //no args
"Invalid BigBytes return data"
//@@zh="保存文件{1}失败:"
,"UqfI:"+ //args: {1}
"Failed to save file {1}: "
//@@zh="当前环境未支持保存本地文件"
,"kxOd:"+ //no args
"The current environment does not support saving local files"
//@@zh=" | RecordApp的uni-app支持文档和示例{1} "
,"1f2V:"+ //args: {1}
" | RecordApps uni-app support documentation and examples: {1}"
//@@zh="当前录音由原生录音插件提供支持"
,"XSYY:"+ //no args
"Current recording is powered by native recording plug-in"
//@@zh="当前录音由uts插件提供支持"
,"nnM6:"+ //no args
"Current recording is powered by uts plugin"
//@@zh="当前已配置RecordApp.UniWithoutAppRenderjs必须提供原生录音插件或uts插件才能录音请参考RecordApp.UniNativeUtsPlugin配置"
,"fqhr:"+ //no args
"RecordApp.UniWithoutAppRenderjs is currently configured. A native recording plug-in or uts plug-in must be provided to record. Please refer to the RecordApp.UniNativeUtsPlugin configuration"
//@@zh="当前RecordApp运行在逻辑层中性能会略低一些可视化等插件不可用"
,"xYRb:"+ //no args
"Currently RecordApp runs in the logical layer (performance will be slightly lower, and plug-ins such as visualization are not available) "
//@@zh="未找到当前页面renderjs所在的WebView"
,"S3eF:"+ //no args
"The WebView where renderjs of the current page is located is not found"
//@@zh="当前RecordApp运行在renderjs所在的WebView中逻辑层中只能做有限的实时处理可视化等插件均需要在renderjs中进行调用"
,"0hyi:"+ //no args
"The current RecordApp runs in the WebView where renderjs is located (only limited real-time processing can be done in the logic layer, and visualization and other plug-ins need to be called in renderjs) "
//@@zh="请检查此页面代码中是否编写了lang=renderjs的module并且调用了RecordApp.UniRenderjsRegister如果确实没有renderjs比如nvue页面请设置RecordApp.UniWithoutAppRenderjs=true并且搭配配套的原生插件在逻辑层中直接录音"
,"e6Mo:"+ //no args
", please check whether the module with lang=renderjs is written in the code of this page and RecordApp.UniRenderjsRegister is called; if there is indeed no renderjs, such as nvue page, please set RecordApp.UniWithoutAppRenderjs=true and use the matching native plug-in to record directly in the logic layer"
//@@zh="【在App内使用{1}的授权许可】"
,"FabE:"+ //args: {1}
"[License for use of {1} within the App] "
//@@zh="已购买原生录音插件,获得授权许可"
,"w37G:"+ //no args
"Purchased the native recording plug-in and obtained the license"
//@@zh="已购买uts插件获得授权许可"
,"e71S:"+ //no args
"Purchased uts plug-in and obtained license"
//@@zh="UniAppUseLicense填写无效如果已获取到了商用授权请填写{1},否则请使用空字符串"
,"aPoj:"+ //args: {1}
"UniAppUseLicense is invalid. If you have obtained a commercial license, please fill in: {1}, otherwise please use an empty string"
//@@zh="未找到Canvas{1}请确保此DOM已挂载可尝试用$nextTick等待DOM更新"
,"k7im:"+ //args: {1}
"Canvas not found: {1}, please make sure this DOM is mounted (try $nextTick to wait for DOM update) "
//@@zh="RecordApp.UniFindCanvas未适配当前环境"
,"yI24:"+ //no args
"RecordApp.UniFindCanvas does not adapt to the current environment"
//@@zh="未配置RecordApp.UniNativeUtsPlugin原生录音插件"
,"H753:"+ //no args
"RecordApp.UniNativeUtsPlugin native recording plug-in is not configured"
//@@zh="renderjs中不支持设置RecordApp.UniNativeUtsPlugin"
,"l6sY:"+ //no args
"Setting RecordApp.UniNativeUtsPlugin is not supported in renderjs"
//@@zh="当前App未打包进双端原生插件[{1}],尝试加载单端[{2}]"
,"kSjQ:"+ //args: {1}-{2}
"The current App is not packaged into the dual-end native plug-in [{1}], try to load the single-end [{2}]"
//@@zh="已加载原生录音插件[{1}]"
,"Xh1W:"+ //args: {1}
"Native recording plugin loaded [{1}]"
//@@zh="配置了RecordApp.UniNativeUtsPlugin但当前App未打包进原生录音插件[{1}]"
,"SCW9:"+ //args: {1}
"RecordApp.UniNativeUtsPlugin is configured, but the current App is not packaged with the native recording plug-in [{1}]"
//@@zh="提供的RecordApp.UniNativeUtsPlugin值不是RecordApp的uts原生录音插件"
,"TGMm:"+ //no args
"The provided RecordApp.UniNativeUtsPlugin value is not RecordApps uts native recording plug-in"
//@@zh="需在App逻辑层中调用原生插件功能"
,"MrBx:"+ //no args
"The native plug-in function needs to be called in the App logic layer"
//@@zh="未开始录音,不可以调用{1}"
,"0FGq:"+ //args: {1}
"Recording has not started and {1} cannot be called"
//@@zh="需先调用RecordApp.UniWebViewActivate然后才可以调用RequestPermission"
,"PkQ2:"+ //no args
"RecordApp.UniWebViewActivate needs to be called first, and then RequestPermission can be called"
//@@zh="不应当出现的非H5权限请求"
,"Jk72:"+ //no args
"Non-H5 permission requests that should not appear"
//@@zh="正在调用plus.ios@AVAudioSession请求iOS原生录音权限"
,"Y3rC:"+ //no args
"Calling plus.ios@AVAudioSession to request iOS native recording permissions"
//@@zh="项目配置中未声明iOS录音权限{1}"
,"9xoE:"+ //args: {1}
"iOS recording permission {1} is not declared in the project configuration"
//@@zh="已获得iOS原生录音权限"
,"j15C:"+ //no args
"Obtained iOS native recording permissions"
//@@zh="plus.ios请求录音权限状态值: "
,"iKhe:"+ //no args
"plus.ios requests recording permission, status value: "
//@@zh="正在调用plus.android.requestPermissions请求Android原生录音权限"
,"7Noe:"+ //no args
"Calling plus.android.requestPermissions to request Android native recording permissions"
//@@zh="已获得Android原生录音权限"
,"Bgls:"+ //no args
"Obtained Android native recording permission: "
//@@zh="plus.android请求录音权限无权限"
,"Ruxl:"+ //no args
"plus.android requests recording permission: No permission"
//@@zh="plus.android请求录音权限出错"
,"0JQw:"+ //no args
"plus.android error in requesting recording permission: "
//@@zh="调用plus的权限请求出错"
,"Mvl7:"+ //no args
"An error occurred in the permission request to call plus: "
//@@zh="用户拒绝了录音权限"
,"0caE:"+ //no args
"User denied recording permission"
//@@zh="正在调用原生插件请求录音权限"
,"Lx5r:"+ //no args
"Calling the native plug-in to request recording permission"
//@@zh="已获得录音权限"
,"Lx6r:"+ //no args
"Recording permission obtained"
//@@zh="无录音权限"
,"Lx7r:"+ //no args
"No recording permission"
//@@zh="无法调用RequestPermission"
,"ksoA:"+ //no args
"Unable to call RequestPermission: "
//@@zh="无法连接到renderjs"
,"KnF0:"+ //no args
"Unable to connect to renderjs"
//@@zh="需先调用RecordApp.UniWebViewActivate然后才可以调用Start"
,"XCMU:"+ //no args
"RecordApp.UniWebViewActivate needs to be called first, and then Start can be called"
//@@zh="不应当出现的非H5录音Start"
,"rSLO:"+ //no args
"Start of non-H5 recordings that should not appear"
//@@zh="无法调用Start"
,"Bjx9:"+ //no args
"Unable to call Start: "
//@@zh="未开始录音但收到renderjs回传的onRecEncodeChunk"
,"MTdp:"+ //no args
"Recording did not start, but onRecEncodeChunk returned by renderjs was received"
//@@zh="未开始录音但收到Uni Native PCM数据"
,"BjGP:"+ //no args
"Recording did not start, but Uni Native PCM data was received"
//@@zh="未开始录音但收到UniNativeUtsPlugin PCM数据"
,"byzO:"+ //no args
"Recording did not start, but UniNativeUtsPlugin PCM data was received"
//@@zh="未开始录音"
,"YP4V:"+ //no args
"Recording not started"
//@@zh="不应当出现的非H5录音Stop"
,"TPhg:"+ //no args
"Stop non-H5 recordings that should not appear"
//@@zh="未开始录音"
,"pP4O:"+ //no args
"Recording not started"
//@@zh="无法调用Stop"
,"H6cq:"+ //no args
"Unable to call Stop: "
//@@zh="不应该出现的renderjs发回的文件数据丢失"
,"gomD:"+ //no args
"The file data sent back by renderjs should not be lost"
]);
//*************** End srcFile=../app-support-sample/demo_UniApp/uni_modules/Recorder-UniCore/app-uni-support.js ***************
//@@User Code-2 Begin 手写代码放这里 Put the handwritten code here @@
//@@User Code-2 End @@
}));

View File

@ -1,19 +0,0 @@
《许可及服务协议》
**您以下称“用户”下载、使用我以下称“作者”提供的Recorder-UniCore组件含原生录音插件、uts插件以下统称“本组件”应当阅读并遵守本许可协议。请用户务必审慎阅读、充分理解各条款内容特别是免除或者限制责任的条款并选择接受或不接受。除非用户已阅读并接受本协议所有条款否则用户无权下载、使用本组件及相关服务用户的下载、使用等行为即视为用户已阅读并同意本许可协议的约束。**
1. 用户应当直接从作者许可的途径如作者的GitHub、Gitee仓库、已上架的DCloud插件市场、QQ群等途径中获取本组件其他途径获取到的组件代码是未经过作者授权的存在安全隐患可能会导致你的程序、资产受到侵害作者对因此给用户造成的损失不予负责。
2. 作者将积极并采取措施保护用户的信息和隐私;组件本身不会搜集存储任何用户信息。
3. 除法律法规有明确规定外,作者将尽最大努力确保本组件及其所涉及的技术及信息安全、有效、准确、可靠,但受限于现有技术,用户理解作者不能对此进行担保。
4. 用户理解,对于不可抗力及第三方原因导致的您的直接或间接损失,作者无法承担责任。
5. 用户因使用本组件进行生成、处理数据,由此引起或与有关的包括但不限于利润损失、资料损失、业务中断的损害赔偿或其它商业损害赔偿或损失,需由用户自行承担。
6. 如若发生赔偿、退款等行为,赔偿、退款等累计金额不得超过用户实际支付给作者的总金额。
7. 已授予的授权许可包括免费授权和已购买的原生录音插件、uts插件均仅限在授权指定的uni-app的应用标识AppID对应的项目上使用不可在其他项目上使用用户不得对本组件及其中的相关信息擅自出租、出借、销售、逆向工程、破解不得在未取得作者授权的情况下借助本组件发展与本组件有关联的衍生软件产品、服务、插件、外挂等。
8. 用户不得使用本组件从事违反法律法规政策、破坏公序良俗、损害公共利益的行为。

View File

@ -1,88 +0,0 @@
{
"id": "Recorder-UniCore",
"displayName": "跨平台Recorder录音插件支持多种格式、音频可视化、实时上传、语音识别",
"version": "1.0.250331",
"description": "支持H5、Android iOS App、微信小程序mp3 wav pcm g711a g711u ogg amr 录音格式;实时帧回调处理 音频转码 波形动画显示 ASR语音转文字 无录制时长限制",
"keywords": [
"Recorder-UniCore",
"recorder-core",
"RecordApp",
"record",
"recording"
],
"repository": "https://github.com/xiangyuecn/Recorder",
"engines": {
"HBuilderX": "^3.6.11"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "753610399"
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "录音权限"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-uvue": "n",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "n",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -1,425 +0,0 @@
**
**
# Recorder-UniCore组件uni-app内使用RecordApp录音
本组件使用`Recorder`开源库来进行录音和音频数据处理,使用`RecordApp`和本组件内的`app-uni-support.js`来适配到不同平台环境下进行录音。
- 支持vue2、vue3、nvue
- 支持编译成H5、Android App、iOS App、微信小程序
- 支持已有的大部分录音格式mp3、wav、pcm、amr、ogg、g711a、g711u等
- 支持实时处理包括变速变调、实时上传、ASR语音转文字
- 支持可视化波形显示;可配置回声消除、降噪;**注意:不支持通话时录音**
- 支持PCM音频流式播放、完整播放App端用原生插件边录音边播放更流畅
- 支持离线使用,本组件和配套原生插件均不依赖网络
- App端有配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)可供搭配使用,兼容性和体验更好
**详细文档(含Demo项目)** [https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp](https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)
**Recorder开源库地址** [https://github.com/xiangyuecn/Recorder](https://github.com/xiangyuecn/Recorder)
如果github打不开可以[点此访问Gitee仓库地址](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp) 。
**
## 测试方法
**示例项目如果在HBuilder中编译失败请删掉node_modules目录重新手动执行npm install偶尔出现HBuilder自动创建项目依赖包不完整导致无法编译**
1. 在本插件市场页面右侧下载或导入示例项目或打开上面详细文档链接中的Demo源码
2. 在测试项目根目录执行 `npm install --registry=https://registry.npmmirror.com/` ,完成`recorder-core`依赖的安装
3. 在HBuilder中打开本测试项目文件夹
4. 在HBuilder中运行到浏览器、手机、微信小程序即可在不同环境下测试
5. 测试中提供了基础录音、播放、上传、WebSocket实时语音通话对讲、ASR语音识别等功能
**
**
# 集成到自己项目中
你可以直接参考上面的测试示例项目源码,里面的`main_recTest.vue`更容易入门示例项目中已经实现了很多功能简单使用可直接照抄Demo代码到你的项目中。
## 一、引入js文件
1. 在你的项目根目录安装`recorder-core``npm install recorder-core --registry=https://registry.npmmirror.com/`
2. 导入Recorder-UniCore组件插件市场下载本组件然后添加到你的项目中 `/uni_modules/Recorder-UniCore`
3. 项目配置好录音权限,参考下面的录音权限配置章节,**特别注意App后台录音配置、小程序权限声明**
4. 在需要录音的vue文件script内编写以下代码按需引入需要的js
``` html
<template>
<view>
... 建议template下只有一个根节点最外面套一层view如果不小心踩到了vue3的Fragments(multi-root 多个根节点)特性vue2编译会报错vue3不会可能会出现奇奇怪怪的兼容性问题
</view>
</template>
<script> /****/
//必须引入的Recorder核心文件路径是 /src/recorder-core.js 下同使用import、require都行
import Recorder from 'recorder-core' //注意如果未引用Recorder变量可能编译时会被优化删除如vue3 tree-shaking请改成 import 'recorder-core',或随便调用一下 Recorder.a=1 保证强引用
//必须引入的RecordApp核心文件文件路径是 /src/app-support/app.js
import RecordApp from 'recorder-core/src/app-support/app'
//所有平台必须引入的uni-app支持文件如果编译出现路径错误请把@换成 ../../ 这种)
import '@/uni_modules/Recorder-UniCore/app-uni-support.js'
/** 需要编译成微信小程序时,引入微信小程序支持文件 **/
// #ifdef MP-WEIXIN
import 'recorder-core/src/app-support/app-miniProgram-wx-support.js'
// #endif
/** H5、小程序环境中引入需要的格式编码器、可视化插件App环境中在renderjs中引入 **/
// 注意如果App中需要在逻辑层中调用Recorder的编码/转码功能,需要去掉此条件编译,否则会报未加载编码器的错误
// #ifdef H5 || MP-WEIXIN
//按需引入你需要的录音格式支持文件如果需要多个格式支持把这些格式的编码引擎js文件统统引入进来即可
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine' //如果此格式有额外的编码引擎(*-engine.js的话必须要加上
//可选的插件支持项,把需要的插件按需引入进来即可
import 'recorder-core/src/extensions/waveview'
// #endif
// ... 这后面写页面代码用选项式API风格vue2、vue3、setup组合式API风格仅vue3都可以
</script>
```
5. 编译成app时默认需要额外提供一个renderjs模块请照抄下面这段代码放到vue文件末尾
``` html
<!-- #ifdef APP -->
<script module="yourModuleName" lang="renderjs"> //APIvue2vue3setupAPIimport vue
/**需要编译成App时你需要添加一个renderjs模块然后一模一样的import上面那些js微信的js除外
因为App中默认是在renderjsWebView中进行录音和音频编码
。如果配置了 RecordApp.UniWithoutAppRenderjs=true 且未调用依赖renderjs的功能时如nvue、可视化、仅H5中可用的插件
可不提供此renderjs模块同时逻辑层中需要将相关import的条件编译去掉**/
import 'recorder-core'
import RecordApp from 'recorder-core/src/app-support/app'
import '../../uni_modules/Recorder-UniCore/app-uni-support.js' //renderjs中似乎不支持"@/"打头的路径,如果编译路径错误请改正路径即可
//按需引入你需要的录音格式支持文件,和插件
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
import 'recorder-core/src/extensions/waveview'
export default {
mounted(){
//App的renderjs必须调用的函数传入当前模块this
RecordApp.UniRenderjsRegister(this);
},
methods: {
//这里定义的方法,在逻辑层中可通过 RecordApp.UniWebViewVueCall(this,'this.xxxFunc()') 直接调用
//调用逻辑层的方法,请直接用 this.$ownerInstance.callMethod("xxxFunc",{args}) 调用二进制数据需转成base64来传递
}
}
</script>
<!-- #endif -->
```
**
**
## 二、调用录音
``` javascript
/**在逻辑层中编写**/
//import ... 上面那些import代码
//var vue3This=getCurrentInstance().proxy; //当用vue3 setup组合式 API (Composition API) 编写时直接在import后面取到当前实例this在需要this的地方传vue3This变量即可其他的和选项式 API (Options API) 没有任何区别import {getCurrentInstance} from 'vue'详细可以参考Demo项目中的 page_vue3____composition_api.vue
//RecordApp.UniNativeUtsPlugin={ nativePlugin:true }; //App中启用配套的原生录音插件支持配置后会使用原生插件进行录音没有原生插件时依旧使用renderjs H5录音
//App中提升后台录音的稳定性配置了原生插件后可配置 `RecordApp.UniWithoutAppRenderjs=true` 禁用renderjs层音频编码WebWorker加速变成逻辑层中直接编码但会降低逻辑层性能后台运行时可避免部分手机WebView运行受限的影响
//App中提升后台录音的稳定性需要启用后台录音保活服务iOS不需要参考录音权限配置Android 9开始锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音renderjs中H5录音也受影响请调用配套原生插件的`androidNotifyService`接口,或使用第三方保活插件
export default {
data() { return {} } //视图没有引用到的变量无需放data里直接this.xxx使用
,mounted() {
this.isMounted=true;
//页面onShow时【必须调用】的函数传入当前组件this
RecordApp.UniPageOnShow(this);
}
,onShow(){ //onShow可能比mounted先执行页面可能还未准备好
if(this.isMounted) RecordApp.UniPageOnShow(this);
}
,methods:{
//请求录音权限
recReq(){
//编译成App时提供的授权许可编译成H5、小程序为免费授权可不填写如果未填写授权许可将会在App打开后第一次调用请求录音权限时弹出“未获得商用授权时App上仅供测试”提示框
//RecordApp.UniAppUseLicense='我已获得UniAppID=*****的商用授权';
//RecordApp.RequestPermission_H5OpenSet={ audioTrackSet:{ noiseSuppression:true,echoCancellation:true,autoGainControl:true } }; //这个是Start中的audioTrackSet配置在h5H5、App+renderjs中必须提前配置因为h5中RequestPermission会直接打开录音
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.RequestPermission(()=>{
console.log("已获得录音权限,可以开始录音了");
},(msg,isUserNotAllow)=>{
if(isUserNotAllow){//用户拒绝了录音权限
//这里你应当编写代码进行引导用户给录音权限,不同平台分别进行编写
}
console.error("请求录音权限失败:"+msg);
});
}
//开始录音
,recStart(){
//Android App如果要后台录音需要启用后台录音保活服务iOS不需要需使用配套原生插件、或使用第三方保活插件
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ title:"正在录音" ,content:"正在录音中请勿关闭App运行" }).then(()=>{...}).catch((e)=>{...}) 注意必须RecordApp.RequestPermission得到权限后调用
//录音配置信息
var set={
type:"mp3",sampleRate:16000,bitRate:16 //mp3格式指定采样率hz、比特率kbps其他参数使用默认配置注意是数字的参数必须提供数字不要用字符串需要使用的type类型需提前把格式支持文件加载进来比如使用wav格式需要提前加载wav.js编码引擎
/*,audioTrackSet:{ //可选如果需要同时播放声音比如语音通话需要打开回声消除并不一定会生效打开后声音可能会从听筒播放部分环境下如小程序、App原生插件可调用接口切换成扬声器外放
//注意H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
echoCancellation:true,noiseSuppression:true,autoGainControl:true} */
,onProcess:(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd)=>{
//全平台通用可实时上传发送数据配合Recorder.SampleData方法将buffers中的新数据连续的转换成pcm上传或使用mock方法将新数据连续的转码成其他格式上传可以参考Recorder文档里面的Demo片段列表 -> 实时转码并上传-通用版基于本功能可以做到实时转发数据、实时保存数据、实时语音识别ASR
//注意App里面是在renderjs中进行实际的音频格式编码操作此处的buffers数据是renderjs实时转发过来的修改此处的buffers数据不会改变renderjs中buffers所以不会改变生成的音频文件可在onProcess_renderjs中进行修改操作就没有此问题了如需清理buffers内存此处和onProcess_renderjs中均需要进行清理H5、小程序中无此限制
//注意如果你要用只支持在浏览器中使用的Recorder扩展插件App里面请在renderjs中引入此扩展插件然后在onProcess_renderjs中调用这个插件H5可直接在这里进行调用小程序不支持这类插件如果调用插件的逻辑比较复杂建议封装成js文件这样逻辑层、renderjs中直接import不需要重复编写
//H5、小程序等可视化图形绘制直接运行在逻辑层App里面需要在onProcess_renderjs中进行这些操作
// #ifdef H5 || MP-WEIXIN
if(this.waveView) this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
// #endif
/*实时释放清理内存用于支持长时间录音在指定了有效的type时编码器内部可能还会有其他缓冲必须同时提供takeoffEncodeChunk才能清理内存否则type需要提供unknown格式来阻止编码器内部缓冲App的onProcess_renderjs中需要进行相同操作
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
}
,onProcess_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
//App中在这里修改buffers会改变生成的音频文件但注意buffers会先转发到逻辑层onProcess后才会调用本方法因此在逻辑层的onProcess中需要重新修改一遍
//本方法可以返回truerenderjs中的onProcess将开启异步模式处理完后调用asyncEnd结束异步注意这里异步修改的buffers一样的不会在逻辑层的onProcess中生效
//App中是在renderjs中进行的可视化图形绘制因此需要写在这里this是renderjs模块的this也可以用This变量如果代码比较复杂请直接在renderjs的methods里面放个方法xxxFunc这里直接使用this.xxxFunc(args)进行调用
if(this.waveView) this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
/*和onProcess中一样进行释放清理内存用于支持长时间录音
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
}`
,onProcessBefore_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx){
//App中本方法会在逻辑层onProcess之前调用因此修改的buffers会转发给逻辑层onProcess本方法没有asyncEnd参数不支持异步处理
//一般无需提供本方法只用onProcess_renderjs就行renderjs的onProcess内部调用过程onProcessBefore_renderjs -> 转发给逻辑层onProcess -> onProcess_renderjs
}`
,takeoffEncodeChunk:true?null:(chunkBytes)=>{
//全平台通用实时接收到编码器编码出来的音频片段数据chunkBytes是Uint8Array二进制数据可以实时上传发送出去
//App中如果未配置RecordApp.UniWithoutAppRenderjs时建议提供此回调因为录音结束后会将整个录音文件从renderjs传回逻辑层由于uni-app的逻辑层和renderjs层数据交互性能实在太拉跨了大点的文件传输会比较慢提供此回调后可避免Stop时产生超大数据回传
//App中使用原生插件时可方便的将数据实时保存到同一文件第一帧时append:false新建文件后面的append:true追加到文件
//RecordApp.UniNativeUtsPluginCallAsync("writeFile",{path:"xxx.mp3",append:回调次数!=1, dataBase64:RecordApp.UniBtoa(chunkBytes.buffer)}).then(...).catch(...)
}
,takeoffEncodeChunk_renderjs:true?null:`function(chunkBytes){
//App中这里可以做一些仅在renderjs中才生效的事情不提供也行this是renderjs模块的this也可以用This变量
}`
,start_renderjs:`function(){
//App中可以放一个函数在Start成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情比如初始化不提供也行
}`
,stop_renderjs:`function(arrayBuffer,duration,mime){
//App中可以放一个函数在Stop成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情不提供也行
}`
};
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.Start(set,()=>{
console.log("已开始录音");
//【稳如老狗WDT】可选的监控是否在正常录音有onProcess回调如果长时间没有回调就代表录音不正常
//var wdt=this.watchDogTimer=setInterval ... 请参考示例Demo的main_recTest.vue中的watchDogTimer实现
//创建音频可视化图形绘制App环境下是在renderjs中绘制H5、小程序等是在逻辑层中绘制因此需要提供两段相同的代码
//view里面放一个canvascanvas需要指定宽高下面style里指定了300*100
//<canvas type="2d" class="recwave-WaveView" style="width:300px;height:100px"></canvas>
RecordApp.UniFindCanvas(this,[".recwave-WaveView"],`
this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
`,(canvas1)=>{
this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
});
},(msg)=>{
console.error("开始录音失败:"+msg);
});
}
//暂停录音
,recPause(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Pause();
console.log("已暂停");
}
}
//继续录音
,recResume(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Resume();
console.log("继续录音中...");
}
}
//停止录音
,recStop(){
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ close:true }) //关闭Android App后台录音保活服务
RecordApp.Stop((arrayBuffer,duration,mime)=>{
//全平台通用arrayBuffer是音频文件二进制数据可以保存成文件或者发送给服务器
//App中如果在Start参数中提供了stop_renderjsrenderjs中的函数会比这个函数先执行
//注意当Start时提供了takeoffEncodeChunk后你需要自行实时保存录音文件数据因此Stop时返回的arrayBuffer的长度将为0字节
//如果是H5环境也可以直接构造成Blob/File文件对象和Recorder使用一致
// #ifdef H5
var blob=new Blob([arrayBuffer],{type:mime});
console.log(blob, (window.URL||webkitURL).createObjectURL(blob));
var file=new File([arrayBuffer],"recorder.mp3");
//uni.uploadFile({file:file, ...}) //参考demo中的test_upload_saveFile.vue
// #endif
//如果是App、小程序环境可以直接保存到本地文件然后调用相关网络接口上传
// #ifdef APP || MP-WEIXIN
RecordApp.UniSaveLocalFile("recorder.mp3",arrayBuffer,(savePath)=>{
console.log(savePath); //app保存的文件夹为`plus.io.PUBLIC_DOWNLOADS`,小程序为 `wx.env.USER_DATA_PATH` 路径
//uni.uploadFile({filePath:savePath, ...}) //参考demo中的test_upload_saveFile.vue
},(errMsg)=>{ console.error(errMsg) });
// #endif
},(msg)=>{
console.error("结束录音失败:"+msg);
});
}
}
}
```
**
**
**
**
# 录音权限配置、需要注意的细节
## 编译成H5时录音和权限
编译成H5时录音功能由Recorder H5提供无需额外处理录音权限。
**
## 编译成微信小程序时录音和权限
编译成微信小程序时,录音功能由小程序的`RecorderManager`提供,屏蔽了微信原有的底层细节(无录音时长限制)。
小程序录音需要用户授予录音权限,调用`RecordApp.RequestPermission`的时候会检查是否能正常录音,如果用户拒绝了录音权限,会进入错误回调,回调里面你应当编写代码检查`wx.getSetting`中的`scope.record`录音权限,然后引导用户进行授权(可调用`wx.openSetting`打开设置页面,方便用户给权限)。
**注意:上架小程序需要到小程序管理后台《[用户隐私保护指引](https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html)》中声明录音权限,否则正式版将无法调用录音功能(请求权限时会直接走错误回调)。**
更多细节请参考 [miniProgram-wx](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/miniProgram-wx) 测试项目文档。
**
## 编译成App时录音和权限
编译成App录音时分两种情况
1. 默认未配置`RecordApp.UniNativeUtsPlugin`未使用原生录音插件和uts插件会在renderjs中使用Recorder H5进行录音录音数据会实时回传到逻辑层。
2. 配置了`RecordApp.UniNativeUtsPlugin`使用原生录音插件或uts插件时会直接调用原生插件进行录音录音数据默认会传递到renderjs中进行音频编码处理WebWorker加速然后再实时回传到逻辑层如果配置了`RecordApp.UniWithoutAppRenderjs=true`时,音频编码处理将会在逻辑层中直接处理。
当App是在renderjs中使用H5进行录音时未使用原生录音插件和uts插件iOS上只支持14.3以上版本,**且iOS上每次进入页面后第一次请求录音权限时、或长时间无操作再请求录音权限时WebView均会弹出录音权限对话框**不同旧iOS版本低于iOS17下H5录音可能存在的问题在App中同样会存在使用配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)或uts插件时无以上问题和版本限制uts插件开发中暂不可用Android也无以上问题。
当音频编码是在renderjs中进行处理时录音结束后会将整个录音文件传回逻辑层由于uni-app的逻辑层和renderjs层大点的文件传输会比较慢**建议Start时使用takeoffEncodeChunk实时获取音频文件数据可避免Stop时产生超大数据回传**;配置了`RecordApp.UniWithoutAppRenderjs=true`后,因为音频编码直接是在逻辑层中进行,将不存在传输性能损耗,但会影响逻辑层的性能(正常情况轻微不明显),需要配套使用原生录音插件才可以进行此项配置。
在调用`RecordApp.RequestPermission`的时候,`Recorder-UniCore`组件会自动处理好App的系统录音权限只需要在uni-app项目的 `manifest.json` 中配置好Android和iOS的录音权限声明。
```
//Android需要勾选的权限第二个也必须勾选
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
【注意】Android如果需要在后台录音需要启用后台录音保活服务Android 9开始锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音renderjs中H5录音、原生插件录音均受影响请调用配套原生插件的`androidNotifyService`接口,或使用第三方保活插件
//iOS需要声明的权限
NSMicrophoneUsageDescription
【注意】iOS需要在 `App常用其它设置`->`后台运行能力`中提供`audio`配置不然App切到后台后立马会停止录音
```
**
## PCM音频流式播放、语音通话、回声消除、声音外放
在App、H5中均可使用H5版的[BufferStreamPlayer](https://gitee.com/xiangyuecn/Recorder/blob/master/src/extensions/buffer_stream.player.js)来实时流式播放语音其中App中需要在renderjs中加载BufferStreamPlayer在逻辑层中调用`RecordApp.UniWebViewVueCall`等方法将逻辑层中接收到的实时语音数据发送到renderjs中播放播放声音的同时进行录音声音可能会被录进去产生回声因此一般需要打开回声消除调用代码参考demo中的[test_realtime_voice.vue](https://gitee.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_UniApp/pages/recTest/test_realtime_voice.vue)。
App中如果搭配使用了配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)可以调用原生实现的PcmPlayer播放器实时流式播放PCM音频边录音边播放更流畅同时也支持完整播放比如AI语音合成的播放调用代码参考demo中的[test_player_nativePlugin_pcmPlayer.vue](https://gitee.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_UniApp/pages/recTest/test_player_nativePlugin_pcmPlayer.vue)。
微信小程序请参考 [miniProgram-wx](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/miniProgram-wx) 文档里面的同名章节使用WebAudioContext播放。
配置audioTrackSet可尝试打开回声消除或者切换听筒播放或外放打开回声消除时一般会转为听筒播放显著降低回声。
``` js
//打开回声消除
RecordApp.Start({
... 更多配置参数请参考RecordApp文档
//此配置App、H5、小程序均可打开回声消除注意H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
,audioTrackSet:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}
//Android指定麦克风源App搭配原生插件、小程序可用0 DEFAULT 默认音频源1 MIC 主麦克风5 CAMCORDER 相机方向的麦6 VOICE_RECOGNITION 语音识别7 VOICE_COMMUNICATION 语音通信(带回声消除)
,android_audioSource:7 //提供此配置时优先级比audioTrackSet更高默认值为0
//iOS的AVAudioSession setCategory的withOptions参数值App搭配原生插件可用取值请参考配套原生插件文档中的iosSetDefault_categoryOptions
//,ios_categoryOptions:0x1|0x4 //默认值为5(0x1|0x4)
});
//App搭配原生插件时尝试切换听筒播放或外放
await RecordApp.UniNativeUtsPluginCallAsync("setSpeakerOff",{off:true或false});
//小程序尝试切换
wx.setInnerAudioOption({ speakerOn:false或true })
//H5不支持切换
```
**
**
**
# 详细文档、RecordApp方法、属性文档
请先阅读 [demo_UniApp文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)含Demo项目更高级使用还需深入阅读 [Recorder文档](https://gitee.com/xiangyuecn/Recorder)、[RecordApp文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample) 均为完整的一个README.md文档Recorder文档中包含了更丰富的示例代码基础录音、实时处理、格式转码、音频分析、音频混音、音频生成 等等大部分能在uniapp中直接使用。
**
**
**
# 本组件的授权许可限制
**本组件内的app-uni-support.js文件在uni-app中编译到App平台时仅供测试用App平台包括Android App、iOS App不可用于正式发布或商用正式发布或商用需先到DCloud插件市场购买[此带授权的插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin-Android)费用为¥199元赠送Android版原生插件即可获得授权许可**编译到其他平台时无此授权限制比如H5、小程序均为免费授权。
在App中如果未获得授权许可将会在App打开后第一次调用`RecordApp.RequestPermission`请求录音权限时弹出“未获得商用授权时App上仅供测试”提示框。
在DCloud插件市场购买了[带授权的插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin-Android)获得了授权后,请在调用`RecordApp.RequestPermission`请求录音权限前,赋值`RecordApp.UniAppUseLicense="我已获得UniAppID=***的商用授权"`星号为你项目的uni-app应用标识就不会弹提示框了或者直接使用配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin),设置`RecordApp.UniNativeUtsPlugin`参数后,也不会弹提示框;其他情况请联系作者咨询,更多细节请参考[本组件的GitHub文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)。
获取授权、需要技术支持、或有不清楚的地方可以联系我们客服联系方式QQ 1251654593 或者直接联系作者QQ 753610399 (回复可能没有客服及时)。
插件开发维护不易,感谢支持~
**
**

View File

@ -1,27 +0,0 @@
## 1.1.22024-07-26
修复第一次录音录不上
## 1.1.12024-05-22
1.新增node后端方便测试
2.修复H5及H5上传示例
## 1.0.72024-05-22
更新H5上传示例新增node后端测试 自行进行测试
## 1.0.62024-03-19
上传示例项目
## 1.0.52024-03-19
修复PC语音录制
## 1.0.42024-03-19
兼容pc
## 1.0.02024-03-19
兼容PC
## 1.0.32024-03-15
修复安卓导入失败
## 1.0.22024-03-14
修复小程序报错支持vue3
## 1.0.12024-03-05
补充说明,原作者信息。本人只是补充
## 1.0.02024-03-05
### 首次发布
1.原地址 https://ext.dcloud.net.cn/plugin?id=9595
2.原有基础上兼容H5,如有侵权联系立刻下架

View File

@ -1,863 +0,0 @@
<template>
<view>
<!-- <customRequestPermission
ref="requestPer"
:permissionInfo="permissionInfo"
></customRequestPermission> -->
<view
class="record-btn"
@longpress="startRecord"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="endRecord"
hover-class="record-btn-hover"
hover-start-time="200"
hover-stay-time="150"
:style="[
{ ...btnStyle, height: chatInputHeight },
{
'--btn-hover-fontcolor': btnHoverFontcolor,
'--btn-hover-bgcolor': btnHoverBgcolor,
},
]"
>
<text>{{ btnTextContent }}</text>
</view>
<!-- <view class="record-popup"
:style="{ '--popup-height': popupHeight, '--popup-width': upx2px(popupMaxWidth), '--popup-bottom': upx2px(popupFixBottom), '--popup-bg-color': popupBgColor }">
<view class="inner-content" v-if="recordPopupShow">
<view class="title">{{ popupTitle }}</view>
<view class="voice-line-wrap" v-if="recording"
:style="{ '--line-height': upx2px(lineHeight), '--line-start-color': lineStartColor, '--line-end-color': lineEndColor }">
<view class="voice-line one"></view>
<view class="voice-line two"></view>
<view class="voice-line three"></view>
<view class="voice-line four"></view>
<view class="voice-line five"></view>
<view class="voice-line six"></view>
<view class="voice-line seven"></view>
<view class="voice-line six"></view>
<view class="voice-line five"></view>
<view class="voice-line four"></view>
<view class="voice-line three"></view>
<view class="voice-line two"></view>
<view class="voice-line one"></view>
</view>
<view class="cancel-icon" v-if="!recording">+</view>
<view class="tips">{{ recording ? popupDefaultTips : popupCancelTips }}</view>
</view>
</view> -->
<view class="record-shadow" v-if="recordPopupShow">
<view
class="record-animation-area"
:style="
recording
? ''
: 'background-image: url(' + recordAnimationAreaCancelBg + ')'
"
>
<view
class="voice-line-wrap"
v-if="!nearTimeLimit"
:style="{
'--line-height': upx2px(lineHeight),
'--line-start-color': lineStartColor,
'--line-end-color': lineEndColor,
}"
>
<view class="voice-line seven"></view>
<view class="voice-line six"></view>
<view class="voice-line five"></view>
<view class="voice-line four"></view>
<view class="voice-line three"></view>
<view class="voice-line two"></view>
<view class="voice-line one"></view>
<view class="voice-line two"></view>
<view class="voice-line three"></view>
<view class="voice-line four"></view>
<view class="voice-line five"></view>
<view class="voice-line six"></view>
<view class="voice-line seven"></view>
<view class="voice-line six"></view>
<view class="voice-line five"></view>
<view class="voice-line four"></view>
<view class="voice-line three"></view>
<view class="voice-line two"></view>
<view class="voice-line one"></view>
<view class="voice-line two"></view>
<view class="voice-line three"></view>
<view class="voice-line four"></view>
<view class="voice-line five"></view>
<view class="voice-line six"></view>
<view class="voice-line seven"></view>
</view>
<view class="record-auto-stop-hint" v-if="nearTimeLimit">
<text>{{ autoStopRecordCountDown + $t('auto_stop_record') }}</text>
</view>
</view>
<view class="record-cancel-btn">
<image
v-if="recording"
src="@/static/image/record/chat-voice-cancel-grey.png"
></image>
<image
v-if="!recording"
src="@/static/image/record/chat-voice-cancel-blue.png"
></image>
<text v-if="recording" class="font-36 font-regular"></text>
<text
v-if="!recording"
class="font-36 font-regular"
style="margin: 20rpx 0 0;"
>
{{ $t('release_hand_to_cancel') }}
</text>
</view>
<view
class="record-shadow-bottom"
:style="recording ? '' : 'background-color:#fff;'"
>
<image
v-if="recording"
src="@/static/image/record/chat-voice-icon-white.png"
></image>
<image
v-if="!recording"
src="@/static/image/record/chat-voice-icon-blue.png"
></image>
<text class="font-36 font-regular">
{{ $t('release_hand_to_send') }}
</text>
</view>
</view>
</view>
</template>
<script>
var that
// #ifdef APP-PLUS
const recorderManager = uni.getRecorderManager()
//
import permision from '../../js_sdk/wa-permission/permission.js'
// #endif
// #ifdef H5
import Recorder from 'recorder-core'
import RecordApp from 'recorder-core/src/app-support/app'
import '@/uni_modules/Recorder-UniCore/app-uni-support.js'
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
import 'recorder-core/src/extensions/waveview'
import recordCancelBg from '@/static/image/record/chat-voice-animation-bg-red.png'
// #endif
import { multiplication } from '@/utils/common'
export default {
name: 'nbVoiceRecord',
/**
* 录音交互动效组件
* @property {Object} recordOptions 录音配置
* @property {Object} btnStyle 按钮样式
* @property {Object} btnHoverFontcolor 按钮长按时字体颜色
* @property {String} btnHoverBgcolor 按钮长按时背景颜色
* @property {String} btnDefaultText 按钮初始文字
* @property {String} btnRecordingText 录制时按钮文字
* @property {Boolean} vibrate 弹窗时是否震动
* @property {String} popupTitle 弹窗顶部文字
* @property {String} popupDefaultTips 录制时弹窗底部提示
* @property {String} popupCancelTips 滑动取消时弹窗底部提示
* @property {String} popupMaxWidth 弹窗展开后宽度
* @property {String} popupMaxHeight 弹窗展开后高度
* @property {String} popupFixBottom 弹窗展开后距底部高度
* @property {String} popupBgColor 弹窗背景颜色
* @property {String} lineHeight 声波高度
* @property {String} lineStartColor 声波波谷时颜色色值
* @property {String} lineEndColor 声波波峰时颜色色值
* @event {Function} startRecord 开始录音回调
* @event {Function} endRecord 结束录音回调
* @event {Function} cancelRecord 滑动取消录音回调
* @event {Function} stopRecord 主动停止录音
*/
props: {
recordOptions: {
type: Object,
default() {
return {
duration: 60000,
} // 使
},
},
chatInputHeight: {
type: String,
default: '32px', // px
},
btnStyle: {
type: Object,
default() {
return {
width: 'calc(100% - 26rpx)',
height: '32px', // 使
borderRadius: '8rpx',
backgroundColor: '#F9F9F9',
border: '1rpx solid whitesmoke',
permisionState: false,
margin: '0 10rpx 0 16rpx',
// boxShadow: '0rpx 6rpx 12rpx 2rpx rgba(0, 0, 0, 0.08)',
}
},
},
btnHoverFontcolor: {
type: String,
default: '#000', // 16
},
btnHoverBgcolor: {
type: String,
default: 'whitesmoke', // 16
},
btnDefaultText: {
type: String,
default: '长按开始录音',
},
btnRecordingText: {
type: String,
default: '录音中',
},
vibrate: {
type: Boolean,
default: true,
},
popupTitle: {
type: String,
default: '正在录制音频',
},
popupDefaultTips: {
type: String,
default: '左右滑动后松手完成录音',
},
popupCancelTips: {
type: String,
default: '松手取消录音',
},
popupMaxWidth: {
type: Number,
default: 600, // rpx
},
popupMaxHeight: {
type: Number,
default: 300, // rpx
},
popupFixBottom: {
type: Number,
default: 200, // rpx
},
popupBgColor: {
type: String,
default: 'whitesmoke',
},
lineHeight: {
type: Number,
default: 50, // rpx
},
lineStartColor: {
type: String,
default: 'royalblue', // 16
},
lineEndColor: {
type: String,
default: 'indianred', // 16
},
field: {
type: String,
default: 'voiceRecord',
},
},
data() {
return {
stopStatus: false, //
btnTextContent: this.btnDefaultText,
startTouchData: {},
popupHeight: '0px', //
recording: true, //
recordPopupShow: false, //
recordTimeout: null, //
recordAnimationAreaCancelBg: recordCancelBg, //
permissionInfo: '', //
nearTimeLimit: false, //
autoStopRecordCountDown: 0, //
autoStopRecordInterval: null, //
permisionState: false, //
}
},
mounted() {
let that = this
that.isMounted = true
RecordApp.UniRenderjsRegister(that)
window.handleAppMicro = that.handleAppMicro
// that.$store.dispatch('updateCurrentComponentRef', that)
},
onShow() {
let that = this
if (that.isMounted) RecordApp.UniPageOnShow(that)
},
created() {
let that = this
//
that.checkPermission()
// #ifdef APP-PLUS
recorderManager.onStop((res) => {
//
if (that.recordTimeout !== null) {
//
clearTimeout(that.recordTimeout)
that.recordTimeout = null //
}
//
if (that.recording) {
that.$emit('endRecord', res)
} else {
//
that.recording = true //
that.$emit('cancelRecord')
}
if (that.autoStopRecordInterval) {
clearInterval(that.autoStopRecordInterval)
that.autoStopRecordInterval = null
}
})
recorderManager.onError((err) => {
console.log('err:', err)
})
// #endif
},
computed: {},
methods: {
upx2px(upx) {
return uni.upx2px(upx) + 'px'
},
handleAppMicro(permissionStatus) {
let that = this
// setTimeout(function () {
// that.$refs.requestPer.hideCustomPermission()
// }, 1000)
if (Number(permissionStatus) === 1) {
that.continueAppMicro()
} else if (Number(permissionStatus) === -1) {
uni.showModal({
title: '提示',
content: '检测到您还未授权麦克风哦',
showCancel: true,
confirmText: '去授权',
cancelText: '保持拒绝',
success: ({ confirm, cancel }) => {
if (confirm) {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview, index) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`handleRequestAndroidPermission('settings')`)
}
})
}
},
})
}
},
handleRequestPer(permissionInfo, permissionID) {
console.log(permissionInfo)
let that = this
if (permissionInfo == 'closePermissionRequest') {
that.permissionInfo = ''
that.$refs.requestPer.hideCustomPermission()
} else if (permissionInfo == 'showPermissionRefuseModal') {
that.$refs.requestPer.showupCustomPermission()
that.permissionInfo = permissionID
console.log('权限被拒绝:' + permissionID)
that.$refs.requestPer.showupPopupPrompt()
} else {
that.$refs.requestPer.showupCustomPermission()
that.permissionInfo = permissionInfo
}
},
continueAppMicro() {
let that = this
RecordApp.UniWebViewActivate(that) //AppWebView
RecordApp.RequestPermission(
() => {
console.log('已获得录音权限,可以开始录音了')
that.permisionState = true
},
(msg, isUserNotAllow) => {
if (isUserNotAllow) {
//
//
}
console.error('请求录音权限失败:' + msg)
},
)
},
async checkPermission() {
let that = this
// #ifdef APP-PLUS
// os
let os = uni.getSystemInfoSync().osName
if (os == 'ios') {
that.permisionState = await permision.judgeIosPermission('record')
} else {
that.permisionState = await permision.requestAndroidPermission(
'android.permission.RECORD_AUDIO',
)
}
if (that.permisionState !== true && that.permisionState !== 1) {
uni.showToast({
title: '请先授权使用录音',
icon: 'none',
})
return
}
// #endif
// #ifndef APP-PLUS
// #ifndef H5
uni.authorize({
scope: 'scope.record',
success: () => {
that.permisionState = true
},
fail() {
uni.showToast({
title: '请授权使用录音',
icon: 'none',
})
},
})
// #endif
// #ifdef H5
if (typeof plus !== 'undefined') {
let OAWebView = plus.webview.all()
OAWebView.forEach((webview, index) => {
if (webview.id === 'webviewId1') {
webview.evalJS(`handleRequestAndroidPermission('micro')`)
}
})
} else {
that.continueAppMicro()
}
// #endif
// #endif
},
startRecord() {
let that = this
if (!that.permisionState) {
that.checkPermission()
return
}
that.stopStatus = false
setTimeout(() => {
that.popupHeight = that.upx2px(that.popupMaxHeight)
setTimeout(() => {
that.recordPopupShow = true
that.btnTextContent = that.btnRecordingText
if (that.vibrate) {
// #ifdef APP-PLUS
plus.device.vibrate(35)
// #endif
// #ifdef MP-WEIXIN
uni.vibrateShort()
// #endif
}
//
// #ifdef APP-PLUS
recorderManager.start(that.recordOptions)
// #endif
// #ifndef APP-PLUS
// #ifdef H5
//
var set = {
type: 'mp3',
sampleRate: 16000,
bitRate: 16,
onProcess: (
buffers,
powerLevel,
duration,
sampleRate,
newBufferIdx,
asyncEnd,
) => {
// #ifdef H5 || MP-WEIXIN
if (that.waveView)
that.waveView.input(
buffers[buffers.length - 1],
powerLevel,
sampleRate,
)
// #endif
if (that.clearBufferIdx > newBufferIdx) {
that.clearBufferIdx = 0
} //
for (var i = that.clearBufferIdx || 0; i < newBufferIdx; i++)
buffers[i] = null
that.clearBufferIdx = newBufferIdx
},
}
RecordApp.UniWebViewActivate(that) //AppWebView
RecordApp.Start(
set,
() => {
console.log('已开始录音')
},
(msg) => {
console.error('开始录音失败:' + msg)
},
)
// #endif
// #endif
that.nearTimeLimit = false
//
that.recordTimeout = setTimeout(
() => {
//
that.stopRecord() // end
that.recordTimeout = null //
},
that.recordOptions.duration ? that.recordOptions.duration : 600000,
)
that.autoStopRecordCountDown = 11
let count = 0
that.autoStopRecordInterval = setInterval(() => {
count += 1000
if (
count >=
that.recordOptions.duration -
multiplication(that.autoStopRecordCountDown, 1000)
) {
that.nearTimeLimit = true
that.autoStopRecordCountDown--
if (that.autoStopRecordCountDown <= 0) {
clearInterval(that.autoStopRecordInterval)
that.autoStopRecordInterval = null
}
}
}, 1000)
that.$emit('startRecord')
}, 100)
}, 200)
},
endRecord() {
let that = this
if (that.stopStatus) {
return
}
that.popupHeight = '0px'
that.recordPopupShow = false
that.btnTextContent = that.btnDefaultText
// #ifdef APP-PLUS
recorderManager.stop()
// #endif
// #ifndef APP-PLUS
// #ifdef H5
RecordApp.Stop(
(arrayBuffer, duration, mime) => {
var blob = new Blob([arrayBuffer], {
type: mime,
})
let url = (window.URL || webkitURL).createObjectURL(blob)
console.log(blob, url)
var file = new File([arrayBuffer], 'recorder.mp3')
//
if (that.recordTimeout !== null) {
//
clearTimeout(that.recordTimeout)
that.recordTimeout = null //
}
//
if (that.recording) {
// console.log(file)
that.$emit('endRecord', file, url, duration)
;(window.URL || webkitURL).revokeObjectURL(url)
} else {
//
that.recording = true //
that.$emit('cancelRecord')
}
if (that.autoStopRecordInterval) {
clearInterval(that.autoStopRecordInterval)
that.autoStopRecordInterval = null
}
},
(msg) => {
console.error('结束录音失败:' + msg)
},
)
// #endif
// #endif
},
stopRecord() {
let that = this
//
that.endRecord()
that.stopStatus = true
},
touchStart(e) {
let that = this
that.startTouchData.clientX = e.changedTouches[0].clientX //X
that.startTouchData.clientY = e.changedTouches[0].clientY //Y
},
touchMove(e) {
let that = this
let touchData = e.touches[0] // Objcet
let moveX = touchData.clientX - that.startTouchData.clientX
let moveY = touchData.clientY - that.startTouchData.clientY
if (moveY < -130) {
if (that.vibrate && that.recording) {
// #ifdef APP-PLUS
plus.device.vibrate(35)
// #endif
// #ifdef MP-WEIXIN
uni.vibrateShort()
// #endif
}
that.recording = false
} else {
that.recording = true
}
},
},
}
</script>
<style lang="scss">
.record-btn {
color: #000;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
transition: 0.25s all;
border: 1rpx solid whitesmoke;
font-size: 36rpx;
font-weight: 500;
color: #1a1a1a;
box-sizing: border-box;
text {
font-size: 30rpx;
font-weight: 600;
line-height: 1;
}
}
.record-btn-hover {
color: var(--btn-hover-fontcolor) !important;
background-color: var(--btn-hover-bgcolor) !important;
}
.record-popup {
position: absolute;
bottom: var(--popup-bottom);
left: calc(50vw - calc(var(--popup-width) / 2));
z-index: 1;
width: var(--popup-width);
height: var(--popup-height);
display: flex;
align-items: center;
justify-content: center;
border-radius: 10rpx;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
background: var(--popup-bg-color);
color: #000;
transition: 0.2s height;
.inner-content {
height: var(--popup-height);
font-size: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
.title {
font-weight: bold;
padding: 20rpx 0;
}
.tips {
color: #999;
padding: 20rpx 0;
}
}
}
.cancel-icon {
width: 100rpx;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 44rpx;
line-height: 44rpx;
background-color: pink;
border-radius: 50%;
transform: rotate(45deg);
}
.voice-line-wrap {
display: flex;
align-items: center;
.voice-line {
width: 5rpx;
height: var(--line-height);
border-radius: 3rpx;
margin: 0 5rpx;
}
.one {
animation: wave 0.4s 1s linear infinite alternate;
}
.two {
animation: wave 0.4s 0.9s linear infinite alternate;
}
.three {
animation: wave 0.4s 0.8s linear infinite alternate;
}
.four {
animation: wave 0.4s 0.7s linear infinite alternate;
}
.five {
animation: wave 0.4s 0.6s linear infinite alternate;
}
.six {
animation: wave 0.4s 0.5s linear infinite alternate;
}
.seven {
animation: wave 0.4s linear infinite alternate;
}
}
.record-auto-stop-hint {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
text {
color: #fff;
margin-top: -24rpx;
}
}
@keyframes wave {
0% {
transform: scale(1, 1);
background-color: var(--line-start-color);
}
100% {
transform: scale(1, 0.2);
background-color: var(--line-end-color);
}
}
.record-shadow {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.4);
z-index: 12;
.record-animation-area {
position: absolute;
bottom: 50%;
left: 50%;
width: 468rpx;
height: 143rpx;
margin-top: -71.5rpx;
margin-left: -234rpx;
background-image: url('@/static/image/record/chat-voice-animation-bg-blue.png');
background-size: 100% 100%;
background-repeat: no-repeat;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.voice-line-wrap {
margin-top: -17rpx;
}
}
.record-cancel-btn {
position: absolute;
bottom: 320rpx;
left: 50%;
margin-left: -74rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
image {
width: 148rpx;
height: 148rpx;
}
text {
color: #fff;
margin: 8rpx;
height: 48rpx;
line-height: 1;
display: inline-block;
}
}
.record-shadow-bottom {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 274rpx;
background-color: #4686e2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
image {
width: 44rpx;
height: 62rpx;
position: relative;
z-index: 2;
}
text {
margin: 22rpx 0 0;
color: #fff;
}
}
.record-shadow-bottom::after {
content: '';
position: absolute;
width: 100%;
height: 68rpx;
border-radius: 100%;
background: inherit;
bottom: 274rpx;
left: 50%;
transform: translate(-50%, 50%);
z-index: 1;
}
}
</style>

View File

@ -1,226 +0,0 @@
class Recoder {
constructor(sampleRate) {
this.leftDataList = []
this.rightDataList = []
this.mediaPlayer = null
this.audioContext = null
this.source = null
this.sampleRate = sampleRate || 44100
}
start() {
return new Promise((resolve, reject) => {
window.navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 8000, // 采样率
channelCount: 1, // 声道
audioBitsPerSecond: 64,
volume: 1.0, // 音量
autoGainControl: true
}
}).then(mediaStream => {
console.log(mediaStream, 'mediaStream')
this.mediaPlayer = mediaStream
this.beginRecord(mediaStream)
resolve()
}).catch(err => {
// 如果用户电脑没有麦克风设备或者用户拒绝了,或者连接出问题了等
// 这里都会抛异常并且通过err.name可以知道是哪种类型的错误
console.error(err)
reject(err)
})
})
}
beginRecord(mediaStream) {
let audioContext = new(window.AudioContext || window.webkitAudioContext)()
// mediaNode包含 mediaStreamaudioContext
let mediaNode = audioContext.createMediaStreamSource(mediaStream)
console.log(mediaNode, 'mediaNode')
// 创建一个jsNode
// audioContext.sampleRate = 8000
console.log(audioContext, 'audioContext')
let jsNode = this.createJSNode(audioContext)
console.log(jsNode, 'jsnode')
// 需要连到扬声器消费掉outputBufferprocess回调才能触发
// 并且由于不给outputBuffer设置内容所以扬声器不会播放出声音
jsNode.connect(audioContext.destination)
jsNode.onaudioprocess = this.onAudioProcess.bind(this)
// 把mediaNode连接到jsNode
mediaNode.connect(jsNode)
this.audioContext = audioContext
}
onAudioProcess(event) {
console.log('is recording')
// 拿到输入buffer Float32Array
let audioBuffer = event.inputBuffer
let leftChannelData = audioBuffer.getChannelData(0)
// let rightChannelData = audioBuffer.getChannelData(1)
// 需要克隆一下
this.leftDataList.push(leftChannelData.slice(0))
//this.rightDataList.push(rightChannelData.slice(0))
}
createJSNode(audioContext) {
const BUFFER_SIZE = 4096
const INPUT_CHANNEL_COUNT = 1
const OUTPUT_CHANNEL_COUNT = 1
// createJavaScriptNode已被废弃
let creator = audioContext.createScriptProcessor || audioContext.createJavaScriptNode
creator = creator.bind(audioContext)
return creator(BUFFER_SIZE, INPUT_CHANNEL_COUNT, OUTPUT_CHANNEL_COUNT)
}
playRecord(arrayBuffer) {
let blob = new Blob([new Int8Array(arrayBuffer)], {
type: 'audio/mp3' // files[0].type
})
let blobUrl = URL.createObjectURL(blob)
this.source = blob
this.blobUrl = blobUrl
// document.querySelector(.audio-node).src = blobUrl
return blobUrl
}
stop() {
// 停止录音
let leftData = this.mergeArray(this.leftDataList)
//let rightData = this.mergeArray(this.rightDataList)
let allData = this.interSingleData(leftData)
let wavBuffer = this.createWavFile(allData)
// 转到播放
let source = this.playRecord(wavBuffer)
// if (source) {
// source = source.slice(5)
// }
console.log("我最后转换完播放的---------------------", source);
this.resetRecord()
return source
}
transformArrayBufferToBase64(buffer) {
var binary = ''
var bytes = new Uint8Array(buffer)
for (var len = bytes.byteLength, i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i])
}
return window.btoa(binary)
}
// 停止控件录音
resetRecord() {
this.leftDataList = []
this.rightDataList = []
this.audioContext.close()
this.mediaPlayer.getAudioTracks().forEach(track => {
track.stop()
this.mediaPlayer.removeTrack(track)
})
}
createWavFile(audioData) {
let channelCount = 1
const WAV_HEAD_SIZE = 44
const sampleBits = 16
let sampleRate = this.sampleRate
let buffer = new ArrayBuffer(audioData.length * 2 + WAV_HEAD_SIZE)
// 需要用一个view来操控buffer
let view = new DataView(buffer)
// 写入wav头部信息
// RIFF chunk descriptor/identifier
this.writeUTFBytes(view, 0, 'RIFF')
// RIFF chunk length
view.setUint32(4, 44 + audioData.length * channelCount, true)
// RIFF type
this.writeUTFBytes(view, 8, 'WAVE')
// format chunk identifier
// FMT sub-chunk
this.writeUTFBytes(view, 12, 'fmt ')
// format chunk length
view.setUint32(16, 16, true)
// sample format (raw)
view.setUint16(20, 1, true)
// stereo (2 channels)
view.setUint16(22, channelCount, true)
// sample rate
view.setUint32(24, sampleRate, true)
// byte rate (sample rate * block align)
view.setUint32(28, sampleRate * 2, true)
// block align (channel count * bytes per sample)
view.setUint16(32, 2 * 2, true)
// bits per sample
view.setUint16(34, 16, true)
// data sub-chunk
// data chunk identifier
this.writeUTFBytes(view, 36, 'data')
// data chunk length
view.setUint32(40, audioData.length * 2, true)
console.log(view, 'view')
let length = audioData.length
let index = 44
let volume = 1
for (let i = 0; i < length; i++) {
view.setInt16(index, audioData[i] * (0x7FFF * volume), true)
index += 2
}
return buffer
}
writeUTFBytes(view, offset, string) {
var lng = string.length
for (var i = 0; i < lng; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
interSingleData(left) {
var t = left.length;
let sampleRate = this.audioContext.sampleRate,
outputSampleRate = this.sampleRate
sampleRate += 0.0;
outputSampleRate += 0.0;
var s = 0,
o = sampleRate / outputSampleRate,
u = Math.ceil(t * outputSampleRate / sampleRate),
a = new Float32Array(u);
for (let i = 0; i < u; i++) {
a[i] = left[Math.floor(s)];
s += o;
}
return a;
}
// 交叉合并左右声道的数据
interleaveLeftAndRight(left, right) {
let totalLength = left.length + right.length
let data = new Float32Array(totalLength)
for (let i = 0; i < left.length; i++) {
let k = i * 2
data[k] = left[i]
data[k + 1] = right[i]
}
return data
}
mergeArray(list) {
let length = list.length * list[0].length
let data = new Float32Array(length)
let offset = 0
for (let i = 0; i < list.length; i++) {
data.set(list[i], offset)
offset += list[i].length
}
return data
}
}
export default Recoder

View File

@ -1,272 +0,0 @@
/**
* 本模块封装了AndroidiOS的应用权限判断打开应用权限设置界面以及位置系统服务是否开启
*/
var isIos
// #ifdef APP-PLUS
isIos = (plus.os.name == "iOS")
// #endif
// 判断推送权限是否开启
function judgeIosPermissionPush() {
var result = false;
var UIApplication = plus.ios.import("UIApplication");
var app = UIApplication.sharedApplication();
var enabledTypes = 0;
if (app.currentUserNotificationSettings) {
var settings = app.currentUserNotificationSettings();
enabledTypes = settings.plusGetAttribute("types");
console.log("enabledTypes1:" + enabledTypes);
if (enabledTypes == 0) {
console.log("推送权限没有开启");
} else {
result = true;
console.log("已经开启推送功能!")
}
plus.ios.deleteObject(settings);
} else {
enabledTypes = app.enabledRemoteNotificationTypes();
if (enabledTypes == 0) {
console.log("推送权限没有开启!");
} else {
result = true;
console.log("已经开启推送功能!")
}
console.log("enabledTypes2:" + enabledTypes);
}
plus.ios.deleteObject(app);
plus.ios.deleteObject(UIApplication);
return result;
}
// 判断定位权限是否开启
function judgeIosPermissionLocation() {
var result = false;
var cllocationManger = plus.ios.import("CLLocationManager");
var status = cllocationManger.authorizationStatus();
result = (status != 2)
console.log("定位权限开启:" + result);
// 以下代码判断了手机设备的定位是否关闭,推荐另行使用方法 checkSystemEnableLocation
/* var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
console.log("enable:" + enable);
console.log("status:" + status);
if (enable && status != 2) {
result = true;
console.log("手机定位服务已开启且已授予定位权限");
} else {
console.log("手机系统的定位没有打开或未给予定位权限");
} */
plus.ios.deleteObject(cllocationManger);
return result;
}
// 判断麦克风权限是否开启
function judgeIosPermissionRecord() {
var result = false;
var avaudiosession = plus.ios.import("AVAudioSession");
var avaudio = avaudiosession.sharedInstance();
var permissionStatus = avaudio.recordPermission();
console.log("permissionStatus:" + permissionStatus);
if (permissionStatus == 1684369017 || permissionStatus == 1970168948) {
console.log("麦克风权限没有开启");
} else {
result = true;
console.log("麦克风权限已经开启");
}
plus.ios.deleteObject(avaudiosession);
return result;
}
// 判断相机权限是否开启
function judgeIosPermissionCamera() {
var result = false;
var AVCaptureDevice = plus.ios.import("AVCaptureDevice");
var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');
console.log("authStatus:" + authStatus);
if (authStatus == 3) {
result = true;
console.log("相机权限已经开启");
} else {
console.log("相机权限没有开启");
}
plus.ios.deleteObject(AVCaptureDevice);
return result;
}
// 判断相册权限是否开启
function judgeIosPermissionPhotoLibrary() {
var result = false;
var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary");
var authStatus = PHPhotoLibrary.authorizationStatus();
console.log("authStatus:" + authStatus);
if (authStatus == 3) {
result = true;
console.log("相册权限已经开启");
} else {
console.log("相册权限没有开启");
}
plus.ios.deleteObject(PHPhotoLibrary);
return result;
}
// 判断通讯录权限是否开启
function judgeIosPermissionContact() {
var result = false;
var CNContactStore = plus.ios.import("CNContactStore");
var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);
if (cnAuthStatus == 3) {
result = true;
console.log("通讯录权限已经开启");
} else {
console.log("通讯录权限没有开启");
}
plus.ios.deleteObject(CNContactStore);
return result;
}
// 判断日历权限是否开启
function judgeIosPermissionCalendar() {
var result = false;
var EKEventStore = plus.ios.import("EKEventStore");
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);
if (ekAuthStatus == 3) {
result = true;
console.log("日历权限已经开启");
} else {
console.log("日历权限没有开启");
}
plus.ios.deleteObject(EKEventStore);
return result;
}
// 判断备忘录权限是否开启
function judgeIosPermissionMemo() {
var result = false;
var EKEventStore = plus.ios.import("EKEventStore");
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);
if (ekAuthStatus == 3) {
result = true;
console.log("备忘录权限已经开启");
} else {
console.log("备忘录权限没有开启");
}
plus.ios.deleteObject(EKEventStore);
return result;
}
// Android权限查询
function requestAndroidPermission(permissionID) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
[permissionID], // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装
function(resultObj) {
var result = 0;
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i];
console.log('已获取的权限:' + grantedPermission);
result = 1
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i];
console.log('拒绝本次申请的权限:' + deniedPresentPermission);
result = 0
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i];
console.log('永久拒绝申请的权限:' + deniedAlwaysPermission);
result = -1
}
resolve(result);
// 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
// if (result != 1) {
// gotoAppPermissionSetting()
// }
},
function(error) {
console.log('申请权限错误:' + error.code + " = " + error.message);
resolve({
code: error.code,
message: error.message
});
}
);
});
}
// 使用一个方法,根据参数判断权限
function judgeIosPermission(permissionID) {
if (permissionID == "location") {
return judgeIosPermissionLocation()
} else if (permissionID == "camera") {
return judgeIosPermissionCamera()
} else if (permissionID == "photoLibrary") {
return judgeIosPermissionPhotoLibrary()
} else if (permissionID == "record") {
return judgeIosPermissionRecord()
} else if (permissionID == "push") {
return judgeIosPermissionPush()
} else if (permissionID == "contact") {
return judgeIosPermissionContact()
} else if (permissionID == "calendar") {
return judgeIosPermissionCalendar()
} else if (permissionID == "memo") {
return judgeIosPermissionMemo()
}
return false;
}
// 跳转到**应用**的权限页面
function gotoAppPermissionSetting() {
if (isIos) {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
// var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES");
var setting2 = NSURL2.URLWithString("app-settings:");
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
// console.log(plus.device.vendor);
var Intent = plus.android.importClass("android.content.Intent");
var Settings = plus.android.importClass("android.provider.Settings");
var Uri = plus.android.importClass("android.net.Uri");
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
}
// 检查系统的设备服务是否开启
// var checkSystemEnableLocation = async function () {
function checkSystemEnableLocation() {
if (isIos) {
var result = false;
var cllocationManger = plus.ios.import("CLLocationManager");
var result = cllocationManger.locationServicesEnabled();
console.log("系统定位开启:" + result);
plus.ios.deleteObject(cllocationManger);
return result;
} else {
var context = plus.android.importClass("android.content.Context");
var locationManager = plus.android.importClass("android.location.LocationManager");
var main = plus.android.runtimeMainActivity();
var mainSvr = main.getSystemService(context.LOCATION_SERVICE);
var result = mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER);
console.log("系统定位开启:" + result);
return result
}
}
export default {
judgeIosPermission: judgeIosPermission,
requestAndroidPermission: requestAndroidPermission,
checkSystemEnableLocation: checkSystemEnableLocation,
gotoAppPermissionSetting: gotoAppPermissionSetting
}

View File

@ -1,83 +0,0 @@
{
"id": "all-speech",
"displayName": "allspeech长按录音动画组件多端权限判断可监听开始、结束、取消事件",
"version": "1.1.2",
"description": "H5、App、小程序兼容",
"keywords": [
"长按",
"录音",
"动画组件"
],
"repository": "",
"engines": {
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "n",
"QQ浏览器(Android)": "n"
},
"H5-pc": {
"Chrome": "y",
"IE": "n",
"Edge": "n",
"Firefox": "y",
"Safari": "n"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -1,89 +0,0 @@
### nbVoiceRecord概述
- 这是个基于uni-app 符合uni_modules 的插件
- 无任何依赖、纯css动画
- nb是NeverBug的意思
### 主要功能
- 长按组件后弹出录音弹窗,松手完成录音,手指向上滑动可取消;
- 支持各种自定义,如弹窗高度、宽度、各处文字甚至声纹波形的尺寸和颜色;
- 已完成多端适配,自动根据授权情况提示完成授权、已获得授权才开始录音
- endRecord回调事件附带录音文件
### 动画预览
- 默认样式
![默认样式](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-613ff9e2-568b-4845-987f-93626e21bcde/84cf3c4f-f4f2-41e6-bb82-1414465a944d.gif)
- 自定义按钮为圆形(红背景、白字)、弹窗为正方形
![正方形](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-613ff9e2-568b-4845-987f-93626e21bcde/893bf1d6-593f-40e6-aeff-12afe4ebbc37.gif)
### 基本用法:
```
<template>
<view>
<all-speech @startRecord="start" @endRecord="end" @cancelRecord="cancel"></all-speech>
</view>
</template>
<script>
methods: {
start() {
// 开始录音
},
end(event) {
// 结束录音并处理得到的录音文件
// event中app端仅有tempFilePath字段微信小程序还有duration和fileSize两个字段
},
cancel() {
// 用户取消录音
}
}
</script>
```
### 全部支持参数
| 参数名 | 类型 | 默认值 | 作用 | 注意事项 |
| ----- | ----- | ------ | ------- | --- |
| recordOptions | Object | {duration:60000} | 录音配置 |各端支持情况不同,请自行查看[官方说明](https://uniapp.dcloud.net.cn/api/media/record-manager.html#getrecordermanager) |
| btnStyle | Object | 请查看源码 | 按钮样式 |对象格式 |
| btnHoverFontcolor | String | #000 | 按钮长按时文字颜色 | |
| btnHoverBgcolor | String | whitesmoke | 按钮长按时背景颜色 | |
| btnDefaultText | String | 长按开始录音 | 初始按钮文字 | |
| btnRecordingText | String | 录音中 | 录制时按钮文字 | |
| vibrate | Boolean | true | 震动反馈 | 弹窗、滑动取消时 |
| popupTitle | String | 正在录制音频 | 弹窗顶部文字 | |
| popupDefaultTips | String | 松手完成录音 | 录制时弹窗底部提示 | |
| popupCancelTips | String | 松手取消录音 | 滑动取消时弹窗底部提示 | |
| popupMaxWidth | Number | 600 | 弹窗展开后宽度 |注意这里几个单位都是rpx |
| popupMaxHeight | Number | 300 | 弹窗展开后高度 | |
| popupFixBottom | Number | 200 | 弹窗展开后距底部高度 | |
| popupBgColor | String | whitesmoke | 弹窗背景颜色 | |
| lineHeight | Number | 50 | 声波高度 | |
| lineStartColor | String | royalblue | 声波波谷时颜色色值 | 色值或者颜色名均可 |
| lineEndColor | String | indianred | 声波波峰时颜色色值 | |
### 原作者其他插件
- [bwinBrand多端自适应企业官网、uniCloud云端一体【用户端】](https://ext.dcloud.net.cn/plugin?id=7821)
- [bwinBrand多端自适应企业官网、uniCloud云端一体【管理端】](https://ext.dcloud.net.cn/plugin?id=7822)
- [bwinAgent多端、多项目全民经纪人、uniCloud云端一体【经纪人端】](https://ext.dcloud.net.cn/plugin?id=8606)
- [bwinAgent多端、多项目全民经纪人、uniCloud云端一体【管理员端】](https://ext.dcloud.net.cn/plugin?id=8607)
- [必闻优学,教育培训机构模板(单校区版,纯模板)](https://ext.dcloud.net.cn/plugin?id=7709)
### 一个有趣的社区
- [NeverBug.cn 弹幕式互动社区](https://neverbug.cn)
### 原作者
- QQ123060128
- Emailkarma.zhao@gmail.com
- 官网https://brand.neverbug.cn
### 补充作者
- QQ27836407
- Emailzgdabao.zhao@gmail.com

View File

@ -71,37 +71,5 @@
"message.customerService.text":"Please call customer service",
"message.action.text":"perform logout operation",
"index.mine.upload":"Click To Upload",
"index.mine.reUpload":"Re-upload",
"complaint": {
"title": "Complaint Report",
"selectType": "Select Complaint Type",
"selectPlaceholder": "Please select complaint type",
"imageEvidence": "Image Evidence (Max 9)",
"addImage": "Add Image",
"complaintContent": "Complaint Content (Optional)",
"contentPlaceholder": "Please describe in detail...",
"noticeTitle": "Notice",
"noticeone": "1. Please select the correct complaint category.",
"noticetwo":"2. Provide valid evidence such as images, chat records, etc.",
"noticethree": "3. Describe the violation details to help us review faster.",
"noticefour": "4. Do not submit duplicate complaints for the same issue.",
"noticefive":"5. Do not abuse the complaint system.",
"noticenoticeContent":"Thank you for helping us build a safe community. We will process your complaint as soon as possible. We hope your complaint is made in good faith with accurate information. We will take reasonable measures to protect your privacy and will not disclose your information to third parties without your consent, except as required by law. Abuse of the complaint system may result in restrictions.",
"submit": "Submit",
"expand": "Expand",
"collapse": "Collapse",
"typeOptions": {
"porn": "Underage Exploitation",
"illegal": "Fraud",
"gambling": "Illegal Activities",
"violence": "Harassment",
"selfHarm": "Harmful Content",
"other": "Other"
},
"toast": {
"selectType": "Please select complaint type",
"submitting": "Submitting...",
"success": "Complaint submitted successfully"
}
}
}
"index.mine.reUpload":"Re-upload"
}

View File

@ -130,7 +130,6 @@
"groupNotice.confirm.clear": "清空",
"groupNotice.quit.edit": "退出本次编辑",
"groupNotice.continue.edit": "继续编辑",
"groupNotice.notice.empty": "暂无内容",
"groupNotice.confirm.quit": "退出",
"chatSettings.btn.removeAdmin": "移除",
"groupManage.disband.hint": "退出后,本群将被解散",
@ -151,47 +150,5 @@
"button.text.close": "关闭",
"choose.deps.all": "全部",
"choose.deps.current": "当前",
"chat.mention.select": "选择提醒的人",
"do.phone.call": "拨打",
"popup.title.phone": "电话",
"pageTitle.view.deps": "查看部门",
"group.dismiss.confirm": "确定解散本群",
"chat.msgRead.list": "消息接收人列表",
"chat.settings.report": "投诉",
"complaint": {
"title": "投诉举报",
"selectType": "选择投诉内容",
"selectPlaceholder": "请选择投诉类型",
"imageEvidence": "图片证据(最多9张)",
"addImage": "添加图片",
"complaintContent": "投诉内容(选填)",
"contentPlaceholder": "请详细描述投诉内容...",
"noticeTitle": "投诉须知",
"noticeone": "1、请选择正确的投诉类目。",
"noticetwo": "2、提供有效的违规证据如:图片、聊天信息等",
"noticethree": "3、详细描述违规问题详情有助于我们的审核人员快速研判处置。",
"noticefour": "4、请勿针对同一问题重复投诉以免造成资源浪费。",
"noticefive": "5、请勿滥用投诉以免造成资源浪费",
"noticenoticeContent": "感谢您与我们共建安全社区环境,我们会尽快对您的投诉进行处理。同时我们希望您的投诉行为基于善意,提供准确有效的违规信息帮助我们更好的进行判断并且处理。同时我们会采取必要合理的措施保护投诉人的个人隐私信息,除法律法规规定的情形之外,在未获得用户许可的情况下,不会向第三方公开投诉人信息。如果存在滥用、重复无效投诉,我们可能会对投诉账号采取包括但不限于限制投诉频次、禁止投诉等限制",
"submit": "提交投诉",
"expand": "展开",
"collapse": "收起",
"typeOptions": {
"porn": "侵犯未成年",
"illegal": "欺诈骗钱",
"gambling": "违法违规",
"violence": "骚扰",
"selfHarm": "不良价值导向",
"other": "其他"
},
"toast": {
"selectType": "请选择投诉类型",
"submitting": "提交中...",
"success": "投诉提交成功"
}
},
"release_hand_to_send": "松开发送",
"release_hand_to_cancel": "松开取消",
"hold_to": "按住",
"speak": "说话"
"chat.mention.select": "选择提醒的人"
}

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