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

View File

@@ -0,0 +1,313 @@
<template>
<div class="lum-dialog-mask">
<div class="container animated bounceInDown" :class="{ 'full-screen': isFullScreen }">
<el-container class="full-height">
<el-header class="header no-padding" height="50px">
<div class="tools">
<span>选择编程语言:&nbsp;&nbsp;</span>
<el-select v-model="language" size="mini" filterable placeholder="语言类型" :disabled="!editMode">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<i class="el-icon-close close-btn" @click="close" />
<i class="iconfont icon-full-screen" :class="{
'icon-tuichuquanping': isFullScreen,
'icon-quanping ': !isFullScreen,
}" :title="isFullScreen ? '关闭全屏模式' : '打开全屏模式'" @click="isFullScreen = !isFullScreen" />
</el-header>
<el-main class="main no-padding">
<PrismEditor class="peditor" style="border-radius: 0" :code="code" :language="language" :line-numbers="true"
@change="codeChanged" />
</el-main>
<el-footer class="footer no-padding" height="50px">
<div class="code-num">
<span>代码字数{{ code.length }}</span>
<span v-show="code.length > 10000 && editMode" class="code-warning">
(字数不能超过10000字)
</span>
</div>
<div class="buttom-group">
<el-button size="small" plain @click="close">
{{ editMode ? '取消编辑' : '关闭预览' }}
</el-button>
<el-button v-show="editMode" type="primary" size="small" @click="submit">发送代码
</el-button>
</div>
</el-footer>
</el-container>
</div>
</div>
</template>
<script>
import PrismEditor from "vue-prism-editor";
import "vue-prism-editor/dist/VuePrismEditor.css";
import "prismjs/themes/prism-okaidia.css";
import Vue from "vue";
import { Select, Option } from "element-ui";
Vue.use(Select);
Vue.use(Option);
export default {
name: "TalkCodeBlock",
components: {
PrismEditor,
},
props: {
loadCode: {
type: String,
default: "",
},
loadLang: {
type: String,
default: "",
},
editMode: {
type: Boolean,
default: false,
},
},
data() {
return {
language: "",
code: "",
options: [
{
value: "css",
label: "css",
},
{
value: "less",
label: "less",
},
{
value: "javascript",
label: "javascript",
},
{
value: "json",
label: "json",
},
{
value: "bash",
label: "bash",
},
{
value: "c",
label: "c",
},
{
value: "cil",
label: "cil",
},
{
value: "docker",
label: "docker",
},
{
value: "git",
label: "git",
},
{
value: "go",
label: "go",
},
{
value: "java",
label: "java",
},
{
value: "lua",
label: "lua",
},
{
value: "nginx",
label: "nginx",
},
{
value: "objectivec",
label: "objectivec",
},
{
value: "php",
label: "php",
},
{
value: "python",
label: "python",
},
{
value: "ruby",
label: "ruby",
},
{
value: "rust",
label: "rust",
},
{
value: "sql",
label: "sql",
},
{
value: "swift",
label: "swift",
},
{
value: "vim",
label: "vim",
},
{
value: "visual-basic",
label: "visual-basic",
},
{
value: "shell",
label: "shell",
},
],
isFullScreen: false,
};
},
watch: {
loadCode(value) {
this.code = value;
},
loadLang(value) {
this.language = value;
},
},
created() {
this.code = this.loadCode;
this.language = this.loadLang;
},
methods: {
submit() {
if (!this.code) {
this.$message.error("代码块不能为空...");
return false;
}
if (this.language == "") {
this.$message.error("请选择语言");
return false;
}
if (this.code.length > 10000) {
this.$message.error("代码字数不能超过10000字");
return false;
}
this.$emit("confirm", {
language: this.language,
code: this.code,
});
},
close() {
this.$emit("close");
},
codeChanged(code) {
this.code = code;
},
},
};
</script>
<style lang="less" scoped>
.container {
width: 80%;
max-width: 800px;
height: 600px;
overflow: hidden;
box-shadow: 0 2px 8px 0 rgba(31, 35, 41, 0.2);
transition: 0.5s ease;
background: #2d2d2d;
.header {
position: relative;
background-color: white;
.close-btn {
position: absolute;
right: 12px;
top: 13px;
font-size: 24px;
cursor: pointer;
}
.icon-full-screen {
position: absolute;
right: 45px;
top: 13px;
font-size: 20px;
cursor: pointer;
}
.tools {
line-height: 50px;
padding-left: 10px;
}
}
.footer {
background-color: #3c3c3c;
padding-right: 20px;
line-height: 50px;
.code-num {
float: left;
color: white;
padding-left: 10px;
font-size: 14px;
}
.code-warning {
color: red;
}
.buttom-group {
float: right;
height: 100%;
line-height: 50px;
text-align: right;
button {
border-radius: 0;
}
}
}
}
.full-screen {
width: 100%;
height: 100%;
max-width: 100%;
}
/deep/ .el-input__inner {
border-radius: 0;
width: 130px;
}
/deep/ pre {
border-radius: 0;
}
/deep/ .prism-editor-wrapper pre::-webkit-scrollbar {
background-color: #272822;
}
/deep/ .prism-editor-wrapper pre::-webkit-scrollbar-thumb {
background-color: #41413f;
cursor: pointer;
}
/deep/ .prism-editor-wrapper::-webkit-scrollbar {
background-color: #272822;
}
/deep/ .prism-editor-wrapper::-webkit-scrollbar-thumb {
background-color: rgb(114, 112, 112);
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<div class="lum-dialog-mask" v-show="isShow">
<el-container class="lum-dialog-box" v-outside="close">
<el-header class="no-padding header" height="60px">
<p>会话记录 ({{ records.length }})</p>
<p class="tools">
<i class="el-icon-close" @click="close" />
</p>
</el-header>
<el-main class="no-padding main" v-loading="loading">
<el-scrollbar class="full-height" tag="section" :native="false">
<div v-for="record in records" :key="record.id" class="message-group">
<div class="left-box">
<el-avatar
fit="contain"
shape="square"
:size="30"
:src="record.avatar"
/>
</div>
<div class="right-box">
<div class="msg-header">
<span class="name">
{{
record.nickname_remarks
? record.nickname_remarks
: record.nickname
}}
</span>
<el-divider direction="vertical" />
<span class="time">{{ record.created_at }}</span>
</div>
<!-- 文本消息 -->
<text-message
v-if="record.msg_type == 1"
:content="record.content"
/>
<!-- 文件 - 图片消息 -->
<image-message
v-else-if="record.msg_type == 2 && record.file.file_type == 1"
:src="record.file.file_url"
/>
<!-- 文件 - 音频消息 -->
<audio-message
v-else-if="record.msg_type == 2 && record.file.file_type == 2"
:src="record.file.file_url"
/>
<!-- 文件 - 视频消息 -->
<video-message
v-else-if="record.msg_type == 2 && record.file.file_type == 3"
/>
<!-- 文件 - 其它格式文件 -->
<file-message
v-else-if="record.msg_type == 2 && record.file.file_type == 4"
:file="record.file"
:record_id="record.id"
/>
<!-- 代码块消息 -->
<code-message
v-else-if="record.msg_type == 4"
:code="record.code_block.code"
:lang="record.code_block.code_lang"
/>
<div v-else class="other-message">未知消息类型</div>
</div>
</div>
</el-scrollbar>
</el-main>
</el-container>
</div>
</template>
<script>
import { ServeGetForwardRecords } from '@/api/chat'
export default {
name: 'TalkForwardRecord',
data() {
return {
record_id: 0,
records: [],
loading: false,
isShow: false,
}
},
methods: {
open(record_id) {
if (record_id !== this.record_id) {
this.record_id = record_id
this.records = []
this.loadRecords()
}
this.isShow = true
},
close() {
this.isShow = false
},
loadRecords() {
this.loading = true
ServeGetForwardRecords({
record_id: this.record_id,
})
.then(res => {
if (res.code == 200) {
this.records = res.data.rows
}
})
.finally(() => {
this.loading = false
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-mask {
z-index: 99999;
}
.lum-dialog-box {
width: 500px;
max-width: 500px;
height: 600px;
}
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
@import '~@/assets/css/talk/talk-records.less';
</style>

View File

@@ -0,0 +1,602 @@
<template>
<div class="lum-dialog-mask">
<el-container class="lum-dialog-box" :class="{ 'full-screen': fullscreen }">
<el-header height="60px" class="header">
<p>消息管理器</p>
<p class="title">
<span>{{ query.talk_type == 1 ? "好友" : "群" }}{{ title }}</span>
</p>
<p class="tools">
<i
class="iconfont"
style="transform: scale(0.85)"
:class="fullscreen ? 'icon-tuichuquanping' : 'icon-quanping'"
@click="fullscreen = !fullscreen"
/>
<i class="el-icon-close" @click="$emit('close')" />
</p>
</el-header>
<el-header height="38px" class="sub-header">
<i
class="iconfont pointer"
:class="{ 'icon-shouqi2': broadside, 'icon-zhankai': !broadside }"
@click="triggerBroadside"
/>
<div class="search-box no-select">
<i class="el-icon-search" />
<input
v-model="search.keyword"
type="text"
maxlength="30"
placeholder="关键字搜索"
@keyup.enter="searchText($event)"
/>
</div>
</el-header>
<el-container class="full-height ov-hidden">
<el-aside width="200px" class="broadside" v-show="broadside">
<el-container class="full-height">
<el-header height="40px" class="aside-header">
<div
class="item"
:class="{ selected: contacts.show == 'friends' }"
@click="contacts.show = 'friends'"
>
我的好友({{ contacts.friends.length }})
</div>
<div class="item-shuxian">|</div>
<div
class="item"
:class="{ selected: contacts.show == 'groups' }"
@click="contacts.show = 'groups'"
>
我的群组({{ contacts.groups.length }})
</div>
</el-header>
<el-main class="no-padding">
<el-scrollbar class="full-height" tag="section" :native="false">
<div
v-for="item in contacts[contacts.show]"
class="contacts-item pointer"
:class="{
selected:
query.talk_type == item.type &&
query.receiver_id == item.id,
}"
:key="item.id"
@click="triggerMenuItem(item)"
>
<div class="avatar">
<el-avatar :size="20" :src="item.avatar">
<img src="~@/assets/image/detault-avatar.jpg" />
</el-avatar>
</div>
<div class="content" v-text="item.name"></div>
</div>
</el-scrollbar>
</el-main>
</el-container>
</el-aside>
<!-- 聊天记录阅览 -->
<el-main v-show="showBox == 0" class="no-padding">
<el-container class="full-height">
<el-header height="40px" class="type-items">
<span
v-for="tab in tabType"
:class="{ active: query.msg_type == tab.type }"
@click="triggerLoadType(tab.type)"
>{{ tab.name }}
</span>
</el-header>
<el-main
v-if="records.isEmpty"
class="history-record animated fadeIn"
>
<div class="empty-records">
<img src="~@/assets/image/chat-search-no-message.png" />
<p>暂无聊天记录</p>
</div>
</el-main>
<el-main v-else class="history-record">
<el-scrollbar class="full-height" tag="section" :native="false">
<div
v-for="record in records.items"
:key="record.id"
class="message-group"
>
<div class="left-box">
<el-avatar
shape="square"
fit="contain"
:size="30"
:src="record.avatar"
/>
</div>
<div class="right-box">
<div class="msg-header">
<span class="name">
{{
record.nickname_remarks
? record.nickname_remarks
: record.nickname
}}
</span>
<el-divider direction="vertical" />
<span class="time">{{ record.created_at }}</span>
</div>
<!-- 文本消息 -->
<text-message
v-if="record.msg_type == 1"
:content="record.content"
/>
<!-- 文件 - 图片消息 -->
<image-message
v-else-if="
record.msg_type == 2 && record.file.file_type == 1
"
:src="record.file.file_url"
/>
<!-- 文件 - 音频消息 -->
<audio-message
v-else-if="
record.msg_type == 2 && record.file.file_type == 2
"
:src="record.file.file_url"
/>
<!-- 文件 - 视频消息 -->
<video-message
v-else-if="
record.msg_type == 2 && record.file.file_type == 3
"
/>
<!-- 文件 - 其它格式文件 -->
<file-message
v-else-if="
record.msg_type == 2 && record.file.file_type == 4
"
:file="record.file"
:record_id="record.id"
/>
<!-- 会话记录消息 -->
<forward-message
v-else-if="record.msg_type == 3"
:forward="record.forward"
:record_id="record.id"
/>
<!-- 代码块消息 -->
<code-message
v-else-if="record.msg_type == 4"
:code="record.code_block.code"
:lang="record.code_block.code_lang"
/>
<!-- 投票消息 -->
<vote-message
v-else-if="record.msg_type == 5"
:record_id="record.id"
:vote="record.vote"
/>
<div v-else class="other-message">未知消息类型</div>
</div>
</div>
<!-- 数据加载栏 -->
<div v-show="records.loadStatus == 1" class="load-button blue">
<i class="el-icon-loading" />
<span>加载数据中...</span>
</div>
<div v-show="records.loadStatus == 0" class="load-button">
<i class="el-icon-arrow-down" />
<span @click="loadChatRecord">加载更多...</span>
</div>
</el-scrollbar>
</el-main>
</el-container>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import { ServeGetContacts } from "@/api/contacts";
import { ServeFindTalkRecords } from "@/api/chat";
import { formatSize as renderSize, download, imgZoom } from "@/utils/functions";
export default {
name: "TalkSearchRecord",
props: {
params: {
type: Object,
default: () => {
return {
talk_type: 0,
receiver_id: 0,
title: "",
};
},
},
},
data() {
return {
fullscreen: false,
user_id: this.$store.state.user.id,
title: "",
// 侧边栏相关信息
broadside: false,
contacts: {
show: "friends",
friends: [],
groups: [],
},
query: {
talk_type: 0,
receiver_id: 0,
msg_type: 0,
},
// 用户聊天记录
records: {
record_id: 0,
items: [],
isEmpty: false,
loadStatus: 0,
},
showBox: 0,
tabType: [
{ name: "全部", type: 0 },
{ name: "文件", type: 2 },
{ name: "会话记录", type: 3 },
{ name: "代码块", type: 4 },
{ name: "群投票", type: 5 },
],
search: {
keyword: "", // 关键字查询
date: "", // 时间查询
page: 1, // 当前分页
totalPage: 50, // 总分页
items: [], // 数据列表
isShowDate: false,
},
};
},
mounted() {
this.title = this.params.title;
this.query = {
talk_type: this.params.talk_type,
receiver_id: this.params.receiver_id,
msg_type: 0,
};
this.loadChatRecord(0);
},
created() {
this.loadFriends();
},
methods: {
download,
renderSize,
// 获取图片信息
getImgStyle(url) {
return imgZoom(url, 200);
},
// 获取会话记录消息名称
getForwardTitle(item) {
let arr = [...new Set(item.map((v) => v.nickname))];
return arr.join("、") + "的会话记录";
},
// 获取好友列表
loadFriends() {
ServeGetContacts().then(({ code, data }) => {
if (code == 200) {
this.contacts.friends = data.map((item) => {
return {
id: item.id,
type: 1,
avatar: item.avatar,
name: item.friend_remark ? item.friend_remark : item.nickname,
};
});
}
});
},
// 左侧联系人菜单点击事件
triggerMenuItem(item) {
this.title = item.name;
this.query.talk_type = item.type;
this.query.receiver_id = item.id;
this.showBox = 0;
this.triggerLoadType(0);
},
// 加载历史记录
loadChatRecord() {
let data = {
talk_type: this.query.talk_type,
receiver_id: this.query.receiver_id,
record_id: this.records.record_id,
msg_type: this.query.msg_type,
};
if (this.records.loadStatus == 1) return;
this.records.loadStatus = 1;
ServeFindTalkRecords(data)
.then((res) => {
if (res.code != 200) return;
let records = data.record_id == 0 ? [] : this.records.items;
records.push(...res.data.rows);
this.records.items = records;
this.records.loadStatus =
res.data.rows.length < res.data.limit ? 2 : 0;
if (this.records.items.length == 0) {
this.records.isEmpty = true;
} else {
this.records.record_id =
this.records.items[this.records.items.length - 1].id;
}
})
.catch(() => {
this.records.loadStatus = 0;
});
},
triggerLoadType(type) {
this.records.record_id = 0;
this.query.msg_type = type;
this.records.isEmpty = false;
this.records.items = [];
this.loadChatRecord();
},
searchText() {
if (this.search.keyword == "") {
this.showBox = 0;
return false;
}
this.$notify.info({
title: "消息",
message: "查询功能正在开发中...",
});
},
triggerBroadside() {
this.broadside = !this.broadside;
},
},
};
</script>
<style lang="less" scoped>
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
.lum-dialog-mask {
z-index: 1;
}
.lum-dialog-box {
width: 100%;
height: 600px;
max-width: 800px;
transition: 1s ease;
&.full-screen {
width: 100%;
height: 100%;
max-width: unset;
margin: 0;
border-radius: 0px;
}
.sub-header {
height: 38px;
line-height: 38px;
font-size: 12px;
border-bottom: 1px solid #f9f4f4;
margin-top: 10px;
padding: 0 10px;
position: relative;
i {
font-size: 22px;
color: #6f6a6a;
}
.search-box {
position: absolute;
width: 230px;
height: 32px;
top: 2px;
right: 10px;
background: #f9f4f4;
border-radius: 5px;
i {
position: absolute;
left: 10px;
top: 8px;
font-size: 16px;
}
input {
position: absolute;
left: 35px;
top: 3px;
height: 25px;
width: 184px;
color: #7d7171;
background: #f9f4f4;
}
}
}
.broadside {
@border: 1px solid #f9f9f9;
border-right: @border;
user-select: none;
transition: 3s ease;
.aside-header {
display: flex;
flex-direction: row;
height: 100%;
border-bottom: @border;
padding: 0;
> div {
text-align: center;
line-height: 40px;
font-size: 13px;
font-weight: 400;
}
.item {
flex: 1;
cursor: pointer;
&.selected {
color: #66b1ff;
}
}
.item-shuxian {
flex-basis: 1px;
flex-shrink: 0;
color: rgb(232 224 224);
}
}
.contacts-item {
height: 35px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-left: 10px;
position: relative;
.avatar {
flex-basis: 40px;
flex-shrink: 0;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.content {
flex: 1 1;
height: 100%;
line-height: 35px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
padding-right: 10px;
}
&:hover,
&.selected {
background-color: #f5f5f5;
}
}
}
}
/* first box */
.type-items {
padding: 0 0 0 10px;
line-height: 40px;
user-select: none;
border-bottom: 1px solid #f9f4f4;
.active {
color: #03a9f4;
font-weight: 500;
font-size: 13px;
}
span {
height: 40px;
width: 45px;
text-align: center;
cursor: pointer;
margin: 0 10px;
font-size: 12px;
font-weight: 400;
}
}
.history-record {
padding: 10px 0;
}
.load-button {
width: 100%;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
&.blue {
color: #51b2ff;
}
span {
margin-left: 5px;
font-size: 13px;
cursor: pointer;
user-select: none;
}
}
.empty-records {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: #cccccc;
font-weight: 300;
font-size: 14px;
img {
width: 100px;
}
}
@import "~@/assets/css/talk/talk-records.less";
</style>

View File

@@ -0,0 +1,193 @@
<template>
<div class="audio-message">
<div class="videodisc">
<div class="disc" :class="{ play: isPlay }" @click="toPlay">
<i v-if="loading" class="el-icon-loading" />
<i v-else-if="isPlay" class="el-icon-video-pause" />
<i v-else class="el-icon-video-play" />
<audio
ref="audio"
type="audio/mp3"
:src="src"
@timeupdate="timeupdate"
@ended="ended"
@canplay="canplay"
></audio>
</div>
</div>
<div class="detail">
<div class="text">
<i class="el-icon-service" />
<span>{{ getCurrDuration }} / {{ getTotalDuration }}</span>
</div>
<div class="process">
<el-progress :percentage="progress" :show-text="false" />
</div>
</div>
</div>
</template>
<script>
function formatSeconds(value) {
var theTime = parseInt(value) // 秒
var theTime1 = 0 // 分
var theTime2 = 0 // 小时
if (theTime > 60) {
theTime1 = parseInt(theTime / 60)
theTime = parseInt(theTime % 60)
if (theTime1 > 60) {
theTime2 = parseInt(theTime1 / 60)
theTime1 = parseInt(theTime1 % 60)
}
}
var result = '' + parseInt(theTime) //秒
if (10 > theTime > 0) {
result = '0' + parseInt(theTime) //秒
} else {
result = '' + parseInt(theTime) //秒
}
if (10 > theTime1 > 0) {
result = '0' + parseInt(theTime1) + ':' + result //分不足两位数首位补充0
} else {
result = '' + parseInt(theTime1) + ':' + result //分
}
if (theTime2 > 0) {
result = '' + parseInt(theTime2) + ':' + result //时
}
return result
}
export default {
name: 'AudioMessage',
props: {
src: {
type: String,
default: '',
},
},
data() {
return {
loading: true,
isPlay: false,
duration: 0,
currentTime: 0,
progress: 0,
}
},
computed: {
getTotalDuration() {
return formatSeconds(this.duration)
},
getCurrDuration() {
return formatSeconds(this.currentTime)
},
},
methods: {
toPlay() {
if (this.loading) {
return
}
let audio = this.$refs.audio
if (this.isPlay) {
audio.pause()
} else {
audio.play()
}
this.isPlay = !this.isPlay
},
// 当目前的播放位置已更改时
timeupdate() {
let audio = this.$refs.audio
this.currentTime = audio.currentTime
this.progress = (audio.currentTime / audio.duration) * 100
},
// 当浏览器可以播放音频/视频时
canplay() {
this.duration = this.$refs.audio.duration
this.loading = false
},
// 当目前的播放列表已结束时
ended() {
this.isPlay = false
},
},
}
</script>
<style scoped lang="less">
.audio-message {
width: 200px;
height: 60px;
border-radius: 5px;
background: #ffffff;
display: flex;
align-items: center;
border: 1px solid #03a9f4;
overflow: hidden;
> div {
height: 100%;
}
.videodisc {
flex-basis: 60px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
.disc {
width: 42px;
height: 42px;
background: #e9e5e5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
transition: ease 0.5;
&.play {
background: #ff5722;
box-shadow: 0 0 4px 0px #f76a3e;
}
i {
font-size: 24px;
}
&:active i {
transform: scale(1.1);
}
}
}
.detail {
flex: 1 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 10px;
.text {
width: 90%;
font-size: 12px;
i {
margin-right: 5px;
}
}
.process {
padding-top: 10px;
height: 20px;
width: 90%;
}
}
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<div
class="code-message"
:class="{
'max-height': lineNumber > 6,
'max-width': maxwidth,
'full-screen': fullscreen,
}"
>
<i
:class="
fullscreen ? 'el-icon-close' : 'iconfont icon-tubiao_chakangongyi'
"
@click="fullscreen = !fullscreen"
/>
<pre class="lum-scrollbar" v-html="formatCode(code, lang)" />
</div>
</template>
<script>
import Prism from 'prismjs'
import 'prismjs/themes/prism-okaidia.css'
export default {
name: 'CodeMessage',
props: {
code: {
type: [String, Number],
default: '',
},
lang: {
type: String,
default: '',
},
maxwidth: {
type: Boolean,
default: false,
},
},
data() {
return {
fullscreen: false,
lineNumber: 0,
}
},
created() {
this.lineNumber = this.code.split(/\n/).length
},
methods: {
formatCode(code, lang) {
try {
return Prism.highlight(code, Prism.languages[lang], lang) + '<br/>'
} catch (error) {
return code
}
},
},
}
</script>
<style lang="less" scoped>
.code-message {
position: relative;
overflow: hidden;
border-radius: 5px;
box-sizing: border-box;
&.max-width {
max-width: 500px;
}
&.max-height {
height: 208px;
}
i {
position: absolute;
right: 0px;
top: 0px;
font-size: 16px;
cursor: pointer;
color: white;
display: inline-block;
opacity: 0;
width: 50px;
height: 30px;
background: #171616;
text-align: center;
line-height: 30px;
border-radius: 0 0 0px 8px;
transition: 1s ease;
}
&:hover {
i {
opacity: 1;
}
}
pre {
box-sizing: border-box;
height: 100%;
width: 100%;
overflow: auto;
padding: 10px;
line-height: 24px;
background: #272822;
color: #d5d4d4;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
font-size: 85%;
&.lum-scrollbar {
&::-webkit-scrollbar {
background-color: black;
}
}
}
&.full-screen {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
max-width: unset;
max-height: unset;
border-radius: 0px;
background: #272822;
z-index: 99999999;
i {
position: fixed;
top: 15px;
right: 15px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 24px;
&:active {
box-shadow: 0 0 5px 0px #ccc;
}
}
}
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div class="file-message">
<div class="main">
<div class="ext">{{ ext }}</div>
<div class="file-box">
<p class="info">
<span class="name">{{ fileName }}</span>
<span class="size">({{ fileSize }})</span>
</p>
<p class="notice">文件已成功发送, 文件助手永久保存</p>
</div>
</div>
<div class="footer">
<a @click="download(record_id)">下载</a>
<a>在线预览</a>
</div>
</div>
</template>
<script>
import { formatSize, download } from '@/utils/functions'
export default {
name: 'FileMessage',
props: {
file: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
file_id: 0,
ext: '',
fileName: '',
fileSize: '',
}
},
created() {
this.file_id = this.file.id
this.ext = this.file.file_suffix.toUpperCase()
this.fileName = this.file.original_name
this.fileSize = formatSize(this.file.file_size)
},
methods: {
download,
},
}
</script>
<style lang="less" scoped>
.file-message {
width: 250px;
height: 85px;
background: white;
box-shadow: 0 0 5px 0px #e8e4e4;
padding: 10px;
border-radius: 3px;
transition: all 0.5s;
&:hover {
box-shadow: 0 0 5px 0px #cac6c6;
}
.main {
height: 45px;
display: flex;
flex-direction: row;
.ext {
display: flex;
justify-content: center;
align-items: center;
width: 45px;
height: 45px;
flex-shrink: 0;
color: #ffffff;
background: #49a4ff;
border-radius: 5px;
font-size: 12px;
}
.file-box {
flex: 1 1;
height: 45px;
margin-left: 10px;
overflow: hidden;
.info {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
height: 24px;
color: rgb(76, 76, 76);
font-size: 14px;
.name {
flex: 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.size {
font-size: 12px;
color: #cac6c6;
}
}
.notice {
height: 25px;
line-height: 25px;
font-size: 12px;
color: #929191;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.footer {
height: 30px;
line-height: 37px;
color: #409eff;
text-align: right;
font-size: 12px;
border-top: 1px solid #eff7ef;
margin-top: 10px;
a {
margin: 0 3px;
user-select: none;
cursor: pointer;
&:hover {
color: royalblue;
}
}
}
}
</style>

View File

@@ -0,0 +1,116 @@
<template>
<div>
<div class="forward-message" @click="catForwardRecords">
<div class="title">{{ title }}</div>
<div v-for="(record, index) in records" :key="index" class="lists">
<p>
<span>{{ record.nickname }}</span>
<span>{{ record.text }}</span>
</p>
</div>
<div class="footer">
<span>转发聊天会话记录 ({{ num }})</span>
</div>
</div>
<!-- 会话记录查看器 -->
<talk-forward-record ref="forwardRecordsRef" />
</div>
</template>
<script>
import TalkForwardRecord from '@/components/chat/TalkForwardRecord'
export default {
name: 'ForwardMessage',
components: {
TalkForwardRecord,
},
props: {
forward: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
title: '',
records: [],
num: 0,
}
},
methods: {
catForwardRecords() {
this.$refs.forwardRecordsRef.open(this.record_id)
},
getForwardTitle(list) {
let arr = [...new Set(list.map(v => v.nickname))]
return arr.join('、') + '的会话记录'
},
},
created() {
let forward = this.forward
this.num = forward.num
this.records = forward.list
this.title = this.getForwardTitle(this.records)
},
}
</script>
<style lang="less" scoped>
/* 会话记录消息 */
.forward-message {
width: 250px;
min-height: 95px;
max-height: 150px;
border-radius: 3px;
background-color: white;
padding: 3px 10px;
cursor: pointer;
box-shadow: 0 0 5px 0px #e8e4e4;
text-align: left;
user-select: none;
.title {
height: 30px;
line-height: 30px;
font-size: 14px;
color: #565353;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 400;
}
.lists p {
height: 18px;
line-height: 18px;
font-size: 10px;
color: #aaa9a9;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 100;
}
.footer {
height: 32px;
line-height: 35px;
color: #858282;
border-top: 1px solid #f1ebeb;
font-size: 12px;
margin-top: 12px;
font-weight: 300;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover {
box-shadow: 0 0 5px 0px #cac6c6;
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<!-- 好友申请消息 -->
<div class="apply-card">
<div class="card-header">
<img class="avatar" :src="avatar" />
<div class="nickname">No. {{ nickname }}</div>
<div class="datetime">{{ datetime }}</div>
<div class="remarks">
<span>备注信息{{ remarks }}</span>
</div>
</div>
<div class="card-footer">
<div class="mini-button" @click="handle(1)">同意</div>
<el-divider direction="vertical"></el-divider>
<div class="mini-button" @click="handle(2)">拒绝</div>
</div>
</div>
</template>
<script>
export default {
name: 'FriendApplyMessage',
props: {
data: {
type: Object,
default() {
return {}
},
},
},
data() {
return {
avatar:
'http://im-img.gzydong.club/media/images/avatar/20210602/60b6f03598ed0104301.png',
nickname: '独特态度',
datetime: '05/09 12:13 分',
remarks: '编辑个签,展示我的独特态度 展示我的独特态度。',
apply_id: 0,
}
},
created() {},
methods: {
handle(type) {
alert(type)
},
},
}
</script>
<style lang="less" scoped>
.apply-card {
position: relative;
width: 170px;
min-height: 180px;
border-radius: 15px;
overflow: hidden;
transition: all 0.5s;
box-sizing: border-box;
background-image: linear-gradient(-84deg, #1ab6ff 0, #1ab6ff 0, #82c1f3 100%);
// #028fff
&:hover {
transform: scale(1.02);
}
.card-header {
position: relative;
width: 100%;
height: 135px;
.avatar {
position: absolute;
top: 18px;
left: 8px;
width: 40px;
height: 40px;
border-radius: 50%;
background: white;
}
.nickname {
position: absolute;
top: 15px;
right: 8px;
width: 90px;
height: 25px;
font-size: 10px;
text-align: center;
line-height: 25px;
color: white;
border-bottom: 1px dashed white;
}
.datetime {
position: absolute;
top: 42px;
right: 11.5px;
color: white;
font-size: 10px;
transform: scale(0.9);
}
.remarks {
position: absolute;
bottom: 5px;
color: white;
font-size: 10px;
padding: 3px 5px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
transform: scale(0.95);
}
}
.card-footer {
position: absolute;
bottom: 0;
width: 100%;
height: 40px;
border-top: 1px solid white;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
/deep/.el-divider {
background: white;
}
.mini-button {
display: flex;
width: 50px;
height: 25px;
margin: 0 10px;
text-align: center;
align-items: center;
justify-content: center;
font-size: 13px;
color: white;
cursor: pointer;
&:hover {
font-size: 14px;
}
}
}
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<div class="image-message no-select">
<el-image
fit="cover"
:src="src"
:lazy="true"
:style="getImgStyle(src)"
:preview-src-list="[src]"
>
<div slot="error" class="image-slot">图片加载失败...</div>
<div slot="placeholder" class="image-slot">图片加载中...</div>
</el-image>
</div>
</template>
<script>
import { imgZoom } from '@/utils/functions'
export default {
name: 'ImageMessage',
props: {
src: {
type: String,
default: '',
},
},
methods: {
getImgStyle(url) {
return imgZoom(url, 200)
},
},
}
</script>
<style lang="less" scoped>
.image-message {
/deep/.el-image {
border-radius: 5px;
cursor: pointer;
background: #f1efef;
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
font-size: 13px;
color: #908686;
background: #efeaea;
}
}
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<div class="invite-message">
<div v-if="invite.type == 1 || invite.type == 3" class="system-msg">
<a @click="toUser(invite.operate_user.id)">
{{ invite.operate_user.nickname }}
</a>
<span>{{ invite.type == 1 ? '邀请了' : '将' }}</span>
<template v-for="(user, uidx) in invite.users">
<a @click="toUser(user.id)">{{ user.nickname }}</a>
<em v-show="uidx < invite.users.length - 1"></em>
</template>
<span>{{ invite.type == 1 ? '加入了群聊' : '踢出了群聊' }}</span>
</div>
<div v-else-if="invite.type == 2" class="system-msg">
<a @click="toUser(invite.operate_user.id)">
{{ invite.operate_user.nickname }}
</a>
<span>退出了群聊</span>
</div>
</div>
</template>
<script>
export default {
name: 'InviteMessage',
props: {
invite: {
type: Object,
required: true,
},
},
methods: {
toUser(user_id) {
this.$emit('cat', user_id)
},
},
}
</script>
<style lang="less" scoped>
.invite-message {
display: flex;
justify-content: center;
}
.system-msg {
margin: 10px auto;
background-color: #f5f5f5;
font-size: 11px;
line-height: 30px;
padding: 0 8px;
word-break: break-all;
word-wrap: break-word;
color: #979191;
user-select: none;
font-weight: 300;
display: inline-block;
border-radius: 3px;
span {
margin: 0 5px;
}
a {
color: #939596;
cursor: pointer;
font-size: 12px;
font-weight: 400;
&:hover {
color: black;
}
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<!-- 登录消息 -->
<div class="login-message">
<h4>登录操作通知</h4>
<p>登录时间{{ datetime }} (CST)</p>
<p>IP地址{{ ip }}</p>
<p>登录地点{{ address }}</p>
<p>登录设备{{ platform }}</p>
<p>异常原因{{ reason }}</p>
</div>
</template>
<script>
import { parseTime } from '@/utils/functions'
export default {
name: 'LoginMessage',
props: {
detail: {
type: Object,
required: true,
},
},
data() {
return {
datetime: '',
ip: '',
address: '',
platform: '',
reason: '常用设备登录',
}
},
created() {
this.ip = this.detail.ip
this.datetime = parseTime(
this.detail.created_at,
'{y}年{m}月{d}日 {h}:{i}:{s}'
)
this.address = this.detail.address
this.reason = this.detail.reason
this.platform =
this.getExploreName(this.detail.agent) +
' / ' +
this.getExploreOs(this.detail.agent)
},
methods: {
getExploreName(userAgent = '') {
if (userAgent.indexOf('Opera') > -1 || userAgent.indexOf('OPR') > -1) {
return 'Opera'
} else if (
userAgent.indexOf('compatible') > -1 &&
userAgent.indexOf('MSIE') > -1
) {
return 'IE'
} else if (userAgent.indexOf('Edge') > -1) {
return 'Edge'
} else if (userAgent.indexOf('Firefox') > -1) {
return 'Firefox'
} else if (
userAgent.indexOf('Safari') > -1 &&
userAgent.indexOf('Chrome') == -1
) {
return 'Safari'
} else if (
userAgent.indexOf('Chrome') > -1 &&
userAgent.indexOf('Safari') > -1
) {
return 'Chrome'
} else {
return 'Unkonwn'
}
},
getExploreOs(userAgent = '') {
if (userAgent.indexOf('Mac OS') > -1) {
return 'Mac OS'
} else {
return 'Windows'
}
},
},
}
</script>
<style lang="less" scoped>
.login-message {
width: 300px;
min-height: 50px;
background: #f7f7f7;
border-radius: 5px;
padding: 15px;
p {
font-size: 13px;
margin: 10px 0;
&:last-child {
margin-bottom: 0;
}
}
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<div class="reply-message">这是回复的消息[预留]</div>
</template>
<script>
export default {
name: 'ReplyMessage',
data() {
return {}
},
created() {},
methods: {},
}
</script>
<style lang="less" scoped>
.reply-message {
margin-top: 5px;
min-height: 28px;
background: #f7f1f1;
line-height: 28px;
font-size: 12px;
padding: 0 10px;
border-radius: 3px;
color: #a7a2a2;
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="revoke-message">
<div class="content">
<span v-if="$store.state.user.id == item.user_id">
你撤回了一条消息 | {{ sendTime(item.created_at) }}
</span>
<span v-else-if="item.talk_type == 1">
对方撤回了一条消息 | {{ sendTime(item.created_at) }}
</span>
<span v-else>
"{{ item.nickname }}" 撤回了一条消息 | {{ sendTime(item.created_at) }}
</span>
</div>
</div>
</template>
<script>
import { formatTime as sendTime } from "@/utils/functions";
export default {
name: "RevokeMessage",
props: {
item: {
type: Object,
},
},
methods: {
sendTime,
},
};
</script>
<style lang="less" scoped>
.revoke-message {
display: flex;
justify-content: center;
.content {
margin: 10px auto;
background-color: #f5f5f5;
font-size: 11px;
line-height: 30px;
padding: 0 8px;
word-break: break-all;
word-wrap: break-word;
color: #979191;
user-select: none;
font-weight: 300;
display: inline-block;
border-radius: 3px;
span {
margin: 0 5px;
}
a {
color: #939596;
cursor: pointer;
font-size: 12px;
font-weight: 400;
&:hover {
color: black;
}
}
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="system-text-message">
<div class="content">{{ content }}</div>
</div>
</template>
<script>
import { formatTime as sendTime } from '@/utils/functions'
export default {
name: 'SystemTextMessage',
props: {
content: String,
},
methods: {
sendTime,
},
}
</script>
<style lang="less" scoped>
.system-text-message {
display: flex;
justify-content: center;
.content {
margin: 10px auto;
background-color: #f5f5f5;
font-size: 11px;
line-height: 30px;
padding: 0 8px;
word-break: break-all;
word-wrap: break-word;
color: #979191;
user-select: none;
font-weight: 300;
display: inline-block;
border-radius: 3px;
}
}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<div
class="text-message"
:class="{
left: float == 'left',
right: float == 'right',
'max-width': !fullWidth,
}"
>
<div v-if="arrow" class="arrow"></div>
<pre v-html="html" />
</div>
</template>
<script>
import { textReplaceLink } from "@/utils/functions";
import { textReplaceEmoji } from "@/utils/emojis";
export default {
name: "TextMessage",
props: {
content: {
type: [String, Number],
default: "",
},
float: {
type: String,
default: "left",
},
fullWidth: {
type: Boolean,
default: true,
},
arrow: {
type: Boolean,
default: false,
},
},
data() {
return {
html: "",
};
},
created() {
const text = textReplaceLink(
this.content,
this.float == "right" ? "#ffffff" : "rgb(9 149 208)"
);
this.html = textReplaceEmoji(text);
},
};
</script>
<style lang="less" scoped>
@bg-left-color: #f5f5f5;
@bg-right-color: #1ebafc;
.text-message {
position: relative;
min-width: 30px;
min-height: 30px;
border-radius: 5px;
padding: 5px;
.arrow {
position: absolute;
width: 0;
height: 0;
font-size: 0;
border: 5px solid;
top: 6px;
left: -10px;
}
&.max-width {
max-width: calc(100% - 50px);
}
&.left {
color: #3a3a3a;
background: @bg-left-color;
.arrow {
border-color: transparent @bg-left-color transparent transparent;
}
}
&.right {
color: #fff;
background: @bg-right-color;
.arrow {
right: -10px;
left: unset;
border-color: transparent transparent transparent @bg-right-color;
}
}
pre {
white-space: pre-wrap;
overflow: hidden;
word-break: break-word;
word-wrap: break-word;
font-size: 15px;
padding: 3px 10px;
font-family: "Microsoft YaHei";
line-height: 25px;
}
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<!-- 用户卡片消息 - 预留 -->
<div></div>
</template>
<script>
export default {
name: 'UserCardMessage',
components: {},
data() {
return {}
},
computed: {},
watch: {},
methods: {},
created() {},
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,18 @@
<template>
<div class="video-message">
视频消息
</div>
</template>
<script>
export default {
name: 'VideoMessage',
components: {},
data() {
return {}
},
methods: {},
created() {},
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,134 @@
<template>
<div class="visit-card-message">
<div class="user flex-center">
<div class="avatar flex-center">
<el-avatar :size="28" :src="avatar" />
</div>
<div class="content flex-center">
<p class="ellipsis">{{ nickname }}</p>
</div>
<div class="tools flex-center">
<span class="flex-center pointer">
<i class="el-icon-plus" /> 加好友
</span>
</div>
</div>
<div class="sign"><span>个性签名 : </span>{{ sign }}</div>
<div class="share no-select ellipsis">
<a class="pointer" @click="openVisitCard(friendId)">你是谁?</a>
分享了用户名片可点击添加好友 ...
</div>
</div>
</template>
<script>
export default {
name: 'VisitCardMessage',
data() {
return {
userId: 0,
friendId: 0,
avatar:
'http://im-serve0.gzydong.club/static/image/sys-head/2019012107542668696.jpg',
sign:
'这个社会,是赢家通吃,输者一无所有,社会,永远都是只以成败论英雄。',
nickname:
'氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳',
}
},
created() {},
methods: {
openVisitCard(user_id) {
this.$emit('openVisitCard', user_id)
},
},
}
</script>
<style lang="less" scoped>
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.visit-card-message {
min-height: 130px;
min-width: 100px;
max-width: 300px;
border-radius: 5px;
padding: 10px;
box-sizing: border-box;
border: 1px solid #ece5e5;
transition: all 0.5s;
&:hover {
box-shadow: 0 0 8px #e2d3d3;
transform: scale(1.01);
}
.user {
height: 40px;
overflow: hidden;
box-sizing: border-box;
> div {
height: inherit;
}
.avatar {
flex-basis: 30px;
flex-shrink: 0;
}
.content {
flex: 1 1;
margin: 0 10px;
font-size: 14px;
justify-content: flex-start;
overflow: hidden;
}
.tools {
flex-basis: 60px;
flex-shrink: 0;
span {
width: 65px;
height: 30px;
background: #409eff;
color: white;
font-size: 13px;
border-radius: 20px;
padding: 0 8px;
transform: scale(0.8);
user-select: none;
&:active {
background: #83b0f3;
transform: scale(0.83);
}
}
}
}
.sign {
min-height: 22px;
line-height: 22px;
border-radius: 3px;
padding: 5px 8px;
background: #f3f5f7;
color: #7d7d7d;
font-size: 12px;
margin: 10px 0;
span {
font-weight: bold;
}
}
.share {
font-size: 12px;
color: #7d7d7d;
a {
color: #4cabf7;
}
}
}
</style>

View File

@@ -0,0 +1,16 @@
<template>
<div class="voice-message"></div>
</template>
<script>
export default {
name: 'VoiceMessage',
components: {},
data() {
return {}
},
methods: {},
created() {},
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,299 @@
<template>
<div>
<div class="vote-message">
<div class="vote-from">
<div class="vheader">
<p>
{{ answer_mode == 1 ? "[多选投票]" : "[单选投票]" }}
<i
v-show="is_vote"
class="pointer"
:class="{
'el-icon-loading': refresh,
'el-icon-refresh': !refresh,
}"
title="刷新投票结果"
@click="loadRefresh"
></i>
</p>
<p>{{ title }}</p>
</div>
<template v-if="is_vote">
<div class="vbody">
<div class="vote-view" v-for="(option, index) in options">
<p class="vote-option">{{ option.value }}. {{ option.text }}</p>
<p class="vote-census">
{{ option.num }} {{ option.progress }}%
</p>
<p class="vote-progress">
<el-progress
:show-text="false"
:percentage="parseInt(option.progress)"
/>
</p>
</div>
</div>
<div class="vfooter vote-view">
<p>应参与人数{{ answer_num }} </p>
<p>实际参与人数{{ answered_num }} </p>
</div>
</template>
<template v-else>
<div class="vbody">
<p class="option" v-for="(option, index) in options">
<el-checkbox
v-model="option.is_checked"
@change="toSelect2(option)"
/>
<span @click="toSelect(option, index)" style="margin-left: 10px">
{{ option.value }} {{ option.text }}
</span>
</p>
</div>
<div class="vfooter">
<el-button plain round @click="toVote">
{{ isUserVote ? "立即投票" : "请选择进行投票" }}
</el-button>
</div>
</template>
</div>
</div>
</div>
</template>
<script>
import { ServeConfirmVoteHandle } from "@/api/chat";
export default {
name: "VoteMessage",
props: {
vote: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
},
},
data() {
return {
answer_mode: 0,
title: "啊谁叫你打开你卡沙发那,那就是看、卡收纳是你",
radio_value: "",
options: [],
is_vote: false,
answer_num: 0,
answered_num: 0,
refresh: false,
};
},
computed: {
isUserVote() {
return this.options.some((iten) => {
return iten.is_checked;
});
},
},
created() {
let user_id = this.$store.state.user.id;
let { detail, statistics, vote_users } = this.vote;
this.answer_mode = detail.answer_mode;
this.answer_num = detail.answer_num;
this.answered_num = detail.answered_num;
detail.answer_option.forEach((item) => {
this.options.push({
value: item.key,
text: item.value,
is_checked: false,
num: 0,
progress: "00.0",
});
});
this.is_vote = vote_users.some((value) => {
return value == user_id;
});
this.updateStatistics(statistics);
},
methods: {
loadRefresh() {
this.refresh = true;
setTimeout(() => {
this.refresh = false;
}, 500);
},
updateStatistics(data) {
let count = data.count;
this.options.forEach((option) => {
option.num = data.options[option.value];
if (count > 0) {
option.progress = (data.options[option.value] / count) * 100;
}
});
},
toSelect(option, index) {
if (this.answer_mode == 0) {
this.options.forEach((option) => {
option.is_checked = false;
});
}
this.options[index].is_checked = !option.is_checked;
},
toSelect2(option) {
if (this.answer_mode == 0) {
this.options.forEach((item) => {
if (option.value == item.value) {
item.is_checked = option.is_checked;
} else {
item.is_checked = false;
}
});
}
},
toVote() {
if (this.isUserVote == false) {
return false;
}
let items = [];
this.options.forEach((item) => {
if (item.is_checked) {
items.push(item.value);
}
});
ServeConfirmVoteHandle({
record_id: this.record_id,
options: items.join(","),
}).then((res) => {
if (res.code == 200) {
this.is_vote = true;
this.updateStatistics(res.data);
}
});
},
},
};
</script>
<style lang="less" scoped>
.vote-message {
width: 300px;
min-height: 150px;
border: 1px solid #eceff1;
box-sizing: border-box;
border-radius: 5px;
overflow: hidden;
.vote-from {
width: 100%;
.vheader {
min-height: 50px;
background: #4e83fd;
padding: 8px;
position: relative;
p {
margin: 3px 0;
&:first-child {
color: rgb(245, 237, 237);
font-size: 13px;
margin-bottom: 8px;
}
&:last-child {
color: white;
}
}
&::before {
content: "投票";
position: absolute;
font-size: 60px;
color: white;
opacity: 0.1;
top: -5px;
right: 10px;
}
}
.vbody {
min-height: 80px;
width: 100%;
padding: 5px 15px;
box-sizing: border-box;
.option {
margin: 14px 0px;
font-size: 13px;
span {
cursor: pointer;
user-select: none;
line-height: 22px;
}
.el-radio {
margin-right: 0;
.el-radio__label {
padding-left: 5px;
}
}
}
margin-bottom: 10px;
}
.vfooter {
height: 55px;
text-align: center;
box-sizing: border-box;
.el-button {
width: 80%;
font-weight: 400;
}
&.vote-view {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding-left: 15px;
p {
border-left: 2px solid #2196f3;
padding-left: 5px;
}
}
}
}
.vote-view {
width: 100%;
min-height: 30px;
margin: 15px 0;
box-sizing: border-box;
> p {
margin: 6px 0px;
font-size: 13px;
}
.vote-option {
min-height: 20px;
line-height: 20px;
}
.vote-census {
height: 20px;
line-height: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,33 @@
import AudioMessage from './AudioMessage.vue';
import CodeMessage from './CodeMessage.vue';
import ForwardMessage from './ForwardMessage.vue';
import ImageMessage from './ImageMessage.vue';
import TextMessage from './TextMessage.vue';
import VideoMessage from './VideoMessage.vue';
import VoiceMessage from './VoiceMessage.vue';
import SystemTextMessage from './SystemTextMessage.vue';
import FileMessage from './FileMessage.vue';
import InviteMessage from './InviteMessage.vue';
import RevokeMessage from './RevokeMessage.vue';
import VisitCardMessage from './VisitCardMessage.vue';
import ReplyMessage from './ReplyMessage.vue';
import VoteMessage from './VoteMessage.vue';
import LoginMessage from './LoginMessage.vue';
export {
AudioMessage,
CodeMessage,
ForwardMessage,
ImageMessage,
TextMessage,
VideoMessage,
VoiceMessage,
SystemTextMessage,
FileMessage,
InviteMessage,
RevokeMessage,
VisitCardMessage,
ReplyMessage,
VoteMessage,
LoginMessage
}

View File

@@ -0,0 +1,130 @@
<template>
<el-tabs v-model="activeName" @tab-click="handleClick" type="card">
<el-tab-pane :label="toUser.storeFlag ? '想要咨询' : '他的足迹'" name="history">
<div style="margin-left: 12px;" v-if="toUser.storeFlag">
<GoodsLink :goodsDetail="goodsDetail" v-if="toUser.userId === goodsDetail.storeId"/>
<FootPrint :list="footPrintList"/>
</div>
<div v-else>
</div>
</el-tab-pane>
<el-tab-pane label="店铺信息" name="UserInfo" v-if="toUser.storeFlag">
<div v-if="toUser.storeFlag">
<StoreDetail :storeInfo="storeInfo"/>
</div>
</el-tab-pane>
</el-tabs>
</template>
<script>
import { Tabs, TabPane } from 'element-ui'
import { ServeGetStoreDetail, ServeGetUserDetail, ServeGetFootPrint } from '@/api/user'
import { ServeGetGoodsDetail } from '@/api/goods'
import StoreDetail from "@/components/chat/panel/template/storeDetail.vue";
import FootPrint from "@/components/chat/panel/template/footPrint.vue";
import GoodsLink from "@/components/chat/panel/template/goodsLink.vue";
export default {
components: {
"el-tabs": Tabs,
"el-tab-pane": TabPane,
StoreDetail,
FootPrint,
GoodsLink
},
props: {
toUser: {
type: Object,
default: null,
},
id: {
type: String,
default: '',
},
goodsParams: {
type: Object,
default: null,
},
},
data() {
return {
activeName: 'history',
storeInfo: {}, //店铺信息
memberInfo: {}, //会员信息
footPrintParams: {
memberId: '',
storeId: '',
},
goodsDetail: {},
footPrintList: [],
}
},
mounted() {
console.log(this.id)
console.log(this.toUser)
if(this.toUser.storeFlag){
this.getStoreDetail()
}else{
this.getMemberDetail()
}
this.getFootPrint()
if(this.goodsParams){
this.getGoodsDetail()
}
},
methods: {
getStoreDetail() {
ServeGetStoreDetail(this.toUser.userId).then(res => {
if (res.success) {
this.storeInfo = res.result
}
})
},
handleClick(){},
getMemberDetail() {
ServeGetUserDetail(this.toUser.userId).then(res => {
if (res.success) {
this.memberInfo = res.result
}
})
},
getGoodsDetail(){
ServeGetGoodsDetail(this.goodsParams).then(res => {
if(res.success){
this.goodsDetail = res.result.data
}
})
},
getFootPrint(){
if(this.toUser.storeFlag){
this.footPrintParams.memberId = this.id
this.footPrintParams.storeId = this.toUser.userId
}else{
this.footPrintParams.memberId = this.toUser.userId
this.footPrintParams.storeId = this.id
}
console.log(this.footPrintParams)
ServeGetFootPrint(this.footPrintParams).then(res => {
res.result.records.forEach((item,index) => {
if(item.goodsId === this.goodsParams.goodsId){
res.result.records.splice(index,1)
}
});
this.footPrintList = res.result.records
})
//删除掉刚加入的商品
},
}
}
</script>
<style scoped lang="less">
/deep/ .el-tabs__nav {
height: 60px;
line-height: 60px;
}
/deep/ .el-tab-pane {
margin-left: 12px;
}
</style>

View File

@@ -0,0 +1,278 @@
<template>
<el-header id="panel-header">
<div class="module left-module">
<span
class="icon-badge"
v-show="params.is_robot == 0"
:class="{ 'red-color': params.talk_type == 1 }"
>
{{ params.talk_type == 1 ? '好友' : '群组' }}
</span>
<span class="nickname">{{ params.nickname }}</span>
<span v-show="params.talk_type == 2" class="num">({{ groupNum }})</span>
</div>
<div v-show="params.talk_type == 1 && params.is_robot == 0" class="module center-module">
<p class="online">
<span v-show="isOnline" class="online-status"></span>
<span>{{ isOnline ? '在线' : '离线' }}</span>
</p>
<p class="keyboard-status" v-show="isKeyboard">对方正在输入 ...</p>
</div>
<div class="module right-module" >
<el-tooltip content="历史消息" placement="top">
<p v-show="params.is_robot == 0">
<i class="el-icon-time" @click="triggerEvent('history')" />
</p>
</el-tooltip>
<el-tooltip content="群公告" placement="top">
<p v-show="params.talk_type == 2">
<i class="iconfont icon-gonggao2" @click="triggerEvent('notice')" />
</p>
</el-tooltip>
<el-tooltip content="群设置" placement="top">
<p v-show="params.talk_type == 2">
<i class="el-icon-setting" @click="triggerEvent('setting')" />
</p>
</el-tooltip>
</div>
</el-header>
</template>
<script>
export default {
props: {
data: {
type: Object,
default: () => {
return {
talk_type: 0,
receiver_id: 0,
params: 0,
nickname: '',
}
},
},
online: {
type: Boolean,
default: false,
},
keyboard: {
type: [Boolean, Number],
default: false,
},
},
data() {
return {
params: {
talk_type: 0,
receiver_id: 0,
params: 0,
nickname: '',
},
isOnline: false,
isKeyboard: false,
groupNum: 0,
}
},
created() {
this.setParamsData(this.data)
this.setOnlineStatus(this.online)
},
watch: {
data(value) {
this.setParamsData(value)
},
online(value) {
this.setOnlineStatus(value)
},
keyboard(value) {
this.isKeyboard = value
setTimeout(() => {
this.isKeyboard = false
}, 2000)
},
},
methods: {
setOnlineStatus(value) {
this.isOnline = value
},
setParamsData(object) {
Object.assign(this.params, object)
},
setGroupNum(value) {
this.groupNum = value
},
triggerEvent(event_name) {
this.$emit('event', event_name)
},
},
}
</script>
<style lang="less" scoped>
#panel-header {
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
border-bottom: 1px solid #f5eeee;
.module {
width: 100%/3;
height: 100%;
display: flex;
align-items: center;
}
.left-module {
padding-right: 5px;
.icon-badge {
background: rgb(81 139 254);
height: 18px;
line-height: 18px;
padding: 1px 3px;
font-size: 10px;
color: white;
border-radius: 3px;
margin-right: 8px;
flex-shrink: 0;
&.red-color {
background: #f97348;
}
}
.nickname {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.center-module {
flex-direction: column;
justify-content: center;
.online {
color: #cccccc;
font-weight: 300;
font-size: 15px;
&.color {
color: #1890ff;
}
.online-status {
position: relative;
top: -1px;
display: inline-block;
width: 6px;
height: 6px;
vertical-align: middle;
border-radius: 50%;
position: relative;
background-color: #1890ff;
margin-right: 5px;
&:after {
position: absolute;
top: -1px;
left: -1px;
width: 100%;
height: 100%;
border: 1px solid #1890ff;
border-radius: 50%;
-webkit-animation: antStatusProcessing 1.2s ease-in-out infinite;
animation: antStatusProcessing 1.2s ease-in-out infinite;
content: '';
}
}
}
.keyboard-status {
height: 20px;
line-height: 18px;
font-size: 10px;
animation: inputfade 600ms infinite;
-webkit-animation: inputfade 600ms infinite;
}
}
.right-module {
display: flex;
justify-content: flex-end;
align-items: center;
p {
cursor: pointer;
margin: 0 8px;
font-size: 20px;
color: #828f95;
&:active i {
font-size: 26px;
transform: scale(1.3);
transition: ease 0.5s;
color: red;
}
}
}
}
/* css 动画 */
@keyframes inputfade {
from {
opacity: 1;
}
50% {
opacity: 0.4;
}
to {
opacity: 1;
}
}
@-webkit-keyframes inputfade {
from {
opacity: 1;
}
50% {
opacity: 0.4;
}
to {
opacity: 1;
}
}
@-webkit-keyframes antStatusProcessing {
0% {
-webkit-transform: scale(0.8);
transform: scale(0.8);
opacity: 0.5;
}
to {
-webkit-transform: scale(2.4);
transform: scale(2.4);
opacity: 0;
}
}
@keyframes antStatusProcessing {
0% {
-webkit-transform: scale(0.8);
transform: scale(0.8);
opacity: 0.5;
}
to {
-webkit-transform: scale(2.4);
transform: scale(2.4);
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="multi-select">
<div class="multi-title">
<span>已选中{{ value }} 条消息</span>
</div>
<div class="multi-main">
<div class="btn-group">
<div
class="multi-icon pointer"
@click="$emit('event', 'merge_forward')"
>
<i class="el-icon-position" />
</div>
<p>合并转发</p>
</div>
<div class="btn-group">
<div class="multi-icon pointer" @click="$emit('event', 'forward')">
<i class="el-icon-position" />
</div>
<p>逐条转发</p>
</div>
<div class="btn-group">
<div class="multi-icon pointer" @click="$emit('event', 'delete')">
<i class="el-icon-delete" />
</div>
<p>批量删除</p>
</div>
<div class="btn-group">
<div class="multi-icon pointer" @click="$emit('event', 'close')">
<i class="el-icon-close" />
</div>
<p>关闭</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
value: {
default: 0,
},
},
}
</script>
<style lang="less" scoped>
.multi-select {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
.multi-title {
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
color: #878484;
font-weight: 300;
font-size: 14px;
}
.multi-main {
.btn-group {
display: inline-block;
width: 70px;
height: 70px;
margin-right: 15px;
.multi-icon {
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
border-radius: 50%;
margin: 0 auto;
border: 1px solid transparent;
&:hover {
color: red;
border-color: red;
background: transparent;
font-size: 18px;
}
}
p {
font-size: 12px;
margin-top: 8px;
text-align: center;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
<template>
<div>
最近浏览
<dl>
<dd v-for="(item, index) in list">
<div class="base" @click="linkToGoods(item.goodsId,item.id)">
<div>
<img :src="item.thumbnail" class="image" />
</div>
<div style="margin-left: 13px">
<a>{{ item.goodsName }}</a>
<div>
<span style="color: red;">{{ item.price }}</span>
</div>
</div>
</div>
</dd>
</dl>
</div>
</template>
<script>
import { Tag, button } from 'element-ui'
export default {
data() {
return {
}
},
components: {
"el-tag": Tag,
"el-button": button,
},
methods:{
},
props: {
list: {
type: Array,
default: [],
},
},
}
</script>
<style scoped lang="less">
.store-button {
background-color: white;
border-color: #F56C6C;
}
.base {
margin-top: 5px;
height: 120px;
display: flex;
div {
margin-top: 4px;
}
.image {
height: 100px;
margin-top: 3px;
width: 100px
}
}
.separate {
margin-top: 8px;
}
</style>

View File

@@ -0,0 +1,168 @@
<template>
<div>
当前浏览
<div class="base">
<div>
<img :src="goodsDetail.thumbnail" class="image" />
</div>
<div style="margin-left: 13px">
<a @click="linkToGoods(goodsDetail.goodsId,goodsDetail.id)"> {{ goodsDetail.goodsName }} </a>
<div>
<span style="color: red;">{{ goodsDetail.price }}</span>
</div>
<el-button class="store-button" type="danger" v-if="!sendFlag" size="mini" @click="submitSendMessage()"
plain>发送</el-button>
</div>
</div>
<hr class="separate" />
</div>
</template>
<script>
import { Tag, button } from 'element-ui'
import { mapState, mapGetters } from "vuex";
import SocketInstance from "@/im-server/socket-instance";
export default {
data() {
return {
sendFlag: false,
}
},
computed: {
...mapGetters(["talkItems"]),
...mapState({
id: (state) => state.user.id,
index_name: (state) => state.dialogue.index_name,
toUser: (state) => state.user.toUser,
}),
},
mounted(){
},
components: {
"el-tag": Tag,
"el-button": button,
Storage
},
methods: {
toGoods() {
alert("toGoods")
},
toMessage() {
alert(JSON.stringify(this.toUser))
alert("toMessage")
},
// 回车键发送消息回调事件
submitSendMessage() {
console.log("发送");
const context = this.goodsDetail
const record = {
operation_type: "MESSAGE",
to: this.toUser.userId,
from: this.id,
message_type: "GOODS",
context: context,
talk_id: this.toUser.id,
};
SocketInstance.emit("event_talk", record);
this.$store.commit("UPDATE_TALK_ITEM", {
index_name: this.index_name,
draft_text: "",
});
/**
* 插入数据
*/
const insterChat = {
createTime: this.formateDateAndTimeToString(new Date()),
fromUser: this.id,
toUser: record.to,
isRead: false,
messageType: "GOODS",
text: context,
float: "right",
};
console.log("insterChat", insterChat);
// console.log("插入对话记录",'')
// 插入对话记录
this.$store.commit("PUSH_DIALOGUE", insterChat);
// 获取聊天面板元素节点
let el = document.getElementById("lumenChatPanel");
// 判断的滚动条是否在底部
let isBottom =
Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight;
if (isBottom || record.to == this.id) {
this.$nextTick(() => {
el.scrollTop = el.scrollHeight;
});
} else {
this.$store.commit("SET_TLAK_UNREAD_MESSAGE", {
content: content,
nickname: record.name,
});
}
},
formateDateAndTimeToString(date) {
var hours = date.getHours();
var mins = date.getMinutes();
var secs = date.getSeconds();
var msecs = date.getMilliseconds();
if (hours < 10) hours = "0" + hours;
if (mins < 10) mins = "0" + mins;
if (secs < 10) secs = "0" + secs;
if (msecs < 10) secs = "0" + msecs;
return (
this.formatDateToString(date) + " " + hours + ":" + mins + ":" + secs
);
},
formatDateToString(date) {
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
if (month < 10) month = "0" + month;
if (day < 10) day = "0" + day;
return year + "-" + month + "-" + day;
},
},
props: {
goodsDetail: {
type: Object,
default: null,
},
},
}
</script>
<style scoped lang="less">
.store-button {
background-color: white;
border-color: #F56C6C;
}
.base {
margin-top: 5px;
height: 120px;
display: flex;
div {
margin-top: 8px;
}
.image {
height: 100px;
margin-top: 3px;
width: 100px
}
}
.separate {
margin-top: 8px;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div>
<div class="base" >
<div>
<img :src="storeInfo.storeLogo" class="image"/>
</div>
<div style="margin-left: 13px">
<div class="div-zoom">
{{ storeInfo.storeName }}
<el-tag type="danger" v-if=" storeInfo.selfOperated " size="mini">自营</el-tag>
</div>
<div>
联系方式: {{ storeInfo.memberName }}
</div>
<div>
<el-button class="store-button" type="danger" @click="linkToStore(storeInfo.id)" size="mini" plain >进入店铺</el-button>
</div>
</div>
</div>
<hr class="separate"/>
<div class="separate">店铺评分: <span>{{ storeInfo.serviceScore }}</span></div>
<div class="separate">服务评分: <span>{{ storeInfo.descriptionScore }}</span></div>
<div class="separate">物流评分: <span>{{ storeInfo.deliveryScore }}</span></div>
</div>
</template>
<script>
import { Tag,button } from 'element-ui'
export default {
data() {
return {
}
},
components: {
"el-tag": Tag,
"el-button": button,
},
methods:{
},
props: {
storeInfo: {
type: Object,
default: null,
},
},
}
</script>
<style scoped lang="less">
.store-button{
background-color: white;
border-color: #F56C6C;
}
.base{
margin-top: 5px;
height: 120px;
display: flex;
div {
margin-top: 8px;
}
.image{
height: 100px;
margin-top: 3px;
width: 100px
}
}
.separate{
margin-top: 8px;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
<template>
<img :src="'https://avatars.dicebear.com/api/initials/'+text+'.svg?fontSize=38'" alt=""/>
</template>
<script>
export default {
props:{
text:{
type:null,
default:''
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,18 @@
<template>
<img :src="text" alt=""/>
</template>
<script>
export default {
props:{
text:{
type:null,
default:''
}
}
}
</script>
<style lang="scss" scoped>
</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,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/minisite/goods?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,44 @@
<template>
<div class="welcome-box">
<div class="famous-box">
<img src="~@/assets/image/chat.png" width="300" />
</div>
</div>
</template>
<script>
export 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,103 @@
<template>
<!-- 新消息提示组件 -->
<div class="notify-box">
<div class="lbox">
<el-avatar size="medium" :src="avatar" />
</div>
<div class="rbox">
<div class="xheader">
<p class="title">好友申请消息</p>
<p class="time">{{ datetime }}</p>
</div>
<div class="xbody">
<h4>申请备注:</h4>
<div>{{ content }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
components: {},
props: {
params: {
type: Object,
default() {},
},
},
data() {
return {
avatar:
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
talk_type: 1,
nickname: '阿萨纳斯卡',
content: '阿斯纳俺家你卡萨啊看看番按实际开发n',
datetime: '2021-06-18 23:15:12',
}
},
computed: {},
methods: {},
created() {},
}
</script>
<style lang="less" scoped>
.notify-box {
width: 300px;
min-height: 100px;
// background: rebeccapurple;
display: flex;
box-sizing: border-box;
padding: 5px;
.lbox {
flex-basis: 50px;
flex-shrink: 1;
display: flex;
justify-content: center;
}
.rbox {
flex: 1 auto;
margin-left: 5px;
.xheader {
height: 35px;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.title {
font-size: 13px;
font-weight: 500;
}
.time {
font-size: 12px;
}
}
.xbody {
min-height: 60px;
width: 100%;
h4 {
font-size: 13px;
font-weight: 400;
margin-bottom: 3px;
}
div {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
background: #f3f5f7;
font-size: 13px;
padding: 5px;
border-radius: 5px;
}
}
}
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<!-- 新消息提示组件 -->
<div class="notify-box pointer">
<div class="lbox">
<el-avatar size="medium" shape="square" :src="avatar" />
</div>
<div class="rbox">
<div class="xheader">
<p class="title">
{{ talk_type == 1 ? '私信消息通知' : '群聊消息通知' }}
</p>
<p class="time"><i class="el-icon-time" /> {{ datetime | format }}</p>
</div>
<div class="xbody">
<p>@{{ nickname }}</p>
<div>{{ content }}</div>
</div>
</div>
</div>
</template>
<script>
import { parseTime } from '@/utils/functions'
export default {
components: {},
props: {
avatar: {
type: String,
default:
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
},
talk_type: {
type: Number,
default: 1,
},
nickname: {
type: String,
default: '',
},
content: {
type: String,
default: '',
},
datetime: {
type: String,
default: '',
},
},
data() {
return {}
},
filters: {
format(datetime) {
datetime = datetime || new Date()
return parseTime(datetime, '{m}/{d} {h}:{i} 分')
},
},
}
</script>
<style lang="less" scoped>
.notify-box {
width: 300px;
min-height: 100px;
display: flex;
box-sizing: border-box;
padding: 5px;
.lbox {
flex-basis: 50px;
flex-shrink: 1;
display: flex;
justify-content: center;
}
.rbox {
flex: 1 auto;
margin-left: 5px;
.xheader {
height: 25px;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.title {
font-size: 13px;
font-weight: 500;
}
.time {
font-size: 12px;
}
}
.xbody {
min-height: 60px;
width: 100%;
margin-top: 5px;
p {
font-size: 13px;
font-weight: 400;
color: #fb4208;
margin-bottom: 4px;
}
div {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
background: #f3f5f7;
font-size: 13px;
padding: 5px;
border-radius: 5px;
}
}
}
}
</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,119 @@
<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>
</div>
</template>
<script>
import { ServeSearchContact } from '@/api/contacts'
export default {
name: 'UserSearch',
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.$user(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>

View File

@@ -0,0 +1,483 @@
<template>
<div 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="detail.bag" class="img-banner" />
</div>
<div class="user-header">
<div class="avatar">
<div class="avatar-box">
<img :src="detail.avatar" :onerror="$store.state.defaultAvatar" />
</div>
</div>
<div class="nickname">
<i class="iconfont icon-qianming" />
<span>{{ detail.nickname || "未设置昵称" }}</span>
<div class="share no-select">
<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>{{ detail.mobile | mobile }}</span>
</div>
<div class="card-row">
<label>昵称</label>
<span>{{ detail.nickname || "未设置昵称" }}</span>
</div>
<div class="card-row">
<label>性别</label>
<span>{{ detail.gender | gender }}</span>
</div>
<div v-show="detail.friend_status == 2" class="card-row">
<label>备注</label>
<span v-if="editRemark.isShow == false">{{
detail.nickname_remark ? detail.nickname_remark : "暂无备注"
}}</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="detail.friend_status !== 0"
class="no-padding footer"
height="50px"
>
<el-button
v-if="detail.friend_status == 1 && detail.friend_apply == 0"
type="primary"
size="small"
icon="el-icon-circle-plus-outline"
@click="apply.isShow = true"
>添加好友
</el-button>
<el-button
v-else-if="detail.friend_apply == 1"
type="primary"
size="small"
>已发送好友申请请耐心等待...
</el-button>
<el-button
v-else-if="detail.friend_status == 2"
type="primary"
size="small"
icon="el-icon-s-promotion"
@click="sendMessage(detail)"
>发消息
</el-button>
</el-footer>
<!-- 添加好友申请表单 -->
<div
v-outside="closeApply"
class="friend-from"
:class="{ 'friend-from-show': apply.isShow }"
>
<p>
<span>请填写好友申请备注</span>
<span @click="closeApply">取消</span>
</p>
<div>
<input
v-model="apply.text"
type="text"
placeholder="(必填项)"
@keyup.enter="sendApply"
/>
<el-button type="primary" size="small" @click="sendApply">
立即提交
</el-button>
</div>
</div>
</el-container>
</div>
</template>
<script>
// import { ServeSearchUser } from "@/api/user";
import { ServeCreateContact, ServeEditContactRemark } from "@/api/contacts";
import { toTalk } from "@/utils/talk";
export default {
name: "UserCardDetail",
props: {
user_id: {
type: Number,
default: 0,
},
},
filters: {
gender(value) {
let arr = ["未知", "男", "女"];
return arr[value] || "未知";
},
// 手机号格式化
mobile(value) {
return (
value.substr(0, 3) + " " + value.substr(3, 4) + " " + value.substr(7, 4)
);
},
},
data() {
return {
detail: {
mobile: "",
nickname: "",
avatar: "",
motto: "",
friend_status: 0,
friend_apply: 0,
nickname_remark: "",
bag: require("@/assets/image/default-user-banner.png"),
gender: 0,
},
// 好友备注表单
editRemark: {
isShow: false,
text: "",
},
// 好友申请表单
apply: {
isShow: false,
text: "",
},
contacts: false,
};
},
created() {
// this.loadUserDetail();
},
methods: {
close() {
if (this.contacts) return false;
this.$emit("close");
},
// 点击编辑备注信息
clickEditRemark() {
this.editRemark.isShow = true;
this.editRemark.text = this.detail.nickname_remark;
},
// 获取用户信息
// loadUserDetail() {
// ServeSearchUser({
// user_id: this.user_id,
// }).then((res) => {
// if (res.code == 200) {
// this.detail.user_id = res.data.id;
// Object.assign(this.detail, res.data);
// }
// });
// },
// 发送添加好友申请
sendApply() {
if (this.apply.text == "") return;
ServeCreateContact({
friend_id: this.detail.user_id,
remark: this.apply.text,
}).then((res) => {
if (res.code == 200) {
this.apply.isShow = false;
this.apply.text = "";
this.detail.friend_apply = 1;
} else {
this.$message.error("发送好友申请失败,请稍后再试...");
}
});
},
// 编辑好友备注信息
editRemarkSubmit() {
let data = {
friend_id: this.detail.user_id,
remarks: this.editRemark.text,
};
if (data.remarks == this.detail.nickname_remark) {
this.editRemark.isShow = false;
return;
}
ServeEditContactRemark(data).then((res) => {
if (res.code == 200) {
this.editRemark.isShow = false;
this.detail.nickname_remark = data.remarks;
this.$emit("changeRemark", data);
}
});
},
// 隐藏申请表单
closeApply() {
this.apply.isShow = false;
},
// 发送好友消息
sendMessage() {
this.close();
toTalk(1, this.user_id);
},
},
};
</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,33 @@
import UserCardDetail from './UserCardDetail'
export default {
install(Vue) {
function user(user_id, options) {
let _vm = this
const el = new Vue({
router: _vm.$router,
store: _vm.$store,
render(h) {
return h(UserCardDetail, {
on: {
close: () => {
el.$destroy()
document.body.removeChild(el.$el)
},
changeRemark: data => {
options.editRemarkCallbak && options.editRemarkCallbak(data)
},
},
props: {
user_id,
},
})
},
}).$mount()
document.body.appendChild(el.$el)
}
Vue.prototype.$user = user
},
}