no message

This commit is contained in:
kuaifan 2021-06-06 01:38:40 +08:00
parent c6285c57c8
commit 4c51e52a30
15 changed files with 453 additions and 39 deletions

View File

@ -17,6 +17,61 @@ use Request;
*/
class DialogController extends AbstractController
{
/**
* 对话列表
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:100,最大:200
*/
public function lists()
{
$user = User::authE();
if (Base::isError($user)) {
return $user;
} else {
$user = User::IDE($user['data']);
}
//
$list = WebSocketDialog::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('u.userid', $user->userid)
->orderByDesc('web_socket_dialogs.last_at')
->paginate(Base::getPaginate(200, 100));
$list->transform(function (WebSocketDialog $item) use ($user) {
return WebSocketDialog::formatData($item, $user->userid);
});
//
return Base::retSuccess('success', $list);
}
/**
* 单个对话信息
*
* @apiParam {Number} dialog_id 对话ID
*/
public function one()
{
$user = User::authE();
if (Base::isError($user)) {
return $user;
} else {
$user = User::IDE($user['data']);
}
//
$dialog_id = intval(Request::input('dialog_id'));
//
$item = WebSocketDialog::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.id', $dialog_id)
->where('u.userid', $user->userid)
->first();
if ($item) {
$item = WebSocketDialog::formatData($item, $user->userid);
}
//
return Base::retSuccess('success', $item);
}
/**
* 消息列表
*

View File

@ -375,6 +375,18 @@ class User extends AbstractModel
return $_A["__static_userid2basic_" . $userid] = ($userInfo ?: []);
}
/**
* userid 获取 昵称
* @param $userid
* @return string
*/
public static function userid2nickname($userid)
{
$basic = self::userid2basic($userid);
return $basic ? $basic->nickname : '';
}
/**
* 更新首字母
* @param $userid

View File

@ -31,6 +31,29 @@ use App\Module\Base;
*/
class WebSocketDialog extends AbstractModel
{
/**
* 格式化对话
* @param WebSocketDialog $dialog
* @param int $userid 会员ID
* @return WebSocketDialog
*/
public static function formatData(WebSocketDialog $dialog, $userid)
{
// 最后消息
$last_msg = WebSocketDialogMsg::whereDialogId($dialog->id)->orderByDesc('id')->first();
$dialog->last_msg = $last_msg;
// 未读信息
$dialog->unread = WebSocketDialogMsgRead::whereDialogId($dialog->id)->whereUserid($userid)->whereReadAt(null)->count();
// 对方信息
$dialog->dialog_user = null;
if ($dialog->type === 'user') {
$dialog_user = WebSocketDialogUser::whereDialogId($dialog->id)->where('userid', '!=', $userid)->first();
$dialog->name = User::userid2nickname($dialog_user->userid);
$dialog->dialog_user = $dialog_user;
}
return $dialog;
}
/**
* 创建聊天室
* @param string $name 聊天室名称

View File

@ -99,6 +99,7 @@ class WebSocketDialogMsg extends AbstractModel
$msgRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereUserid($userid)->lockForUpdate()->first();
if (empty($msgRead)) {
$msgRead = WebSocketDialogMsgRead::createInstance([
'dialog_id' => $this->dialog_id,
'msg_id' => $this->id,
'userid' => $userid,
]);

View File

@ -7,12 +7,14 @@ namespace App\Models;
*
* @package App\Models
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $msg_id 消息ID
* @property int|null $userid 发送会员ID
* @property string|null $read_at 阅读时间
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereReadAt($value)

View File

@ -35,12 +35,14 @@ class WebSocketDialogMsgTask extends AbstractTask
$userids = is_array($this->userid) ? $this->userid : [$this->userid];
$msgId = intval($this->dialogMsgArray['id']);
$send = intval($this->dialogMsgArray['send']);
$dialogId = intval($this->dialogMsgArray['dialog_id']);
if (empty($userids) || empty($msgId)) {
return;
}
$pushIds = [];
foreach ($userids AS $userid) {
$msgRead = WebSocketDialogMsgRead::createInstance([
'dialog_id' => $dialogId,
'msg_id' => $msgId,
'userid' => $userid,
]);

View File

@ -7,17 +7,21 @@
</div>
<ul>
<li @click="toggleRoute('dashboard')" :class="classNameRoute('dashboard')">
<Icon type="md-speedometer" />
<Icon type="ios-speedometer-outline" />
<div class="menu-title">{{$L('仪表板')}}</div>
</li>
<li @click="toggleRoute('setting/personal')" :class="classNameRoute('setting')">
<Icon type="md-cog" />
<div class="menu-title">{{$L('设置')}}</div>
</li>
<li @click="toggleRoute('calendar')" :class="classNameRoute('calendar')">
<Icon type="md-calendar" />
<Icon type="ios-calendar-outline" />
<div class="menu-title">{{$L('日历')}}</div>
</li>
<li @click="toggleRoute('dialog')" :class="classNameRoute('dialog')">
<Icon type="ios-chatbubbles-outline" />
<div class="menu-title">{{$L('消息')}}</div>
</li>
<li @click="toggleRoute('setting/personal')" :class="classNameRoute('setting')">
<Icon type="ios-cog-outline" />
<div class="menu-title">{{$L('设置')}}</div>
</li>
<li class="menu-project">
<ul>
<li v-for="(item, key) in projectList" :key="key" @click="toggleRoute('project/' + item.id)" :class="classNameRoute('project/' + item.id)">{{item.name}}</li>
@ -131,7 +135,7 @@
padding: 0 4%;
border-radius: 4px;
> i {
opacity: 0.3;
opacity: 0.5;
font-size: 22px;
margin-right: 10px;
margin-top: -1px;

View File

@ -20,9 +20,9 @@
<ScrollerY class="dialog-chat dialog-scroller" @on-scroll="chatScroll">
<div ref="manageList" class="dialog-list">
<ul>
<li v-if="dialogLoad > 0" class="loading"><Loading/></li>
<li v-else-if="dialogList.length === 0" class="nothing">{{$L('暂无消息')}}</li>
<li v-for="(item, key) in dialogList" :key="key" :class="{self:item.userid == userId}">
<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>
@ -230,7 +230,7 @@ export default {
},
computed: {
...mapState(['userId', 'projectDetail', 'projectMsgUnread', 'dialogLoad', 'dialogList']),
...mapState(['userId', 'projectDetail', 'projectMsgUnread', 'dialogMsgLoad', 'dialogMsgList']),
},
watch: {
@ -242,7 +242,7 @@ export default {
this.$store.commit('getDialogMsg', id);
},
dialogList(list) {
dialogMsgList(list) {
if (!this.autoBottom) {
let length = list.length - this.msgLength;
if (length > 0) {
@ -256,7 +256,7 @@ export default {
methods: {
sendMsg() {
let tempId = $A.randomString(16);
this.dialogList.push({
this.dialogMsgList.push({
id: tempId,
type: 'text',
userid: this.userId,
@ -341,7 +341,7 @@ export default {
chatFile(type, file) {
switch (type) {
case 'progress':
this.dialogList.push({
this.dialogMsgList.push({
id: file.tempId,
type: 'loading',
userid: this.userId,

View File

@ -1,17 +1,175 @@
<template>
<div class="calendar">
<PageTitle>{{$L('Calendar')}}</PageTitle>
<div class="dialog">
<PageTitle>{{ $L('消息') }}</PageTitle>
<div class="dialog-wrapper">
<div class="dialog-select">
<div class="dialog-search">
<Input prefix="ios-search" v-model="dialogKey" :placeholder="$L('搜索...')" clearable />
</div>
<div class="dialog-list overlay-y">
<ul>
<li v-for="(dialog, key) in dialogLists" :key="key" :class="{active: dialog.id == dialogId}">
<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">
<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>
<Badge class="user-msg-num" :count="dialog.unread"/>
</li>
</ul>
</div>
<div class="dialog-menu">
<Icon class="active" type="ios-chatbubbles" />
<Icon type="md-person" />
</div>
</div>
<div class="dialog-msg"></div>
</div>
</div>
</template>
<style lang="scss" scoped>
:global {
.dialog {
display: flex;
}
}
</style>
<script>
import {mapState} from "vuex";
export default {
data() {
return {}
return {
dialogLoad: 0,
dialogKey: '',
dialogList: [],
}
},
mounted() {
mounted() {
this.getDialogLists();
},
computed: {
...mapState(['dialogId', 'wsMsg']),
dialogLists() {
const {dialogKey} = this;
if (dialogKey == '') {
return this.dialogList;
}
return this.dialogList.filter(({name}) => {
return $A.strExists(name, dialogKey);
})
},
},
watch: {
/**
* 收到新消息
* @param msg
*/
wsMsg(msg) {
const {type, data} = msg;
if (type === "dialog") {
if (this.dialogId == data.dialog_id) {
return;
}
let dialog = this.dialogList.find(({id}) => id == data.dialog_id);
if (dialog) {
this.$set(dialog, 'unread', dialog.unread + 1);
this.$set(dialog, 'last_msg', data);
} else {
this.getDialogOne(data.dialog_id)
}
}
},
/**
* 打开对话标记已读
* @param dialog_id
*/
dialogId(dialog_id) {
let dialog = this.dialogList.find(({id}) => id == dialog_id);
if (dialog) {
this.$set(dialog, 'unread', 0);
}
}
},
methods: {
getDialogLists() {
this.dialogLoad++;
$A.apiAjax({
url: 'dialog/lists',
complete: () => {
this.dialogLoad--;
},
success: ({ret, data, msg}) => {
if (ret === 1) {
this.dialogList = data.data;
}
}
});
},
getDialogOne(dialog_id) {
$A.apiAjax({
url: 'dialog/one',
data: {
dialog_id,
},
success: ({ret, data, msg}) => {
if (ret === 1) {
let index = this.dialogList.findIndex(({id}) => id == data.id);
if (index > -1) {
this.dialogList.splice(index, 1, data);
} else {
this.dialogList.unshift(data)
}
}
}
});
},
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 || '';
},
formatLastMsg(data) {
if ($A.isJson(data)) {
switch (data.type) {
case 'text':
return data.msg.text
case 'file':
if (data.msg.type == 'img') {
return '[' + this.$L('图片') + ']'
}
return '[' + this.$L('文件') + '] ' + data.msg.name
default:
return '[' + this.$L('未知的消息') + ']'
}
}
return '';
},
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="project-detail">
<div class="project">
<PageTitle>{{ $L('项目面板') }}</PageTitle>
<ProjectList/>
<ProjectDialog/>
@ -8,7 +8,7 @@
<style lang="scss" scoped>
:global {
.project-detail {
.project {
display: flex;
align-items: flex-start;
.project-list {

View File

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

View File

@ -202,9 +202,9 @@ export default {
return;
}
if (state.method.isArray(state.cacheDialog[dialog_id])) {
state.dialogList = state.cacheDialog[dialog_id]
state.dialogMsgList = state.cacheDialog[dialog_id]
} else {
state.dialogList = [];
state.dialogMsgList = [];
}
state.dialogId = dialog_id;
//
@ -213,14 +213,14 @@ export default {
}
state.cacheDialog[dialog_id + "::load"] = true;
//
state.dialogLoad++;
state.dialogMsgLoad++;
$A.apiAjax({
url: 'dialog/msg/lists',
data: {
dialog_id: dialog_id,
},
complete: () => {
state.dialogLoad--;
state.dialogMsgLoad--;
state.cacheDialog[dialog_id + "::load"] = false;
},
success: ({ret, data, msg}) => {
@ -228,11 +228,11 @@ export default {
state.cacheDialog[dialog_id] = data.data.reverse();
if (state.dialogId === dialog_id) {
state.cacheDialog[dialog_id].forEach((item) => {
let index = state.dialogList.findIndex(({id}) => id === item.id);
let index = state.dialogMsgList.findIndex(({id}) => id === item.id);
if (index === -1) {
state.dialogList.push(item);
state.dialogMsgList.push(item);
} else {
state.dialogList.splice(index, 1, item);
state.dialogMsgList.splice(index, 1, item);
}
})
}
@ -252,16 +252,16 @@ export default {
return;
}
if (state.method.isJson(data)) {
if (data.id && state.dialogList.find(m => m.id == data.id)) {
if (data.id && state.dialogMsgList.find(m => m.id == data.id)) {
data = null;
}
}
let index = state.dialogList.findIndex(m => m.id == id);
let index = state.dialogMsgList.findIndex(m => m.id == id);
if (index > -1) {
if (data) {
state.dialogList.splice(index, 1, state.method.cloneJSON(data));
state.dialogMsgList.splice(index, 1, state.method.cloneJSON(data));
} else {
state.dialogList.splice(index, 1);
state.dialogMsgList.splice(index, 1);
}
}
},
@ -339,14 +339,14 @@ export default {
const msgData = msgDetail.data;
const dialog_id = msgData.dialog_id;
if (dialog_id == state.dialogId) {
let index = state.dialogList.findIndex(({id}) => id === msgData.id);
let index = state.dialogMsgList.findIndex(({id}) => id === msgData.id);
if (index === -1) {
if (state.dialogList.length >= 200) {
state.dialogList.splice(0, 1);
if (state.dialogMsgList.length >= 200) {
state.dialogMsgList.splice(0, 1);
}
state.dialogList.push(msgData);
state.dialogMsgList.push(msgData);
} else {
state.dialogList.splice(index, 1, msgData);
state.dialogMsgList.splice(index, 1, msgData);
}
}
}

View File

@ -184,8 +184,8 @@ state.projectMsgUnread = 0;
// 会话消息
state.dialogId = 0;
state.dialogLoad = 0;
state.dialogList = [];
state.dialogMsgLoad = 0;
state.dialogMsgList = [];
// 任务优先级
state.taskPriority = [];

View File

@ -897,3 +897,155 @@ body {
background-color: #F4F4F5;
}
}
.dialog-wrapper {
flex: 1;
display: flex;
align-items: flex-start;
.dialog-select {
position: relative;
height: 100%;
width: 30%;
min-width: 240px;
max-width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 1px;
background-color: #f2f2f2;
}
.dialog-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;
}
}
}
.dialog-list {
flex: 1;
height: 0;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
> ul {
> li {
display: flex;
flex-direction: row;
align-items: center;
height: 86px;
padding: 0 12px;
position: relative;
cursor: pointer;
&.active {
background-color: #F4F5F7;
}
.common-avatar {
flex-grow: 0;
flex-shrink: 0;
}
.group-avatar {
width: 46px;
height: 46px;
line-height: 46px;
border-radius: 50%;
font-size: 26px;
background-color: #61B2F9;
color: #ffffff;
flex-grow: 0;
flex-shrink: 0;
}
.user-msg-box {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 12px;
.user-msg-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
line-height: 24px;
> span {
flex: 1;
max-width: 130px;
color: #333333;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> em {
font-style: normal;
color: #999999;
font-size: 12px;
padding-left: 10px;
}
}
.user-msg-text {
max-width: 170px;
color: #999999;
font-size: 12px;
line-height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.user-msg-num {
position: absolute;
top: 12px;
left: 38px;
font-size: 12px;
.ivu-badge-count {
height: 18px;
min-width: 18px;
line-height: 16px;
padding: 0 4px;
}
}
}
}
}
.dialog-menu {
display: flex;
align-items: center;
justify-content: center;
height: 54px;
flex-shrink: 0;
border-top: 1px solid #f2f2f2;
> i {
cursor: pointer;
font-size: 28px;
margin: 0 24px;
color: #aaaaaa;
opacity: 0.9;
&.active {
opacity: 1;
color: #2d8cf0;
}
&:hover {
opacity: 1;
}
}
}
}
.dialog-msg {
flex: 1;
width: 0;
height: 100%;
}
}

View File

@ -27,7 +27,7 @@
background: rgba(0, 0, 0, 0);
}
.overlay-x {
.overlay {
overflow: overlay !important;
}