no message

This commit is contained in:
kuaifan 2021-06-06 12:20:48 +08:00
parent 4c51e52a30
commit 6981ee03bf
17 changed files with 783 additions and 457 deletions

View File

@ -105,7 +105,10 @@ class DialogController extends AbstractController
return $item;
});
//
return Base::retSuccess('success', $list);
$data = $list->toArray();
$data['dialog'] = WebSocketDialog::formatData($dialog, $user->userid);
//
return Base::retSuccess('success', $data);
}
/**

View File

@ -44,10 +44,13 @@ class WebSocketDialog extends AbstractModel
$dialog->last_msg = $last_msg;
// 未读信息
$dialog->unread = WebSocketDialogMsgRead::whereDialogId($dialog->id)->whereUserid($userid)->whereReadAt(null)->count();
// 对话人数
$builder = WebSocketDialogUser::whereDialogId($dialog->id);
$dialog->people = $builder->count();
// 对方信息
$dialog->dialog_user = null;
if ($dialog->type === 'user') {
$dialog_user = WebSocketDialogUser::whereDialogId($dialog->id)->where('userid', '!=', $userid)->first();
$dialog_user = $builder->where('userid', '!=', $userid)->first();
$dialog->name = User::userid2nickname($dialog_user->userid);
$dialog->dialog_user = $dialog_user;
}

View File

@ -12,6 +12,7 @@ services:
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
- ./docker/log/supervisor:/var/log/supervisor
- ./:/var/www
- /etc/localtime:/etc/localtime:ro
environment:
TZ: "Asia/Shanghai"
LANG: "C.UTF-8"
@ -38,6 +39,7 @@ services:
volumes:
- ./docker/nginx:/etc/nginx/conf.d
- ./public:/var/www/public
- /etc/localtime:/etc/localtime:ro
environment:
TZ: "Asia/Shanghai"
networks:
@ -65,6 +67,7 @@ services:
volumes:
- ./docker/mysql/conf.d:/etc/mysql/conf.d
- ./docker/mysql/data:/var/lib/mysql
- /etc/localtime:/etc/localtime:ro
environment:
TZ: "Asia/Shanghai"
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"

View File

@ -1,6 +1,7 @@
<template>
<div ref="scrollerView" class="app-scroller" :class="[static ? 'app-scroller-static' : '']">
<slot/>
<div ref="bottom" class="app-scroller-bottom"></div>
</div>
</template>
@ -14,6 +15,11 @@
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.app-scroller-bottom {
height: 0;
margin: 0;
padding: 0;
}
}
.app-scroller-static {
@ -29,16 +35,49 @@ export default {
type: Boolean,
default: false
},
autoBottom: {
type: Boolean,
default: false
},
autoRecovery: {
type: Boolean,
default: true
},
autoRecoveryAnimate: {
type: Boolean,
default: false
},
},
data() {
return {
scrollY: 0,
scrollDiff: 0,
scrollInfo: {},
autoInterval: null,
}
},
mounted() {
this.$nextTick(() => {
this.openInterval()
this.$nextTick(this.initScroll);
},
activated() {
this.openInterval()
this.recoveryScroll()
},
destroyed() {
this.closeInterval()
},
deactivated() {
this.closeInterval()
},
methods: {
initScroll() {
this.autoToBottom();
let scrollListener = typeof this.$listeners['on-scroll'] === "function";
let scrollerView = $A(this.$refs.scrollerView);
scrollerView.scroll(() => {
@ -72,16 +111,31 @@ export default {
});
}
});
});
},
activated() {
if (this.scrollY > 0) {
this.$nextTick(() => {
this.scrollTo(this.scrollY);
});
}
},
methods: {
},
recoveryScroll() {
if (this.autoRecovery && (this.scrollY > 0 || this.autoBottom)) {
this.$nextTick(() => {
if (this.autoBottom) {
this.autoToBottom();
} else {
this.scrollTo(this.scrollY, this.autoRecoveryAnimate);
}
});
}
},
openInterval() {
this.autoToBottom();
this.autoInterval && clearInterval(this.autoInterval);
this.autoInterval = setInterval(this.autoToBottom, 300)
},
closeInterval() {
clearInterval(this.autoInterval);
this.autoInterval = null;
},
scrollTo(top, animate) {
if (animate === false) {
$A(this.$refs.scrollerView).stop().scrollTop(top);
@ -89,10 +143,16 @@ export default {
$A(this.$refs.scrollerView).stop().animate({"scrollTop": top});
}
},
scrollToBottom(animate) {
this.scrollTo(this.$refs.scrollerView.scrollHeight, animate);
},
getScrollInfo() {
autoToBottom() {
this.autoBottom && this.$refs.bottom.scrollIntoView(false);
},
scrollInfo() {
let scrollerView = $A(this.$refs.scrollerView);
let wInnerH = Math.round(scrollerView.innerHeight());
let wScrollY = scrollerView.scrollTop();

View File

@ -9,8 +9,8 @@
</div>
<div class="avatar-wrapper">
<div :class="['avatar-box', user.online ? 'online' : '']">
<Avatar v-if="showImg" :src="user.userimg" :size="size"/>
<Avatar v-else :size="size" class="avatar-text">{{nickname}}</Avatar>
<WAvatar v-if="showImg" :src="user.userimg" :size="size"/>
<WAvatar v-else :size="size" class="avatar-text">{{nickname}}</WAvatar>
</div>
<div v-if="showName" class="avatar-name">{{user.nickname}}</div>
</div>
@ -18,8 +18,11 @@
</template>
<script>
import WAvatar from "./WAvatar";
import {mapState} from "vuex";
export default {
name: 'UserAvatar',
components: {WAvatar},
props: {
userid: {
type: [String, Number],
@ -47,6 +50,8 @@
this.getData()
},
computed: {
...mapState(["userOnline"]),
showImg() {
const {userimg} = this.user
if (!userimg) {
@ -54,6 +59,7 @@
}
return !$A.rightExists(userimg, '/avatar.png');
},
nickname() {
const {nickname} = this.user;
if (!nickname) {
@ -69,6 +75,12 @@
watch: {
userid() {
this.getData()
},
userOnline(data) {
if (this.user && data[this.user.userid]) {
this.$set(this.user, 'online', data[this.user.userid]);
}
}
},
methods: {

View File

@ -19,7 +19,7 @@
<div v-if="multipleMax" slot="drop-prepend" class="user-drop-prepend">{{$L('最多只能选择' + multipleMax + '')}}</div>
<Option v-for="(item, key) in lists" :value="item.userid" :key="key" :label="item.nickname" :avatar="item.userimg">
<div class="user-input-option">
<div class="user-input-avatar"><Avatar :src="item.userimg"/></div>
<div class="user-input-avatar"><WAvatar :src="item.userimg"/></div>
<div class="user-input-nickname">{{ item.nickname }}</div>
<div class="user-input-userid">ID: {{ item.userid }}</div>
</div>
@ -30,8 +30,10 @@
</template>
<script>
import WAvatar from "./WAvatar";
export default {
name: 'UserInput',
components: {WAvatar},
props: {
value: {
type: [String, Number, Array],

View File

@ -0,0 +1,124 @@
<template>
<span :class="classes" :style="styles">
<img :src="src" v-if="src" @error="handleError">
<Icon :type="icon" :custom="customIcon" v-else-if="icon || customIcon"></Icon>
<span ref="children" :class="[prefixCls + '-string']" :style="childrenStyle" v-else><slot></slot></span>
</span>
</template>
<script>
import Icon from 'view-design-hi/src/components/icon';
import { oneOf } from 'view-design-hi/src/utils/assist';
const prefixCls = 'ivu-avatar';
const sizeList = ['small', 'large', 'default'];
export default {
name: 'WAvatar',
components: { Icon },
props: {
shape: {
validator (value) {
return oneOf(value, ['circle', 'square']);
},
default: 'circle'
},
size: {
type: [String, Number],
default () {
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
}
},
src: {
type: String
},
icon: {
type: String
},
customIcon: {
type: String,
default: ''
},
},
data () {
return {
prefixCls: prefixCls,
scale: 1,
childrenWidth: 0,
isSlotShow: false,
slotTemp: null
};
},
computed: {
classes () {
return [
`${prefixCls}`,
`${prefixCls}-${this.shape}`,
{
[`${prefixCls}-image`]: !!this.src,
[`${prefixCls}-icon`]: !!this.icon || !!this.customIcon,
[`${prefixCls}-${this.size}`]: oneOf(this.size, sizeList)
}
];
},
styles () {
let style = {};
if (this.size && !oneOf(this.size, sizeList)) {
style.width = `${this.size}px`;
style.height = `${this.size}px`;
style.lineHeight = `${this.size}px`;
style.fontSize = `${this.size/2}px`;
}
return style;
},
childrenStyle () {
let style = {};
if (this.isSlotShow) {
style = {
msTransform: `scale(${this.scale})`,
WebkitTransform: `scale(${this.scale})`,
transform: `scale(${this.scale})`,
display: 'inline-block',
};
}
return style;
}
},
watch: {
size (val, oldVal) {
if (val !== oldVal) this.setScale();
}
},
methods: {
setScale () {
this.isSlotShow = !this.src && !this.icon;
if (this.$refs.children) {
// set children width again to make slot centered
this.childrenWidth = this.$refs.children.offsetWidth;
const avatarWidth = this.$el.getBoundingClientRect().width;
// add 4px gap for each side to get better performance
if (avatarWidth - 8 < this.childrenWidth) {
this.scale = (avatarWidth - 8) / this.childrenWidth;
} else {
this.scale = 1;
}
}
},
handleError (e) {
this.$emit('on-error', e);
}
},
beforeCreate () {
this.slotTemp = this.$slots.default;
},
mounted () {
this.setScale();
},
updated () {
if (this.$slots.default !== this.slotTemp) {
this.slotTemp = this.$slots.default;
this.setScale();
}
}
};
</script>

View File

@ -19,10 +19,6 @@
return window.location.origin + '/' + str;
},
webUrl(str) {
return $A.fillUrl(str || '');
},
apiUrl(str) {
if (str.substring(0, 2) === "//" ||
str.substring(0, 7) === "http://" ||
@ -34,6 +30,10 @@
return apiUrl + str;
},
/**
* @param params {url,data,method,timeout,header,spinner,websocket,timeout, before,complete,success,error,after}
* @returns {boolean}
*/
apiAjax(params) {
if (!$A.isJson(params)) return false;
if (typeof params.success === 'undefined') params.success = () => { };
@ -44,21 +44,23 @@
params.header['language'] = $A.getLanguage();
params.header['token'] = $A.store.state.userToken;
//
let beforeCall = params.before;
params.before = () => {
$A.aAjaxLoadNum++;
$A(".common-spinner").show();
typeof beforeCall == "function" && beforeCall();
};
//
let completeCall = params.complete;
params.complete = () => {
$A.aAjaxLoadNum--;
if ($A.aAjaxLoadNum <= 0) {
$A(".common-spinner").hide();
}
typeof completeCall == "function" && completeCall();
};
if (params.spinner === true) {
let beforeCall = params.before;
params.before = () => {
$A.aAjaxLoadNum++;
$A(".common-spinner").show();
typeof beforeCall == "function" && beforeCall();
};
//
let completeCall = params.complete;
params.complete = () => {
$A.aAjaxLoadNum--;
if ($A.aAjaxLoadNum <= 0) {
$A(".common-spinner").hide();
}
typeof completeCall == "function" && completeCall();
};
}
//
let callback = params.success;
params.success = (data, status, xhr) => {
@ -105,9 +107,9 @@
});
//
params.complete = () => { };
params.after = () => { };
params.success = () => { };
params.error = () => { };
params.after = () => { };
params.header['Api-Websocket'] = apiWebsocket;
//
if ($A.aAjaxWsReady === false) {
@ -140,6 +142,7 @@
}
//
$A.ajaxc(params);
return true;
},
aAjaxLoadNum: 0,
aAjaxWsReady: false,

View File

@ -14,7 +14,7 @@
<Icon type="ios-calendar-outline" />
<div class="menu-title">{{$L('日历')}}</div>
</li>
<li @click="toggleRoute('dialog')" :class="classNameRoute('dialog')">
<li @click="toggleRoute('messenger')" :class="classNameRoute('messenger')">
<Icon type="ios-chatbubbles-outline" />
<div class="menu-title">{{$L('消息')}}</div>
</li>

View File

@ -2,18 +2,18 @@
<div class="dialog-view" :data-id="msgData.id">
<!--文本-->
<div v-if="msgData.type === 'text'" class="dialog-content" v-html="textMsg(msgInfo.text)"></div>
<div v-if="msgData.type === 'text'" class="dialog-content" v-html="textMsg(msgData.msg.text)"></div>
<!--等待-->
<div v-else-if="msgData.type === 'loading'" class="dialog-content loading"><Loading/></div>
<!--文件-->
<div v-else-if="msgData.type === 'file'" :class="['dialog-content', msgInfo.type]">
<a :href="msgInfo.url" target="_blank">
<img v-if="msgInfo.type === 'img'" class="file-img" :style="imageStyle(msgInfo)" :src="msgInfo.thumb"/>
<div v-else-if="msgData.type === 'file'" :class="['dialog-content', msgData.msg.type]">
<a :href="msgData.msg.url" target="_blank">
<img v-if="msgData.msg.type === 'img'" class="file-img" :style="imageStyle(msgData.msg)" :src="msgData.msg.thumb"/>
<div v-else class="file-box">
<img class="file-thumb" :src="msgInfo.thumb"/>
<img class="file-thumb" :src="msgData.msg.thumb"/>
<div class="file-info">
<div class="file-name">{{msgInfo.name}}</div>
<div class="file-size">{{$A.bytesToSize(msgInfo.size)}}</div>
<div class="file-name">{{msgData.msg.name}}</div>
<div class="file-size">{{$A.bytesToSize(msgData.msg.size)}}</div>
</div>
</div>
</a>
@ -74,7 +74,6 @@ export default {
data() {
return {
msgInfo: {},
read_list: []
}
},
@ -117,8 +116,6 @@ export default {
},
parsingData() {
this.msgInfo = this.msgData.msg;
//
const {userid, r, id} = this.msgData;
if (userid == this.userId) return;
if ($A.isJson(r) && r.read_at) return;
@ -153,22 +150,22 @@ export default {
imageStyle(info) {
const {width, height} = info;
if (width && height) {
let maxWidth = 220,
maxHeight = 220,
tempWidth = width,
tempHeight = height;
if (width > maxWidth || height > maxHeight) {
let maxW = 220,
maxH = 220,
tempW = width,
tempH = height;
if (width > maxW || height > maxH) {
if (width > height) {
tempWidth = maxWidth;
tempHeight = height * (maxWidth / width);
tempW = maxW;
tempH = height * (maxW / width);
} else {
tempWidth = width * (maxHeight / height);
tempHeight = maxHeight;
tempW = width * (maxH / height);
tempH = maxH;
}
}
return {
width: tempWidth + 'px',
height: tempHeight + 'px',
width: tempW + 'px',
height: tempH + 'px',
};
}
return {};

View File

@ -0,0 +1,235 @@
<template>
<div
class="dialog-wrapper"
@drop.prevent="chatPasteDrag($event, 'drag')"
@dragover.prevent="chatDragOver(true)"
@dragleave.prevent="chatDragOver(false)">
<slot name="head">
<div class="dialog-title">
<h2>{{dialogDetail.name}}</h2>
<em v-if="peopleNum > 0">({{peopleNum}})</em>
</div>
</slot>
<ScrollerY ref="scroller" class="dialog-chat dialog-scroller" :auto-bottom="autoBottom" @on-scroll="chatScroll">
<div ref="manageList" class="dialog-list">
<ul>
<li v-if="dialogMsgLoad > 0" class="loading"><Loading/></li>
<li v-else-if="dialogMsgList.length === 0" class="nothing">{{$L('暂无消息')}}</li>
<li v-for="(item, key) in dialogMsgList" :key="key" :class="{self:item.userid == userId}">
<div class="dialog-avatar">
<UserAvatar :userid="item.userid" :size="30"/>
</div>
<DialogView :msg-data="item" dialog-type="group"/>
</li>
</ul>
</div>
</ScrollerY>
<div :class="['dialog-footer', msgNew > 0 ? 'newmsg' : '']">
<div class="dialog-newmsg" @click="goNewBottom">{{$L('' + msgNew + '条新消息')}}</div>
<DragInput class="dialog-input" v-model="msgText" type="textarea" :rows="1" :autosize="{ minRows: 1, maxRows: 3 }" :maxlength="255" @on-keydown="chatKeydown" @on-input-paste="pasteDrag" :placeholder="$L('输入消息...')" />
<DialogUpload
ref="chatUpload"
class="chat-upload"
@on-progress="chatFile('progress', $event)"
@on-success="chatFile('success', $event)"
@on-error="chatFile('error', $event)"/>
</div>
<div v-if="dialogDrag" class="drag-over" @click="dialogDrag=false">
<div class="drag-text">{{$L('拖动到这里发送')}}</div>
</div>
</div>
</template>
<script>
import DragInput from "../../../components/DragInput";
import ScrollerY from "../../../components/ScrollerY";
import {mapState} from "vuex";
import DialogView from "./DialogView";
import DialogUpload from "./DialogUpload";
export default {
name: "DialogWrapper",
components: {DialogUpload, DialogView, ScrollerY, DragInput},
data() {
return {
autoBottom: true,
autoInterval: null,
memberShowAll: false,
dialogDrag: false,
msgText: '',
msgLength: 0,
msgNew: 0,
}
},
computed: {
...mapState(['userId', 'dialogId', 'dialogDetail', 'dialogMsgLoad', 'dialogMsgList']),
peopleNum() {
return this.dialogDetail.type === 'group' ? $A.runNum(this.dialogDetail.people) : 0;
}
},
watch: {
dialogMsgList(list) {
if (!this.autoBottom) {
let length = list.length - this.msgLength;
if (length > 0) {
this.msgNew+= length;
}
} else {
this.$nextTick(this.goBottom);
}
this.msgLength = list.length;
}
},
methods: {
sendMsg() {
let tempId = $A.randomString(16);
this.dialogMsgList.push({
id: tempId,
type: 'text',
userid: this.userId,
msg: {
text: this.msgText,
},
});
this.goBottom();
//
$A.apiAjax({
url: 'dialog/msg/sendtext',
data: {
dialog_id: this.dialogId,
text: this.msgText,
},
error:() => {
this.$store.commit('spliceDialogMsg', {id: tempId});
},
success: ({ret, data, msg}) => {
if (ret !== 1) {
$A.modalWarning({
title: '发送失败',
content: msg
});
}
this.$store.commit('spliceDialogMsg', {
id: tempId,
data: ret === 1 ? data : null
});
}
});
//
this.msgText = '';
},
chatKeydown(e) {
if (e.keyCode === 13) {
if (e.shiftKey) {
return;
}
e.preventDefault();
this.sendMsg();
}
},
pasteDrag(e, type) {
const files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
const postFiles = Array.prototype.slice.call(files);
if (postFiles.length > 0) {
e.preventDefault();
postFiles.forEach((file) => {
this.$refs.chatUpload.upload(file);
});
}
},
chatDragOver(show) {
let random = (this.__dialogDrag = $A.randomString(8));
if (!show) {
setTimeout(() => {
if (random === this.__dialogDrag) {
this.dialogDrag = show;
}
}, 150);
} else {
this.dialogDrag = show;
}
},
chatPasteDrag(e, type) {
this.dialogDrag = false;
const files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
const postFiles = Array.prototype.slice.call(files);
if (postFiles.length > 0) {
e.preventDefault();
postFiles.forEach((file) => {
this.$refs.chatUpload.upload(file);
});
}
},
chatFile(type, file) {
switch (type) {
case 'progress':
this.dialogMsgList.push({
id: file.tempId,
type: 'loading',
userid: this.userId,
msg: { },
});
break;
case 'error':
this.$store.commit('spliceDialogMsg', {id: file.tempId});
break;
case 'success':
this.$store.commit('spliceDialogMsg', {id: file.tempId, data: file.data});
break;
}
},
chatScroll(res) {
switch (res.directionreal) {
case 'up':
if (res.scrollE < 10) {
this.autoBottom = true;
}
break;
case 'down':
this.autoBottom = false;
break;
}
},
goBottom() {
if (this.autoBottom) {
this.$refs.scroller.autoToBottom();
}
},
goNewBottom() {
this.msgNew = 0;
this.autoBottom = true;
this.goBottom();
},
formatTime(date) {
let time = Math.round(new Date(date).getTime() / 1000),
string = '';
if ($A.formatDate('Ymd') === $A.formatDate('Ymd', time)) {
string = $A.formatDate('H:i', time)
} else if ($A.formatDate('Y') === $A.formatDate('Y', time)) {
string = $A.formatDate('m-d', time)
} else {
string = $A.formatDate('Y-m-d', time)
}
return string || '';
},
}
}
</script>

View File

@ -1,74 +1,26 @@
<template>
<div
v-if="$store.state.projectChatShow"
class="project-dialog"
@drop.prevent="chatPasteDrag($event, 'drag')"
@dragover.prevent="chatDragOver(true)"
@dragleave.prevent="chatDragOver(false)">
<div class="dialog-user">
<div class="member-head">
<div class="member-title">{{$L('项目成员')}}<span>({{projectDetail.project_user.length}})</span></div>
<div class="member-view-all" @click="memberShowAll=!memberShowAll">{{$L('查看所有')}}</div>
<div v-if="$store.state.projectChatShow" class="project-dialog">
<DialogWrapper class="project-dialog-wrapper">
<div slot="head">
<div class="dialog-user">
<div class="member-head">
<div class="member-title">{{$L('项目成员')}}<span>({{projectDetail.project_user.length}})</span></div>
<div class="member-view-all" @click="memberShowAll=!memberShowAll">{{$L('查看所有')}}</div>
</div>
<ul :class="['member-list', memberShowAll ? 'member-all' : '']">
<li v-for="item in projectDetail.project_user">
<UserAvatar :userid="item.userid" :size="36"/>
</li>
</ul>
</div>
<div class="dialog-title">
<h2>{{$L('群聊')}}</h2>
</div>
</div>
<ul :class="['member-list', memberShowAll ? 'member-all' : '']">
<li v-for="item in projectDetail.project_user">
<UserAvatar :userid="item.userid" :size="36"/>
</li>
</ul>
</div>
<div class="dialog-title">{{$L('群聊')}}</div>
<ScrollerY class="dialog-chat dialog-scroller" @on-scroll="chatScroll">
<div ref="manageList" class="dialog-list">
<ul>
<li v-if="dialogMsgLoad > 0" class="loading"><Loading/></li>
<li v-else-if="dialogMsgList.length === 0" class="nothing">{{$L('暂无消息')}}</li>
<li v-for="(item, key) in dialogMsgList" :key="key" :class="{self:item.userid == userId}">
<div class="dialog-avatar">
<UserAvatar :userid="item.userid" :size="30"/>
</div>
<DialogView :msg-data="item" dialog-type="group"/>
</li>
<li ref="bottom" class="bottom"></li>
</ul>
</div>
</ScrollerY>
<div :class="['dialog-footer', msgNew > 0 ? 'newmsg' : '']">
<div class="dialog-newmsg" @click="goNewBottom">{{$L('' + msgNew + '条新消息')}}</div>
<DragInput class="dialog-input" v-model="msgText" type="textarea" :rows="1" :autosize="{ minRows: 1, maxRows: 3 }" :maxlength="255" @on-keydown="chatKeydown" @on-input-paste="pasteDrag" :placeholder="$L('输入消息...')" />
<DialogUpload
ref="chatUpload"
class="chat-upload"
@on-progress="chatFile('progress', $event)"
@on-success="chatFile('success', $event)"
@on-error="chatFile('error', $event)"/>
</div>
<div v-if="dialogDrag" class="drag-over" @click="dialogDrag=false">
<div class="drag-text">{{$L('拖动到这里发送')}}</div>
</div>
</DialogWrapper>
</div>
</template>
<style lang="scss">
:global {
.project-dialog {
.dialog-footer {
.dialog-input {
background-color: #F4F5F7;
padding: 10px 12px;
border-radius: 10px;
.ivu-input {
border: 0;
resize: none;
background-color: transparent;
&:focus {
box-shadow: none;
}
}
}
}
}
}
</style>
<style lang="scss" scoped>
:global {
.project-dialog {
@ -76,118 +28,51 @@
flex-direction: column;
background-color: #ffffff;
z-index: 1;
.dialog-user {
margin-top: 36px;
padding: 0 32px;
.member-head {
display: flex;
align-items: center;
.member-title {
flex: 1;
font-size: 18px;
font-weight: 600;
> span {
padding-left: 6px;
color: #2d8cf0;
}
}
.member-view-all {
color: #999;
font-size: 13px;
cursor: pointer;
&:hover {
color: #777;
}
}
}
.member-list {
display: flex;
align-items: center;
margin-top: 14px;
overflow: auto;
> li {
position: relative;
list-style: none;
margin-right: 14px;
margin-bottom: 8px;
}
&.member-all {
display: block;
> li {
display: inline-block;
}
}
}
}
.dialog-title {
padding: 0 32px;
margin-top: 20px;
font-size: 18px;
font-weight: 600;
}
.dialog-chat {
.project-dialog-wrapper {
flex: 1;
padding: 0 32px;
margin-top: 18px;
}
.dialog-footer {
display: flex;
flex-direction: column;
align-items: flex-end;
padding: 0 28px;
margin-bottom: 20px;
.dialog-newmsg {
display: none;
height: 30px;
line-height: 30px;
color: #ffffff;
font-size: 12px;
background-color: rgba(0, 0, 0, 0.6);
padding: 0 12px;
margin-bottom: 20px;
margin-right: 10px;
border-radius: 16px;
cursor: pointer;
z-index: 2;;
}
.chat-upload {
display: none;
width: 0;
height: 0;
overflow: hidden;
}
&.newmsg {
margin-top: -50px;
.dialog-newmsg {
display: block;
height: 0;
.dialog-user {
margin-top: 36px;
padding: 0 32px;
.member-head {
display: flex;
align-items: center;
.member-title {
flex: 1;
font-size: 18px;
font-weight: 600;
> span {
padding-left: 6px;
color: #2d8cf0;
}
}
.member-view-all {
color: #999;
font-size: 13px;
cursor: pointer;
&:hover {
color: #777;
}
}
}
.member-list {
display: flex;
align-items: center;
margin-top: 14px;
overflow: auto;
> li {
position: relative;
list-style: none;
margin-right: 14px;
margin-bottom: 8px;
}
&.member-all {
display: block;
> li {
display: inline-block;
}
}
}
}
}
.drag-over {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 3;
background-color: rgba(255, 255, 255, 0.78);
display: flex;
align-items: center;
justify-content: center;
&:before {
content: "";
position: absolute;
top: 16px;
left: 16px;
right: 16px;
bottom: 16px;
border: 2px dashed #7b7b7b;
border-radius: 12px;
}
.drag-text {
padding: 12px;
font-size: 18px;
color: #666666;
}
}
}
@ -195,207 +80,37 @@
</style>
<script>
import DragInput from "../../../components/DragInput";
import ScrollerY from "../../../components/ScrollerY";
import {mapState} from "vuex";
import DialogView from "./DialogView";
import DialogUpload from "./DialogUpload";
import DialogWrapper from "./DialogWrapper";
export default {
name: "ProjectDialog",
components: {DialogUpload, DialogView, ScrollerY, DragInput},
components: {DialogWrapper},
data() {
return {
autoBottom: true,
autoInterval: null,
memberShowAll: false,
dialogId: 0,
dialogDrag: false,
msgText: '',
msgLength: 0,
msgNew: 0,
}
},
mounted() {
this.goBottom();
this.autoInterval = setInterval(this.goBottom, 200)
},
beforeDestroy() {
clearInterval(this.autoInterval)
},
computed: {
...mapState(['userId', 'projectDetail', 'projectMsgUnread', 'dialogMsgLoad', 'dialogMsgList']),
...mapState(['projectDetail', 'projectChatShow']),
},
watch: {
projectDetail(detail) {
this.dialogId = detail.dialog_id;
projectDetail() {
this.getDialogMsg()
},
dialogId(id) {
this.$store.commit('getDialogMsg', id);
},
dialogMsgList(list) {
if (!this.autoBottom) {
let length = list.length - this.msgLength;
if (length > 0) {
this.msgNew+= length;
}
}
this.msgLength = list.length;
projectChatShow() {
this.getDialogMsg()
}
},
methods: {
sendMsg() {
let tempId = $A.randomString(16);
this.dialogMsgList.push({
id: tempId,
type: 'text',
userid: this.userId,
msg: {
text: this.msgText,
},
});
this.goBottom();
//
$A.apiAjax({
url: 'dialog/msg/sendtext',
data: {
dialog_id: this.projectDetail.dialog_id,
text: this.msgText,
},
error:() => {
this.$store.commit('spliceDialogMsg', {id: tempId});
},
success: ({ret, data, msg}) => {
if (ret !== 1) {
$A.modalWarning({
title: '发送失败',
content: msg
});
}
this.$store.commit('spliceDialogMsg', {
id: tempId,
data: ret === 1 ? data : null
});
}
});
//
this.msgText = '';
},
chatKeydown(e) {
if (e.keyCode === 13) {
if (e.shiftKey) {
return;
}
e.preventDefault();
this.sendMsg();
getDialogMsg() {
if (this.projectChatShow && this.projectDetail.dialog_id) {
this.$store.commit('getDialogMsg', this.projectDetail.dialog_id);
}
},
pasteDrag(e, type) {
const files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
const postFiles = Array.prototype.slice.call(files);
if (postFiles.length > 0) {
e.preventDefault();
postFiles.forEach((file) => {
this.$refs.chatUpload.upload(file);
});
}
},
chatDragOver(show) {
let random = (this.__dialogDrag = $A.randomString(8));
if (!show) {
setTimeout(() => {
if (random === this.__dialogDrag) {
this.dialogDrag = show;
}
}, 150);
} else {
this.dialogDrag = show;
}
},
chatPasteDrag(e, type) {
this.dialogDrag = false;
const files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
const postFiles = Array.prototype.slice.call(files);
if (postFiles.length > 0) {
e.preventDefault();
postFiles.forEach((file) => {
this.$refs.chatUpload.upload(file);
});
}
},
chatFile(type, file) {
switch (type) {
case 'progress':
this.dialogMsgList.push({
id: file.tempId,
type: 'loading',
userid: this.userId,
msg: { },
});
break;
case 'error':
this.$store.commit('spliceDialogMsg', {id: file.tempId});
break;
case 'success':
this.$store.commit('spliceDialogMsg', {id: file.tempId, data: file.data});
break;
}
},
chatScroll(res) {
switch (res.directionreal) {
case 'up':
if (res.scrollE < 10) {
this.autoBottom = true;
}
break;
case 'down':
this.autoBottom = false;
break;
}
},
goBottom() {
if (this.autoBottom && this.$refs.bottom) {
this.$refs.bottom.scrollIntoView(false);
}
},
goNewBottom() {
this.msgNew = 0;
this.autoBottom = true;
this.goBottom();
},
formatTime(date) {
let time = Math.round(new Date(date).getTime() / 1000),
string = '';
if ($A.formatDate('Ymd') === $A.formatDate('Ymd', time)) {
string = $A.formatDate('H:i', time)
} else if ($A.formatDate('Y') === $A.formatDate('Y', time)) {
string = $A.formatDate('m-d', time)
} else {
string = $A.formatDate('Y-m-d', time)
}
return string || '';
},
}
}
}
</script>

View File

@ -1,35 +1,43 @@
<template>
<div class="dialog">
<div class="messenger">
<PageTitle>{{ $L('消息') }}</PageTitle>
<div class="dialog-wrapper">
<div class="messenger-wrapper">
<div class="dialog-select">
<div class="dialog-search">
<Input prefix="ios-search" v-model="dialogKey" :placeholder="$L('搜索...')" clearable />
<div class="messenger-select">
<div class="messenger-search">
<div class="search-wrapper">
<Input prefix="ios-search" v-model="dialogKey" :placeholder="$L('搜索...')" clearable />
</div>
</div>
<div class="dialog-list overlay-y">
<div class="messenger-list overlay-y">
<ul>
<li v-for="(dialog, key) in dialogLists" :key="key" :class="{active: dialog.id == dialogId}">
<li
v-for="(dialog, key) in dialogLists"
:key="key"
:class="{active: dialog.id == dialogId}"
@click="openDialog(dialog)">
<Icon v-if="dialog.type=='group'" class="group-avatar" type="ios-people" />
<UserAvatar v-else-if="dialog.dialog_user" :userid="dialog.dialog_user.userid" :size="46"/>
<div class="user-msg-box">
<div class="user-msg-title">
<div class="dialog-box">
<div class="dialog-title">
<span>{{dialog.name}}</span>
<em v-if="dialog.last_at">{{formatTime(dialog.last_at)}}</em>
</div>
<div class="user-msg-text">{{formatLastMsg(dialog.last_msg)}}</div>
<div class="dialog-text">{{formatLastMsg(dialog.last_msg)}}</div>
</div>
<Badge class="user-msg-num" :count="dialog.unread"/>
<Badge class="dialog-num" :count="dialog.unread"/>
</li>
</ul>
</div>
<div class="dialog-menu">
<div class="messenger-menu">
<Icon class="active" type="ios-chatbubbles" />
<Icon type="md-person" />
</div>
</div>
<div class="dialog-msg"></div>
<div class="messenger-msg">
<DialogWrapper v-if="dialogId > 0"/>
</div>
</div>
</div>
@ -37,7 +45,7 @@
<style lang="scss" scoped>
:global {
.dialog {
.messenger {
display: flex;
}
}
@ -45,8 +53,10 @@
<script>
import {mapState} from "vuex";
import DialogWrapper from "./components/DialogWrapper";
export default {
components: {DialogWrapper},
data() {
return {
dialogLoad: 0,
@ -67,8 +77,14 @@ export default {
if (dialogKey == '') {
return this.dialogList;
}
return this.dialogList.filter(({name}) => {
return $A.strExists(name, dialogKey);
return this.dialogList.filter(({name, last_msg}) => {
if ($A.strExists(name, dialogKey)) {
return true;
}
if (last_msg && last_msg.type === 'text' && $A.strExists(last_msg.msg.text, dialogKey)) {
return true;
}
return false;
})
},
},
@ -107,6 +123,10 @@ export default {
},
methods: {
openDialog(dialog) {
this.$store.commit('getDialogMsg', dialog.id);
},
getDialogLists() {
this.dialogLoad++;
$A.apiAjax({

View File

@ -23,9 +23,9 @@ export default [
component: () => import('./pages/manage/calendar.vue'),
},
{
name: 'manage-dialog',
path: 'dialog',
component: () => import('./pages/manage/dialog.vue'),
name: 'manage-messenger',
path: 'messenger',
component: () => import('./pages/manage/messenger.vue'),
},
{
name: 'manage-setting',

View File

@ -66,6 +66,18 @@ export default {
this.commit('wsConnection');
},
/**
* 更新会员在线
* @param state
* @param info
*/
setUserOnlineStatus(state, info) {
const {userid, online} = info;
if (state.userOnline[userid] !== online) {
state.userOnline = Object.assign({}, state.userOnline, {[userid]: online});
}
},
/**
* 获取项目列表
* @param state
@ -101,6 +113,9 @@ export default {
if (state.method.runNum(project_id) === 0) {
return;
}
if (state.projectDetail.id === project_id) {
return;
}
if (state.method.isJson(state.cacheProject[project_id])) {
state.projectDetail = state.cacheProject[project_id];
}
@ -183,6 +198,7 @@ export default {
time,
data: item
};
this.commit('setUserOnlineStatus', item);
typeof success === "function" && success(item, true)
});
} else {
@ -201,10 +217,20 @@ export default {
if (state.method.runNum(dialog_id) === 0) {
return;
}
if (state.method.isArray(state.cacheDialog[dialog_id])) {
state.dialogMsgList = state.cacheDialog[dialog_id]
} else {
state.dialogMsgList = [];
if (state.dialogId === dialog_id) {
return;
}
//
state.dialogMsgList = [];
if (state.method.isJson(state.cacheDialog[dialog_id])) {
setTimeout(() => {
let length = state.cacheDialog[dialog_id].data.length;
if (length > 50) {
state.cacheDialog[dialog_id].data.splice(0, length - 50);
}
state.dialogDetail = state.cacheDialog[dialog_id].dialog
state.dialogMsgList = state.cacheDialog[dialog_id].data
});
}
state.dialogId = dialog_id;
//
@ -225,9 +251,13 @@ export default {
},
success: ({ret, data, msg}) => {
if (ret === 1) {
state.cacheDialog[dialog_id] = data.data.reverse();
state.cacheDialog[dialog_id] = {
dialog: data.dialog,
data: data.data.reverse(),
};
if (state.dialogId === dialog_id) {
state.cacheDialog[dialog_id].forEach((item) => {
state.dialogDetail = state.cacheDialog[dialog_id].dialog;
state.cacheDialog[dialog_id].data.forEach((item) => {
let index = state.dialogMsgList.findIndex(({id}) => id === item.id);
if (index === -1) {
state.dialogMsgList.push(item);

View File

@ -164,6 +164,7 @@ state.userInfo = state.method.getStorageJson('userInfo');
state.userId = state.userInfo.userid = state.method.runNum(state.userInfo.userid);
state.userToken = state.userInfo.token;
state.userIsAdmin = state.method.inArray('admin', state.userInfo.identity);
state.userOnline = {};
// Websocket
state.ws = null;
@ -177,6 +178,7 @@ state.projectLoad = 0;
state.projectList = [];
state.projectDetail = {
id: 0,
dialog_id: 0,
project_column: [],
project_user: []
};
@ -184,6 +186,7 @@ state.projectMsgUnread = 0;
// 会话消息
state.dialogId = 0;
state.dialogDetail = {};
state.dialogMsgLoad = 0;
state.dialogMsgList = [];

View File

@ -701,6 +701,8 @@ body {
position: relative;
margin-bottom: 20px;
flex-shrink: 0;
width: 30px;
height: 30px;
}
.dialog-view {
max-width: 70%;
@ -899,10 +901,111 @@ body {
}
.dialog-wrapper {
display: flex;
flex-direction: column;
background-color: #ffffff;
z-index: 1;
.dialog-title {
display: flex;
align-items: center;
padding: 0 32px;
margin-top: 20px;
> h2 {
font-size: 18px;
font-weight: 600;
}
> em {
font-style: normal;
font-size: 17px;
font-weight: 500;
padding-left: 6px;
}
}
.dialog-chat {
flex: 1;
padding: 0 32px;
margin-top: 18px;
}
.dialog-footer {
display: flex;
flex-direction: column;
align-items: flex-end;
padding: 0 28px;
margin-bottom: 20px;
.dialog-newmsg {
display: none;
height: 30px;
line-height: 30px;
color: #ffffff;
font-size: 12px;
background-color: rgba(0, 0, 0, 0.6);
padding: 0 12px;
margin-bottom: 20px;
margin-right: 10px;
border-radius: 16px;
cursor: pointer;
z-index: 2;;
}
.dialog-input {
background-color: #F4F5F7;
padding: 10px 12px;
border-radius: 10px;
.ivu-input {
border: 0;
resize: none;
background-color: transparent;
&:focus {
box-shadow: none;
}
}
}
.chat-upload {
display: none;
width: 0;
height: 0;
overflow: hidden;
}
&.newmsg {
margin-top: -50px;
.dialog-newmsg {
display: block;
}
}
}
.drag-over {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 3;
background-color: rgba(255, 255, 255, 0.78);
display: flex;
align-items: center;
justify-content: center;
&:before {
content: "";
position: absolute;
top: 16px;
left: 16px;
right: 16px;
bottom: 16px;
border: 2px dashed #7b7b7b;
border-radius: 12px;
}
.drag-text {
padding: 12px;
font-size: 18px;
color: #666666;
}
}
}
.messenger-wrapper {
flex: 1;
display: flex;
align-items: flex-start;
.dialog-select {
.messenger-select {
position: relative;
height: 100%;
width: 30%;
@ -920,22 +1023,31 @@ body {
width: 1px;
background-color: #f2f2f2;
}
.dialog-search {
.messenger-search {
display: flex;
align-items: center;
justify-content: center;
height: 54px;
padding: 0 12px;
flex-shrink: 0;
.ivu-input {
border-color: transparent;
&:hover,
&:focus {
box-shadow: none;
.search-wrapper {
flex: 1;
background-color: #F7F7F7;
padding: 0 8px;
margin: 0 4px;
border-radius: 12px;
overflow: hidden;
.ivu-input {
border-color: transparent;
background-color: transparent;
&:hover,
&:focus {
box-shadow: none;
}
}
}
}
.dialog-list {
.messenger-list {
flex: 1;
height: 0;
width: 100%;
@ -946,7 +1058,7 @@ body {
display: flex;
flex-direction: row;
align-items: center;
height: 86px;
height: 80px;
padding: 0 12px;
position: relative;
cursor: pointer;
@ -968,12 +1080,12 @@ body {
flex-grow: 0;
flex-shrink: 0;
}
.user-msg-box {
.dialog-box {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 12px;
.user-msg-title {
.dialog-title {
display: flex;
flex-direction: row;
align-items: center;
@ -995,7 +1107,7 @@ body {
padding-left: 10px;
}
}
.user-msg-text {
.dialog-text {
max-width: 170px;
color: #999999;
font-size: 12px;
@ -1005,7 +1117,7 @@ body {
text-overflow: ellipsis;
}
}
.user-msg-num {
.dialog-num {
position: absolute;
top: 12px;
left: 38px;
@ -1020,7 +1132,7 @@ body {
}
}
}
.dialog-menu {
.messenger-menu {
display: flex;
align-items: center;
justify-content: center;
@ -1043,9 +1155,13 @@ body {
}
}
}
.dialog-msg {
.messenger-msg {
flex: 1;
width: 0;
height: 100%;
display: flex;
.dialog-wrapper {
flex: 1;
}
}
}