提交后台前端基础框架

This commit is contained in:
wanghao
2025-03-12 12:48:13 +08:00
parent 67a3fc68c9
commit 8e19156555
174 changed files with 36652 additions and 0 deletions

View File

@@ -0,0 +1,441 @@
<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="$refs.restFile.click()">
<i class="el-icon-picture-outline-round" />
<p class="tip-title">图片</p>
</li>
<li @click="$refs.materialManage.show()" style="width: 56px; font-size: 13px; color: #409EFF;">
素材库
</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>
<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>-->
<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. 支持视频上传 ( 文件小于30M ) </p>-->
<p>4. 按Enter发送 / Shift+Enter 换行</p>
<p>5.注意当图片和视频正在上传时请勿关闭网页或离开当前对话框否则将导致文件停止上传或上传失败</p>
</div>
<i class="el-icon-info" slot="reference" />
</el-popover>
</p>
</ul>
<el-popover
ref="popoverEmoticon"
placement="top-start"
width="500"
trigger="click"
popper-class="no-padding"
>
<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)"
/>
</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" />
<material-manage ref="materialManage" @confirm="confirmUploadMaterials" />
</div>
</template>
<script>
import MeEditorEmoticon from './MeEditorEmoticon'
import MeEditorFileManage from './MeEditorFileManage'
import MeEditorImageView from './MeEditorImageView'
import MeEditorRecorder from './MeEditorRecorder'
import TalkCodeBlock from '@/components/chat/TalkCodeBlock'
import { getPasteImgs, getDragPasteImg } from '@/utils/editor'
import { findTalk } from '@/utils/talk'
import MaterialManage from "@/views/message/components/MaterialManage";
export default {
name: 'MeEditor',
components: {
MaterialManage,
MeEditorEmoticon,
MeEditorFileManage,
MeEditorImageView,
TalkCodeBlock,
MeEditorRecorder,
},
computed: {
talkUser() {
return this.$store.state.dialogue.index_name
},
},
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,
},
// 录音器
recorder: false,
// 上次发送消息的时间
sendtime: 0,
// 发送间隔时间默认1秒
interval: 1000,
}
},
methods: {
setText(text) {
this.editorText = text
},
// 读取对话编辑草稿信息
getDraftText(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])
},
// 键盘按下监听事件
keydownEvent(e) {
if (e.keyCode == 13 && this.editorText == '') {
e.preventDefault()
}
// 回车发送消息
if (e.keyCode == 13 && e.shiftKey == false && this.editorText != '') {
this.$emit('send', this.editorText)
this.editorText = ''
e.preventDefault()
}
},
// 选择图片文件后回调方法
uploadImageChange(e) {
this.openImageViewer(e.target.files[0])
this.$refs.restFile.value = null
},
// 选择文件回调事件
uploadFileChange(e) {
let maxsize = 30 * 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: '上传文件不能大于30M',
})
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
},
// 确认上传图片消息回调事件
confirmUploadImage() {
let fileData = new FormData()
fileData.append('image', this.imageViewer.file)
let ref1 = this.$refs.imageViewer
ref1.loading = false
ref1.closeBox()
this.$emit('send-image', fileData)
},
confirmUploadMaterials(materials) {
this.$emit('send-materials', materials)
},
// 选中表情包回调事件
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 { source, receive_id } = this.$store.state.dialogue
ServeSendEmoticon({
source,
receive_id,
emoticon_id: data.value,
})
}
this.$refs.popoverEmoticon.doClose()
},
},
}
</script>
<style scoped>
.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,348 @@
<template>
<div>
<el-container class="container">
<el-header height="30px" class="no-padding header">
<span v-text="showTitle"></span>
<!--<div class="addbtn" @click="systemEmojiBox = true">系统表情</div>-->
</el-header>
<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"
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 - 1)
},
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;
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 { formateSize, 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: formateSize(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,
receive_id: this.$store.state.dialogue.receive_id,
source: this.$store.state.dialogue.source,
})
}
} 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,518 @@
<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
console.log('销毁了...')
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.url" 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,49 @@
<template>
<div class="empty-content">
<div class="image">
<img :src="src" />
</div>
<div class="text" v-text="text" />
</div>
</template>
<script>
export default {
name: 'Empty',
props: {
text: {
type: String,
default: '数据为空...',
},
src: {
type: String,
default: require('@/assets/image/no-oncall.6b776fcf.png'),
},
},
data() {
return {}
},
created() {},
methods: {},
}
</script>
<style lang="less" scoped>
.empty-content {
width: 100%;
height: 60%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 13px;
.image {
width: 200px;
height: 200px;
img {
width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,313 @@
<template>
<div class="loading-content">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin">
<i class="ant-spin-dot-item" />
<i class="ant-spin-dot-item" />
<i class="ant-spin-dot-item" />
<i class="ant-spin-dot-item" />
</span>
</div>
<p>{{ text }}</p>
</div>
</template>
<script>
export default {
name: 'Loading',
props: {
text: {
type: String,
default: '数据加载中 ...',
},
},
}
</script>
<style lang="less" scoped>
.loading-content {
width: 100%;
height: 60%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 13px;
p {
margin-top: 10px;
}
}
/* ant-spin 加载动画 start */
.ant-spin {
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5715;
list-style: none;
-webkit-font-feature-settings: 'tnum';
font-feature-settings: 'tnum';
position: absolute;
display: none;
color: #1890ff;
text-align: center;
vertical-align: middle;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s
cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-nested-loading {
position: relative;
}
.ant-spin-nested-loading > div > .ant-spin {
position: absolute;
top: 0;
left: 0;
z-index: 4;
display: block;
width: 100%;
height: 100%;
max-height: 400px;
}
.ant-spin-nested-loading > div > .ant-spin .ant-spin-dot {
position: absolute;
top: 50%;
left: 50%;
margin: -10px;
}
.ant-spin-nested-loading > div > .ant-spin .ant-spin-text {
position: absolute;
top: 50%;
width: 100%;
padding-top: 5px;
text-shadow: 0 1px 2px #fff;
}
.ant-spin-nested-loading > div > .ant-spin.ant-spin-show-text .ant-spin-dot {
margin-top: -20px;
}
.ant-spin-nested-loading > div > .ant-spin-sm .ant-spin-dot {
margin: -7px;
}
.ant-spin-nested-loading > div > .ant-spin-sm .ant-spin-text {
padding-top: 2px;
}
.ant-spin-nested-loading > div > .ant-spin-sm.ant-spin-show-text .ant-spin-dot {
margin-top: -17px;
}
.ant-spin-nested-loading > div > .ant-spin-lg .ant-spin-dot {
margin: -16px;
}
.ant-spin-nested-loading > div > .ant-spin-lg .ant-spin-text {
padding-top: 11px;
}
.ant-spin-nested-loading > div > .ant-spin-lg.ant-spin-show-text .ant-spin-dot {
margin-top: -26px;
}
.ant-spin-container {
position: relative;
-webkit-transition: opacity 0.3s;
transition: opacity 0.3s;
}
.ant-spin-container::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
display: none \9;
width: 100%;
height: 100%;
background: #fff;
opacity: 0;
-webkit-transition: all 0.3s;
transition: all 0.3s;
content: '';
pointer-events: none;
}
.ant-spin-blur {
clear: both;
overflow: hidden;
opacity: 0.5;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
}
.ant-spin-blur::after {
opacity: 0.4;
pointer-events: auto;
}
.ant-spin-tip {
color: rgba(0, 0, 0, 0.45);
}
.ant-spin-dot {
position: relative;
display: inline-block;
font-size: 20px;
width: 1em;
height: 1em;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
-webkit-transform: scale(0.75);
transform: scale(0.75);
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
opacity: 0.3;
-webkit-animation: antSpinMove 1s infinite linear alternate;
animation: antSpinMove 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-animation: antRotate 1.2s infinite linear;
animation: antRotate 1.2s infinite linear;
}
.ant-spin-sm .ant-spin-dot {
font-size: 14px;
}
.ant-spin-sm .ant-spin-dot i {
width: 6px;
height: 6px;
}
.ant-spin-lg .ant-spin-dot {
font-size: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
.ant-spin.ant-spin-show-text .ant-spin-text {
display: block;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1;
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
.ant-spin-rtl {
direction: rtl;
}
.ant-spin-rtl .ant-spin-dot-spin {
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
-webkit-animation-name: antRotateRtl;
animation-name: antRotateRtl;
}
@-webkit-keyframes antRotateRtl {
to {
-webkit-transform: rotate(-405deg);
transform: rotate(-405deg);
}
}
@keyframes antRotateRtl {
to {
-webkit-transform: rotate(-405deg);
transform: rotate(-405deg);
}
}
/* ant-spin 加载动画 end */
</style>

View File

@@ -0,0 +1,448 @@
<template>
<div class="lum-dialog-mask">
<el-container class="lum-dialog-box" v-outside="close">
<el-header class="no-padding header no-select" height="60px">
<p>
{{ from.groupId == 0 ? '创建群组' : '请选择需要邀请的好友' }}
</p>
<p class="tools">
<i class="el-icon-close" @click="close" />
</p>
</el-header>
<el-main class="main no-padding">
<el-container class="full-height">
<el-aside width="250px" class="aside-border">
<el-container class="full-height no-select">
<el-header
class="no-padding search-header"
height="50px"
:class="{ shadow: searchHeaderShadow }"
>
<el-input
v-model="keywords"
placeholder="搜索 | 好友 or 群组"
prefix-icon="el-icon-search"
clearable
size="small"
/>
</el-header>
<el-main class="no-padding">
<el-scrollbar
ref="scrollbar"
class="full-height"
tag="section"
:native="false"
>
<ul class="friend-items no-select">
<li
v-for="item in search"
:key="item.id"
@click="triggerContacts(item)"
>
<el-avatar
class="avatar"
style="margin-top: 5px"
:size="25"
:src="item.avatar"
>
<img src="~@/assets/image/detault-avatar.jpg" />
</el-avatar>
<span class="nickname">{{ item.nickname }}</span>
<span class="select-btn">
<i
class="el-icon-success"
:class="{ 'icon-active': item.checked }"
/>
</span>
</li>
</ul>
</el-scrollbar>
</el-main>
</el-container>
</el-aside>
<el-main class="no-padding">
<el-container class="full-height">
<el-header height="50px" v-show="!readonly">
<div class="group-from no-select">
<label>群名称</label>
<p>
<el-input
v-model="from.groupName"
placeholder="请输入群名称(必填)"
size="small"
:maxlength="20"
/>
</p>
</div>
</el-header>
<el-header height="40px" :class="{ mt40: !readonly }">
<el-divider content-position="left" class="no-select">
<span style="color: #c4c5c7">
邀请成员 ({{ selected.length }})
</span>
</el-divider>
</el-header>
<el-main>
<el-scrollbar :native="false" tag="section" class="full-height">
<div class="selectd-items">
<div
v-for="item in selected"
:key="item.id"
class="selectd-item no-select"
>
<el-avatar :size="25" :src="item.avatar" />
<p>{{ item.nickname }}</p>
<div class="triangle-topleft"></div>
<div class="del-mask" @click="delContacts(item)">
<i class="el-icon-delete" />
</div>
</div>
</div>
</el-scrollbar>
</el-main>
</el-container>
</el-main>
</el-container>
</el-main>
<el-footer height="50px" class="no-padding footer">
<el-button size="small" @click="close" plain>取消</el-button>
<el-button
v-if="from.groupId == 0"
type="primary"
size="small"
@click="createSubmit"
>
创建群组<span v-show="selected.length">({{ selected.length }})</span>
</el-button>
<el-button v-else type="primary" size="small" @click="inviteSubmit">
立即邀请({{ selected.length }})
</el-button>
</el-footer>
</el-container>
</div>
</template>
<script>
import {
ServeCreateGroup,
ServeInviteGroup,
ServeGetInviteFriends,
} from '@/api/group'
export default {
name: 'GroupLaunch',
props: {
groupId: {
type: [String, Number],
default: null,
},
},
data() {
return {
readonly: false,
from: {
groupId: 0,
groupName: '',
},
contacts: [],
search: [],
searchHeaderShadow: false,
keywords: '',
isAvatarCropper: false,
}
},
computed: {
selected() {
return this.contacts.filter(item => item.checked)
},
},
watch: {
keywords(val) {
this.search =
val == ''
? this.contacts
: this.contacts.filter(item => {
return item.nickname.match(this.keywords) != null
})
},
contacts(arr) {
if (this.keywords == '') {
this.search = arr
}
},
},
created() {
if (this.groupId > 0) {
this.readonly = true
this.from.groupId = this.groupId
}
this.friendsApi()
},
mounted() {
this.handleScroll()
},
methods: {
// 触发选择联系人事件
triggerContacts(item) {
let index = this.contacts.findIndex(val => {
return val.id === item.id
})
this.contacts[index].checked = !this.contacts[index].checked
},
// 取消选中的联系人
delContacts(item) {
let index = this.contacts.findIndex(val => {
return val.id === item.id
})
this.contacts[index].checked = false
},
// 移除所有选中选项
delAll() {
this.contacts.forEach((item, i) => {
this.contacts[i].checked = false
})
},
// 关闭窗口
close() {
this.$emit('close')
},
// 获取选中的ID列表
getIds() {
return this.selected.map(item => item.id)
},
// 加载好友列表
friendsApi() {
ServeGetInviteFriends({
group_id: this.from.groupId,
}).then(res => {
if (res.code == 200 && res.data) {
this.contacts = []
let data = res.data.map(item => {
return Object.assign(item, {
nickname: item.friend_remark ? item.friend_remark : item.nickname,
checked: false,
})
})
this.contacts.push(...data)
}
})
},
//创建聊天群
createSubmit() {
let data = {
group_avatar: '',
group_name: this.from.groupName,
group_profile: '',
uids: this.getIds().join(','),
}
if (data.group_name == '') {
this.$message('群聊名称不能为空!')
return
}
if (this.getIds().length < 2) {
this.$message('群聊人数必须大于俩人!')
return
}
ServeCreateGroup(data).then(res => {
if (res.code == 200) {
this.$emit('create-success', res.data)
} else {
this.$message('创建群聊失败!')
}
})
},
//好友邀请提交
inviteSubmit() {
ServeInviteGroup({
group_id: this.from.groupId,
uids: this.getIds().join(','),
}).then(res => {
if (res.code == 200) {
this.$emit('invite-success')
} else {
this.$message('邀请好友失败!')
}
})
},
// 滚动条监听
handleScroll() {
let scrollbarEl = this.$refs.scrollbar.wrap
scrollbarEl.onscroll = () => {
this.searchHeaderShadow = scrollbarEl.scrollTop != 0
}
},
},
}
</script>
<style lang="less" scoped>
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
.lum-dialog-box {
width: 650px;
max-width: 650px;
height: 550px;
.main {
.aside-border {
border-right: 1px solid #f5eeee;
}
}
.footer {
display: flex;
justify-content: flex-end;
align-items: center;
padding-right: 10px;
border-top: 1px solid #f5eeee;
}
}
.search-header {
padding: 8px;
display: flex;
justify-content: center;
align-items: center;
&.shadow {
box-shadow: 0 2px 6px 0 rgba(31, 35, 41, 0.05);
}
}
.friend-items {
li {
padding: 10px;
cursor: pointer;
position: relative;
&:hover {
background: #f5f5f5;
}
.avatar {
margin-top: 3px;
}
.nickname {
width: 60%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: absolute;
top: 10px;
left: 52px;
height: 35px;
line-height: 35px;
font-weight: 400;
font-size: 13px;
}
.select-btn {
position: absolute;
width: 32px;
height: 35px;
right: 3px;
top: 10px;
line-height: 35px;
text-align: center;
i {
color: #ccc;
&.icon-active {
color: #26bcfe !important;
}
}
}
}
}
.group-from {
label {
height: 45px;
line-height: 47px;
width: 50px;
color: #606266;
padding-right: 3px;
font-size: 13px;
}
input {
height: 25px;
width: 100%;
text-indent: 3px;
color: #a9a4a4;
font-size: 12px;
border-bottom: 1px solid #efebeb;
}
}
.selectd-items {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
.selectd-item {
width: 23%;
height: 65px;
margin: 6px 2px 0px 2px;
cursor: pointer;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
box-shadow: 0px 0px 3px 0 rgba(0, 0, 0, 0.1);
p {
width: 90%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 10px;
margin-top: 8px;
text-align: center;
}
.del-mask {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(31, 35, 41, 0.5);
display: none;
justify-content: center;
align-items: center;
color: white;
transition: ease 0.5s;
border-radius: 2px;
}
&:hover .del-mask {
display: flex;
}
}
}
.mt40 {
margin-top: 40px;
}
</style>

View File

@@ -0,0 +1,926 @@
<template>
<div class="lum-dialog-mask">
<div class="lum-dialog-box">
<el-container class="container">
<el-header class="header no-select" height="60px">
<p>群管理 ({{ detail.group_name }})</p>
<p class="tools">
<i class="el-icon-close" @click="$emit('close')" />
</p>
</el-header>
<el-main class="main no-padding">
<el-container class="full-height">
<el-aside width="100px" class="aside-border no-select">
<div
v-for="(menu, index) in menus"
:key="menu.name"
class="menu-list"
:class="{ selectd: tabIndex == index }"
v-text="menu.name"
@click="triggerTab(index)"
/>
</el-aside>
<!-- 群介绍模块 -->
<el-main v-if="tabIndex == 0">
<el-row>
<el-col :span="14">
<el-form ref="groupForm" :model="form" :rules="rules">
<el-form-item label="群名:" prop="group_name">
<el-input
v-model="form.group_name"
size="medium"
placeholder="请输入群名称"
maxlength="30"
show-word-limit
/>
</el-form-item>
<el-form-item label="群描述:">
<el-input
v-model="form.profile"
type="textarea"
rows="3"
placeholder="请输入群描述"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="el-icon-edit"
size="small"
:loading="loading"
@click="editGroup"
>修改信息
</el-button>
</el-form-item>
</el-form>
</el-col>
<el-col :span="10" class="avatar-col">
<div class="avatar-box">
<img v-show="form.avatar" :src="form.avatar" />
<div class="upload-icon">
<i class="el-icon-upload" />
</div>
<div class="upload-mask" @click="isAvatarCropper = true">
<i class="el-icon-plus" />
</div>
</div>
<p style="margin-top: 20px">设置头像</p>
</el-col>
</el-row>
</el-main>
<!-- 群成员模块 -->
<el-main v-else-if="tabIndex == 1" class="no-padding">
<el-container class="full-height">
<el-header height="50px" class="notice-header">
<el-input
v-model="searchMembers"
style="width: 200px"
size="small"
clearable
prefix-icon="el-icon-search"
:placeholder="`搜索群成员 ( 共${members.length}人 )`"
/>
<p>
<el-button
plain
size="small"
icon="el-icon-plus"
@click="inviteFriendBox = true"
>邀请好友
</el-button>
<el-button
v-if="batchDelMember"
type="danger"
size="small"
icon="el-icon-delete"
@click="deleteMembers"
>确认删除
</el-button>
<el-button
v-else
plain
size="small"
icon="el-icon-finished"
@click="batchDelMember = true"
>批量操作
</el-button>
</p>
</el-header>
<el-main class="no-padding">
<el-scrollbar
class="full-height"
tag="section"
:native="false"
>
<div class="members">
<div
v-for="member in filterMembers"
class="member no-select"
:class="{
selectd: member.is_delete && batchDelMember,
}"
:key="member.user_id"
>
<div class="item-header">
<div class="avatar" @click="catUserDetail(member)">
<el-avatar :size="30" :src="member.avatar">
<img src="~@/assets/image/detault-avatar.jpg" />
</el-avatar>
<span class="nickname" v-text="member.nickname" />
<span class="larkc-tag" v-show="member.leader == 2">
群主
</span>
</div>
<div
v-show="batchDelMember && member.leader != 2"
class="tools"
>
<i
class="el-icon-success"
:class="{ 'is-delete': member.is_delete }"
@click.stop="triggerDelBtn(member)"
/>
</div>
</div>
<div class="profile">
签名 | {{ member.motto ? member.motto : '未设置' }}
</div>
</div>
</div>
</el-scrollbar>
</el-main>
</el-container>
</el-main>
<!-- 群公告模块 -->
<el-main v-else-if="tabIndex == 2" class="no-padding">
<el-container class="full-height">
<el-header
class="notice-header"
height="50px"
style="padding-left: 14px"
>
<span>群公告 ({{ notice.items.length }})</span>
<el-button
plain
size="small"
icon="el-icon-plus"
@click="showNoticeBox(0, '', '')"
>
添加公告
</el-button>
</el-header>
<el-main class="no-padding">
<el-scrollbar
class="full-height"
tag="section"
:native="false"
>
<div v-if="notice.items.length == 0" class="empty-notice">
<SvgNotData style="width: 80px; margin-bottom: 10px" />
<span>暂无群公告</span>
</div>
<div v-else class="notices">
<div
v-for="(item, index) in notice.items"
:key="item.id"
class="notice"
>
<div class="title">
<span
class="left-title"
v-text="item.title"
@click="
showNoticeBox(item.id, item.title, item.content)
"
></span>
<span
class="right-tools no-select"
@click="catNoticeDetail(index)"
>{{ item.isShow ? '收起' : '展开' }}</span
>
</div>
<p class="datetime">
<el-avatar :size="15" :src="item.avatar">
<img src="~@/assets/image/detault-avatar.jpg" />
</el-avatar>
<span
class="text nickname"
v-text="item.nickname"
@click="$refs.userBusinessCard.open(item.user_id)"
></span>
<span class="text">
发表于 {{ item.created_at.substr(0, 16) }}
</span>
</p>
<p
class="content"
:class="{ retract: !item.isShow }"
v-text="item.content"
></p>
</div>
</div>
</el-scrollbar>
</el-main>
</el-container>
</el-main>
<el-main v-else-if="tabIndex == 3" class="no-padding"> </el-main>
</el-container>
</el-main>
</el-container>
</div>
<!-- 编辑公告信息 -->
<div class="lum-dialog-mask animated fadeIn" v-show="notice.isShowform">
<div class="notice-box">
<h4>编辑群公告</h4>
<el-form ref="noticeForm" :model="notice.form" :rules="notice.rules">
<el-form-item label="标题" prop="title">
<el-input
v-model="notice.form.title"
size="medium"
placeholder="请输入标题..."
maxlength="30"
show-word-limit
/>
</el-form-item>
<el-form-item label="详情" prop="content">
<el-input
v-model="notice.form.content"
type="textarea"
rows="5"
placeholder="请输入公告详情..."
maxlength="500"
/>
</el-form-item>
<el-form-item style="text-align: right">
<el-button plain size="small" @click="notice.isShowform = false">
取消
</el-button>
<el-button
type="primary"
size="small"
:loading="notice.loading"
@click="onSubmitNotice"
>提交
</el-button>
</el-form-item>
</el-form>
</div>
</div>
<transition name="el-fade-in-linear">
<AvatarCropper v-if="isAvatarCropper" @close="closeAvatarCropper" />
</transition>
<!-- 查看好友用户信息 -->
<UserBusinessCard ref="userBusinessCard" />
<transition name="el-fade-in-linear">
<GroupLaunch
v-if="inviteFriendBox"
:group-id="groupId"
@close="inviteFriendBox = false"
@invite-success="inviteSuccess"
/>
</transition>
</div>
</template>
<script>
import AvatarCropper from '@/components/layout/AvatarCropper'
import UserBusinessCard from '@/components/user/UserBusinessCard'
import GroupLaunch from '@/components/group/GroupLaunch'
import { SvgNotData } from '@/core/icons'
import {
ServeGetGroupMembers,
ServeGetGroupNotices,
ServeEditGroupNotice,
ServeRemoveMembersGroup,
ServeGroupDetail,
ServeEditGroup,
} from '@/api/group'
export default {
name: 'GroupManage',
props: {
groupId: {
type: [String, Number],
default: null,
},
},
components: {
AvatarCropper,
UserBusinessCard,
GroupLaunch,
SvgNotData,
},
data() {
return {
// 当前选中菜单
tabIndex: 0,
menus: [
{ name: '群信息' },
{ name: '群成员' },
{ name: '群公告' },
{ name: '群设置' },
],
loading: false,
form: {
group_name: '',
profile: '',
avatar: '',
},
rules: {
group_name: [
{
required: true,
message: '用户昵称不能为空!',
trigger: 'blur',
},
],
},
detail: {
group_name: '',
profile: '',
avatar: '',
},
// 群成员列表
searchMembers: '',
batchDelMember: false,
members: [],
// 群公告相关数据
notice: {
isShowform: false,
loading: false,
form: {
id: 0,
title: '',
content: '',
},
rules: {
title: [
{
required: true,
message: '标题不能为空!',
trigger: 'blur',
},
],
content: [
{
required: true,
message: '详情不能为空',
trigger: 'blur',
},
],
},
items: [],
},
inviteFriendBox: false,
isAvatarCropper: false,
}
},
computed: {
filterMembers() {
return this.searchMembers == ''
? this.members
: this.members.filter(item => {
return (
item.nickname.match(this.searchMembers) != null ||
item.visit_card.match(this.searchMembers) != null
)
})
},
},
created() {
this.loadGroupDetail()
this.loadMembers()
this.loadNotices()
},
methods: {
// 加载群信息
loadGroupDetail() {
ServeGroupDetail({
group_id: this.groupId,
}).then(({ code, data }) => {
if (code == 200) {
this.form = this.detail = {
group_name: data.group_name,
profile: data.profile,
avatar: data.avatar,
}
}
})
},
// 加载群组成员列表
loadMembers() {
ServeGetGroupMembers({
group_id: this.groupId,
}).then(res => {
if (res.code == 200) {
this.members = res.data.map(item => {
item.is_delete = false
return item
})
}
})
},
// 加载群组公告列表
loadNotices() {
ServeGetGroupNotices({
group_id: this.groupId,
}).then(res => {
if (res.code == 200) {
this.notice.items = res.data.map(item => {
item.isShow = false
return item
})
}
})
},
// 修改群信息
editGroup() {
this.$refs.groupForm.validate(valid => {
if (!valid) return false
this.loading = true
ServeEditGroup({
group_id: this.groupId,
group_name: this.form.group_name,
profile: this.form.profile,
avatar: this.form.avatar,
})
.then(res => {
if (res.code == 200) {
this.detail.group_name = this.form.group_name
this.detail.profile = this.form.profile
this.detail.avatar = this.form.avatar
this.$message({
message: '信息修改成功...',
type: 'success',
})
} else {
this.$message('信息修改失败...')
}
})
.finally(() => {
this.loading = false
})
})
},
// 左侧菜单栏切换事件
triggerTab(i) {
this.tabIndex = i
},
// 关闭头像裁剪弹出层
closeAvatarCropper(type, avatar = '') {
this.isAvatarCropper = false
if (type == 1 && avatar != '') {
this.form.avatar = avatar
}
},
// 显示编辑公告窗口
showNoticeBox(id = 0, title = '', content = '') {
this.notice.isShowform = true
this.notice.form.id = id
this.notice.form.title = title
this.notice.form.content = content
},
// 编辑公告提交事件
onSubmitNotice() {
this.$refs.noticeForm.validate(valid => {
if (!valid) return false
this.notice.loading = true
ServeEditGroupNotice({
notice_id: this.notice.form.id,
group_id: this.groupId,
title: this.notice.form.title,
content: this.notice.form.content,
is_top: 0,
is_confirm: 0,
})
.then(res => {
if (res.code == 200) {
this.notice.isShowform = false
this.loadNotices()
this.$notify({
title: '消息提示',
message: this.notice.form.id
? '群公告修改成功...'
: '群公告添加成功...',
type: 'success',
})
} else {
this.$notify({
title: '消息提示',
message: this.notice.form.id
? '群公告修改失败...'
: '群公告添加失败...',
type: 'success',
})
}
})
.catch(() => {
this.$notify({
title: '消息提示',
message: '网络异常,请稍后再试...',
position: 'bottom-right',
type: 'warning',
})
})
.finally(() => {
this.notice.loading = false
})
})
},
// 展开/收起群公告详情
catNoticeDetail(index) {
this.notice.items[index].isShow = !this.notice.items[index].isShow
},
// 查看群成员信息事件
catUserDetail(item) {
this.$refs.userBusinessCard.open(item.user_id)
},
// 选中删除成员事件
triggerDelBtn(member) {
let i = this.members.findIndex(item => {
return item.id === member.id
})
this.members[i].is_delete = !this.members[i].is_delete
},
// 批量删除群成员
deleteMembers() {
let ids = [],
names = []
this.members.forEach(item => {
if (item.is_delete) {
ids.push(item.user_id)
names.push(item.nickname)
}
})
if (ids.length == 0) {
this.batchDelMember = false
return
}
this.$confirm(`您确定要将【 ${names.join('、')}】移出群聊?`, '温馨提示', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true,
customClass: 'border-radius0',
})
.then(() => {
ServeRemoveMembersGroup({
group_id: this.groupId,
members_ids: ids.join(','),
}).then(res => {
if (res.code == 200) {
this.loadMembers()
this.$notify({
title: '删除成功',
message: `已成功将群成员移除群组...`,
type: 'success',
})
}
})
})
.catch(() => {
this.members.map(item => {
return (item.is_delete = false)
})
this.batchDelMember = false
})
},
// 好友邀请成功回调方法
inviteSuccess() {
this.inviteFriendBox = false
this.loadMembers()
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 80%;
height: 500px;
max-width: 800px;
}
.aside-border {
display: flex;
flex-direction: column;
padding: 8px;
border-right: 1px solid #f5f5f5;
.menu-list {
height: 25px;
line-height: 25px;
margin: 8px 2px;
font-weight: 400;
font-size: 13px;
background-color: white;
cursor: pointer;
border-left: 3px solid white;
padding-left: 10px;
&.selectd {
color: #2196f3;
border-color: #2196f3;
}
}
}
.avatar-col {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
.avatar-box {
width: 100px;
height: 100px;
box-shadow: 0px 0px 7px 1px #e8e4e4;
border-radius: 50%;
position: relative;
cursor: pointer;
transition: ease 0.5s;
.upload-icon {
position: absolute;
right: 0;
top: 0;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(184, 184, 197, 0.2);
display: flex;
justify-content: center;
align-items: center;
i {
font-size: 30px;
color: #1bb0f3;
}
}
img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
z-index: 0;
}
.upload-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.2);
z-index: 3;
display: none;
justify-content: center;
align-items: center;
i {
font-size: 30px;
color: white;
}
}
&:hover .upload-mask {
display: flex;
}
}
/* 群成员相关 start */
.members {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 5px 20px;
justify-content: space-between;
.member {
width: 48%;
height: 70px;
border-radius: 3px;
cursor: pointer;
border: 1px dashed #e2dcdc;
margin: 5px 0;
padding: 3px;
transition: ease 0.5s;
.larkc-tag {
color: #3370ff;
background-color: #e1eaff;
}
.item-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.avatar {
flex: 1 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
flex-direction: row;
align-items: center;
padding: 3px 5px;
.nickname {
font-size: 13px;
margin: 0 5px 0 15px;
}
}
.tools {
flex-basis: 50px;
overflow: hidden;
text-align: right;
padding-right: 5px;
i {
color: #cccccc;
}
.is-delete {
color: #03a9f4;
}
}
}
.profile {
color: #8f8a8a;
font-size: 12px;
padding: 3px 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 3px 0;
}
&:hover,
&.selectd {
border-color: #2196f3;
}
}
}
/* 群成员相关 end */
/* 公告相关 start */
.notice-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.empty-notice {
width: 100%;
height: 100%;
min-height: 200px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
span {
color: #cccccc;
font-size: 13px;
}
}
.notices {
.notice {
cursor: pointer;
min-height: 76px;
overflow: hidden;
border-bottom: 1px dashed #e2dcdc;
margin-bottom: 15px;
margin-right: 15px;
padding-bottom: 5px;
margin: 2px 20px 15px 15px;
h6 {
font-size: 15px;
font-weight: 300;
}
.title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 30px;
.left-title {
flex: 1 1;
height: 100%;
line-height: 30px;
font-size: 14px;
}
.right-tools {
flex-basis: 70px;
flex-shrink: 0;
height: 100%;
line-height: 30px;
text-align: right;
font-weight: 300;
font-size: 12px;
color: #2196f3;
}
}
.datetime {
font-size: 10px;
color: #a59696;
font-weight: 300;
display: flex;
flex-direction: row;
align-items: center;
margin: 10px 0;
.text {
margin: 0 5px;
}
.nickname {
color: #2196f3;
font-weight: 400;
}
}
.retract {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
font-size: 12px;
line-height: 28px;
font-weight: 500;
color: #7d7a7a;
}
}
}
.notice-box {
position: relative;
padding: 28px;
background: #fff;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.2);
border-radius: 4px;
overflow: hidden;
box-sizing: border-box;
height: 415px;
width: 420px;
h4 {
margin-bottom: 20px;
font-weight: 400;
}
}
/* 公告相关 end */
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
</style>

View File

@@ -0,0 +1,231 @@
<template>
<div class="lum-dialog-mask">
<el-container class="lum-dialog-box">
<el-header class="header no-select" height="60px">
<p>群公告({{ items.length }})</p>
<p class="tools">
<i class="el-icon-close" @click="$emit('close')" />
</p>
</el-header>
<el-main class="main no-padding">
<template v-if="loadStatus == 0">
<Loading text="正在努力加载中 ..." />
</template>
<template v-else-if="loadStatus == 1 && items.length == 0">
<div class="loading">
<SvgNotData class="svg-icon" />
<p>暂无群公告</p>
</div>
</template>
<template v-if="loadStatus == 2">
<div class="loading">
<i
class="el-icon-warning"
style="font-size: 50px; color: #ff5151"
/>
<p>
加载失败
<a @click="loadNotices" class="pointer">点击重试</a>...
</p>
</div>
</template>
<template v-else>
<el-scrollbar class="full-height" :native="false" tag="section">
<div
v-for="(item, index) in items"
:key="item.id"
class="notice-item"
>
<div class="title">
<span class="left-title">{{ item.title }}</span>
<span
class="right-tools no-select"
@click="catNoticeDetail(index)"
>
{{ item.isShow ? '收起' : '展开' }}
</span>
</div>
<p class="datetime">
<el-avatar :size="15" :src="item.avatar">
<img src="~@/assets/image/detault-avatar.jpg" />
</el-avatar>
<span
class="text nickname"
@click="$refs.userBusinessCard.open(item.user_id)"
>
{{ item.nickname }}
</span>
<span class="text">发表于 {{ item.created_at }}</span>
</p>
<p class="content" :class="{ retract: !item.isShow }">
{{ item.content }}
</p>
</div>
</el-scrollbar>
</template>
</el-main>
</el-container>
</div>
</template>
<script>
import { ServeGetGroupNotices } from '@/api/group'
import { SvgNotData } from '@/core/icons'
import Loading from '@/components/global/Loading'
export default {
name: 'GroupNotice',
props: {
groupId: {
type: [String, Number],
default: null,
},
},
components: {
SvgNotData,
Loading,
},
data() {
return {
// 公告列表
items: [],
loadStatus: 0,
}
},
created() {
this.loadNotices()
},
methods: {
// 加载群组公告列表
loadNotices() {
this.loadStatus = 0
ServeGetGroupNotices({
group_id: this.groupId,
})
.then(({ code, data }) => {
if (code == 200) {
this.items = data.map(item => {
item.isShow = false
return item
})
this.loadStatus = 1
} else {
this.loadStatus = 2
}
})
.catch(() => {
this.loadStatus = 2
})
},
// 展开/收起群公告详情
catNoticeDetail(index) {
this.items[index].isShow = !this.items[index].isShow
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 650px;
max-width: 650px;
height: 550px;
.main {
overflow: hidden;
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 70%;
font-size: 12px;
p {
margin-top: 20px;
}
.svg-icon {
width: 80px;
margin-bottom: 10px;
}
}
.notice-item {
cursor: pointer;
min-height: 76px;
overflow: hidden;
border-bottom: 1px dashed #e2dcdc;
padding-bottom: 5px;
margin: 2px 20px 15px 15px;
h6 {
font-size: 15px;
font-weight: 300;
}
.title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 30px;
.left-title {
flex: 1 1;
height: 100%;
line-height: 30px;
font-size: 15px;
}
.right-tools {
flex-basis: 70px;
flex-shrink: 0;
height: 100%;
line-height: 30px;
text-align: right;
font-weight: 300;
font-size: 12px;
color: #2196f3;
}
}
.datetime {
font-size: 10px;
color: #a59696;
font-weight: 300;
display: flex;
flex-direction: row;
align-items: center;
margin: 10px 0;
.text {
margin: 0 5px;
}
.nickname {
color: #2196f3;
font-weight: 400;
}
}
.retract {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
font-size: 13px;
line-height: 28px;
font-weight: 400;
color: #706a6a;
}
}
}
}
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
</style>

View File

@@ -0,0 +1,817 @@
<template>
<el-container class="container">
<el-header class="header">
<span>群信息</span>
<el-tooltip content="发送消息" placement="top">
<i class="icon-send el-icon-chat-line-square" @click="sendGroup" />
</el-tooltip>
<i class="el-icon-close" @click="$emit('close')" />
</el-header>
<el-main class="main lum-scrollbar">
<div class="list-item flex">
<p>
<span>群名称</span>
<span class="group-setting-title">{{ detail.groupName }}</span>
</p>
<span
v-show="detail.is_manager"
class="more"
@click="isShowManager = true"
>管理
</span>
</div>
<div class="list-item">
<span>群主</span>
<span class="group-boss-name">{{ detail.groupOwner }}</span>
</div>
<div class="list-item">
<span>我的群昵称</span>
<span v-if="!isEditRemark" class="edit-visit-card">
<span>
{{ detail.visitCard }}
<span v-show="!detail.visitCard" style="font-size: 12px">
添加群名片
</span>
</span>
<i
class="el-icon-edit-outline edit-remark-icon"
@click="
isEditRemark = true
editRemarkText = detail.visitCard
"
/>
</span>
<span v-else>
<input
v-model="editRemarkText"
class="edit-input"
type="text"
v-focus
@keyup.enter="editRemark"
/>
<span class="input-submit" @click="editRemark">确认</span>
</span>
</div>
<div class="list-item flex">
<span>消息免打扰</span>
<el-switch
v-model="detail.disturb"
inactive-color="#e0d6d6"
:disabled="disturbDisabled"
@change="editDisturb"
/>
</div>
<!-- 预留 -->
<div class="list-item flex">
<span>全员禁言</span>
<el-switch v-model="detail.no_message" inactive-color="#e0d6d6" />
</div>
<div class="list-item">
<span>群成员</span>
<span>{{ members.length }} </span>
</div>
<div class="list-item-tips">群主已开启新成员入群可查看所有聊天记录</div>
<div class="list-item">群简介</div>
<div class="list-item-tips">
{{ detail.groupProfile ? detail.groupProfile : '暂无群简介' }}
</div>
<div class="list-item flex">
<span>群公告</span>
<span
v-show="detail.group_notice"
class="more"
@click="isShowGroupNotice = true"
>更多
</span>
</div>
<div class="list-item-tips group-notice">
<span v-if="detail.group_notice.title">
<b>#{{ detail.group_notice.title }}#</b><br />
{{ detail.group_notice.content }}
</span>
<span v-else>暂无群公告</span>
</div>
<div class="list-item">
<p class="group-invite" @click="addGroupMembers">
<i class="el-icon-plus" />
<span>&nbsp;邀请好友</span>
</p>
</div>
<div class="list-item">
<div class="member-box">
<div class="view-box">
<i class="iconfont icon-sousuo i-sousuo" />
<input type="text" placeholder="搜索群成员" v-model="keywords" />
</div>
<el-row class="row-header">
<el-col :span="11">昵称</el-col>
<el-col :span="8">名片</el-col>
<el-col :span="5">性别</el-col>
</el-row>
<template v-if="searchs.length == 0">
<el-row class="row-items">
<el-col :span="24">
<p style="text-align:center;">无数据</p>
</el-col>
</el-row>
</template>
<template v-else>
<el-row
v-for="member in searchs"
:key="member.user_id"
class="row-items"
@click.native="openUserDetail(member.user_id)"
>
<el-col :span="11">
<img
width="20px"
:src="member.avatar"
:onerror="$store.state.detaultAvatar"
/>
<span class="nickname">{{ member.nickname }}</span>
</el-col>
<el-col :span="8">
<span>{{ member.visit_card ? member.visit_card : '-' }}</span>
</el-col>
<el-col :span="5">
<span v-if="member.gender == 1"></span>
<span v-else-if="member.gender == 2"></span>
<span v-else>未知</span>
</el-col>
</el-row>
</template>
</div>
</div>
</el-main>
<el-footer class="footer">
<button @click="isShowSignout = true">退出该群聊</button>
</el-footer>
<!-- 退群提示层 -->
<div class="signout-box no-select" v-show="isShowSignout">
<p v-show="signoutStatus == 0">您确认退出当前群聊吗</p>
<p v-show="signoutStatus == 0">退群后群聊信息将不能查看</p>
<p v-show="signoutStatus == 0" class="signout-btn">
<button @click="signout">确认</button>
<button @click="isShowSignout = false">取消</button>
</p>
<p v-show="signoutStatus == 1" class="signout-btn mt38">
<span style="color: #ccc">
<i class="el-icon-loading" />
正在退出群聊...
</span>
</p>
<p v-show="signoutStatus == 2" class="signout-btn mt38">
<span style="color: #cccccc">退出群聊失败请3(s)后再试...</span>
</p>
<p v-show="signoutStatus == 3" class="signout-btn mt38">
<span style="color: #339e19">
<i class="iconfont icon-success_no_circle" /> 已成功退出群聊...
</span>
</p>
</div>
<!-- 查看好友用户信息 -->
<UserBusinessCard ref="userBusinessCard" />
<!-- 邀请好友组件 -->
<transition name="el-fade-in-linear">
<GroupLaunch
v-if="inviteFriendBox"
:group-id="groupId"
@close="inviteFriendBox = false"
@invite-success="inviteSuccess"
/>
</transition>
<!-- 群管理组件 -->
<transition name="el-fade-in-linear">
<GroupManage
v-if="isShowManager"
:group-id="groupId"
@close="isShowManager = false"
/>
</transition>
<!-- 群公告组件 -->
<transition name="el-fade-in-linear">
<GroupNotice
v-if="isShowGroupNotice"
:group-id="groupId"
@close="isShowGroupNotice = false"
/>
</transition>
</el-container>
</template>
<script>
import { ServeSetNotDisturb } from '@/api/chat'
import {
ServeGroupDetail,
ServeUpdateGroupCard,
ServeSecedeGroup,
ServeGetGroupMembers,
} from '@/api/group'
//创建群聊组件
import GroupLaunch from '@/components/group/GroupLaunch'
import UserBusinessCard from '@/components/user/UserBusinessCard'
import GroupManage from '@/components/group/GroupManage'
import GroupNotice from '@/components/group/GroupNotice'
export default {
name: 'GroupPanel',
components: {
GroupLaunch,
UserBusinessCard,
GroupManage,
GroupNotice,
},
props: {
groupId: {
type: [String, Number],
default: null,
},
},
data() {
return {
detail: {
groupAvatar: '',
groupId: 0,
groupName: '',
groupOwner: '',
groupProfile: '',
disturb: 0,
no_message: false,
visitCard: '',
is_manager: false,
group_notice: [],
},
keywords: '',
members: [],
isEditRemark: false,
editRemarkText: '',
inviteFriendBox: false,
isShowSignout: false,
signoutStatus: 0,
disturbDisabled: false,
// 是否显示群管理窗口
isShowManager: false,
// 是否显示群公告窗口
isShowGroupNotice: false,
}
},
watch: {
groupId: function(value) {
if (value > 0) {
this.loadGroupDetail()
this.loadMembers()
}
},
},
computed: {
searchs() {
return this.keywords == ''
? this.members
: this.members.filter(item => {
return (
item.nickname.match(this.keywords) != null ||
item.user_card.match(this.keywords) != null
)
})
},
},
created() {
if (parseInt(this.groupId) > 0) {
this.loadGroupDetail()
this.loadMembers()
}
},
methods: {
// 加载群组成员列表
loadMembers() {
ServeGetGroupMembers({
group_id: this.groupId,
}).then(res => {
if (res.code == 200) {
this.members = res.data
this.$emit('group-info', {
group_id: this.members.groupId,
members_num: this.members.length,
})
}
})
},
// 加载群信息
loadGroupDetail() {
this.isEditRemark = false
ServeGroupDetail({
group_id: this.groupId,
}).then(res => {
if (res.code == 200) {
let result = res.data
this.detail.groupAvatar = result.avatar
this.detail.groupId = result.group_id
this.detail.userId = res.data.user_id
this.detail.groupName = result.group_name
this.detail.groupOwner = result.manager_nickname
this.detail.groupProfile = result.group_profile
this.detail.disturb = result.not_disturb == 1 ? true : false
this.detail.visitCard = result.visit_card
this.detail.is_manager = result.is_manager
if (result.notice) {
this.detail.group_notice = result.notice
}
}
})
},
// 设置群免打扰状态
editDisturb(value) {
this.disturbDisabled = true
ServeSetNotDisturb({
type: 2,
receive_id: this.groupId,
not_disturb: value ? 1 : 0,
})
.then(res => {
if (res.code == 200) {
this.$emit('disturb-change', {
group_id: this.groupId,
status: value ? 1 : 0,
})
} else {
this.detail.disturb = value ? 0 : 1
}
})
.catch(() => {
this.detail.disturb = value ? 0 : 1
})
.finally(() => {
this.disturbDisabled = false
})
},
// 设置用户群名片
editRemark() {
if (this.editRemarkText == '') {
this.isEditRemark = false
return
}
if (this.detail.visitCard == this.editRemarkText) {
this.isEditRemark = false
return
}
ServeUpdateGroupCard({
group_id: this.groupId,
visit_card: this.editRemarkText,
}).then(res => {
if (res.code == 200) {
this.isEditRemark = false
this.detail.visitCard = this.editRemarkText
}
})
},
// 查看用户信息
openUserDetail(user_id) {
this.$refs.userBusinessCard.open(user_id)
},
// 邀请好友加入群聊
addGroupMembers() {
sessionStorage.setItem('invite_group_id', this.detail.groupId)
this.inviteFriendBox = true
},
// 邀请好友成功之后的回调事件
inviteSuccess() {
this.inviteFriendBox = false
this.loadMembers()
this.$notify({
title: '邀请成功',
message: `好友已成功加入群组...`,
type: 'success',
})
},
// 发送群聊
sendGroup() {
this.$emit('send-group', this.detail.groupId)
},
// 退出群操操作
signout() {
this.signoutStatus = 1
ServeSecedeGroup({
group_id: this.detail.groupId,
})
.then(res => {
if (res.code == 200) {
this.signoutStatus = 3
setTimeout(() => {
this.signoutStatus = 0
this.isShowSignout = false
this.$emit('quit-group')
}, 1500)
} else {
this.signoutStatus = 2
setTimeout(() => {
this.signoutStatus = 0
}, 3000)
}
})
.catch(() => {
this.signoutStatus = 2
setTimeout(() => {
this.signoutStatus = 0
}, 3000)
})
},
},
}
</script>
<style lang="less" scoped>
.container {
height: 100%;
.header {
height: 60px;
line-height: 60px;
padding: 0;
text-align: center;
box-shadow: -1px 0px 5px 0px #cccccc45;
position: relative;
span {
font-size: 16px;
font-weight: 400;
}
.icon-send {
position: absolute;
left: 15px;
top: 22px;
font-size: 18px;
cursor: pointer;
}
.el-icon-close {
position: absolute;
right: 15px;
top: 22px;
font-size: 18px;
cursor: pointer;
}
}
.main {
padding: 0;
}
}
.list-item {
position: relative;
padding: 16px 16px 0;
min-height: 18px;
font-size: 14px;
&.flex {
display: flex;
justify-content: space-between;
}
.edit-visit-card {
position: initial;
color: #a29f9f;
}
.edit-remark-icon {
margin-left: 5px;
color: rgb(169, 184, 187);
position: absolute;
top: 20px;
cursor: pointer;
}
.edit-input {
width: 46%;
height: 25px;
line-height: 25px;
border: 1px solid #92cbff;
padding-left: 5px;
border-radius: 3px;
}
.input-submit {
width: 55px;
text-align: center;
height: 25px;
line-height: 25px;
background-color: #008cee;
border-radius: 2px;
display: inline-block;
color: #fff !important;
font-size: 12px;
margin-left: 10px;
cursor: pointer;
}
.group-setting-title {
max-width: 250px;
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: bottom;
}
.group-boss-name {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
max-width: 180px;
position: relative;
top: 4px;
}
.group-invite {
height: 30px;
line-height: 30px;
text-align: center;
color: #877272;
cursor: pointer;
border-radius: 2px;
border: 1px dashed #d9bbbb;
font-size: 13px;
transition: all 0.5s ease-in-out;
&:hover {
color: #ff5722;
border-color: #ff5722;
transform: scale(1.01);
}
}
.more {
color: #409eff;
cursor: pointer;
font-size: 12px;
}
}
.list-item-tips {
font-size: 12px;
color: #b1b1b1;
margin-top: 5px;
padding-left: 16px;
padding-right: 16px;
font-weight: 300;
overflow: hidden;
word-break: break-word;
}
.group-notice {
line-height: 22px;
}
.member-box {
min-height: 180px;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #ecebeb;
border-radius: 3px;
.view-box {
width: 100%;
height: 30px;
margin-top: 20px;
margin-bottom: 15px;
position: relative;
input {
width: calc(100% - 40px);
height: 30px;
line-height: 28px;
border-radius: 3px;
border: 1px solid #f1e9e9;
color: #b3b0b0;
font-size: 13px;
padding: 0 10px 0 30px;
&::-webkit-input-placeholder {
color: #ccc9c9;
font-size: 13px;
}
}
.i-sousuo {
color: rgb(179, 176, 176);
position: absolute;
left: 10px;
top: 9px;
}
span {
position: relative;
i {
font-size: 24px;
top: -3px;
left: 10px;
position: absolute;
color: #ccc;
}
}
}
.row-header {
width: 100%;
height: 30px;
margin-bottom: 10px;
border-bottom: 1px solid #e0dddd;
div {
height: 30px;
line-height: 30px;
&:nth-child(2) {
text-align: center;
}
&:nth-child(3) {
text-align: right;
}
}
}
.row-items {
width: 100%;
height: 30px;
margin-bottom: 3px;
font-size: 12px;
cursor: pointer;
&:hover {
background: #f6f6f6;
}
div {
height: 30px;
line-height: 30px;
&:nth-child(2) {
text-align: center;
}
&:nth-child(3) {
text-align: right;
}
}
img {
display: inline-block;
border-radius: 50%;
position: relative;
top: 4px;
}
.nickname {
margin-left: 5px;
&:hover {
color: #3685d6;
}
}
}
}
.signout-box {
width: 100%;
height: 100px;
background: #ffffff;
position: absolute;
z-index: 2;
bottom: 0;
box-shadow: -1px -3px 11px 0px #cccccc82;
-webkit-animation: showFooter 0.35s ease-in-out;
-moz-animation: showFooter 0.35s ease-in-out;
animation: showFooter 0.35s ease-in-out;
p {
&:first-child {
text-align: center;
height: 35px;
line-height: 35px;
}
&:nth-child(2) {
text-align: center;
font-size: 12px;
color: #cccccc;
}
}
.mt38 {
margin-top: 38px;
}
}
.signout-btn {
text-align: center;
margin-top: 10px;
button {
height: 30px;
width: 90px;
line-height: 30px;
background: #007fbb;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
&:first-child {
background: #ff3333;
color: white;
}
&:last-child {
background: #f1eded;
}
}
}
.container .footer {
height: 60px;
padding: 0;
line-height: 60px;
text-align: center;
background-color: #f8f8f8;
button {
width: 180px;
height: 35px;
line-height: 35px;
background: #ed3c3b;
border-radius: 3px;
color: white;
cursor: pointer;
font-size: 12px;
margin: auto auto;
&:active {
background: #f5b8b8;
}
}
}
@keyframes showFooter {
0% {
transform: translateY(75px);
}
to {
transform: translateY(0);
}
}
@-webkit-keyframes showFooter {
0% {
-webkit-transform: translateY(75px);
}
to {
-webkit-transform: translateY(0);
}
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div>
<div class="abs-module" v-show="isShow">
<div class="abs-box">
<i class="el-icon-circle-close" @click="close" />
<a href="https://www.aliyun.com/1111/new?userCode=kqyyppx2">
<img src="~@/assets/image/aliyun-abs.jpg" width="300" />
</a>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isShow: false,
}
},
created() {
if (this.getNum() <= 2) {
setTimeout(() => {
this.isShow = true
}, 1000 * 60 * 2)
}
},
methods: {
getNum() {
return parseInt(sessionStorage.getItem('ABS_BOX')) || 0
},
close() {
sessionStorage.setItem('ABS_BOX', this.getNum() + 1)
this.isShow = false
},
},
}
</script>
<style lang="less" scoped>
.abs-module {
position: fixed;
width: 300px;
height: 163.63px;
right: 20px;
top: 20px;
border-radius: 5px;
z-index: 9999;
overflow: hidden;
transition: all 2s;
animation: absfade 1000ms infinite;
.abs-box {
width: 100%;
height: 100%;
position: relative;
i {
position: absolute;
right: 10px;
top: 10px;
color: white;
cursor: pointer;
font-size: 22px;
}
}
}
@keyframes absfade {
from {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
to {
transform: scale(1);
}
}
</style>

View File

@@ -0,0 +1,246 @@
<template>
<div class="lum-dialog-mask">
<el-container class="lum-dialog-box">
<el-header class="header" height="50px">
<p>选择头像</p>
<p class="tools">
<i class="el-icon-close" @click="$emit('close', 0)" />
</p>
</el-header>
<el-main class="main">
<el-container class="full-height">
<el-aside width="400px">
<div class="cropper-box">
<vue-cropper
ref="cropper"
mode="cover"
:img="option.img"
:output-size="option.size"
:output-type="option.outputType"
:info="true"
:full="option.full"
:fixed="fixed"
:fixed-number="fixedNumber"
:can-move="option.canMove"
:can-move-box="option.canMoveBox"
:fixed-box="option.fixedBox"
:original="option.original"
:auto-crop="option.autoCrop"
:auto-crop-width="option.autoCropWidth"
:auto-crop-height="option.autoCropHeight"
:center-box="option.centerBox"
:high="option.high"
@real-time="realTime"
/>
<input
type="file"
id="uploads"
ref="fileInput"
accept="image/png, image/jpeg, image/jpg"
style="display: none"
@change="uploadImg($event, 1)"
/>
</div>
<div class="tools tools-flex">
<el-button
size="small"
plain
icon="el-icon-upload"
@click="clickUpload"
>上传图片
</el-button>
<el-button
size="small"
plain
icon="el-icon-refresh"
@click="refreshCrop"
>刷新
</el-button>
<el-button
size="small"
plain
icon="el-icon-refresh-left"
@click="rotateLeft"
>左转
</el-button>
<el-button
size="small"
plain
icon="el-icon-refresh-right"
@click="rotateRight"
>右转
</el-button>
</div>
</el-aside>
<el-main class="no-padding">
<div class="cropper-box">
<div class="preview-img">
<img v-if="cusPreviewsImg" :src="cusPreviewsImg" />
</div>
</div>
<div class="tools">
<el-button type="primary" size="small" @click="uploadService">
保存图片
</el-button>
</div>
</el-main>
</el-container>
</el-main>
</el-container>
</div>
</template>
<script>
import { VueCropper } from 'vue-cropper'
import { ServeUploadFileStream } from '@/api/upload'
export default {
name: 'AvatarCropper',
components: {
VueCropper,
},
data() {
return {
cusPreviewsImg: '',
previews: {},
option: {
img: '',
size: 1,
full: false,
outputType: 'png',
canMove: true,
fixedBox: true,
original: false,
canMoveBox: true,
autoCrop: true,
// 只有自动截图开启 宽度高度才生效
autoCropWidth: 200,
autoCropHeight: 150,
centerBox: false,
high: true,
},
fixed: true,
fixedNumber: [1, 1],
}
},
methods: {
clickUpload() {
this.$refs.fileInput.click()
},
clearCrop() {
if (!this.cusPreviewsImg) return false
this.$refs.cropper.clearCrop()
},
refreshCrop() {
if (!this.cusPreviewsImg) return false
this.$refs.cropper.refresh()
},
rotateLeft() {
if (!this.cusPreviewsImg) return false
this.$refs.cropper.rotateLeft()
},
rotateRight() {
if (!this.cusPreviewsImg) return false
this.$refs.cropper.rotateRight()
},
// 实时预览函数
realTime() {
this.$refs.cropper.getCropData(img => {
this.cusPreviewsImg = img
})
},
// 上传回调事件
uploadImg(e, num) {
let file = e.target.files[0]
if (!/\.(gif|jpg|jpeg|png|bmp|GIF|JPG|PNG)$/.test(e.target.value)) {
this.$message('图片类型必须是.gif,jpeg,jpg,png,bmp中的一种')
return false
}
let reader = new FileReader()
reader.onload = e => {
let data
if (typeof e.target.result === 'object') {
// 把Array Buffer转化为blob 如果是base64不需要
data = window.URL.createObjectURL(new Blob([e.target.result]))
} else {
data = e.target.result
}
if (num === 1) {
this.option.img = data
} else if (num === 2) {
this.example2.img = data
}
}
// 转化为base64
// reader.readAsDataURL(file)
// 转化为blob
reader.readAsArrayBuffer(file)
},
// 上传图片到服务器
uploadService() {
if (this.cusPreviewsImg == '') return
ServeUploadFileStream({
fileStream: this.cusPreviewsImg,
})
.then(res => {
if (res.code == 200) {
this.$emit('close', 1, res.data.avatar)
} else {
this.$message('文件上传失败,请稍后再试...')
}
})
.catch(() => {
this.$message('文件上传失败,请稍后再试...')
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
height: 550px;
max-width: 800px;
.main {
.cropper-box {
height: 400px;
display: flex;
justify-content: center;
align-items: center;
.preview-img {
width: 180px;
height: 180px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 0 4px #ccc;
img {
width: 100%;
height: 100%;
}
}
}
.tools {
height: 40px;
margin-top: 20px;
text-align: center;
button {
border-radius: 1px;
}
}
.tools-flex {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div>
<div class="reward" v-show="isShow">
<div class="title">
<span>Donate</span>
<i class="el-icon-circle-close" @click="close" />
</div>
<div class="main">
<div class="pay-box">
<img
src="https://cdn.learnku.com/uploads/images/202101/30/46424/PPYHOUhCb4.jpg"
/>
<p>支付宝</p>
</div>
<div class="pay-box">
<img
src="https://cdn.learnku.com/uploads/images/202101/30/46424/XLmCJjbvlQ.png"
/>
<p>微信</p>
</div>
</div>
<div class="footer">
开源不易如果你觉得项目对你有帮助可以请作者喝杯咖啡鼓励下...
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isShow: false,
}
},
created() {
if (this.getNum() <= 3) {
setTimeout(() => {
this.isShow = true
}, 1000 * 30)
}
},
methods: {
getNum() {
return parseInt(localStorage.getItem('REWARD_BOX')) || 0
},
close() {
localStorage.setItem('REWARD_BOX', this.getNum() + 1)
this.isShow = false
},
},
}
</script>
<style lang="less" scoped>
.reward {
position: fixed;
width: 550px;
height: 400px;
right: 20px;
bottom: 20px;
border-radius: 5px;
box-shadow: 0 0 12px #ccc;
border: 1px solid rgb(228, 225, 225);
box-sizing: border-box;
overflow: hidden;
user-select: none;
z-index: 9999;
background: white;
.title {
height: 50px;
line-height: 50px;
padding-left: 20px;
width: 100%;
font-size: 16px;
background: #f9f7f7;
position: relative;
box-sizing: border-box;
i {
position: absolute;
right: 15px;
top: 18px;
font-size: 18px;
cursor: pointer;
}
}
.main {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.pay-box {
width: 200px;
height: 240px;
background: #1977ff;
margin: 0 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 5px;
img {
width: 150px;
height: 150px;
}
p {
margin-top: 20px;
color: white;
}
&:last-child {
background: #22ab38;
}
}
}
.footer {
height: 50px;
line-height: 50px;
text-align: center;
font-size: 13px;
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<div></div>
</template>
<script>
// 待开发
export default {
data() {
return {
skins: [
{
theme: '',
class: 'default',
text: '默认',
},
{
theme: '',
class: 'red',
text: '红色',
},
{
theme: '',
class: 'dark',
text: '暗黑',
},
{
theme: '',
class: 'blue',
text: '浅蓝',
},
],
}
},
created() {},
methods: {},
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,57 @@
<template>
<div class="welcome-box">
<div class="famous-box">
<img src="~@/assets/image/chat.png" width="300" />
<p v-if="text">
{{ text }}
</p>
<p v-else>
不是井里没有水而是你挖的不够深<br />
不是成功来得慢而是你努力的不够多<br />
加油吧 ......
</p>
</div>
</div>
</template>
<script>
export default {
props: {
text: {
type: String,
default: ''
}
},
components: {},
data() {
return {}
},
created() {},
methods: {},
}
</script>
<style lang="less" scoped>
.welcome-box {
height: 100%;
width: 100%;
.famous-box {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100%;
font-size: 24px;
user-select: none;
p {
width: 100%;
font-weight: 300;
text-align: center;
font-size: 15px;
color: #b9b4b4;
margin-top: -30px;
}
}
}
</style>

View File

@@ -0,0 +1,246 @@
<template>
<div>
<p>笔记附件列表({{ files.length }})</p>
<div class="annex-box lum-scrollbar">
<input ref="uploads" type="file" @change="uploadAnnex" />
<div class="annex-main">
<p v-show="files.length == 0" class="empty-text">
暂无附件
</p>
<div v-for="(file, i) in files" :key="file.id" class="file-item">
<div class="suffix">{{ file.file_suffix }}</div>
<div class="content">
<div class="filename">{{ file.original_name }}</div>
<div class="filetool">
<span>{{ formateTime(file.created_at) }}</span>
<span class="size">
{{ formateSize(file.file_size) }}
</span>
<div class="tools">
<i class="el-icon-download" @click="downloadAnnex(file.id)" />
<i class="el-icon-delete" @click="deleteAnnex(file.id, i)" />
</div>
</div>
</div>
</div>
</div>
<div class="annex-footer">
<p class="notice-text">最多可支持上传{{ maxNum }}个附件</p>
<el-button
type="primary"
size="small"
icon="el-icon-upload"
:disabled="files.length >= maxNum"
:loading="loadStatus"
@click="$refs.uploads.click()"
>{{ loadStatus ? '上传中...' : '上传附件' }}
</el-button>
</div>
</div>
</div>
</template>
<script>
import {
ServeDeleteArticleAnnex,
ServeDownloadAnnex as downloadAnnex,
ServeUploadArticleAnnex,
} from '@/api/article'
import { formateSize, formateTime, parseTime } from '@/utils/functions'
export default {
name: 'NoteAnnexBox',
props: {
id: {
type: Number,
default: 0,
},
files: {
type: Array,
default() {
return []
},
},
},
data() {
return {
loadStatus: false,
disabled: false,
maxNum: 10,
}
},
methods: {
// 格式化文件大小
formateSize,
// 格式化时间显示格式
formateTime,
// 下载笔记附件
downloadAnnex,
// 删除笔记附件
deleteAnnex(annex_id, index) {
ServeDeleteArticleAnnex({
annex_id,
}).then(({ code }) => {
if (code == 200) {
this.$delete(this.files, index)
}
})
},
// 上传笔记附件文件
uploadAnnex(e) {
if (e.target.files.length == 0) {
return false
}
let file = e.target.files[0]
if (file.size / (1024 * 1024) > 5) {
this.$message('笔记附件不能大于5M!')
return false
}
let fileData = new FormData()
fileData.append('annex', file)
fileData.append('article_id', this.id)
this.loadStatus = true
ServeUploadArticleAnnex(fileData)
.then(({ code, data }) => {
if (code == 200) {
this.files.push({
id: data.id,
original_name: data.original_name,
created_at: parseTime(new Date()),
file_size: data.file_size,
file_suffix: data.file_suffix,
})
}
})
.finally(() => {
this.loadStatus = false
})
},
},
}
</script>
<style lang="less" scoped>
/* 文件管理弹出层 */
.annex-box {
width: 300px;
min-height: 50px;
max-height: 675px;
background-color: white;
overflow-y: auto;
.annex-main {
min-height: 30px;
border-bottom: 1px solid rgb(239, 233, 233);
margin-bottom: 8px;
padding: 5px 0;
.empty-text {
color: #969292;
font-size: 12px;
margin-top: 10px;
}
.file-item {
height: 50px;
margin-bottom: 5px;
margin-top: 10px;
.suffix {
width: 50px;
height: 100%;
background-color: #ffcc80;
border-radius: 3px;
float: left;
line-height: 50px;
text-align: center;
color: white;
}
.content {
float: left;
width: 247px;
height: 100%;
.filename {
padding-left: 5px;
color: #172b4d;
font-size: 14px;
font-weight: 400;
line-height: 1.6;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.filetool {
color: #505f79;
font-size: 12px;
font-weight: 400;
line-height: 1.6;
padding-left: 5px;
margin-top: 9px;
position: relative;
span {
margin: 0 3px;
&.size {
color: #3a8ee6;
}
}
}
.tools {
position: absolute;
top: -5px;
right: 5px;
width: 55px;
height: 24px;
text-align: right;
line-height: 28px;
i {
font-size: 16px;
cursor: pointer;
margin-right: 5px;
}
.el-icon-download {
color: #66b1ff;
}
.el-icon-delete {
color: red;
}
}
}
}
}
input {
display: none;
}
.annex-footer {
.notice-text {
font-size: 12px;
color: #ccc;
text-align: left;
float: left;
padding-top: 10px;
}
button {
float: right;
}
}
}
</style>

View File

@@ -0,0 +1,206 @@
<template>
<div class="lum-dialog-mask">
<el-container
class="lum-dialog-box animated bounceInDown"
v-outside="close"
>
<el-header height="60px" class="header no-select">
<p> 30 天删除的附件({{ tableData.length }})</p>
<p class="tools">
<i class="el-icon-close" @click="close" />
</p>
</el-header>
<el-main class="main lum-scrollbar">
<el-table :data="tableData" size="mini">
<div slot="empty">暂无相关数据</div>
<el-table-column
prop="original_name"
label="附件名称"
width="180"
:show-overflow-tooltip="true"
>
<template slot-scope="scope">
<el-button type="text" @click="downloadAnnex(scope.row.id)">{{
scope.row.original_name
}}</el-button>
</template>
</el-table-column>
<el-table-column
prop="title"
label="所属笔记"
:show-overflow-tooltip="true"
>
</el-table-column>
<el-table-column
prop="day"
label="剩余天数"
align="center"
width="80"
>
</el-table-column>
<el-table-column
fixed="right"
label="操作"
width="100"
align="center"
>
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="recoverAnnex(scope.row)"
>恢复</el-button
>
<el-popover
placement="top"
@hide="lock(false)"
@show="lock(true)"
:ref="`popover-${scope.$index}`"
>
<p style="margin-bottom: 10px">
{{
scope.row.original_name
}}附件您确定要永久删除吗<br />
</p>
<div style="text-align: right; margin: 0">
<el-button
size="mini"
type="text"
@click="
scope._self.$refs[`popover-${scope.$index}`].doClose()
"
>
取消</el-button
>
<el-button
type="primary"
size="mini"
@click="deleteAnnex(scope.row, scope.$index)"
>确定</el-button
>
</div>
<el-button
slot="reference"
type="text"
size="small"
style="color: #ec5252; margin-left: 5px"
>删除</el-button
>
</el-popover>
</template>
</el-table-column>
</el-table>
</el-main>
<el-footer class="footer" height="30px">
<p class="footer-tip">移动至回收站的附件和笔记将在 30 天后自动清除</p>
</el-footer>
</el-container>
</div>
</template>
<script>
import Vue from 'vue'
import { Table, TableColumn } from 'element-ui'
Vue.use(Table)
Vue.use(TableColumn)
import {
ServeGetRecoverAnnexList,
ServeRecoverArticleAnnex,
ServeDownloadAnnex,
ServeForeverDeleteAnnex,
} from '@/api/article'
export default {
name: 'NoteAnnexRecycle',
data() {
return {
tableData: [],
closeLock: false,
}
},
created() {
this.loadList()
},
methods: {
loadList() {
ServeGetRecoverAnnexList().then(res => {
if (res.code == 200) {
this.tableData = res.data.rows
}
})
},
// 关闭当前窗口
close() {
if (!this.closeLock) this.$emit('close')
},
// 给遮罩层加锁
lock(value) {
this.closeLock = value
},
// 恢复附件
recoverAnnex(data, index) {
ServeRecoverArticleAnnex({
annex_id: data.id,
}).then(res => {
if (res.code == 200) {
this.tableData.splice(index, 1)
}
})
},
//永久删除附件
deleteAnnex(data, index) {
ServeForeverDeleteAnnex({
annex_id: data.id,
})
.then(res => {
this.$refs[`popover-${index}`].doClose()
this.lock(false)
if (res.code == 200) {
this.tableData.splice(index, 1)
} else {
this.$notify({
message: '附件删除失败...',
})
}
})
.catch(() => {
this.$refs[`popover-${index}`].doClose()
this.lock(false)
})
},
//下载笔记附件
downloadAnnex: ServeDownloadAnnex,
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 700px;
height: 80%;
max-width: 700px;
.main {
padding-top: 0;
}
.footer {
.footer-tip {
color: #a7afbc;
font-size: 12px;
font-weight: 400;
line-height: 1.6;
}
}
}
/deep/ .tab-header-row .cell {
font-size: 14px;
font-weight: 400;
color: rgb(172, 167, 167);
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<div class="tag-manage">
<div class="title">
<span>已选择</span>
</div>
<div class="tag-groups">
<p
v-for="(tag, i) in tags"
v-show="tag.isSelectd"
:key="i"
class="tag-item"
>
<span>{{ tag.name }}</span>
<i class="el-icon-close" @click="active(tag.id, tag.isSelectd)" />
</p>
</div>
<div class="title">
<span>标签栏</span>
</div>
<div class="tag-groups">
<p
v-for="(tag, i) in tags"
:key="i"
class="tag-item"
:class="{ active: tag.isSelectd }"
@click="active(tag.id, tag.isSelectd)"
>
<span>{{ tag.name }}</span>
</p>
</div>
<el-button
v-show="!isInput"
type="primary"
class="addbtn"
@click="isInput = !isInput"
>+ 添加标签
</el-button>
<div class="tag-input" v-show="isInput">
<input
ref="editTaginput"
type="text"
placeholder="回车保存..."
v-model.trim="tagText"
@keyup.enter="save"
/>
<el-button type="primary" size="small" @click="isInput = false"
>取消编辑
</el-button>
</div>
</div>
</template>
<script>
import { ServeUpdateArticleTag, ServeEditArticleTag } from '@/api/article'
export default {
name: 'NoteTagBox',
props: {
note_id: {
type: Number,
default: 0,
},
tag_ids: {
type: Array,
default() {
return []
},
},
},
data() {
return {
ids: [],
isInput: false,
tagText: '',
}
},
created() {
this.ids = this.tag_ids.map(tag => tag.id)
},
computed: {
tags() {
let tags = []
this.$store.state.note.tags.forEach(tag => {
tags.push({
id: tag.id,
name: tag.tag_name,
isSelectd: this.ids.includes(tag.id),
})
})
return tags
},
},
methods: {
// 设置笔记标签事件
active(tag_id, isSelect) {
if (isSelect) {
this.ids.forEach((item, index) => {
if (item == tag_id) {
this.ids.splice(index, 1)
}
})
} else {
this.ids.push(tag_id)
}
ServeUpdateArticleTag({
article_id: this.note_id,
tags: this.getSelectTags(),
})
},
// 获取选中的标签ids
getSelectTags() {
let ids = []
for (let item of this.tags) {
if (item.isSelectd) ids.push(item.id)
}
return ids
},
// 保存标签事件
save() {
let tag_name = this.tagText
ServeEditArticleTag({
tag_id: 0,
tag_name,
}).then(({ code, data }) => {
if (code !== 200) return false
this.$store.commit('PUSH_NOTE_TAG', {
id: data.id,
tag_name: tag_name,
count: 0,
})
this.tagText = ''
this.isInput = false
})
},
},
}
</script>
<style lang="less" scoped>
.tag-manage {
.title {
height: 20px;
line-height: 20px;
font-size: 14px;
color: #ccc;
border-bottom: 1px solid #f0e9e9;
padding-bottom: 5px;
position: relative;
}
.tag-groups {
padding: 8px 8px 8px 0;
cursor: pointer;
.tag-item {
display: inline-block;
height: 25px;
line-height: 25px;
padding: 0 10px;
font-size: 12px;
box-sizing: border-box;
white-space: nowrap;
margin: 0 3px 5px 0;
color: #409eff;
background: rgba(64, 158, 255, 0.1);
border-radius: 1px;
i {
cursor: pointer;
margin-left: 5px;
}
&.active {
background: #70b5fb;
color: #ffffff;
}
}
}
.addbtn {
height: 33px;
width: 100%;
margin-top: 20px;
line-height: 9px;
border-radius: 3px;
}
.tag-input {
margin-top: 20px;
min-height: 30px;
display: flex;
align-items: center;
input {
width: 200px;
height: 30px;
border: 1px solid #66b1ff;
padding: 0 5px;
border-radius: 3px;
margin-right: 5px;
&::-webkit-input-placeholder {
font-size: 13px;
color: #ccc;
}
}
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<svg :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
export default {
name: 'svg-icon',
props: {
iconClass: {
type: String,
required: true,
},
className: {
type: String,
default: '',
},
},
computed: {
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
},
}
</script>
<style scoped>
.svg-icon {
fill: currentColor;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,529 @@
<template>
<div v-show="isShow" class="lum-dialog-mask animated fadeIn">
<el-container class="container" v-outside="close">
<el-header class="no-padding header" height="180px">
<i class="close el-icon-error pointer" @click="close" />
<div class="img-banner">
<img :src="userInfo.imgbag" class="img-banner" />
</div>
<div class="user-header">
<div class="avatar">
<div class="avatar-box">
<img
:src="userInfo.avatar"
:onerror="$store.state.detaultAvatar"
/>
</div>
</div>
<div class="nickname">
<i class="iconfont icon-qianming" />
<span>{{ userInfo.nickname || '未设置昵称' }}</span>
<div class="share no-select" @click="contacts = true">
<i class="iconfont icon-fenxiang3" /> <span>分享</span>
</div>
</div>
</div>
</el-header>
<el-main class="no-padding main">
<div class="user-sign">
<div class="sign-arrow"></div>
<i class="iconfont icon-bianji" />
<span>编辑个签展示我的独特态度 </span>
</div>
<div class="card-rows no-select">
<div class="card-row">
<label>手机</label>
<span>{{ mobile(userInfo.mobile) }}</span>
</div>
<div class="card-row">
<label>昵称</label>
<span>{{ userInfo.nickname || '未设置昵称' }}</span>
</div>
<div class="card-row">
<label>性别</label>
<span v-if="userInfo.gender == 1"></span>
<span v-else-if="userInfo.gender == 2"></span>
<span v-else>未知</span>
</div>
<div v-show="userInfo.friendStatus == 2" class="card-row">
<label>备注</label>
<span v-if="!editRemark.isShow">
{{
userInfo.nicknameRemark ? userInfo.nicknameRemark : '暂无备注'
}}
</span>
<span v-else>
<input
v-model="editRemark.text"
v-focus
class="friend-remark"
type="text"
@keyup.enter="editRemarkSubmit"
/>
</span>
<i
v-show="!editRemark.isShow"
class="el-icon-edit-outline"
@click="clickEditRemark"
/>
</div>
<div class="card-row">
<label>邮箱</label>
<span>未设置</span>
</div>
</div>
</el-main>
<el-footer
v-show="userInfo.friendStatus !== 0"
class="no-padding footer"
height="50px"
>
<el-button
v-if="userInfo.friendStatus == 1 && userInfo.friendApply == 0"
type="primary"
size="small"
icon="el-icon-circle-plus-outline"
@click="applyFrom.isShow = true"
>添加好友
</el-button>
<el-button
v-else-if="userInfo.friendApply == 1"
type="primary"
size="small"
>已发送好友申请请耐心等待...
</el-button>
<el-button
v-else-if="userInfo.friendStatus == 2"
type="primary"
size="small"
icon="el-icon-s-promotion"
@click="sendMessage(userInfo)"
>发消息
</el-button>
</el-footer>
<!-- 添加好友申请表单 -->
<div
v-outside="closeApplyFrom"
class="friend-from"
:class="{ 'friend-from-show': applyFrom.isShow }"
>
<p>
<span>请填写好友申请备注</span>
<span @click="closeApplyFrom">取消</span>
</p>
<div>
<input
v-model="applyFrom.text"
type="text"
placeholder="(必填项)"
@keyup.enter="sendApply"
/>
<el-button type="primary" size="small" @click="sendApply">
立即提交
</el-button>
</div>
</div>
</el-container>
<UserContacts
v-if="contacts"
@confirm="confirmContact"
@close="contacts = false"
/>
</div>
</template>
<script>
import UserContacts from '@/components/chat/UserContacts'
import { ServeCreateTalkList } from '@/api/chat'
import { ServeSearchUser } from '@/api/user'
import { ServeCreateContact, ServeEditContactRemark } from '@/api/contacts'
export default {
name: 'UserBusinessCard',
components: {
UserContacts,
},
data() {
return {
isShow: false,
// 用户ID
user_id: 0,
// 用户相关信息
userInfo: {
mobile: '',
nickname: '',
avatar: '',
motto: '',
friendStatus: 0,
friendApply: 0,
nicknameRemark: '',
imgbag: require('@/assets/image/default-user-banner.png'),
gender: 0, //[0:未知;1:男;2:女;默认0]
},
// 好友备注表单
editRemark: {
isShow: false,
text: '',
},
// 好友申请表单
applyFrom: {
isShow: false,
text: '',
},
contacts: false,
}
},
methods: {
// 显示窗口
open(user_id) {
this.isShow = true
this.user_id = user_id
this.findUserDetail()
},
// 关闭窗口
close() {
if (this.contacts) {
return false
}
this.isShow = false
},
// 手机号格式化
mobile(mobile) {
return (
mobile.substr(0, 3) +
' ' +
mobile.substr(3, 4) +
' ' +
mobile.substr(7, 4)
)
},
// 点击编辑备注信息
clickEditRemark() {
this.editRemark.isShow = true
this.editRemark.text = this.userInfo.nicknameRemark
},
// 获取用户信息
findUserDetail() {
ServeSearchUser({
user_id: this.user_id,
}).then(res => {
if (res.code == 200) {
let data = res.data
this.userInfo.user_id = data.id
this.userInfo.mobile = data.mobile
this.userInfo.nickname = data.nickname
this.userInfo.nicknameRemark = data.nickname_remark
this.userInfo.motto = data.motto
this.userInfo.avatar = data.avatar
this.userInfo.friendStatus = data.friend_status
this.userInfo.friendApply = data.friend_apply
this.userInfo.gender = data.gender
}
})
},
// 发送添加好友申请
sendApply() {
if (this.applyFrom.text == '') return
ServeCreateContact({
friend_id: this.userInfo.user_id,
remarks: this.applyFrom.text,
}).then(res => {
if (res.code == 200) {
this.applyFrom.isShow = false
this.applyFrom.text = ''
this.userInfo.friendApply = 1
} else {
alert('发送好友申请失败,请稍后再试...')
}
})
},
// 编辑好友备注信息
editRemarkSubmit() {
let data = {
friend_id: this.userInfo.user_id,
remarks: this.editRemark.text,
}
if (data.remarks == this.userInfo.nicknameRemark) {
this.editRemark.isShow = false
return
}
ServeEditContactRemark(data).then(res => {
if (res.code == 200) {
this.editRemark.isShow = false
this.userInfo.nicknameRemark = data.remarks
this.$emit('changeRemark', data)
}
})
},
// 隐藏申请表单
closeApplyFrom() {
this.applyFrom.isShow = false
},
// 发送好友消息
sendMessage() {
let userInfo = this.userInfo
ServeCreateTalkList({
type: 1,
receive_id: this.user_id,
}).then(res => {
if (res.code !== 200) return
this.$root.dumpTalkPage(`1_${userInfo.user_id}`)
})
},
confirmContact(array) {
this.contacts = false
this.$notify.info({
title: '消息',
message: '分享功能正在开发中...',
})
},
},
}
</script>
<style lang="less" scoped>
.container {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: white;
width: 350px;
height: 600px;
overflow: hidden;
border-radius: 3px;
.header {
position: relative;
.close {
position: absolute;
right: 10px;
top: 10px;
color: white;
transition: all 1s;
z-index: 1;
font-size: 20px;
}
.img-banner {
width: 100%;
height: 100%;
background-image: url(~@/assets/image/default-user-banner.png);
background-size: 100%;
transition: all 0.2s linear;
cursor: pointer;
overflow: hidden;
img:hover {
-webkit-transform: scale(1.1);
transform: scale(1.1);
-webkit-filter: contrast(130%);
filter: contrast(130%);
}
}
}
.main {
background-color: white;
padding: 45px 16px 0;
}
.footer {
display: flex;
justify-content: center;
align-items: center;
border-top: 1px solid #f5eeee;
button {
width: 90%;
}
}
}
.user-header {
width: 100%;
height: 80px;
position: absolute;
bottom: -40px;
display: flex;
flex-direction: row;
.avatar {
width: 100px;
flex-shrink: 0;
display: flex;
justify-content: center;
.avatar-box {
width: 80px;
height: 80px;
background-color: white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
img {
height: 70px;
width: 70px;
border-radius: 50%;
}
}
}
.nickname {
flex: auto;
padding-top: 50px;
font-size: 16px;
font-weight: 400;
span {
margin-left: 5px;
}
.share {
display: inline-flex;
width: 50px;
height: 22px;
background: #ff5722;
color: white;
align-items: center;
justify-content: center;
padding: 3px 8px;
border-radius: 20px;
transform: scale(0.7);
cursor: pointer;
i {
margin-top: 2px;
}
span {
font-size: 14px;
margin-left: 4px;
}
}
}
}
.user-sign {
min-height: 26px;
border-radius: 5px;
padding: 5px;
line-height: 25px;
background: #f3f5f7;
color: #7d7d7d;
font-size: 12px;
margin-bottom: 20px;
position: relative;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
position: relative;
.sign-arrow {
position: absolute;
width: 0;
height: 0;
font-size: 0;
border: 5px solid hsla(0, 0%, 96.9%, 0);
border-bottom-color: #f3f5f7;
left: 28px;
top: -9px;
}
}
.card-rows {
.card-row {
height: 35px;
line-height: 35px;
font-size: 14px;
position: relative;
cursor: pointer;
color: #736f6f;
label {
margin-right: 25px;
color: #cbc5c5;
}
.friend-remark {
border-bottom: 1px dashed #bec3d0;
padding-bottom: 2px;
color: #736f6f;
width: 60%;
padding-right: 5px;
}
.el-icon-edit-outline {
margin-left: 3px !important;
}
}
}
/* 好友申请表单 */
.friend-from {
position: absolute;
background: #fbf6f6;
height: 80px;
z-index: 2;
width: 100%;
bottom: -80px;
left: 0;
transition: all 0.5s ease-in-out;
p {
height: 20px;
line-height: 20px;
padding: 7px 5px 5px 15px;
font-size: 13px;
span {
&:nth-child(2) {
float: right;
margin-right: 13px;
color: #32caff;
cursor: pointer;
}
}
}
div {
height: 31px;
line-height: 20px;
padding: 7px 5px 5px 15px;
font-size: 13px;
}
input {
height: 30px;
line-height: 30px;
width: 220px;
border-radius: 3px;
padding: 0 5px;
margin-right: 5px;
}
}
.friend-from-show {
bottom: 0;
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<div>
<div class="user-card animated fadeIn">
<div class="card-header">
<img :src="$store.state.user.visitCardBag" class="no-select" />
<div class="user-avatar no-select">
<img
:src="$store.state.user.avatar"
:onerror="$store.state.detaultAvatar"
/>
</div>
<div class="user-nickname">
<i class="iconfont icon-qianming" />
<span v-text="$store.state.user.nickname"></span>
</div>
</div>
<div class="card-main">
<div class="usersign">
<div class="arrow"></div>
<span v-if="$store.state.user.signature">
<span style="font-weight: 600">个性签名</span>
{{ $store.state.user.signature }}
</span>
<span v-else>
<i class="iconfont icon-bianji" />
<span>编辑个签展示我的独特态度</span>
</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'UserCard',
}
</script>
<style lang="less" scoped>
.user-card {
width: 320px;
min-height: 300px;
background: #ffffff;
box-shadow: -1px 1px 9px 0px #e6e3e3;
padding-bottom: 10px;
.card-header {
height: 230px;
overflow: hidden;
img {
width: 100%;
height: 180px;
-webkit-transition: all 0.2s linear;
transition: all 0.2s linear;
&:hover {
-webkit-transform: scale(1.1);
transform: scale(1.1);
-webkit-filter: contrast(130%);
filter: contrast(130%);
}
}
.user-avatar {
height: 70px;
width: 70px;
border: 5px solid #fff;
background-color: #fff;
border-radius: 50%;
position: relative;
top: -35px;
margin-left: 15px;
img {
width: 100%;
height: 100%;
border-radius: 50%;
cursor: pointer;
}
}
.user-nickname {
position: relative;
top: -72px;
text-align: left;
margin-left: 105px;
margin-right: 5px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
span {
font-size: 16px;
font-weight: 400;
margin-left: 8px;
}
}
}
}
.user-card .card-main {
margin-top: 10px;
min-height: 50px;
text-align: left;
padding: 0 16px;
.usersign {
min-height: 26px;
border-radius: 5px;
padding: 5px;
line-height: 25px;
background: #f3f5f7;
color: #7d7d7d;
font-size: 12px;
margin-bottom: 20px;
position: relative;
cursor: pointer;
.icon-bianji {
margin-left: 10px;
}
.arrow {
position: absolute;
width: 0;
height: 0;
font-size: 0;
border: solid 5px;
top: 5px;
border-color: rgba(247, 247, 247, 0) rgba(247, 247, 247, 0) #f3f5f7
rgba(247, 247, 247, 0);
top: -10px;
left: 31px;
}
}
}
</style>

View File

@@ -0,0 +1,223 @@
<template>
<div class="lum-dialog-mask" v-show="isShow">
<el-container class="lum-dialog-box" v-outside="close">
<el-header class="header" height="50px">
<p>绑定邮箱</p>
<p class="tools">
<i class="el-icon-close" @click="close" />
</p>
</el-header>
<el-main class="main">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item prop="email" label="邮箱">
<el-input
v-model="form.email"
class="cuborder-radius"
size="medium"
placeholder="请填写邮箱地址"
@keyup.enter.native="onSubmit('form')"
/>
</el-form-item>
<el-form-item prop="sms_code" label="验证码">
<el-input
v-model="form.sms_code"
class="cuborder-radius"
style="width: 185px"
size="medium"
maxlength="6"
placeholder="邮件验证码"
@keyup.enter.native="onSubmit('form')"
/>
<div v-if="smsLock" class="code-btn disable">正在发送 ...</div>
<div
v-else-if="smsLock == false && smsLockObj.time == null"
class="code-btn"
@click="sendSms"
>
获取验证码
</div>
<div v-else class="code-btn disable">
重新发送({{ smsLockObj.time }}s)
</div>
</el-form-item>
<el-form-item prop="password" label="密码">
<el-input
v-model="form.password"
class="cuborder-radius no-border"
type="password"
size="medium"
placeholder="登录密码验证"
@keyup.enter.native="onSubmit('form')"
/>
</el-form-item>
<el-form-item style="margin-top: 40px">
<el-button
class="submit-btn"
type="primary"
size="medium"
:loading="loading"
@click="onSubmit('form')"
>立即修改
</el-button>
</el-form-item>
</el-form>
</el-main>
</el-container>
</div>
</template>
<script>
import SmsLock from '@/plugins/sms-lock'
import { ServeSendEmailCode, ServeUpdateEmail } from '@/api/user'
import { isEmail } from '@/utils/validate'
export default {
name: 'UserEditEmail',
data() {
return {
loading: false,
form: {
email: '',
password: '',
sms_code: '',
},
rules: {
email: [
{
required: true,
message: '请输入邮箱地址',
trigger: 'blur',
},
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: 'blur',
},
],
password: [
{
required: true,
message: '登录密码不能为空!',
trigger: 'blur',
},
],
sms_code: [
{
required: true,
message: '验证码不能为空!',
trigger: 'blur',
},
],
},
smsLock: false,
smsLockObj: null,
isShow: false,
}
},
created() {
this.smsLockObj = new SmsLock('CHANGE_EMAIL_SMS', 120)
},
destroyed() {
this.smsLockObj.clearInterval()
},
methods: {
// 显示窗口
open() {
this.$refs['form'].resetFields()
this.isShow = true
},
// 关闭窗口
close() {
this.isShow = false
},
//点击发送邮件验证码
sendSms() {
if (!isEmail(this.form.email)) {
this.$refs.form.validateField('email')
return false
}
this.smsLock = true
ServeSendEmailCode({
email: this.form.email,
})
.then(res => {
if (res.code == 200) {
this.smsLockObj.start()
}
})
.finally(() => {
this.smsLock = false
})
},
// 表单验证
onSubmit(formName) {
this.$refs[formName].validate(valid => {
if (!valid) return false
this.changeEmail()
})
},
// 提交修改邮箱
changeEmail() {
this.loading = true
ServeUpdateEmail({
email: this.form.email,
email_code: this.form.sms_code,
password: this.form.password,
})
.then(res => {
if (res.code == 200) {
this.$refs['form'].resetFields()
this.$emit('success')
this.close()
this.$notify({
title: '成功',
message: '修改邮箱成功...',
type: 'success',
})
} else {
this.$message(res.message)
}
})
.finally(() => {
this.loading = false
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 450px;
max-width: 450px;
.main {
.code-btn {
width: 140px;
height: 36px;
line-height: 36px;
display: inline-block;
background: #f3ecec;
text-align: center;
color: #777373;
cursor: pointer;
user-select: none;
margin-left: 5px;
&:active {
background: #e4dbdb;
}
&.disable {
cursor: not-allowed !important;
background: #f7f7f7 !important;
color: silver !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<div v-show="isShow" class="lum-dialog-mask">
<el-container class="lum-dialog-box" v-outside="close">
<el-header class="header" height="50px">
<p>绑定手机</p>
<p class="tools">
<i class="close-btn el-icon-close" @click="close" />
</p>
</el-header>
<el-main class="main">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item prop="username" label="手机号">
<el-input
v-model="form.username"
class="cuborder-radius"
maxlength="11"
size="medium"
placeholder="请填写新手机号"
@keyup.enter.native="onSubmit('form')"
/>
</el-form-item>
<el-form-item prop="sms_code" label="验证码">
<el-input
v-model="form.sms_code"
class="cuborder-radius"
style="width: 185px"
maxlength="6"
size="medium"
placeholder="验证码"
@keyup.enter.native="onSubmit('form')"
/>
<div v-if="smsLock" class="send-code-btn disable">正在发送 ...</div>
<div
v-else-if="smsLock == false && smsLockObj.time == null"
class="send-code-btn"
@click="sendSms"
>
获取短信
</div>
<div v-else class="send-code-btn disable">
重新发送({{ smsLockObj.time }}s)
</div>
</el-form-item>
<el-form-item prop="password" label="密码">
<el-input
v-model="form.password"
class="cuborder-radius no-border"
type="password"
size="medium"
placeholder="登录密码验证"
@keyup.enter.native="onSubmit('form')"
/>
</el-form-item>
<el-form-item style="margin-top: 40px">
<el-button
class="submit-btn"
type="primary"
size="medium"
:loading="loading"
@click="onSubmit('form')"
>立即修改
</el-button>
</el-form-item>
</el-form>
</el-main>
</el-container>
</div>
</template>
<script>
import { isMobile } from '@/utils/validate'
import SmsLock from '@/plugins/sms-lock'
import { ServeSendMobileCode, ServeUpdateMobile } from '@/api/user'
export default {
name: 'UserEditMobile',
data() {
let validateMobile = (rule, value, callback) => {
if (value === '') {
callback(new Error('登录手机号不能为空!'))
} else {
if (!isMobile(value)) {
callback(new Error('登录手机号格式不正确!'))
} else {
callback()
}
}
}
return {
loading: false,
form: {
username: '',
password: '',
sms_code: '',
},
rules: {
username: [
{
required: true,
validator: validateMobile,
trigger: 'blur',
},
{
min: 11,
max: 11,
message: '手机号格式不正确!',
trigger: 'blur',
},
],
password: [
{
required: true,
message: '登录密码不能为空!',
trigger: 'blur',
},
],
sms_code: [
{
required: true,
message: '验证码不能为空!',
trigger: 'blur',
},
],
},
smsLock: false,
smsLockObj: null,
isShow: false,
}
},
created() {
this.smsLockObj = new SmsLock('EDIT_MOBILE_SMS', 120)
},
destroyed() {
this.smsLockObj.clearInterval()
},
methods: {
// 显示窗口
open() {
this.$refs['form'].resetFields()
this.isShow = true
},
// 关闭窗口
close() {
this.isShow = false
},
//点击发送验证码
sendSms() {
if (!isMobile(this.form.username)) {
this.$refs.form.validateField('username')
return false
}
this.smsLock = true
ServeSendMobileCode({
mobile: this.form.username,
})
.then(res => {
if (res.code !== 200) {
this.$notify({
title: '提示',
message: res.message,
})
return
}
this.smsLockObj.start()
if (res.data.is_debug) {
this.form.sms_code = res.data.sms_code
setTimeout(() => {
this.$notify({
title: '提示',
message: '已自动填充验证码',
type: 'success',
})
this.form.sms_code = res.data.sms_code
}, 500)
}
})
.finally(() => {
this.smsLock = false
})
},
// 表单验证
onSubmit(formName) {
this.$refs[formName].validate(valid => {
if (!valid) return false
this.changeMobile()
})
},
// 提交修改手机号
changeMobile() {
this.loading = true
ServeUpdateMobile({
mobile: this.form.username,
sms_code: this.form.sms_code,
password: this.form.password,
})
.then(res => {
if (res.code == 200) {
this.$refs['form'].resetFields()
this.$notify({
title: '成功',
message: '更换手机号成功...',
type: 'success',
})
this.$emit('success')
this.close()
} else {
this.$message(res.message)
}
})
.finally(() => {
this.loading = false
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 450px;
max-width: 450px;
.main {
.send-code-btn {
width: 140px;
height: 36px;
line-height: 36px;
display: inline-block;
background: #f3ecec;
text-align: center;
color: #777373;
cursor: pointer;
user-select: none;
margin-left: 5px;
&:active {
background: #e4dbdb;
}
&.disable {
cursor: not-allowed !important;
background: #f7f7f7 !important;
color: silver !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,161 @@
<template>
<!-- 用户密码修改组件 -->
<div class="lum-dialog-mask" v-show="isShow">
<el-container class="lum-dialog-box" v-outside="close">
<el-header class="header" height="50px">
<p>修改密码</p>
<p class="tools">
<i class="el-icon-close" @click="close" />
</p>
</el-header>
<el-main class="main">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item prop="old_password" label="旧密码">
<el-input
v-model="form.old_password"
class="cuborder-radius no-border"
type="password"
size="medium"
placeholder="请填写旧密码"
@keyup.enter.native="onSubmit('form')"
/>
</el-form-item>
<el-form-item prop="new_password" label="新密码">
<el-input
v-model="form.new_password"
class="cuborder-radius no-border"
type="password"
size="medium"
placeholder="请填写新的密码"
@keyup.enter.native="onSubmit('form')"
/>
</el-form-item>
<el-form-item prop="new_password2" label="重复密码">
<el-input
v-model="form.new_password2"
class="cuborder-radius no-border"
size="medium"
type="password"
placeholder="请再次填写新密码"
@keyup.enter.native="onSubmit('form')"
/>
</el-form-item>
<el-form-item style="margin-top: 40px">
<el-button
class="submit-btn"
type="primary"
size="medium"
:loading="loading"
@click="onSubmit('form')"
>立即修改
</el-button>
</el-form-item>
</el-form>
</el-main>
</el-container>
</div>
</template>
<script>
import { ServeUpdatePassword } from '@/api/user'
export default {
name: 'UserEditPassword',
data() {
var validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.form.new_password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
return {
loading: false,
form: {
old_password: '',
new_password: '',
new_password2: '',
},
rules: {
old_password: [
{
required: true,
message: '旧密码不能为空!',
trigger: 'blur',
},
],
new_password: [
{
required: true,
message: '新密码不能为空!',
trigger: 'blur',
},
],
new_password2: [
{
required: true,
validator: validatePass2,
trigger: 'blur',
},
],
},
isShow: false,
}
},
methods: {
// 显示窗口
open() {
this.$refs['form'].resetFields()
this.isShow = true
},
// 关闭窗口
close() {
this.isShow = false
},
// 表单验证
onSubmit(formName) {
this.$refs[formName].validate(valid => {
if (!valid) return false
this.changePassword()
})
},
// 提交修改手机号
changePassword() {
this.loading = true
ServeUpdatePassword({
old_password: this.form.old_password,
new_password: this.form.new_password,
})
.then(res => {
if (res.code == 200) {
this.$refs['form'].resetFields()
this.$notify({
title: '成功',
message: '密码修改成功...',
type: 'success',
})
} else {
this.$message(res.message)
}
this.loading = false
})
.catch(() => {
this.loading = false
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 450px;
max-width: 450px;
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<div>
<div class="lum-dialog-mask" v-show="isShow">
<el-container class="lum-dialog-box" v-outside="close">
<el-header class="header" height="50px">
<p>添加好友</p>
<p class="tools">
<i class="el-icon-close" @click="close" />
</p>
</el-header>
<el-main class="main">
<el-input
v-model="mobile"
id="serach-mobile"
class="input"
prefix-icon="el-icon-search"
placeholder="请输入对方手机号(精确查找)"
clearable
@keyup.enter.native="onSubmit"
@input="error = false"
/>
<p v-show="error" class="error">
无法找到该用户请检查搜索内容并重试
</p>
<el-button
type="primary"
size="small"
:loading="loading"
@click="onSubmit"
>立即查找
</el-button>
</el-main>
</el-container>
</div>
<!-- 查看好友用户信息 -->
<UserBusinessCard ref="userBusinessCard" />
</div>
</template>
<script>
import { ServeSearchContact } from '@/api/contacts'
import UserBusinessCard from '@/components/user/UserBusinessCard'
export default {
name: 'UserSearch',
components: {
UserBusinessCard,
},
data() {
return {
loading: false,
isShow: false,
mobile: '',
error: false,
}
},
methods: {
// 显示窗口
open() {
this.mobile = ''
this.isShow = true
this.$nextTick(() => {
document.getElementById('serach-mobile').focus()
})
},
// 关闭窗口
close() {
this.isShow = false
},
onSubmit() {
let { mobile } = this
if (mobile == '') return false
this.loading = true
ServeSearchContact({
mobile,
})
.then(res => {
if (res.code == 200) {
this.$refs.userBusinessCard.open(res.data.id)
this.close()
} else {
this.error = true
}
})
.finally(() => {
this.loading = false
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 450px;
max-width: 450px;
height: 250px;
.main {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.input {
width: 85%;
}
.error {
width: 85%;
color: red;
font-size: 12px;
height: 50px;
line-height: 50px;
}
button {
margin-top: 20px;
width: 100px;
}
}
}
</style>