This commit is contained in:
2022-12-28 10:08:51 +08:00
parent 0fa93d545e
commit 3881370b6e
151 changed files with 17044 additions and 0 deletions

View File

@@ -0,0 +1,506 @@
<template>
<div>
<el-container class="editor-container">
<el-header class="no-padding toolbar" height="35px">
<ul>
<li v-popover:popoverEmoticon>
<i class="iconfont icon-icon_im_face" style="font-size: 15px" />
<p class="tip-title">表情符号</p>
</li>
<!-- <li @click="codeBlock.isShow = true">
<i class="iconfont icon-daima" />
<p class="tip-title">代码片段</p>
</li>
<li @click="recorder = true">
<i class="el-icon-headset" />
<p class="tip-title">语音消息</p>
</li> -->
<!-- #TODO 发图片功能暂时隐藏 此处会涉及到token过期 -->
<!-- <li @click="$refs.restFile.click()">
<i class="el-icon-picture-outline-round" />
<p class="tip-title">图片</p>
</li> -->
<!-- <li @click="$refs.restFile2.click()">
<i class="el-icon-folder" />
<p class="tip-title">附件</p>
</li>
<li @click="filesManager.isShow = true">
<i class="el-icon-folder-opened" />
<p class="tip-title">上传管理</p>
</li>
<li v-show="isGroupTalk" @click="vote.isShow = true">
<i class="el-icon-s-data" />
<p class="tip-title">发起投票</p>
</li> -->
<!-- <p class="text-tips no-select">-->
<!-- <span>按Enter发送 / Shift+Enter 换行</span>-->
<!-- <el-popover placement="top-end" width="600" trigger="click">-->
<!-- <div class="editor-books">-->
<!-- <div class="books-title">编辑说明:</div>-->
<!-- <p>-->
<!-- 1.-->
<!-- 支持上传QQ及微信截图在QQ或微信中截图后使用Ctrl+v上传图片-->
<!-- </p>-->
<!-- <p>-->
<!-- 2.-->
<!-- 支持浏览器及Word文档中的图片复制上传复制后使用Ctrl+v上传图片-->
<!-- </p>-->
<!-- <p>3. 支持图片拖拽上传</p>-->
<!-- <p>4. 支持文件上传 ( 文件小于100M ) </p>-->
<!-- <p>5. 按Enter发送 / Shift+Enter 换行</p>-->
<!-- <p>-->
<!-- 6.-->
<!-- 注意当文件正在上传时请勿关闭网页或离开当前对话框否则将导致文件停止上传或上传失败-->
<!-- </p>-->
<!-- </div>-->
<!-- <i class="el-icon-info" slot="reference" />-->
<!-- </el-popover>-->
<!-- </p>-->
</ul>
<el-popover
ref="popoverEmoticon"
placement="top-start"
trigger="click"
width="300"
popper-class="no-padding el-popover-em"
>
<MeEditorEmoticon ref="editorEmoticon" @selected="selecteEmoticon" />
</el-popover>
<form
enctype="multipart/form-data"
style="display: none"
ref="fileFrom"
>
<input
type="file"
ref="restFile"
accept="image/*"
@change="uploadImageChange"
/>
<input type="file" ref="restFile2" @change="uploadFileChange" />
</form>
</el-header>
<el-main class="no-padding textarea">
<textarea
ref="textarea"
v-paste="pasteImage"
v-drag="dragPasteImage"
v-model.trim="editorText"
rows="6"
placeholder="你想要的聊点什么呢 ..."
@keydown="keydownEvent($event)"
@input="inputEvent($event)"
/>
</el-main>
</el-container>
<!-- 图片查看器 -->
<MeEditorImageView
ref="imageViewer"
v-model="imageViewer.isShow"
:file="imageViewer.file"
@confirm="confirmUploadImage"
/>
<MeEditorRecorder v-if="recorder" @close="recorder = false" />
<!-- 代码块编辑器 -->
<TalkCodeBlock
v-if="codeBlock.isShow"
:edit-mode="codeBlock.editMode"
@close="codeBlock.isShow = false"
@confirm="confirmCodeBlock"
/>
<!-- 文件上传管理器 -->
<MeEditorFileManage ref="filesManager" v-model="filesManager.isShow" />
<MeEditorVote
v-if="vote.isShow"
@close="
() => {
this.vote.isShow = false;
}
"
/>
</div>
</template>
<script>
import MeEditorEmoticon from "./MeEditorEmoticon";
import MeEditorFileManage from "./MeEditorFileManage";
import MeEditorImageView from "./MeEditorImageView";
import MeEditorRecorder from "./MeEditorRecorder";
import MeEditorVote from "./MeEditorVote";
import TalkCodeBlock from "@/components/chat/TalkCodeBlock";
import { getPasteImgs, getDragPasteImg } from "@/utils/editor";
import { findTalk } from "@/utils/talk";
import {
ServeSendTalkCodeBlock,
ServeSendTalkImage,
ServeSendEmoticon,
} from "@/api/chat";
export default {
name: "MeEditor",
components: {
MeEditorEmoticon,
MeEditorFileManage,
MeEditorImageView,
TalkCodeBlock,
MeEditorRecorder,
MeEditorVote,
},
computed: {
talkUser() {
return this.$store.state.dialogue.index_name;
},
isGroupTalk() {
return this.$store.state.dialogue.talk_type == 2;
},
},
watch: {
talkUser(n_index_name) {
this.$refs.filesManager.clear();
this.editorText = this.getDraftText(n_index_name);
},
},
data() {
return {
// 当前编辑的内容
editorText: "",
// 图片查看器相关信息
imageViewer: {
isShow: false,
file: null,
},
codeBlock: {
isShow: false,
editMode: true,
},
filesManager: {
isShow: false,
},
vote: {
isShow: false,
},
// 录音器
recorder: false,
// 上次发送消息的时间
sendtime: 0,
// 发送间隔时间默认1秒
interval: 1000,
};
},
methods: {
// 读取对话编辑草稿信息 并赋值给当前富文本
getDraftText(index_name) {
console.log("findTalk(index_name)", findTalk(index_name));
return findTalk(index_name)?.draft_text || "";
},
//复制粘贴图片回调方法
pasteImage(e) {
let files = getPasteImgs(e);
if (files.length == 0) return;
this.openImageViewer(files[0]);
},
//拖拽上传图片回调方法
dragPasteImage(e) {
let files = getDragPasteImg(e);
if (files.length == 0) return;
this.openImageViewer(files[0]);
},
inputEvent(e) {
this.$emit("keyboard-event", e.target.value);
},
// 键盘按下监听事件
keydownEvent(e) {
if (e.keyCode == 13 && this.editorText == "") {
e.preventDefault();
}
// 回车发送消息
if (e.keyCode == 13 && e.shiftKey == false && this.editorText != "") {
let currentTime = new Date().getTime();
if (this.sendtime > 0) {
// 判断 1秒内只能发送一条消息
if (currentTime - this.sendtime < this.interval) {
e.preventDefault();
return false;
}
}
this.$emit("send", this.editorText);
this.editorText = "";
this.sendtime = currentTime;
e.preventDefault();
}
},
// 选择图片文件后回调方法
uploadImageChange(e) {
this.openImageViewer(e.target.files[0]);
this.$refs.restFile.value = null;
},
// 选择文件回调事件
uploadFileChange(e) {
let maxsize = 100 * 1024 * 1024;
if (e.target.files.length == 0) {
return false;
}
let file = e.target.files[0];
if (/\.(gif|jpg|jpeg|png|webp|GIF|JPG|PNG|WEBP)$/.test(file.name)) {
this.openImageViewer(file);
return;
}
if (file.size > maxsize) {
this.$notify.info({
title: "消息",
message: "上传文件不能大于100M",
});
return;
}
this.filesManager.isShow = true;
this.$refs.restFile2.value = null;
this.$refs.filesManager.upload(file);
},
// 打开图片查看器
openImageViewer(file) {
this.imageViewer.isShow = true;
this.imageViewer.file = file;
},
// 代码块编辑器确认完成回调事件
confirmCodeBlock(data) {
const { talk_type, receiver_id } = this.$store.state.dialogue;
ServeSendTalkCodeBlock({
talk_type,
receiver_id,
code: data.code,
lang: data.language,
}).then((res) => {
if (res.code == 200) {
this.codeBlock.isShow = false;
} else {
this.$notify({
title: "友情提示",
message: res.message,
type: "warning",
});
}
});
},
// 确认上传图片消息回调事件
confirmUploadImage() {
let fileData = new FormData();
fileData.append("file", this.imageViewer.file);
let ref = this.$refs.imageViewer;
ServeSendTalkImage(fileData)
.then((res) => {
ref.loading = false;
if (res.code == 200) {
ref.closeBox();
} else {
this.$notify({
title: "友情提示",
message: res.message,
type: "warning",
});
}
})
.finally(() => {
ref.loading = false;
});
},
// 选中表情包回调事件
selecteEmoticon(data) {
if (data.type == 1) {
let value = this.editorText;
let el = this.$refs.textarea;
let startPos = el.selectionStart;
let endPos = el.selectionEnd;
let newValue =
value.substring(0, startPos) +
data.value +
value.substring(endPos, value.length);
this.editorText = newValue;
if (el.setSelectionRange) {
setTimeout(() => {
let index = startPos + data.value.length;
el.setSelectionRange(index, index);
el.focus();
}, 0);
}
} else {
const { talk_type, receiver_id } = this.$store.state.dialogue;
ServeSendEmoticon({
talk_type,
receiver_id,
emoticon_id: data.value,
});
}
this.$refs.popoverEmoticon.doClose();
},
},
};
</script>
<style scoped lang="less">
.editor-container {
height: 160px;
width: 100%;
background-color: white;
}
.editor-container .toolbar {
line-height: 35px;
border-bottom: 1px solid #f5f0f0;
border-top: 1px solid #f5f0f0;
}
.editor-container .toolbar li {
list-style: none;
float: left;
width: 35px;
margin-left: 3px;
cursor: pointer;
text-align: center;
line-height: 35px;
position: relative;
color: #8d8d8d;
}
.editor-container .toolbar li .tip-title {
display: none;
position: absolute;
top: 38px;
left: 0px;
height: 26px;
line-height: 26px;
background-color: rgba(31, 35, 41, 0.9);
color: white;
min-width: 30px;
font-size: 10px;
padding: 0 5px;
border-radius: 2px;
white-space: pre;
text-align: center;
user-select: none;
z-index: 1;
}
.editor-container .toolbar li:hover .tip-title {
display: block;
}
.editor-container .toolbar li:hover {
background-color: #f7f5f5;
}
.editor-container .toolbar .text-tips {
float: right;
margin-right: 15px;
font-size: 12px;
color: #ccc;
}
.editor-container .toolbar .text-tips i {
font-size: 14px;
cursor: pointer;
margin-left: 5px;
color: rgb(255, 181, 111);
}
.editor-container .textarea {
overflow: hidden;
position: relative;
}
textarea {
width: calc(100% - 10px);
width: -moz-calc(100% - 10px);
width: -webkit-calc(100% - 10px);
height: calc(100% - 10px);
height: -moz-calc(100% - 10px);
height: -webkit-calc(100% - 10px);
border: 0 none;
outline: none;
resize: none;
font-size: 15px;
overflow-y: auto;
color: #464545;
padding: 5px;
position: relative;
}
textarea::-webkit-scrollbar {
width: 4px;
height: 1px;
}
textarea::-webkit-scrollbar-thumb {
background: #d5cfcf;
}
textarea::-webkit-scrollbar-track {
background: #ededed;
}
textarea::-webkit-input-placeholder {
color: #dccdcd;
font-size: 12px;
font-weight: 400;
}
/* 编辑器文档说明 --- start */
.editor-books .books-title {
font-size: 16px;
height: 30px;
line-height: 22px;
margin-top: 10px;
margin-bottom: 10px;
border-bottom: 1px solid #cbcbcb;
color: #726f6f;
font-weight: 400;
margin-left: 11px;
}
.editor-books p {
text-indent: 10px;
font-size: 12px;
height: 30px;
line-height: 30px;
color: #7f7c7c;
}
/* 编辑器文档说明 --- end */
</style>

View File

@@ -0,0 +1,345 @@
<template>
<div>
<el-container class="container">
<el-main class="no-padding main lum-scrollbar">
<input
type="file"
ref="fileCustomEmoji"
accept="image/*"
style="display: none"
@change="customUploadEmoji"
/>
<div v-show="showEmoticonId == -1" class="emoticon">
<div class="title">QQ表情</div>
<div
v-for="(elImg, text) in emoji.emojis"
v-html="elImg"
:key="text"
class="emoticon-item"
@click="clickEmoticon(text)"
></div>
<div class="clear"></div>
<div class="title">符号表情</div>
<div
v-for="(item, i) in emoji.symbol"
:key="i"
class="emoticon-item symbol"
@click="clickEmoticon(item)"
>
{{ item }}
</div>
<div class="clear"></div>
</div>
<div
v-for="item in emojiItem.slice(1)"
v-show="item.emoticon_id == showEmoticonId"
:key="item.emoticon_id"
class="emoji-box"
>
<div
v-if="item.emoticon_id == 0"
class="emoji-item custom-emoji"
@click="$refs.fileCustomEmoji.click()"
>
<i class="el-icon-picture" />
<span>自定义</span>
</div>
<div
v-for="subitem in item.list"
:key="subitem.src"
class="emoji-item"
@click="clickImageEmoticon(subitem)"
>
<el-image :src="subitem.src" fit="cover" />
</div>
<div class="clear"></div>
</div>
</el-main>
<!-- <el-footer height="40px" class="no-padding footer">
<div class="toolbar-items">
<div
v-show="emojiItem.length > 13"
class="toolbar-item prev-page"
@click="turnPage(1)"
>
<i class="el-icon-caret-left" />
</div>
<div
v-for="(item, index) in showItems"
:key="index"
class="toolbar-item"
@click="triggerItem(item)"
>
<img :src="item.url" />
<p class="title">{{ item.name }}</p>
</div>
<div
v-show="emojiItem.length > 13 && showItems.length == 13"
class="toolbar-item next-page"
@click="turnPage(2)"
>
<i class="el-icon-caret-right" />
</div>
</div>
</el-footer> -->
</el-container>
<MeEditorSystemEmoticon
v-if="systemEmojiBox"
@close="systemEmojiBox = false"
/>
</div>
</template>
<script>
import MeEditorSystemEmoticon from "@/components/editor/MeEditorSystemEmoticon";
import { emojiList as emoji } from "@/utils/emojis";
import { mapState } from "vuex";
export default {
name: "MeEditorEmoticon",
components: {
MeEditorSystemEmoticon,
},
computed: {
...mapState({
emojiItem: (state) => state.emoticon.items,
}),
showItems() {
let start = (this.page - 1) * this.pageSize;
let end = start + this.pageSize;
return this.emojiItem.slice(start, end);
},
pageTotal() {
return this.emojiItem.length / this.pageSize;
},
},
data() {
return {
emoji,
// 系统表情包套弹出窗
systemEmojiBox: false,
showEmoticonId: -1,
showTitle: "QQ表情/符号表情",
page: 1,
pageSize: 13,
};
},
created() {
this.$store.commit("LOAD_USER_EMOTICON");
},
methods: {
// 表情包导航翻页
turnPage(type) {
if (type == 1) {
if (this.page == 1) return false;
this.page--;
} else {
if (this.page >= this.pageTotal) return false;
this.page++;
}
},
// 点击表情包导航
triggerItem(item) {
this.showEmoticonId = item.emoticon_id;
this.showTitle = item.name;
},
// 选中表情
clickEmoticon(emoji) {
this.callback({
type: 1,
value: emoji,
});
},
// 发送图片表情包
clickImageEmoticon(item) {
this.callback({
type: 2,
value: item.media_id,
});
},
callback(data) {
this.$emit("selected", data);
},
// 自定义上传表情
customUploadEmoji(e) {
if (e.target.files.length == 0) {
return false;
}
this.$store.commit("UPLOAD_USER_EMOTICON", {
file: e.target.files[0],
});
},
},
};
</script>
<style lang="less" scoped>
.container {
height: 300px;
max-width: 500px;
background-color: white;
.header {
line-height: 30px;
font-size: 13px;
font-weight: 400;
padding-left: 5px;
user-select: none;
position: relative;
border-bottom: 1px solid #fbf5f5;
.addbtn {
position: absolute;
right: 10px;
top: 1px;
color: #409eff;
cursor: pointer;
}
}
.footer {
background-color: #eff1f7;
.toolbar-items {
width: 100%;
height: 40px;
line-height: 40px;
display: flex;
flex-direction: row;
align-items: center;
.toolbar-item {
height: 30px;
width: 30px;
margin: 0 2px;
background-color: #fff;
display: inline-block;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
position: relative;
img {
width: 20px;
height: 20px;
}
.title {
display: none;
position: absolute;
top: -25px;
left: 0px;
height: 20px;
line-height: 20px;
background: #353434;
color: white;
min-width: 30px;
font-size: 10px;
padding-left: 5px;
padding-right: 5px;
border-radius: 2px;
white-space: pre;
text-align: center;
}
&:hover .title {
display: block;
}
}
}
}
}
.container .footer .toolbar-items .prev-page:active i,
.container .footer .toolbar-items .next-page:active i {
transform: scale(1.2);
}
.emoji-box,
.emoticon {
width: 100%;
}
.emoticon {
.title {
width: 50%;
height: 25px;
line-height: 25px;
color: #ccc;
font-weight: 400;
padding-left: 3px;
font-size: 12px;
}
.emoticon-item {
width: 30px;
height: 30px;
margin: 2px;
float: left;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
&:hover {
transform: scale(1.3);
}
}
.symbol {
font-size: 22px;
}
}
.emoji-box {
.emoji-item {
width: 67px;
height: 67px;
margin: 2px;
background-color: #eff1f7;
float: left;
cursor: pointer;
transition: ease-in 0.3s;
}
.custom-emoji {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: 10px;
i {
font-size: 30px;
margin-bottom: 3px;
}
&:active {
color: #409eff;
}
}
}
/deep/ .el-image {
width: 100%;
height: 100%;
transition: ease-in 0.3s;
}
.emoji-box .emoji-item:hover .el-image,
.emoji-box .emoji-item:hover {
border-radius: 10px;
}
</style>

View File

@@ -0,0 +1,440 @@
<template>
<el-container
class="container animated bounceInUp"
v-outside="closeBox"
v-if="show"
>
<el-header class="no-padding header" height="50px">
<p>
上传管理 <span v-show="total">({{ successNum }}/{{ total }})</span>
</p>
<i class="close-btn el-icon-close" @click="closeBox" />
</el-header>
<el-main class="no-padding mian lum-scrollbar">
<div class="empty-data" v-show="total == 0">
<SvgNotData />
<p>暂无上传文件</p>
</div>
<div
v-for="file in items"
v-show="!file.isDelete"
:key="file.hashName"
class="file-item"
>
<div class="file-header">
<div class="type-icon">{{ file.ext }}</div>
<el-tooltip :content="file.filename" placement="top-start">
<div class="filename">{{ file.filename }}</div>
</el-tooltip>
<div class="status">
<span v-if="file.status == 0">等待上传</span>
<span v-else-if="file.status == 1" style="color: #66b1ff">
正在上传...
</span>
<span v-else-if="file.status == 2" style="color: #67c23a">
已完成
</span>
<span v-else style="color: red">网络异常</span>
</div>
</div>
<div class="file-mian">
<div class="progress">
<el-progress
type="dashboard"
:percentage="file.progress"
:width="50"
:color="colors"
/>
<span class="name">上传进度</span>
</div>
<div class="detail">
<p>
文件类型<span>{{ file.filetype }}</span>
</p>
<p>
文件大小<span>{{ file.filesize }}</span>
</p>
<p>
上传时间<span>{{ file.datetime }}</span>
</p>
</div>
</div>
<div v-show="file.status == 2 || file.status == 3" class="file-means">
<div class="btns" @click="removeFile(file.hashName)">删除</div>
<div
v-show="file.status == 3"
class="btns"
@click="triggerUpload(file.hashName)"
>
继续上传
</div>
</div>
</div>
</el-main>
</el-container>
</template>
<script>
import Vue from 'vue'
import { SvgNotData } from '@/core/icons'
import { Progress } from 'element-ui'
Vue.use(Progress)
import { ServeFindFileSplitInfo, ServeFileSubareaUpload } from '@/api/upload'
import { formatSize, getFileExt, parseTime } from '@/utils/functions'
import { ServeSendTalkFile } from '@/api/chat'
export default {
name: 'MeEditorFileManage',
model: {
prop: 'show',
event: 'close',
},
props: {
show: Boolean,
},
components: {
SvgNotData,
},
data() {
return {
colors: [
{
color: '#f56c6c',
percentage: 20,
},
{
color: '#e6a23c',
percentage: 40,
},
{
color: '#5cb87a',
percentage: 60,
},
{
color: '#1989fa',
percentage: 80,
},
{
color: '#11ce65',
percentage: 100,
},
],
items: [],
}
},
computed: {
total() {
return this.items.filter(item => {
return item.isDelete === false
}).length
},
successNum() {
return this.items.filter(item => {
return item.isDelete === false && item.status == 2
}).length
},
},
methods: {
closeBox() {
this.$emit('close', false)
},
upload(file) {
ServeFindFileSplitInfo({
file_name: file.name,
file_size: file.size,
}).then(res => {
if (res.code == 200) {
const { hash_name, split_size } = res.data
this.items.unshift({
hashName: hash_name,
originalFile: file,
filename: file.name,
status: 0, // 文件上传状态 0:等待上传 1:上传中 2:上传完成 3:网络异常
progress: 0,
filesize: formatSize(file.size),
filetype: file.type || '未知',
datetime: parseTime(new Date(), '{m}-{d} {h}:{i}'),
ext: getFileExt(file.name),
forms: this.fileSlice(file, hash_name, split_size),
successNum: 0,
isDelete: false,
})
this.triggerUpload(hash_name)
}
})
},
// 处理拆分上传文件
fileSlice(file, hash, eachSize) {
const ext = getFileExt(file.name)
const splitNum = Math.ceil(file.size / eachSize) // 分片总数
const forms = []
// 处理每个分片的上传操作
for (let i = 0; i < splitNum; i++) {
let start = i * eachSize
let end = Math.min(file.size, start + eachSize)
// 构建表单
const form = new FormData()
form.append('file', file.slice(start, end))
form.append('name', file.name)
form.append('hash', hash)
form.append('ext', ext)
form.append('size', file.size)
form.append('split_index', i)
form.append('split_num', splitNum)
forms.push(form)
}
return forms
},
// 触发上传文件
triggerUpload(hashName) {
let $index = this.getFileIndex(hashName)
if ($index < 0 || this.items[$index].isDelte) {
return
}
let i = this.items[$index].successNum
let form = this.items[$index].forms[i]
let length = this.items[$index].forms.length
this.items[$index].status = 1
ServeFileSubareaUpload(form)
.then(res => {
if (res.code == 200) {
$index = this.getFileIndex(hashName)
this.items[$index].successNum++
this.items[$index].progress = Math.floor(
(this.items[$index].successNum / length) * 100
)
if (this.items[$index].successNum == length) {
this.items[$index].status = 2
if (res.data.is_file_merge) {
ServeSendTalkFile({
hash_name: res.data.hash,
receiver_id: this.$store.state.dialogue.receiver_id,
talk_type: this.$store.state.dialogue.talk_type,
})
}
} else {
this.triggerUpload(hashName)
}
} else {
this.items[$index].status = 3
}
})
.catch(() => {
$index = this.getFileIndex(hashName)
this.items[$index].status = 3
})
},
// 获取分片文件数组索引
getFileIndex(hashName) {
return this.items.findIndex(item => {
return item.hashName === hashName
})
},
removeFile(hashName) {
let index = this.getFileIndex(hashName)
this.items[index].isDelete = true
},
clear() {
this.items = []
},
},
}
</script>
<style lang="less" scoped>
.container {
position: fixed;
right: 0;
bottom: 0;
width: 400px;
height: 600px;
background-color: white;
box-shadow: 0 0 5px #eae5e5;
border: 1px solid #eae5e5;
overflow: hidden;
border-radius: 3px 3px 0 0;
.header {
height: 50px;
line-height: 50px;
position: relative;
text-indent: 20px;
border-bottom: 1px solid #f5eeee;
i {
position: absolute;
right: 20px;
top: 15px;
font-size: 20px;
cursor: pointer;
}
}
.mian {
.empty-data {
width: 100%;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 50%;
svg {
font-size: 70px;
}
p {
margin-top: 30px;
color: #cccccc;
font-size: 10px;
}
}
}
}
.file-item {
width: 95%;
min-height: 100px;
background-color: white;
display: flex;
flex-direction: column;
border-radius: 5px;
margin: 15px auto;
box-shadow: 0 0 5px #eae5e5;
overflow: hidden;
.file-header {
height: 45px;
display: flex;
flex-direction: row;
align-items: center;
position: relative;
border-bottom: 1px solid #f7f4f4;
.type-icon {
height: 30px;
width: 30px;
background-color: #66b1ff;
border-radius: 50%;
margin-left: 5px;
font-size: 10px;
font-weight: 200;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
color: white;
}
.filename {
margin-left: 10px;
font-size: 14px;
width: 65%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status {
position: absolute;
right: 14px;
top: 12px;
font-size: 13px;
color: #6b6868;
font-weight: 200;
}
}
.file-mian {
padding: 8px;
display: flex;
flex-direction: row;
.progress {
width: 80px;
height: 80px;
flex-shrink: 0;
background: #f9f6f6;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: pointer;
.name {
font-size: 12px;
color: #ada8a8;
font-weight: 300;
}
}
.detail {
flex: auto;
flex-shrink: 0;
display: flex;
flex-direction: column;
padding-left: 20px;
justify-content: center;
align-items: flex-start;
font-size: 13px;
p {
margin: 3px;
color: #ada8a8;
span {
color: #595a5a;
font-weight: 500;
}
}
}
}
.file-means {
width: 96.5%;
height: 35px;
border-top: 1px dashed rgb(234, 227, 227);
margin: 3px auto;
padding-top: 5px;
display: flex;
justify-content: flex-end;
align-items: center;
.btns {
width: 80px;
height: 25px;
border: 1px solid #e6e1e1;
display: flex;
justify-content: center;
align-items: center;
margin: 3px;
border-radius: 15px;
font-size: 12px;
color: #635f5f;
cursor: pointer;
&:active {
box-shadow: 0 0 5px #eae5e5;
font-size: 13px;
}
}
}
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<div v-if="show" class="lum-dialog-mask animated fadeIn">
<el-container class="lum-dialog-box" v-outside="closeBox">
<el-header class="no-padding header" height="50px">
<p>发送图片</p>
<p class="tools">
<i class="el-icon-close" @click="closeBox" />
</p>
</el-header>
<el-main class="no-padding mian">
<img v-show="src" :src="src" />
<div v-show="src">
<span class="filename">{{ fileName }}</span>
<br />
<span class="size">{{ fileSize }} KB</span>
</div>
</el-main>
<el-footer class="footer" height="50px">
<el-button
class="btn"
type="primary"
size="medium"
:loading="loading"
@click="uploadImage"
>立即发送
</el-button>
</el-footer>
</el-container>
</div>
</template>
<script>
export default {
name: 'MeEditorImageView',
model: {
prop: 'show',
event: 'close',
},
props: {
show: Boolean,
file: File,
},
watch: {
file(file) {
this.loadFile(file)
},
},
data() {
return {
src: '',
fileSize: '',
fileName: '',
loading: false,
}
},
methods: {
closeBox() {
if (this.loading) {
return false
}
this.$emit('close', false)
},
loadFile(file) {
let reader = new FileReader()
this.fileSize = Math.ceil(file.size / 1024)
this.fileName = file.name
reader.onload = () => {
this.src = reader.result
}
reader.readAsDataURL(file)
},
// 确认按钮事件
uploadImage() {
this.loading = true
this.$emit('confirm')
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 500px;
max-width: 500px;
height: 450px;
.mian {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
img {
max-width: 80%;
max-height: 80%;
border-radius: 3px;
cursor: pointer;
box-shadow: 0 0 8px #e0dbdb;
}
div {
margin-top: 10px;
text-align: center;
overflow: hidden;
max-width: 80%;
.filename {
font-weight: 400;
}
.size {
color: rgb(148, 140, 140);
font-size: 12px;
}
}
}
.footer {
height: 50px;
background: rgba(247, 245, 245, 0.66);
text-align: center;
line-height: 50px;
.btn {
width: 150px;
border-radius: 2px;
}
}
}
</style>

View File

@@ -0,0 +1,517 @@
<template>
<div class="lum-dialog-mask animated fadeIn">
<el-container class="lum-dialog-box">
<el-header class="no-padding header no-select" height="50px">
<p>语音消息</p>
<p class="tools"><i class="el-icon-close" @click="closeBox" /></p>
</el-header>
<el-main class="no-padding mian">
<div class="music">
<span class="line line1" :class="{ 'line-ani': animation }"></span>
<span class="line line2" :class="{ 'line-ani': animation }"></span>
<span class="line line3" :class="{ 'line-ani': animation }"></span>
<span class="line line4" :class="{ 'line-ani': animation }"></span>
<span class="line line5" :class="{ 'line-ani': animation }"></span>
</div>
<div style="margin-top: 35px; color: #676262; font-weight: 300">
<template v-if="recorderStatus == 0">
<p style="font-size: 13px; margin-top: 5px">
<span>语音消息让聊天更简单方便 ...</span>
</p>
</template>
<template
v-else-if="
recorderStatus == 1 || recorderStatus == 2 || recorderStatus == 3
"
>
<p>{{ datetime }}</p>
<p style="font-size: 13px; margin-top: 5px">
<span v-if="recorderStatus == 1">正在录音</span>
<span v-else-if="recorderStatus == 2">已暂停录音</span>
<span v-else-if="recorderStatus == 3">录音时长</span>
</p>
</template>
<template
v-else-if="
recorderStatus == 4 || recorderStatus == 5 || recorderStatus == 6
"
>
<p>{{ formatPlayTime }}</p>
<p style="font-size: 13px; margin-top: 5px">
<span v-if="recorderStatus == 4">正在播放</span>
<span v-else-if="recorderStatus == 5">已暂停播放</span>
<span v-else-if="recorderStatus == 6">播放已结束</span>
</p>
</template>
</div>
</el-main>
<el-footer class="footer" height="50px">
<!-- 0:未开始录音 1:正在录音 2:暂停录音 3:结束录音 4:播放录音 5:停止播放 -->
<el-button
v-show="recorderStatus == 0"
type="primary"
size="mini"
round
icon="el-icon-microphone"
@click="startRecorder"
>开始录音
</el-button>
<el-button
v-show="recorderStatus == 1"
type="primary"
size="mini"
round
icon="el-icon-video-pause"
@click="pauseRecorder"
>暂停录音
</el-button>
<el-button
v-show="recorderStatus == 2"
type="primary"
size="mini"
round
icon="el-icon-microphone"
@click="resumeRecorder"
>继续录音
</el-button>
<el-button
v-show="recorderStatus == 2"
type="primary"
size="mini"
round
icon="el-icon-microphone"
@click="stopRecorder"
>结束录音
</el-button>
<el-button
v-show="recorderStatus == 3 || recorderStatus == 6"
type="primary"
size="mini"
round
icon="el-icon-video-play"
@click="playRecorder"
>播放录音
</el-button>
<el-button
v-show="
recorderStatus == 3 || recorderStatus == 5 || recorderStatus == 6
"
type="primary"
size="mini"
round
icon="el-icon-video-play"
@click="startRecorder"
>重新录音
</el-button>
<el-button
v-show="recorderStatus == 4"
type="primary"
size="mini"
round
icon="el-icon-video-pause"
@click="pausePlayRecorder"
>暂停播放
</el-button>
<el-button
v-show="recorderStatus == 5"
type="primary"
size="mini"
round
icon="el-icon-video-play"
@click="resumePlayRecorder"
>继续播放
</el-button>
<el-button
v-show="
recorderStatus == 3 || recorderStatus == 5 || recorderStatus == 6
"
type="primary"
size="mini"
round
@click="submit"
>立即发送
</el-button>
</el-footer>
</el-container>
</div>
</template>
<script>
import Recorder from 'js-audio-recorder'
export default {
name: 'MeEditorRecorder',
data() {
return {
// 录音实例
recorder: null,
// 录音时长
duration: 0,
// 播放时长
playTime: 0,
animation: false,
// 当前状态
recorderStatus: 0, //0:未开始录音 1:正在录音 2:暂停录音 3:结束录音 4:播放录音 5:停止播放 6:播放结束
playTimeout: null,
}
},
computed: {
datetime() {
let hour = parseInt((this.duration / 60 / 60) % 24) //小时
let minute = parseInt((this.duration / 60) % 60) //分钟
let seconds = parseInt(this.duration % 60) //秒
if (hour < 10) hour = `0${hour}`
if (minute < 10) minute = `0${minute}`
if (seconds < 10) seconds = `0${seconds}`
return `${hour}:${minute}:${seconds}`
},
formatPlayTime() {
let hour = parseInt((this.playTime / 60 / 60) % 24) //小时
let minute = parseInt((this.playTime / 60) % 60) //分钟
let seconds = parseInt(this.playTime % 60) //秒
if (hour < 10) hour = `0${hour}`
if (minute < 10) minute = `0${minute}`
if (seconds < 10) seconds = `0${seconds}`
return `${hour}:${minute}:${seconds}`
},
},
destroyed() {
if (this.recorder) {
this.destroyRecorder()
}
},
methods: {
closeBox() {
if (this.recorder == null) {
this.$emit('close', false)
return
}
if (this.recorderStatus == 1) {
this.stopRecorder()
} else if (this.recorderStatus == 4) {
this.pausePlayRecorder()
}
// 销毁录音后关闭窗口
this.destroyRecorder(() => {
this.$emit('close', false)
})
},
// 开始录音
startRecorder() {
let _this = this
// http://recorder.api.zhuyuntao.cn/Recorder/event.html
// https://blog.csdn.net/qq_41619796/article/details/107865602
this.recorder = new Recorder()
this.recorder.onprocess = duration => {
duration = parseInt(duration)
_this.duration = duration
}
this.recorder.start().then(
() => {
this.recorderStatus = 1
this.animation = true
},
error => {
console.log(`${error.name} : ${error.message}`)
}
)
},
// 暂停录音
pauseRecorder() {
this.recorder.pause()
this.recorderStatus = 2
this.animation = false
},
// 继续录音
resumeRecorder() {
this.recorderStatus = 1
this.recorder.resume()
this.animation = true
},
// 结束录音
stopRecorder() {
this.recorderStatus = 3
this.recorder.stop()
this.animation = false
},
// 录音播放
playRecorder() {
this.recorderStatus = 4
this.recorder.play()
this.playTimeouts()
this.animation = true
},
// 暂停录音播放
pausePlayRecorder() {
this.recorderStatus = 5
this.recorder.pausePlay()
clearInterval(this.playTimeout)
this.animation = false
},
// 恢复录音播放
resumePlayRecorder() {
this.recorderStatus = 4
this.recorder.resumePlay()
this.playTimeouts()
this.animation = true
},
// 销毁录音
destroyRecorder(callBack) {
this.recorder.destroy().then(() => {
this.recorder = null
if (callBack) {
callBack()
}
})
},
// 获取录音文件大小(单位:字节)
recorderSize() {
return this.recorder.fileSize
},
playTimeouts() {
this.playTimeout = setInterval(() => {
let time = parseInt(this.recorder.getPlayTime())
this.playTime = time
if (time == this.duration) {
clearInterval(this.playTimeout)
this.animation = false
this.recorderStatus = 6
}
}, 100)
},
submit() {
alert('功能研发中,敬请期待...')
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 500px;
max-width: 500px;
height: 450px;
.mian {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.footer {
height: 50px;
text-align: center;
line-height: 50px;
border-top: 1px solid #f7f3f3;
}
}
.music {
position: relative;
width: 180px;
height: 160px;
border: 8px solid #bebebe;
border-bottom: 0px;
border-top-left-radius: 110px;
border-top-right-radius: 110px;
}
.music:before,
.music:after {
content: '';
position: absolute;
bottom: -20px;
width: 40px;
height: 82px;
background-color: #bebebe;
border-radius: 15px;
}
.music:before {
right: -25px;
}
.music:after {
left: -25px;
}
.line {
position: absolute;
width: 6px;
min-height: 30px;
transition: 0.5s;
vertical-align: middle;
bottom: 0 !important;
box-shadow: inset 0px 0px 16px -2px rgba(0, 0, 0, 0.15);
}
.line-ani {
animation: equalize 4s 0s infinite;
animation-timing-function: linear;
}
.line1 {
left: 30%;
bottom: 0px;
animation-delay: -1.9s;
background-color: #ff5e50;
}
.line2 {
left: 40%;
height: 60px;
bottom: -15px;
animation-delay: -2.9s;
background-color: #a64de6;
}
.line3 {
left: 50%;
height: 30px;
bottom: -1.5px;
animation-delay: -3.9s;
background-color: #5968dc;
}
.line4 {
left: 60%;
height: 65px;
bottom: -16px;
animation-delay: -4.9s;
background-color: #27c8f8;
}
.line5 {
left: 70%;
height: 60px;
bottom: -12px;
animation-delay: -5.9s;
background-color: #cc60b5;
}
@keyframes equalize {
0% {
height: 48px;
}
4% {
height: 42px;
}
8% {
height: 40px;
}
12% {
height: 30px;
}
16% {
height: 20px;
}
20% {
height: 30px;
}
24% {
height: 40px;
}
28% {
height: 10px;
}
32% {
height: 40px;
}
36% {
height: 48px;
}
40% {
height: 20px;
}
44% {
height: 40px;
}
48% {
height: 48px;
}
52% {
height: 30px;
}
56% {
height: 10px;
}
60% {
height: 30px;
}
64% {
height: 48px;
}
68% {
height: 30px;
}
72% {
height: 48px;
}
76% {
height: 20px;
}
80% {
height: 48px;
}
84% {
height: 38px;
}
88% {
height: 48px;
}
92% {
height: 20px;
}
96% {
height: 48px;
}
100% {
height: 48px;
}
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<div class="lum-dialog-mask">
<el-container class="lum-dialog-box" v-outside="closeBox">
<el-header class="no-padding header" height="50px">
<p>系统表情</p>
<p class="tools">
<i class="el-icon-close" @click="closeBox" />
</p>
</el-header>
<el-main class="no-padding mian lum-scrollbar">
<ul>
<li v-for="(item, i) in items" :key="item.id" class="no-select">
<div class="pkg-avatar">
<el-image :src="item.icon" fit="cover" :lazy="true" />
</div>
<div class="pkg-info" v-text="item.name"></div>
<div class="pkg-status">
<button
:class="{
'add-emoji': item.status == 0,
'remove-emoji': item.status != 0,
}"
@click="setEmoticon(i, item, item.status == 0 ? 1 : 2)"
>
{{ item.status == 0 ? '添加' : '移除' }}
</button>
</div>
</li>
</ul>
</el-main>
<el-footer class="footer" height="50px">
<el-button type="primary" size="medium" class="btn" @click="closeBox">
关闭窗口
</el-button>
</el-footer>
</el-container>
</div>
</template>
<script>
import { ServeFindSysEmoticon, ServeSetUserEmoticon } from '@/api/emoticon'
export default {
name: 'MeEditorSystemEmoticon',
data() {
return {
items: [],
}
},
created() {
this.loadSysEmoticon()
},
methods: {
closeBox() {
this.$emit('close')
},
// 获取系统表情包列表
loadSysEmoticon() {
ServeFindSysEmoticon().then(res => {
if (res.code == 200) {
this.items = res.data
}
})
},
setEmoticon(index, item, type) {
ServeSetUserEmoticon({
emoticon_id: item.id,
type: type,
}).then(res => {
if (res.code == 200) {
if (type == 1) {
this.items[index].status = 1
this.$store.commit('APPEND_SYS_EMOTICON', res.data)
} else {
this.items[index].status = 0
this.$store.commit('REMOVE_SYS_EMOTICON', {
emoticon_id: item.id,
})
}
}
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 350px;
max-width: 350px;
height: 500px;
.mian {
height: 480px;
overflow-y: auto;
li {
display: flex;
cursor: pointer;
height: 68px;
align-items: center;
border-bottom: 3px solid #fbf2fb;
padding-left: 5px;
.pkg-avatar {
flex-shrink: 0;
.el-image {
width: 50px;
height: 50px;
border-radius: 3px;
}
}
.pkg-info {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 14px;
margin-right: 14px;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
width: 200px;
color: #615d5d;
font-size: 13px;
}
.pkg-status {
flex-shrink: 0;
button {
font-size: 12px;
text-align: center;
line-height: 28px;
border-radius: 20px;
width: 50px;
cursor: pointer;
color: white;
}
.add-emoji {
background-color: #38adff;
}
.remove-emoji {
background-color: #ff5722;
}
}
}
}
.footer {
height: 50px;
background: rgba(247, 245, 245, 0.66);
text-align: center;
line-height: 50px;
.btn {
width: 150px;
border-radius: 2px;
}
}
}
</style>

View File

@@ -0,0 +1,226 @@
<template>
<div class="lum-dialog-mask animated fadeIn">
<el-container class="lum-dialog-box">
<el-header class="header no-select" height="60px">
<p>发起投票</p>
<p class="tools">
<i class="el-icon-close" @click="$emit('close')" />
</p>
</el-header>
<el-main class="main no-padding vote-from">
<div class="vote-title">投票方式</div>
<div>
<el-radio-group v-model="mode">
<el-radio :label="0">单选</el-radio>
<el-radio :label="1">多选</el-radio>
</el-radio-group>
</div>
<div class="vote-title">投票主题</div>
<div>
<el-input
size="medium"
clear="vote-input"
v-model.trim="title"
placeholder="请输入投票主题最多50字"
:maxlength="50"
/>
</div>
<div class="vote-title">投票选项</div>
<div>
<div class="vote-options" v-for="(option, index) in options">
<div class="lbox">
<el-input
size="medium"
clear="vote-input"
v-model.trim="option.value"
placeholder="请输入选项内容"
:maxlength="120"
>
<span
slot="prefix"
style="margin-left:7px;"
v-text="String.fromCharCode(65 + index)"
/>
</el-input>
</div>
<div class="rbox">
<i class="el-icon-close" @click="removeOption(index)"></i>
</div>
</div>
<h6 class="pointer add-option" @click="addOption">
<i class="el-icon-plus"></i> 添加选项
</h6>
</div>
</el-main>
<el-footer class="footer">
<el-button plain size="small">取消</el-button>
<el-button
type="primary"
size="small"
:disabled="isCheck"
:loading="loading"
@click="submit"
>发起投票</el-button
>
</el-footer>
</el-container>
</div>
</template>
<script>
import { ServeSendVote } from '@/api/chat'
export default {
name: 'MeEditorVote',
props: {
group_id: {
type: [String, Number],
default: 0,
},
},
data() {
return {
loading: false,
mode: 0,
title: '',
options: [
{
value: '',
},
{
value: '',
},
{
value: '',
},
],
}
},
computed: {
isCheck() {
if (this.title == '') return true
return this.options.some(option => option.value == '')
},
},
methods: {
submit() {
let items = []
const { receiver_id } = this.$store.state.dialogue
this.options.forEach(option => {
items.push(option.value)
})
this.loading = true
ServeSendVote({
receiver_id,
mode: this.mode,
title: this.title,
options: items,
})
.then(res => {
if (res.code == 200) {
this.$emit('close')
this.$notify({
title: '友情提示',
message: '发起投票成功!',
type: 'success',
})
} else {
this.$notify({
title: '友情提示',
message: res.message,
type: 'warning',
})
}
})
.catch(() => {
this.loading = false
})
},
addOption() {
if (this.options.length >= 6) {
return false
}
this.options.push({
value: '',
})
},
removeOption(index) {
if (this.options.length <= 2) {
return false
}
this.$delete(this.options, index)
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
height: 600px;
max-width: 450px;
.vote-from {
box-sizing: border-box;
padding: 15px 25px;
overflow: hidden;
.vote-title {
margin: 20px 0 10px;
&:first-child {
margin-top: 0;
}
}
.vote-options {
display: flex;
min-height: 30px;
margin: 10px 0;
.lbox {
width: 100%;
/deep/.el-input__prefix {
height: 36px;
line-height: 36px;
}
}
.rbox {
flex-basis: 50px;
display: flex;
justify-content: center;
align-items: center;
i {
font-size: 18px;
cursor: pointer;
&:hover {
color: red;
}
}
}
}
.add-option {
margin-top: 5px;
font-weight: 400;
color: #3370ff;
}
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 32px;
}
}
/deep/.el-radio__input.is-checked + .el-radio__label {
color: #606266;
}
</style>