no message
This commit is contained in:
parent
4c51e52a30
commit
6981ee03bf
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}"
|
||||
|
@ -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();
|
||||
|
@ -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: {
|
||||
|
@ -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],
|
||||
|
124
resources/assets/js/components/WAvatar.vue
Normal file
124
resources/assets/js/components/WAvatar.vue
Normal 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>
|
43
resources/assets/js/functions/web.js
vendored
43
resources/assets/js/functions/web.js
vendored
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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 {};
|
||||
|
235
resources/assets/js/pages/manage/components/DialogWrapper.vue
Normal file
235
resources/assets/js/pages/manage/components/DialogWrapper.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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({
|
6
resources/assets/js/routes.js
vendored
6
resources/assets/js/routes.js
vendored
@ -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',
|
||||
|
42
resources/assets/js/store/mutations.js
vendored
42
resources/assets/js/store/mutations.js
vendored
@ -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);
|
||||
|
3
resources/assets/js/store/state.js
vendored
3
resources/assets/js/store/state.js
vendored
@ -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 = [];
|
||||
|
||||
|
146
resources/assets/sass/main.scss
vendored
146
resources/assets/sass/main.scss
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user