feat:聊天界面
This commit is contained in:
1 parent
3aaa1cb425
commit
b57bd5cb65
503 files changed
+52442
-1595
No files matched your search
@@ -1,64 +1,63 @@
|
||||
<template>
|
||||
<view class="arrow-bar">
|
||||
<text class="icon iconfont" :class="icon" :style="{color: textColor}"></text>
|
||||
<text class="title">{{ title }}</text>
|
||||
<uni-icons class="arrow" type="right" size="16"></uni-icons>
|
||||
</view>
|
||||
<view class="arrow-bar">
|
||||
<text class="icon iconfont" :class="icon" :style="{ color: textColor }"></text>
|
||||
<text class="title">{{ title }}</text>
|
||||
<uni-icons class="arrow" type="right" size="16"></uni-icons>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "arrow-bar",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
|
||||
"#c9b455", "#326eb6"]
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
textColor() {
|
||||
let hash = 0;
|
||||
for (var i = 0; i < this.title.length; i++) {
|
||||
hash += this.title.charCodeAt(i);
|
||||
}
|
||||
return this.colors[hash % this.colors.length];
|
||||
}
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: 'arrow-bar',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
colors: ['#5daa31', '#c7515a', '#e03697', '#85029b', '#c9b455', '#326eb6'],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
let hash = 0;
|
||||
for (var i = 0; i < this.title.length; i++) {
|
||||
hash += this.title.charCodeAt(i);
|
||||
}
|
||||
return this.colors[hash % this.colors.length];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.arrow-bar {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
font-size: $im-font-size;
|
||||
color: $im-text-color;
|
||||
margin-top: 5rpx;
|
||||
background-color: white;
|
||||
line-height: 90rpx;
|
||||
display: flex;
|
||||
|
||||
.icon {
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
.arrow-bar {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
font-size: $im-font-size;
|
||||
color: $im-text-color;
|
||||
margin-top: 5rpx;
|
||||
background-color: white;
|
||||
line-height: 90rpx;
|
||||
display: flex;
|
||||
|
||||
.arrow {
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.icon {
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -14,13 +14,13 @@
|
||||
</view>
|
||||
<view class="chat-content">
|
||||
<view class="chat-at-text">{{ atText }}</view>
|
||||
<view class="chat-send-name" v-if="isShowSendName">{{
|
||||
chat.sendNickName + ': '
|
||||
}}</view>
|
||||
<view class="chat-send-name" v-if="isShowSendName">
|
||||
{{ chat.sendNickName + ': ' }}
|
||||
</view>
|
||||
<view v-if="!isTextMessage" class="chat-content-text">{{ chat.lastContent }}</view>
|
||||
<rich-text v-else class="chat-content-text" :nodes="nodesText"></rich-text>
|
||||
<view v-if="chat.isDnd" class="icon iconfont icon-dnd"></view>
|
||||
<uni-badge v-else-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" />
|
||||
<wd-badge v-else-if="chat.unreadCount > 0" :max="99" :model-value="chat.unreadCount" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -111,8 +111,9 @@
|
||||
position: relative;
|
||||
padding: 18rpx 20rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
background-color: $im-bg;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid $im-border;
|
||||
|
||||
&:hover {
|
||||
background-color: $im-bg-active;
|
||||
@@ -175,7 +176,7 @@
|
||||
display: flex;
|
||||
font-size: $im-font-size-smaller;
|
||||
color: $im-text-color-lighter;
|
||||
padding-top: 8rpx;
|
||||
padding: 8rpx 30rpx 0 0;
|
||||
align-items: center;
|
||||
|
||||
.chat-at-text {
|
||||
@@ -191,6 +192,7 @@
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -1,243 +1,274 @@
|
||||
<template>
|
||||
<view class="chat-record">
|
||||
<view class="chat-record-bar" :class="{ recording: recording }" id="chat-record-bar" @click.stop=""
|
||||
@touchstart.prevent="onStartRecord" @touchmove.prevent="onTouchMove" @touchend.prevent="onEndRecord">
|
||||
{{ recording ? '正在录音' : '长按 说话' }}</view>
|
||||
<view v-if="recording" class="chat-record-window" :style="recordWindowStyle">
|
||||
<view class="rc-wave">
|
||||
<text class="note" style="--d: 0"></text>
|
||||
<text class="note" style="--d: 1"></text>
|
||||
<text class="note" style="--d: 2"></text>
|
||||
<text class="note" style="--d: 3"></text>
|
||||
<text class="note" style="--d: 4"></text>
|
||||
<text class="note" style="--d: 5"></text>
|
||||
<text class="note" style="--d: 6"></text>
|
||||
</view>
|
||||
<view class="rc-tip">{{ recordTip }}</view>
|
||||
<view class="cancel-btn" @click="onCancel">
|
||||
<uni-icons :class="moveToCancel ? 'red' : 'black'" type="clear" :size="moveToCancel ? 45 : 40"></uni-icons>
|
||||
</view>
|
||||
<view class="opt-tip" :class="moveToCancel ? 'red' : 'black'">{{ moveToCancel ? '松手取消' : '松手发送,上划取消' }}</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="chat-record">
|
||||
<view
|
||||
class="chat-record-bar"
|
||||
:class="{ recording: recording }"
|
||||
id="chat-record-bar"
|
||||
@click.stop=""
|
||||
@touchstart.prevent="onStartRecord"
|
||||
@touchmove.prevent="onTouchMove"
|
||||
@touchend.prevent="onEndRecord"
|
||||
>
|
||||
{{ recording ? '正在录音' : '长按 说话' }}</view
|
||||
>
|
||||
<view v-if="recording" class="chat-record-window" :style="recordWindowStyle">
|
||||
<view class="rc-wave">
|
||||
<text class="note" style="--d: 0"></text>
|
||||
<text class="note" style="--d: 1"></text>
|
||||
<text class="note" style="--d: 2"></text>
|
||||
<text class="note" style="--d: 3"></text>
|
||||
<text class="note" style="--d: 4"></text>
|
||||
<text class="note" style="--d: 5"></text>
|
||||
<text class="note" style="--d: 6"></text>
|
||||
</view>
|
||||
<view class="rc-tip">{{ recordTip }}</view>
|
||||
<view class="cancel-btn" @click="onCancel">
|
||||
<uni-icons
|
||||
:class="moveToCancel ? 'red' : 'black'"
|
||||
type="clear"
|
||||
:size="moveToCancel ? 45 : 40"
|
||||
></uni-icons>
|
||||
</view>
|
||||
<view class="opt-tip" :class="moveToCancel ? 'red' : 'black'">{{
|
||||
moveToCancel ? '松手取消' : '松手发送,上划取消'
|
||||
}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "chat-record",
|
||||
data() {
|
||||
return {
|
||||
recording: false,
|
||||
moveToCancel: false,
|
||||
recordBarTop: 0,
|
||||
druation: 0,
|
||||
rcTimer: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTouchMove(e) {
|
||||
const moveY = e.touches[0].clientY;
|
||||
this.moveToCancel = moveY < this.recordBarTop - 40;
|
||||
},
|
||||
onCancel() {
|
||||
if (this.recording) {
|
||||
this.moveToCancel = true;
|
||||
this.onEndRecord();
|
||||
}
|
||||
},
|
||||
onStartRecord() {
|
||||
/* 用户第一次使用语音会唤醒录音权限请求,此时会导致@touchend失效,
|
||||
export default {
|
||||
name: 'chat-record',
|
||||
data() {
|
||||
return {
|
||||
recording: false,
|
||||
moveToCancel: false,
|
||||
recordBarTop: 0,
|
||||
druation: 0,
|
||||
rcTimer: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onTouchMove(e) {
|
||||
const moveY = e.touches[0].clientY;
|
||||
this.moveToCancel = moveY < this.recordBarTop - 40;
|
||||
},
|
||||
onCancel() {
|
||||
if (this.recording) {
|
||||
this.moveToCancel = true;
|
||||
this.onEndRecord();
|
||||
}
|
||||
},
|
||||
onStartRecord() {
|
||||
/* 用户第一次使用语音会唤醒录音权限请求,此时会导致@touchend失效,
|
||||
一直处于录音状态,这里允许用户再次点击发送语音并结束录音 */
|
||||
if (this.recording) {
|
||||
return;
|
||||
}
|
||||
console.log("开始录音")
|
||||
this.moveToCancel = false;
|
||||
this.initRecordBar();
|
||||
if(!this.$rc.checkIsEnable()){
|
||||
return;
|
||||
}
|
||||
this.$rc.start().then(() => {
|
||||
this.recording = true;
|
||||
console.log("开始录音成功")
|
||||
// 开始计时
|
||||
this.startTimer();
|
||||
}).catch((e) => {
|
||||
console.log("录音失败" + JSON.stringify(e))
|
||||
uni.showToast({
|
||||
title: "录音失败",
|
||||
icon: "none"
|
||||
});
|
||||
});
|
||||
},
|
||||
onEndRecord() {
|
||||
if(!this.recording){
|
||||
return;
|
||||
}
|
||||
this.recording = false;
|
||||
// 停止计时
|
||||
this.stopTimer();
|
||||
// 停止录音
|
||||
this.$rc.close();
|
||||
// 触屏位置是否移动到了取消区域
|
||||
if (this.moveToCancel) {
|
||||
console.log("录音取消")
|
||||
return;
|
||||
}
|
||||
// 大于1秒才发送
|
||||
if (this.druation <= 1) {
|
||||
uni.showToast({
|
||||
title: "说话时间太短",
|
||||
icon: 'none'
|
||||
})
|
||||
return;
|
||||
}
|
||||
this.$rc.upload().then((data) => {
|
||||
this.$emit("send", data);
|
||||
}).catch((e) => {
|
||||
uni.showToast({
|
||||
title: e,
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
startTimer() {
|
||||
this.druation = 0;
|
||||
this.stopTimer();
|
||||
this.rcTimer = setInterval(() => {
|
||||
this.druation++;
|
||||
// 大于60s,直接结束
|
||||
if (this.druation >= 60) {
|
||||
this.onEndRecord();
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
stopTimer() {
|
||||
this.rcTimer && clearInterval(this.rcTimer);
|
||||
this.rcTimer = null;
|
||||
},
|
||||
initRecordBar() {
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query.select('#chat-record-bar').boundingClientRect((rect) => {
|
||||
// 顶部高度位置
|
||||
this.recordBarTop = rect.top;
|
||||
}).exec()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
recordWindowStyle() {
|
||||
const windowHeight = uni.getSystemInfoSync().windowHeight;
|
||||
const bottom = windowHeight - this.recordBarTop + 12;
|
||||
return `bottom:${bottom}px;`
|
||||
},
|
||||
recordTip() {
|
||||
if (this.druation > 50) {
|
||||
return `${60 - this.druation}s后将停止录音`;
|
||||
}
|
||||
return `录音时长:${this.druation}s`;
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
this.stopTimer();
|
||||
this.recording = false;
|
||||
}
|
||||
|
||||
}
|
||||
if (this.recording) {
|
||||
return;
|
||||
}
|
||||
console.log('开始录音');
|
||||
this.moveToCancel = false;
|
||||
this.initRecordBar();
|
||||
if (!this.$rc.checkIsEnable()) {
|
||||
return;
|
||||
}
|
||||
this.$rc
|
||||
.start()
|
||||
.then(() => {
|
||||
this.recording = true;
|
||||
console.log('开始录音成功');
|
||||
// 开始计时
|
||||
this.startTimer();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log('录音失败' + JSON.stringify(e));
|
||||
uni.showToast({
|
||||
title: '录音失败',
|
||||
icon: 'none',
|
||||
});
|
||||
});
|
||||
},
|
||||
onEndRecord() {
|
||||
if (!this.recording) {
|
||||
return;
|
||||
}
|
||||
this.recording = false;
|
||||
// 停止计时
|
||||
this.stopTimer();
|
||||
// 停止录音
|
||||
this.$rc.close();
|
||||
// 触屏位置是否移动到了取消区域
|
||||
if (this.moveToCancel) {
|
||||
console.log('录音取消');
|
||||
return;
|
||||
}
|
||||
// 大于1秒才发送
|
||||
if (this.druation <= 1) {
|
||||
uni.showToast({
|
||||
title: '说话时间太短',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$rc
|
||||
.upload()
|
||||
.then((data) => {
|
||||
this.$emit('send', data);
|
||||
})
|
||||
.catch((e) => {
|
||||
uni.showToast({
|
||||
title: e,
|
||||
icon: 'none',
|
||||
});
|
||||
});
|
||||
},
|
||||
startTimer() {
|
||||
this.druation = 0;
|
||||
this.stopTimer();
|
||||
this.rcTimer = setInterval(() => {
|
||||
this.druation++;
|
||||
// 大于60s,直接结束
|
||||
if (this.druation >= 60) {
|
||||
this.onEndRecord();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
stopTimer() {
|
||||
this.rcTimer && clearInterval(this.rcTimer);
|
||||
this.rcTimer = null;
|
||||
},
|
||||
initRecordBar() {
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query
|
||||
.select('#chat-record-bar')
|
||||
.boundingClientRect((rect) => {
|
||||
// 顶部高度位置
|
||||
this.recordBarTop = rect.top;
|
||||
})
|
||||
.exec();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
recordWindowStyle() {
|
||||
const windowHeight = uni.getSystemInfoSync().windowHeight;
|
||||
const bottom = windowHeight - this.recordBarTop + 12;
|
||||
return `bottom:${bottom}px;`;
|
||||
},
|
||||
recordTip() {
|
||||
if (this.druation > 50) {
|
||||
return `${60 - this.druation}s后将停止录音`;
|
||||
}
|
||||
return `录音时长:${this.druation}s`;
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
this.stopTimer();
|
||||
this.recording = false;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-record {
|
||||
.rc-wave {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
height: 80rpx;
|
||||
.chat-record {
|
||||
.rc-wave {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
height: 80rpx;
|
||||
|
||||
.note {
|
||||
background: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
width: 4px;
|
||||
height: 50%;
|
||||
border-radius: 5rpx;
|
||||
margin-right: 4px;
|
||||
animation: loading 0.5s infinite linear;
|
||||
animation-delay: calc(0.1s * var(--d));
|
||||
.note {
|
||||
background: $im-bg-linear;
|
||||
width: 4px;
|
||||
height: 50%;
|
||||
border-radius: 5rpx;
|
||||
margin-right: 4px;
|
||||
animation: loading 0.5s infinite linear;
|
||||
animation-delay: calc(0.1s * var(--d));
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-image: linear-gradient(to right, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
height: 20%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
$im-color-primary-light-1 0%,
|
||||
$im-color-primary-light-6 100%
|
||||
);
|
||||
height: 20%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
height: 80%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
50% {
|
||||
background-image: linear-gradient(
|
||||
to top,
|
||||
$im-color-primary-light-1 0%,
|
||||
$im-color-primary-light-6 100%
|
||||
);
|
||||
height: 80%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
height: 20%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100% {
|
||||
background-image: linear-gradient(
|
||||
to top,
|
||||
$im-color-primary-light-1 0%,
|
||||
$im-color-primary-light-6 100%
|
||||
);
|
||||
height: 20%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-record-bar {
|
||||
padding: 10rpx;
|
||||
margin: 10rpx;
|
||||
border-radius: 10rpx;
|
||||
text-align: center;
|
||||
box-shadow: $im-box-shadow;
|
||||
.chat-record-bar {
|
||||
padding: 10rpx;
|
||||
margin: 10rpx;
|
||||
border-radius: 10rpx;
|
||||
text-align: center;
|
||||
box-shadow: $im-box-shadow;
|
||||
background-color: $im-bg-active;
|
||||
|
||||
&.recording {
|
||||
background-color: $im-color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
&.recording {
|
||||
background-color: $im-color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-record-window {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 360rpx;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
padding: 30rpx;
|
||||
.chat-record-window {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 360rpx;
|
||||
background-color: $im-bg-active;
|
||||
padding: 30rpx;
|
||||
|
||||
.icon-microphone {
|
||||
text-align: center;
|
||||
font-size: 80rpx;
|
||||
padding: 10rpx;
|
||||
.icon-microphone {
|
||||
text-align: center;
|
||||
font-size: 80rpx;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
}
|
||||
.rc-tip {
|
||||
text-align: center;
|
||||
font-size: $im-font-size-small;
|
||||
color: gray;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.rc-tip {
|
||||
text-align: center;
|
||||
font-size: $im-font-size-small;
|
||||
color: $im-text-color-light;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.cancel-btn {
|
||||
text-align: center;
|
||||
margin-top: 40rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
text-align: center;
|
||||
margin-top: 40rpx;
|
||||
height: 80rpx;
|
||||
.opt-tip {
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.opt-tip {
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: $im-color-danger !important;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.red {
|
||||
color: $im-color-danger !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,122 +1,130 @@
|
||||
<template>
|
||||
<view>
|
||||
<lsj-upload ref="lsjUpload" :height="'100%'" :option="option" @uploadEnd="onUploadEnd" @change="onChange"
|
||||
:size="maxSize" :instantly="true">
|
||||
<slot></slot>
|
||||
</lsj-upload>
|
||||
</view>
|
||||
<view>
|
||||
<lsj-upload
|
||||
ref="lsjUpload"
|
||||
:height="'100%'"
|
||||
:option="option"
|
||||
@uploadEnd="onUploadEnd"
|
||||
@change="onChange"
|
||||
:size="maxSize"
|
||||
:instantly="true"
|
||||
>
|
||||
<slot></slot>
|
||||
</lsj-upload>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UNI_APP from '@/.env.js';
|
||||
import CHAT_ENV from '@/chat-env.js';
|
||||
|
||||
export default {
|
||||
name: "file-upload",
|
||||
data() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
option: {
|
||||
url: UNI_APP.BASE_URL + '/file/upload',
|
||||
name: 'file',
|
||||
header: {
|
||||
accessToken: uni.getStorageSync('loginInfo').accessToken
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
onBefore: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onError: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs.lsjUpload.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.lsjUpload.hide();
|
||||
},
|
||||
onUploadEnd(item) {
|
||||
let file = this.fileMap.get(item.path);
|
||||
if (item.type == 'fail') {
|
||||
this.onError(file)
|
||||
return;
|
||||
}
|
||||
let res = JSON.parse(item.responseText);
|
||||
if (res.code == 200) {
|
||||
// 上传成功
|
||||
this.onOk(file, res);
|
||||
} else if (res.code == 401) {
|
||||
// token已过期,重新获取token
|
||||
this.refreshToken().then((res) => {
|
||||
let newToken = res.data.accessToken;
|
||||
this.option.header.accessToken = newToken;
|
||||
this.$refs.lsjUpload.setData(this.option);
|
||||
// 重新上传
|
||||
this.$refs.lsjUpload.upload(file.name);
|
||||
}).catch(() => {
|
||||
this.onError(file, res);
|
||||
})
|
||||
} else {
|
||||
// 上传失败
|
||||
this.onError(file, res);
|
||||
}
|
||||
},
|
||||
onChange(files) {
|
||||
if (!files.size) {
|
||||
return;
|
||||
}
|
||||
files.forEach((file, name) => {
|
||||
if (!this.fileMap.has(file.path)) {
|
||||
this.onBefore && this.onBefore(file)
|
||||
this.fileMap.set(file.path, file);
|
||||
}
|
||||
})
|
||||
},
|
||||
onOk(file, res) {
|
||||
this.fileMap.delete(file.path);
|
||||
this.$refs.lsjUpload.clear(file.name);
|
||||
this.onSuccess && this.onSuccess(file, res);
|
||||
},
|
||||
onFailed(file, res) {
|
||||
this.fileMap.delete(file.path);
|
||||
this.$refs.lsjUpload.clear(file.name);
|
||||
this.onError && this.onError(file, res);
|
||||
},
|
||||
refreshToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let loginInfo = uni.getStorageSync('loginInfo')
|
||||
uni.request({
|
||||
method: 'PUT',
|
||||
url: UNI_APP.BASE_URL + '/refreshToken',
|
||||
header: {
|
||||
refreshToken: loginInfo.refreshToken
|
||||
},
|
||||
success: (res) => {
|
||||
resolve(res.data);
|
||||
},
|
||||
fail: (res) => {
|
||||
reject(res);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
export default {
|
||||
name: 'file-upload',
|
||||
data() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
option: {
|
||||
url: CHAT_ENV.BASE_URL + '/file/upload',
|
||||
name: 'file',
|
||||
header: {
|
||||
accessToken: uni.getStorageSync('loginInfo').accessToken,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
onBefore: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
onError: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs.lsjUpload.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.lsjUpload.hide();
|
||||
},
|
||||
onUploadEnd(item) {
|
||||
let file = this.fileMap.get(item.path);
|
||||
if (item.type == 'fail') {
|
||||
this.onError(file);
|
||||
return;
|
||||
}
|
||||
let res = JSON.parse(item.responseText);
|
||||
if (res.code == 200) {
|
||||
// 上传成功
|
||||
this.onOk(file, res);
|
||||
} else if (res.code == 401) {
|
||||
// token已过期,重新获取token
|
||||
this.refreshToken()
|
||||
.then((res) => {
|
||||
let newToken = res.data.accessToken;
|
||||
this.option.header.accessToken = newToken;
|
||||
this.$refs.lsjUpload.setData(this.option);
|
||||
// 重新上传
|
||||
this.$refs.lsjUpload.upload(file.name);
|
||||
})
|
||||
.catch(() => {
|
||||
this.onError(file, res);
|
||||
});
|
||||
} else {
|
||||
// 上传失败
|
||||
this.onError(file, res);
|
||||
}
|
||||
},
|
||||
onChange(files) {
|
||||
if (!files.size) {
|
||||
return;
|
||||
}
|
||||
files.forEach((file, name) => {
|
||||
if (!this.fileMap.has(file.path)) {
|
||||
this.onBefore && this.onBefore(file);
|
||||
this.fileMap.set(file.path, file);
|
||||
}
|
||||
});
|
||||
},
|
||||
onOk(file, res) {
|
||||
this.fileMap.delete(file.path);
|
||||
this.$refs.lsjUpload.clear(file.name);
|
||||
this.onSuccess && this.onSuccess(file, res);
|
||||
},
|
||||
onFailed(file, res) {
|
||||
this.fileMap.delete(file.path);
|
||||
this.$refs.lsjUpload.clear(file.name);
|
||||
this.onError && this.onError(file, res);
|
||||
},
|
||||
refreshToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let loginInfo = uni.getStorageSync('loginInfo');
|
||||
uni.request({
|
||||
method: 'PUT',
|
||||
url: CHAT_ENV.BASE_URL + '/refreshToken',
|
||||
header: {
|
||||
refreshToken: loginInfo.refreshToken,
|
||||
},
|
||||
success: (res) => {
|
||||
resolve(res.data);
|
||||
},
|
||||
fail: (res) => {
|
||||
reject(res);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style></style>
|
||||
@@ -1,100 +1,100 @@
|
||||
<template>
|
||||
<view @click="selectAndUpload()">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view @click="selectAndUpload()">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UNI_APP from '@/.env.js'
|
||||
import CHAT_ENV from '@/chat-env.js';
|
||||
|
||||
export default {
|
||||
name: "image-upload",
|
||||
data() {
|
||||
return {
|
||||
uploadHeaders: {
|
||||
"accessToken": uni.getStorageSync('loginInfo').accessToken
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 5 * 1024 * 1024
|
||||
},
|
||||
sourceType: {
|
||||
type: String,
|
||||
default: 'album'
|
||||
},
|
||||
isPermanent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
thumbSize: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
onBefore: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onError: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectAndUpload() {
|
||||
uni.chooseImage({
|
||||
count: this.maxCount, //最多可以选择的图片张数,默认9
|
||||
sourceType: [this.sourceType], //album 从相册选图,camera 使用相机,默认二者都有。如需直接开相机或直接选相册,请只使用一个选项
|
||||
sizeType: ['original'], //original 原图,compressed 压缩图,默认二者都有
|
||||
success: (res) => {
|
||||
res.tempFiles.forEach((file) => {
|
||||
if (!this.onBefore || this.onBefore(file)) {
|
||||
// 调用上传图片的接口
|
||||
this.uploadImage(file);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadImage(file) {
|
||||
let action = `/image/upload?isPermanent=${this.isPermanent}&thumbSize=${this.thumbSize}`
|
||||
uni.uploadFile({
|
||||
url: UNI_APP.BASE_URL + action,
|
||||
header: {
|
||||
accessToken: uni.getStorageSync("loginInfo").accessToken
|
||||
},
|
||||
filePath: file.path, // 要上传文件资源的路径
|
||||
name: 'file',
|
||||
success: (res) => {
|
||||
let data = JSON.parse(res.data);
|
||||
if (data.code != 200) {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: data.message,
|
||||
})
|
||||
this.onError && this.onError(file, data);
|
||||
} else {
|
||||
this.onSuccess && this.onSuccess(file, data);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(err);
|
||||
this.onError && this.onError(file, err);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: 'image-upload',
|
||||
data() {
|
||||
return {
|
||||
uploadHeaders: {
|
||||
accessToken: uni.getStorageSync('loginInfo').accessToken,
|
||||
},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 5 * 1024 * 1024,
|
||||
},
|
||||
sourceType: {
|
||||
type: String,
|
||||
default: 'album',
|
||||
},
|
||||
isPermanent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
thumbSize: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
onBefore: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
onError: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectAndUpload() {
|
||||
uni.chooseImage({
|
||||
count: this.maxCount, //最多可以选择的图片张数,默认9
|
||||
sourceType: [this.sourceType], //album 从相册选图,camera 使用相机,默认二者都有。如需直接开相机或直接选相册,请只使用一个选项
|
||||
sizeType: ['original'], //original 原图,compressed 压缩图,默认二者都有
|
||||
success: (res) => {
|
||||
res.tempFiles.forEach((file) => {
|
||||
if (!this.onBefore || this.onBefore(file)) {
|
||||
// 调用上传图片的接口
|
||||
this.uploadImage(file);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
uploadImage(file) {
|
||||
let action = `/image/upload?isPermanent=${this.isPermanent}&thumbSize=${this.thumbSize}`;
|
||||
uni.uploadFile({
|
||||
url: CHAT_ENV.BASE_URL + action,
|
||||
header: {
|
||||
accessToken: uni.getStorageSync('loginInfo').accessToken,
|
||||
},
|
||||
filePath: file.path, // 要上传文件资源的路径
|
||||
name: 'file',
|
||||
success: (res) => {
|
||||
let data = JSON.parse(res.data);
|
||||
if (data.code != 200) {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: data.message,
|
||||
});
|
||||
this.onError && this.onError(file, data);
|
||||
} else {
|
||||
this.onSuccess && this.onSuccess(file, data);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(err);
|
||||
this.onError && this.onError(file, err);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style></style>
|
||||
@@ -1,129 +1,145 @@
|
||||
<template>
|
||||
<view class="long-press-menu none-pointer-events">
|
||||
<view @longpress.prevent.stop="onLongPress($event)" @touchmove="onTouchMove" @touchend="onTouchEnd">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view v-if="isShowMenu" class="menu-mask" @touchstart="onClose()" @contextmenu.prevent=""></view>
|
||||
<view v-if="isShowMenu" class="menu" :style="menuStyle">
|
||||
<view class="menu-item" v-for="(item) in items" :key="item.key" @click.prevent="onSelectMenu(item)">
|
||||
<text :style="itemStyle(item)"> {{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="long-press-menu none-pointer-events">
|
||||
<view
|
||||
@longpress.prevent.stop="onLongPress($event)"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view
|
||||
v-if="isShowMenu"
|
||||
class="menu-mask"
|
||||
@touchstart="onClose()"
|
||||
@contextmenu.prevent=""
|
||||
></view>
|
||||
<view v-if="isShowMenu" class="menu" :style="menuStyle">
|
||||
<view
|
||||
class="menu-item"
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
@click.prevent="onSelectMenu(item)"
|
||||
>
|
||||
<text :style="itemStyle(item)"> {{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "long-press-menu",
|
||||
data() {
|
||||
return {
|
||||
isShowMenu: false,
|
||||
isTouchMove: false,
|
||||
style: ""
|
||||
}
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onLongPress(e) {
|
||||
if (this.isTouchMove) {
|
||||
// 屏幕移动时不弹出
|
||||
return;
|
||||
}
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
let touches = e.touches[0];
|
||||
let style = "";
|
||||
/* 因 非H5端不兼容 style 属性绑定 Object ,所以拼接字符 */
|
||||
if (touches.clientY > (res.windowHeight / 2)) {
|
||||
style = `bottom:${res.windowHeight - touches.clientY + 20}px;`;
|
||||
} else {
|
||||
style = `top:${touches.clientY + 20}px;`;
|
||||
}
|
||||
if (touches.clientX > (res.windowWidth / 2)) {
|
||||
style += `right:${res.windowWidth - touches.clientX}px;`;
|
||||
} else {
|
||||
style += `left:${touches.clientX}px;`;
|
||||
}
|
||||
this.menuStyle = style;
|
||||
//
|
||||
this.$nextTick(() => {
|
||||
this.isShowMenu = true;
|
||||
});
|
||||
this.menuTouch = touches
|
||||
}
|
||||
})
|
||||
},
|
||||
onTouchMove(e) {
|
||||
this.isTouchMove = true;
|
||||
let touches = e.touches[0];
|
||||
// 屏幕拖动大于50px时,取消菜单
|
||||
if (Math.abs(touches.clientX - this.menuTouch.clientX) > 50 ||
|
||||
Math.abs(touches.clientY - this.menuTouch.clientY) > 50) {
|
||||
this.onClose();
|
||||
}
|
||||
},
|
||||
onTouchEnd() {
|
||||
this.isTouchMove = false;
|
||||
},
|
||||
onSelectMenu(item) {
|
||||
this.$emit("select", item);
|
||||
this.isShowMenu = false;
|
||||
},
|
||||
onClose() {
|
||||
this.isShowMenu = false;
|
||||
},
|
||||
itemStyle(item) {
|
||||
if (item.color) {
|
||||
return `color:${item.color};`
|
||||
}
|
||||
return `color:#000;`;
|
||||
}
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: 'long-press-menu',
|
||||
data() {
|
||||
return {
|
||||
isShowMenu: false,
|
||||
isTouchMove: false,
|
||||
style: '',
|
||||
};
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onLongPress(e) {
|
||||
if (this.isTouchMove) {
|
||||
// 屏幕移动时不弹出
|
||||
return;
|
||||
}
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
let touches = e.touches[0];
|
||||
let style = '';
|
||||
/* 因 非H5端不兼容 style 属性绑定 Object ,所以拼接字符 */
|
||||
if (touches.clientY > res.windowHeight / 2) {
|
||||
style = `bottom:${res.windowHeight - touches.clientY + 20}px;`;
|
||||
} else {
|
||||
style = `top:${touches.clientY + 20}px;`;
|
||||
}
|
||||
if (touches.clientX > res.windowWidth / 2) {
|
||||
style += `right:${res.windowWidth - touches.clientX}px;`;
|
||||
} else {
|
||||
style += `left:${touches.clientX}px;`;
|
||||
}
|
||||
this.menuStyle = style;
|
||||
//
|
||||
this.$nextTick(() => {
|
||||
this.isShowMenu = true;
|
||||
});
|
||||
this.menuTouch = touches;
|
||||
},
|
||||
});
|
||||
},
|
||||
onTouchMove(e) {
|
||||
this.isTouchMove = true;
|
||||
let touches = e.touches[0];
|
||||
// 屏幕拖动大于50px时,取消菜单
|
||||
if (
|
||||
Math.abs(touches.clientX || 0 - this.menuTouch.clientX || 0) > 50 ||
|
||||
Math.abs(touches.clientY || 0 - this.menuTouch.clientY || 0) > 50
|
||||
) {
|
||||
this.onClose();
|
||||
}
|
||||
},
|
||||
onTouchEnd() {
|
||||
this.isTouchMove = false;
|
||||
},
|
||||
onSelectMenu(item) {
|
||||
this.$emit('select', item);
|
||||
this.isShowMenu = false;
|
||||
},
|
||||
onClose() {
|
||||
this.isShowMenu = false;
|
||||
},
|
||||
itemStyle(item) {
|
||||
if (item.color) {
|
||||
return `color:${item.color};`;
|
||||
}
|
||||
return `color:#fff;`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.long-press-menu {
|
||||
.menu-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
.long-press-menu {
|
||||
.menu-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
z-index: 1000;
|
||||
box-shadow: $im-box-shadow-dark;
|
||||
.menu {
|
||||
position: fixed;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: $im-bg-active;
|
||||
z-index: 1000;
|
||||
box-shadow: $im-box-shadow-dark;
|
||||
|
||||
.menu-item {
|
||||
height: 28px;
|
||||
min-width: 120rpx;
|
||||
line-height: 28px;
|
||||
font-size: $im-font-size-small;
|
||||
display: flex;
|
||||
padding: 6px 20px;
|
||||
justify-content: flex-start;
|
||||
.menu-item {
|
||||
height: 28px;
|
||||
min-width: 120rpx;
|
||||
line-height: 28px;
|
||||
font-size: $im-font-size-small;
|
||||
display: flex;
|
||||
padding: 6px 20px;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:hover {
|
||||
background: $im-bg-active;
|
||||
}
|
||||
&:hover {
|
||||
background: $im-bg-active-dark;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.menu-icon {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user