no message
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\WebSocketDialog;
|
use App\Models\WebSocketDialog;
|
||||||
use App\Models\WebSocketDialogMsg;
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Models\WebSocketDialogMsgRead;
|
||||||
use App\Models\WebSocketDialogUser;
|
use App\Models\WebSocketDialogUser;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use Request;
|
use Request;
|
||||||
@ -44,6 +45,10 @@ class DialogController extends AbstractController
|
|||||||
}
|
}
|
||||||
//
|
//
|
||||||
$list = WebSocketDialogMsg::whereDialogId($dialog_id)->orderByDesc('id')->paginate(Base::getPaginate(100, 50));
|
$list = WebSocketDialogMsg::whereDialogId($dialog_id)->orderByDesc('id')->paginate(Base::getPaginate(100, 50));
|
||||||
|
$list->transform(function (WebSocketDialogMsg $item) use ($user) {
|
||||||
|
$item->r = $item->userid === $user->userid ? null : WebSocketDialogMsgRead::whereMsgId($item->id)->whereUserid($user->userid)->first();
|
||||||
|
return $item;
|
||||||
|
});
|
||||||
//
|
//
|
||||||
return Base::retSuccess('success', $list);
|
return Base::retSuccess('success', $list);
|
||||||
}
|
}
|
||||||
@ -94,4 +99,128 @@ class DialogController extends AbstractController
|
|||||||
return WebSocketDialogMsg::addUserMsg($dialog_id, 'text', $msg, $user->userid, $extra_int, $extra_str);
|
return WebSocketDialogMsg::addUserMsg($dialog_id, 'text', $msg, $user->userid, $extra_int, $extra_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {post}文件上传
|
||||||
|
*
|
||||||
|
* @apiParam {Number} dialog_id 对话ID
|
||||||
|
* @apiParam {Number} [extra_int] 额外参数(数字)
|
||||||
|
* @apiParam {String} [extra_str] 额外参数(字符)
|
||||||
|
* @apiParam {String} [filename] post-文件名称
|
||||||
|
* @apiParam {String} [image64] post-base64图片(二选一)
|
||||||
|
* @apiParam {File} [files] post-文件对象(二选一)
|
||||||
|
*/
|
||||||
|
public function msg__sendfile()
|
||||||
|
{
|
||||||
|
$user = User::authE();
|
||||||
|
if (Base::isError($user)) {
|
||||||
|
return $user;
|
||||||
|
} else {
|
||||||
|
$user = User::IDE($user['data']);
|
||||||
|
}
|
||||||
|
//
|
||||||
|
$dialog_id = Base::getPostInt('dialog_id');
|
||||||
|
$extra_int = Base::getPostInt('extra_int');
|
||||||
|
$extra_str = Base::getPostValue('extra_str');
|
||||||
|
//
|
||||||
|
if (!WebSocketDialogUser::whereDialogId($dialog_id)->whereUserid($user->userid)->exists()) {
|
||||||
|
return Base::retError('不在成员列表内');
|
||||||
|
}
|
||||||
|
$dialog = WebSocketDialog::whereId($dialog_id)->first();
|
||||||
|
if (empty($dialog)) {
|
||||||
|
return Base::retError('对话不存在或已被删除');
|
||||||
|
}
|
||||||
|
//
|
||||||
|
$path = "uploads/chat/" . $user->userid . "/";
|
||||||
|
$image64 = Base::getPostValue('image64');
|
||||||
|
$fileName = Base::getPostValue('filename');
|
||||||
|
if ($image64) {
|
||||||
|
$data = Base::image64save([
|
||||||
|
"image64" => $image64,
|
||||||
|
"path" => $path,
|
||||||
|
"fileName" => $fileName,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$data = Base::upload([
|
||||||
|
"file" => Request::file('files'),
|
||||||
|
"type" => 'file',
|
||||||
|
"path" => $path,
|
||||||
|
"fileName" => $fileName,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
//
|
||||||
|
if (Base::isError($data)) {
|
||||||
|
return Base::retError($data['msg']);
|
||||||
|
} else {
|
||||||
|
$fileData = $data['data'];
|
||||||
|
$fileData['thumb'] = $fileData['thumb'] ?: 'images/ext/file.png';
|
||||||
|
switch ($fileData['ext']) {
|
||||||
|
case "docx":
|
||||||
|
$fileData['thumb'] = 'images/ext/doc.png';
|
||||||
|
break;
|
||||||
|
case "xlsx":
|
||||||
|
$fileData['thumb'] = 'images/ext/xls.png';
|
||||||
|
break;
|
||||||
|
case "pptx":
|
||||||
|
$fileData['thumb'] = 'images/ext/ppt.png';
|
||||||
|
break;
|
||||||
|
case "ai":
|
||||||
|
case "avi":
|
||||||
|
case "bmp":
|
||||||
|
case "cdr":
|
||||||
|
case "doc":
|
||||||
|
case "eps":
|
||||||
|
case "gif":
|
||||||
|
case "mov":
|
||||||
|
case "mp3":
|
||||||
|
case "mp4":
|
||||||
|
case "pdf":
|
||||||
|
case "ppt":
|
||||||
|
case "pr":
|
||||||
|
case "psd":
|
||||||
|
case "rar":
|
||||||
|
case "svg":
|
||||||
|
case "tif":
|
||||||
|
case "txt":
|
||||||
|
case "xls":
|
||||||
|
case "zip":
|
||||||
|
$fileData['thumb'] = 'images/ext/' . $fileData['ext'] . '.png';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
$msg = $fileData;
|
||||||
|
$msg['size'] *= 1024;
|
||||||
|
//
|
||||||
|
if ($dialog->type == 'group') {
|
||||||
|
return WebSocketDialogMsg::addGroupMsg($dialog_id, 'file', $msg, $user->userid, $extra_int, $extra_str);
|
||||||
|
} else {
|
||||||
|
return WebSocketDialogMsg::addUserMsg($dialog_id, 'file', $msg, $user->userid, $extra_int, $extra_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息阅读情况
|
||||||
|
*
|
||||||
|
* @apiParam {Number} msg_id 消息ID(需要是消息的发送人)
|
||||||
|
*/
|
||||||
|
public function msg__readlist()
|
||||||
|
{
|
||||||
|
$user = User::authE();
|
||||||
|
if (Base::isError($user)) {
|
||||||
|
return $user;
|
||||||
|
} else {
|
||||||
|
$user = User::IDE($user['data']);
|
||||||
|
}
|
||||||
|
//
|
||||||
|
$msg_id = intval(Request::input('msg_id'));
|
||||||
|
//
|
||||||
|
$msg = WebSocketDialogMsg::whereId($msg_id)->whereUserid($user->userid)->first();
|
||||||
|
if (empty($msg)) {
|
||||||
|
return Base::retError('不是发送人');
|
||||||
|
}
|
||||||
|
//
|
||||||
|
$read = WebSocketDialogMsgRead::whereMsgId($msg_id)->get();
|
||||||
|
return Base::retSuccess('success', $read ?: []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,5 +23,8 @@ class VerifyCsrfToken extends Middleware
|
|||||||
|
|
||||||
// 添加任务
|
// 添加任务
|
||||||
'api/project/task/add/',
|
'api/project/task/add/',
|
||||||
|
|
||||||
|
// 聊天发文件
|
||||||
|
'api/dialog/msg/sendfile/',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,9 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Tasks\PushTask;
|
use App\Tasks\PushTask;
|
||||||
|
use App\Tasks\WebSocketDialogMsgTask;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class WebSocketDialogMsg
|
* Class WebSocketDialogMsg
|
||||||
@ -15,11 +17,13 @@ use Carbon\Carbon;
|
|||||||
* @property int|null $userid 发送会员ID
|
* @property int|null $userid 发送会员ID
|
||||||
* @property string|null $type 消息类型
|
* @property string|null $type 消息类型
|
||||||
* @property array|mixed $msg 详细消息
|
* @property array|mixed $msg 详细消息
|
||||||
* @property int|null $read 是否已读
|
* @property int|null $read 已阅数量
|
||||||
|
* @property int|null $send 发送数量
|
||||||
* @property int|null $extra_int 额外数字参数
|
* @property int|null $extra_int 额外数字参数
|
||||||
* @property string|null $extra_str 额外字符参数
|
* @property string|null $extra_str 额外字符参数
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property-read int|mixed $percentage
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg query()
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg query()
|
||||||
@ -30,6 +34,7 @@ use Carbon\Carbon;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUpdatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUserid($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUserid($value)
|
||||||
@ -37,12 +42,32 @@ use Carbon\Carbon;
|
|||||||
*/
|
*/
|
||||||
class WebSocketDialogMsg extends AbstractModel
|
class WebSocketDialogMsg extends AbstractModel
|
||||||
{
|
{
|
||||||
|
protected $appends = [
|
||||||
|
'percentage',
|
||||||
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'updated_at',
|
'updated_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息
|
* 阅读占比
|
||||||
|
* @return int|mixed
|
||||||
|
*/
|
||||||
|
public function getPercentageAttribute()
|
||||||
|
{
|
||||||
|
if (!isset($this->attributes['percentage'])) {
|
||||||
|
if ($this->read > $this->send || empty($this->send)) {
|
||||||
|
$this->attributes['percentage'] = 100;
|
||||||
|
} else {
|
||||||
|
$this->attributes['percentage'] = intval($this->read / $this->send * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->attributes['percentage'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息格式化
|
||||||
* @param $value
|
* @param $value
|
||||||
* @return array|mixed
|
* @return array|mixed
|
||||||
*/
|
*/
|
||||||
@ -51,27 +76,54 @@ class WebSocketDialogMsg extends AbstractModel
|
|||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
return Base::json2array($value);
|
$value = Base::json2array($value);
|
||||||
|
if ($this->type === 'file') {
|
||||||
|
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'png', 'gif']) ? 'img' : 'file';
|
||||||
|
$value['url'] = Base::fillUrl($value['path']);
|
||||||
|
$value['thumb'] = Base::fillUrl($value['thumb']);
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标记已送达 同时 告诉发送人已送达
|
* 标记已送达 同时 告诉发送人已送达
|
||||||
* @return $this
|
* @param $userid
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function readSuccess()
|
public function readSuccess($userid)
|
||||||
{
|
{
|
||||||
if (empty($this->read)) {
|
if (empty($userid)) {
|
||||||
$this->read = 1;
|
return false;
|
||||||
$this->save();
|
|
||||||
PushTask::push([
|
|
||||||
'userid' => $this->userid,
|
|
||||||
'msg' => [
|
|
||||||
'type' => 'dialog',
|
|
||||||
'data' => $this->toArray(),
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
return $this;
|
$result = self::transaction(function() use ($userid) {
|
||||||
|
$msgRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereUserid($userid)->lockForUpdate()->first();
|
||||||
|
if (empty($msgRead)) {
|
||||||
|
$msgRead = WebSocketDialogMsgRead::createInstance([
|
||||||
|
'msg_id' => $this->id,
|
||||||
|
'userid' => $userid,
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
$msgRead->saveOrFail();
|
||||||
|
$this->send = WebSocketDialogMsgRead::whereMsgId($this->id)->count();
|
||||||
|
$this->save();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$msgRead = $msgRead->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($msgRead && !$msgRead->read_at) {
|
||||||
|
$msgRead->read_at = Carbon::now();
|
||||||
|
$msgRead->save();
|
||||||
|
$this->increment('read');
|
||||||
|
PushTask::push([
|
||||||
|
'userid' => $this->userid,
|
||||||
|
'msg' => [
|
||||||
|
'type' => 'dialog',
|
||||||
|
'data' => $this->toArray(),
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Base::isSuccess($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,19 +153,14 @@ class WebSocketDialogMsg extends AbstractModel
|
|||||||
}
|
}
|
||||||
$dialog->last_at = Carbon::now();
|
$dialog->last_at = Carbon::now();
|
||||||
$dialog->save();
|
$dialog->save();
|
||||||
|
$userids = WebSocketDialogUser::whereDialogId($dialog->id)->where('userid', '!=', $dialogMsg->userid)->pluck('userid')->toArray();
|
||||||
|
$dialogMsg->send = count($userids);
|
||||||
$dialogMsg->dialog_id = $dialog->id;
|
$dialogMsg->dialog_id = $dialog->id;
|
||||||
$dialogMsg->save();
|
$dialogMsg->save();
|
||||||
//
|
//
|
||||||
$userids = WebSocketDialogUser::whereDialogId($dialog->id)->where('userid', '!=', $dialogMsg->userid)->pluck('userid')->toArray();
|
$task = new WebSocketDialogMsgTask($userids, $dialogMsg->toArray());
|
||||||
if ($userids) {
|
Task::deliver($task);
|
||||||
PushTask::push([
|
//
|
||||||
'userid' => $userids,
|
|
||||||
'msg' => [
|
|
||||||
'type' => 'dialog',
|
|
||||||
'data' => $dialogMsg->toArray(),
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return Base::retSuccess('发送成功', $dialogMsg);
|
return Base::retSuccess('发送成功', $dialogMsg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -146,16 +193,13 @@ class WebSocketDialogMsg extends AbstractModel
|
|||||||
}
|
}
|
||||||
$dialog->last_at = Carbon::now();
|
$dialog->last_at = Carbon::now();
|
||||||
$dialog->save();
|
$dialog->save();
|
||||||
|
$dialogMsg->send = 1;
|
||||||
$dialogMsg->dialog_id = $dialog->id;
|
$dialogMsg->dialog_id = $dialog->id;
|
||||||
$dialogMsg->save();
|
$dialogMsg->save();
|
||||||
//
|
//
|
||||||
PushTask::push([
|
$task = new WebSocketDialogMsgTask($userid, $dialogMsg->toArray());
|
||||||
'userid' => $userid,
|
Task::deliver($task);
|
||||||
'msg' => [
|
//
|
||||||
'type' => 'dialog',
|
|
||||||
'data' => $dialogMsg->toArray(),
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
return Base::retSuccess('发送成功', $dialogMsg);
|
return Base::retSuccess('发送成功', $dialogMsg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
29
app/Models/WebSocketDialogMsgRead.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class WebSocketDialogMsgRead
|
||||||
|
*
|
||||||
|
* @package App\Models
|
||||||
|
* @property int $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 whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMsgId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereReadAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereUserid($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class WebSocketDialogMsgRead extends AbstractModel
|
||||||
|
{
|
||||||
|
function __construct(array $attributes = [])
|
||||||
|
{
|
||||||
|
parent::__construct($attributes);
|
||||||
|
$this->timestamps = false;
|
||||||
|
}
|
||||||
|
}
|
@ -744,7 +744,11 @@ class Base
|
|||||||
) {
|
) {
|
||||||
return $str;
|
return $str;
|
||||||
} else {
|
} else {
|
||||||
return url($str);
|
try {
|
||||||
|
return url($str);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return self::getSchemeAndHost() . "/" . $str;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -761,7 +765,22 @@ class Base
|
|||||||
}
|
}
|
||||||
return $str;
|
return $str;
|
||||||
}
|
}
|
||||||
return Base::leftDelete($str, url('') . '/');
|
try {
|
||||||
|
$find = url('');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$find = self::getSchemeAndHost();
|
||||||
|
}
|
||||||
|
return Base::leftDelete($str, $find . '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主地址
|
||||||
|
* @return string 如:http://127.0.0.1:8080
|
||||||
|
*/
|
||||||
|
public static function getSchemeAndHost()
|
||||||
|
{
|
||||||
|
$scheme = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://';
|
||||||
|
return $scheme.($_SERVER['HTTP_HOST'] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1922,12 +1941,16 @@ class Base
|
|||||||
|
|
||||||
public static function getPostValue($key, $default = null)
|
public static function getPostValue($key, $default = null)
|
||||||
{
|
{
|
||||||
return self::newTrim(self::getContentValue($key, $default));
|
$value = self::newTrim(self::getContentValue($key, $default));
|
||||||
|
if (empty($value)) {
|
||||||
|
$value = self::newTrim(Request::post($key, $default));
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getPostInt($key, $default = null)
|
public static function getPostInt($key, $default = null)
|
||||||
{
|
{
|
||||||
return intval(self::getContentValue($key, $default));
|
return intval(self::getPostValue($key, $default));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2301,7 +2324,7 @@ class Base
|
|||||||
$array['thumb'] = $array['path'];
|
$array['thumb'] = $array['path'];
|
||||||
if ($param['autoThumb'] === "false") $param['autoThumb'] = false;
|
if ($param['autoThumb'] === "false") $param['autoThumb'] = false;
|
||||||
if ($param['autoThumb'] !== false) {
|
if ($param['autoThumb'] !== false) {
|
||||||
if (Base::imgThumb($array['file'], $array['file'] . "_thumb.jpg", 180, 0)) {
|
if (Base::imgThumb($array['file'], $array['file'] . "_thumb.jpg", 320, 0)) {
|
||||||
$array['thumb'] .= "_thumb.jpg";
|
$array['thumb'] .= "_thumb.jpg";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@ class WebSocketService implements WebSocketHandlerInterface
|
|||||||
*/
|
*/
|
||||||
case 'readMsg':
|
case 'readMsg':
|
||||||
$dialogMsg = WebSocketDialogMsg::whereId(intval($data['id']))->first();
|
$dialogMsg = WebSocketDialogMsg::whereId(intval($data['id']))->first();
|
||||||
$dialogMsg && $dialogMsg->readSuccess();
|
$dialogMsg && $dialogMsg->readSuccess($this->getUserid($frame->fd));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
@ -178,4 +178,14 @@ class WebSocketService implements WebSocketHandlerInterface
|
|||||||
{
|
{
|
||||||
WebSocket::whereFd($fd)->delete();
|
WebSocket::whereFd($fd)->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据fd获取会员ID
|
||||||
|
* @param $fd
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function getUserid($fd)
|
||||||
|
{
|
||||||
|
return intval(WebSocket::whereFd($fd)->value('userid'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
71
app/Tasks/WebSocketDialogMsgTask.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tasks;
|
||||||
|
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Models\WebSocketDialogMsgRead;
|
||||||
|
|
||||||
|
@error_reporting(E_ALL & ~E_NOTICE);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息通知任务
|
||||||
|
* Class WebSocketDialogMsgTask
|
||||||
|
* @package App\Tasks
|
||||||
|
*/
|
||||||
|
class WebSocketDialogMsgTask extends AbstractTask
|
||||||
|
{
|
||||||
|
protected $userid;
|
||||||
|
protected $dialogMsgArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NoticeTask constructor.
|
||||||
|
*/
|
||||||
|
public function __construct($userid, array $dialogMsgArray)
|
||||||
|
{
|
||||||
|
$this->userid = $userid;
|
||||||
|
$this->dialogMsgArray = $dialogMsgArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function start()
|
||||||
|
{
|
||||||
|
$userids = is_array($this->userid) ? $this->userid : [$this->userid];
|
||||||
|
$msgId = intval($this->dialogMsgArray['id']);
|
||||||
|
$send = intval($this->dialogMsgArray['send']);
|
||||||
|
if (empty($userids) || empty($msgId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$pushIds = [];
|
||||||
|
foreach ($userids AS $userid) {
|
||||||
|
$msgRead = WebSocketDialogMsgRead::createInstance([
|
||||||
|
'msg_id' => $msgId,
|
||||||
|
'userid' => $userid,
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
$msgRead->saveOrFail();
|
||||||
|
$pushIds[] = $userid;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 更新已发送数量
|
||||||
|
if ($send != count($pushIds)) {
|
||||||
|
$send = WebSocketDialogMsgRead::whereMsgId($msgId)->count();
|
||||||
|
WebSocketDialogMsg::whereId($msgId)->update([ 'send' => $send ]);
|
||||||
|
$this->dialogMsgArray['send'] = $send;
|
||||||
|
}
|
||||||
|
// 开始推送消息
|
||||||
|
if ($pushIds) {
|
||||||
|
PushTask::push([
|
||||||
|
'userid' => $pushIds,
|
||||||
|
'msg' => [
|
||||||
|
'type' => 'dialog',
|
||||||
|
'data' => $this->dialogMsgArray,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<Tooltip v-if="user"
|
<Tooltip v-if="user"
|
||||||
:class="['common-avatar', user.online ? 'online' : '']"
|
class="common-avatar"
|
||||||
:delay="600"
|
:delay="600"
|
||||||
:transfer="transfer">
|
:transfer="transfer">
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<p>{{$L('昵称')}}: {{user.nickname}}</p>
|
<p>{{$L('昵称')}}: {{user.nickname}}</p>
|
||||||
<p>{{$L('职位/职称')}}: {{user.profession || '-'}}</p>
|
<p>{{$L('职位/职称')}}: {{user.profession || '-'}}</p>
|
||||||
</div>
|
</div>
|
||||||
<Avatar v-if="showImg" :src="user.userimg" :size="size"/>
|
<div class="avatar-wrapper">
|
||||||
<Avatar v-else :size="size" class="common-avatar-text">{{nickname}}</Avatar>
|
<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>
|
||||||
|
</div>
|
||||||
|
<div v-if="showName" class="avatar-name">{{user.nickname}}</div>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -24,6 +29,10 @@
|
|||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
default: 'default'
|
default: 'default'
|
||||||
},
|
},
|
||||||
|
showName: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
transfer: {
|
transfer: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
125
resources/assets/js/components/WCircle.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="common-circle" :data-id="percent" :style="style">
|
||||||
|
<svg viewBox="0 0 28 28">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path class="common-circle-path" d="M-500-100h997V48h-997z"></path>
|
||||||
|
<g fill-rule="nonzero">
|
||||||
|
<path class="common-circle-g-path-ring" stroke-width="3" d="M14 25.5c6.351 0 11.5-5.149 11.5-11.5S20.351 2.5 14 2.5 2.5 7.649 2.5 14 7.649 25.5 14 25.5z"></path>
|
||||||
|
<path class="common-circle-g-path-core" :d="arc(args)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:global {
|
||||||
|
.common-circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
.common-circle-path {
|
||||||
|
fill: transparent;
|
||||||
|
}
|
||||||
|
.common-circle-g-path-ring {
|
||||||
|
stroke: #87d068;
|
||||||
|
}
|
||||||
|
.common-circle-g-path-core {
|
||||||
|
fill: #87d068;
|
||||||
|
transform: scale(0.56);
|
||||||
|
transform-origin: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'WCircle',
|
||||||
|
props: {
|
||||||
|
percent: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 120
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
style() {
|
||||||
|
let {size} = this;
|
||||||
|
if (this.isNumeric(size)) {
|
||||||
|
size += 'px';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
args() {
|
||||||
|
const {percent} = this;
|
||||||
|
let end = Math.min(360, 360 / 100 * percent);
|
||||||
|
if (end == 360) {
|
||||||
|
end = 0;
|
||||||
|
} else if (end == 0) {
|
||||||
|
end = 360;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: 14,
|
||||||
|
y: 14,
|
||||||
|
r: 14,
|
||||||
|
start: 360,
|
||||||
|
end: end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
isNumeric(n) {
|
||||||
|
return n !== '' && !isNaN(parseFloat(n)) && isFinite(n);
|
||||||
|
},
|
||||||
|
|
||||||
|
point(x, y, r, angel) {
|
||||||
|
return [
|
||||||
|
(x + Math.sin(angel) * r).toFixed(2),
|
||||||
|
(y - Math.cos(angel) * r).toFixed(2),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
full(x, y, R, r) {
|
||||||
|
if (r <= 0) {
|
||||||
|
return `M ${x - R} ${y} A ${R} ${R} 0 1 1 ${x + R} ${y} A ${R} ${R} 1 1 1 ${x - R} ${y} Z`;
|
||||||
|
}
|
||||||
|
return `M ${x - R} ${y} A ${R} ${R} 0 1 1 ${x + R} ${y} A ${R} ${R} 1 1 1 ${x - R} ${y} M ${x - r} ${y} A ${r} ${r} 0 1 1 ${x + r} ${y} A ${r} ${r} 1 1 1 ${x - r} ${y} Z`;
|
||||||
|
},
|
||||||
|
|
||||||
|
part(x, y, R, r, start, end) {
|
||||||
|
const [s, e] = [(start / 360) * 2 * Math.PI, (end / 360) * 2 * Math.PI];
|
||||||
|
const P = [
|
||||||
|
this.point(x, y, r, s),
|
||||||
|
this.point(x, y, R, s),
|
||||||
|
this.point(x, y, R, e),
|
||||||
|
this.point(x, y, r, e),
|
||||||
|
];
|
||||||
|
const flag = e - s > Math.PI ? '1' : '0';
|
||||||
|
return `M ${P[0][0]} ${P[0][1]} L ${P[1][0]} ${P[1][1]} A ${R} ${R} 0 ${flag} 1 ${P[2][0]} ${P[2][1]} L ${P[3][0]} ${P[3][1]} A ${r} ${r} 0 ${flag} 0 ${P[0][0]} ${P[0][1]} Z`;
|
||||||
|
},
|
||||||
|
|
||||||
|
arc(opts) {
|
||||||
|
const { x = 0, y = 0 } = opts;
|
||||||
|
let {R = 0, r = 0, start, end,} = opts;
|
||||||
|
|
||||||
|
[R, r] = [Math.max(R, r), Math.min(R, r)];
|
||||||
|
if (R <= 0) return '';
|
||||||
|
if (start !== +start || end !== +end) return this.full(x, y, R, r);
|
||||||
|
if (Math.abs(start - end) < 0.000001) return '';
|
||||||
|
if (Math.abs(start - end) % 360 < 0.000001) return this.full(x, y, R, r);
|
||||||
|
|
||||||
|
[start, end] = [start % 360, end % 360];
|
||||||
|
|
||||||
|
if (start > end) end += 360;
|
||||||
|
return this.part(x, y, R, r, start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1306,27 +1306,30 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 对象中有Date格式的转成指定格式
|
* 对象中有Date格式的转成指定格式
|
||||||
* @param myObj
|
* @param params
|
||||||
* @param format 默认格式:Y-m-d
|
* @param format 默认格式:Y-m-d H:i:s
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
date2string(myObj, format) {
|
date2string(params, format) {
|
||||||
if (myObj === null) {
|
if (params === null) {
|
||||||
return myObj;
|
return params;
|
||||||
}
|
}
|
||||||
if (typeof format === "undefined") {
|
if (typeof format === "undefined") {
|
||||||
format = "Y-m-d";
|
format = "Y-m-d H:i:s";
|
||||||
}
|
}
|
||||||
if (typeof myObj === "object") {
|
if (params instanceof Date) {
|
||||||
if (myObj instanceof Date) {
|
params = $A.formatDate(format, params);
|
||||||
return $A.formatDate(format, myObj);
|
} else if ($A.isJson(params)) {
|
||||||
|
for (let key in params) {
|
||||||
|
if (!params.hasOwnProperty(key)) continue;
|
||||||
|
params[key] = $A.date2string(params[key], format);
|
||||||
}
|
}
|
||||||
$A.each(myObj, (key, val)=>{
|
} else if ($A.isArray(params)) {
|
||||||
myObj[key] = $A.date2string(val, format);
|
params.forEach((val, index) => {
|
||||||
|
params[index] = $A.date2string(val, format);
|
||||||
});
|
});
|
||||||
return myObj;
|
|
||||||
}
|
}
|
||||||
return myObj;
|
return params;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
if (typeof params.success === 'undefined') params.success = () => { };
|
if (typeof params.success === 'undefined') params.success = () => { };
|
||||||
if (typeof params.header === 'undefined') params.header = {};
|
if (typeof params.header === 'undefined') params.header = {};
|
||||||
params.url = this.apiUrl(params.url);
|
params.url = this.apiUrl(params.url);
|
||||||
|
params.data = this.date2string(params.data);
|
||||||
params.header['Content-Type'] = 'application/json';
|
params.header['Content-Type'] = 'application/json';
|
||||||
params.header['language'] = $A.getLanguage();
|
params.header['language'] = $A.getLanguage();
|
||||||
params.header['token'] = $A.store.state.userToken;
|
params.header['token'] = $A.store.state.userToken;
|
||||||
|
@ -8,15 +8,15 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li @click="toggleRoute('dashboard')" :class="classNameRoute('dashboard')">
|
<li @click="toggleRoute('dashboard')" :class="classNameRoute('dashboard')">
|
||||||
<Icon type="md-speedometer" />
|
<Icon type="md-speedometer" />
|
||||||
<div class="menu-title">Dashboard</div>
|
<div class="menu-title">{{$L('仪表板')}}</div>
|
||||||
</li>
|
</li>
|
||||||
<li @click="toggleRoute('setting/personal')" :class="classNameRoute('setting')">
|
<li @click="toggleRoute('setting/personal')" :class="classNameRoute('setting')">
|
||||||
<Icon type="md-cog" />
|
<Icon type="md-cog" />
|
||||||
<div class="menu-title">Setting</div>
|
<div class="menu-title">{{$L('设置')}}</div>
|
||||||
</li>
|
</li>
|
||||||
<li @click="toggleRoute('calendar')" :class="classNameRoute('calendar')">
|
<li @click="toggleRoute('calendar')" :class="classNameRoute('calendar')">
|
||||||
<Icon type="md-calendar" />
|
<Icon type="md-calendar" />
|
||||||
<div class="menu-title">Calendar</div>
|
<div class="menu-title">{{$L('日历')}}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-project">
|
<li class="menu-project">
|
||||||
<ul>
|
<ul>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<Loading v-if="loadIng > 0"/>
|
<Loading v-if="loadIng > 0"/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Button class="manage-box-new" type="primary" icon="md-add" @click="addShow=true">New Project</Button>
|
<Button class="manage-box-new" type="primary" icon="md-add" @click="addShow=true">{{$L('新建项目')}}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="manage-box-main">
|
<div class="manage-box-main">
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<Upload
|
||||||
|
name="files"
|
||||||
|
ref="upload"
|
||||||
|
:action="actionUrl"
|
||||||
|
:data="params"
|
||||||
|
multiple
|
||||||
|
:format="uploadFormat"
|
||||||
|
:show-upload-list="false"
|
||||||
|
:max-size="maxSize"
|
||||||
|
:on-progress="handleProgress"
|
||||||
|
:on-success="handleSuccess"
|
||||||
|
:on-format-error="handleFormatError"
|
||||||
|
:on-exceeded-size="handleMaxSize">
|
||||||
|
</Upload>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MessageUpload',
|
||||||
|
props: {
|
||||||
|
maxSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 204800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
uploadFormat: ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz', 'ai', 'avi', 'bmp', 'cdr', 'eps', 'mov', 'mp3', 'mp4', 'pr', 'psd', 'svg', 'tif'],
|
||||||
|
actionUrl: $A.apiUrl('dialog/msg/sendfile'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(['userToken', 'dialogId']),
|
||||||
|
|
||||||
|
params() {
|
||||||
|
return {
|
||||||
|
dialog_id: this.dialogId,
|
||||||
|
token: this.userToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleProgress(event, file) {
|
||||||
|
//上传时
|
||||||
|
if (typeof file.tempId === "undefined") {
|
||||||
|
file.tempId = $A.randomString(8);
|
||||||
|
this.$emit('on-progress', file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSuccess(res, file) {
|
||||||
|
//上传完成
|
||||||
|
if (res.ret === 1) {
|
||||||
|
file.data = res.data;
|
||||||
|
this.$emit('on-success', file);
|
||||||
|
} else {
|
||||||
|
$A.modalWarning({
|
||||||
|
title: '发送失败',
|
||||||
|
content: '文件 ' + file.name + ' 发送失败,' + res.msg
|
||||||
|
});
|
||||||
|
this.$emit('on-error', file);
|
||||||
|
this.$refs.upload.fileList.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFormatError(file) {
|
||||||
|
//上传类型错误
|
||||||
|
$A.modalWarning({
|
||||||
|
title: '文件格式不正确',
|
||||||
|
content: '文件 ' + file.name + ' 格式不正确,仅支持发送:' + this.uploadFormat.join(',')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMaxSize(file) {
|
||||||
|
//上传大小错误
|
||||||
|
$A.modalWarning({
|
||||||
|
title: '超出文件大小限制',
|
||||||
|
content: '文件 ' + file.name + ' 太大,不能发送超过' + $A.bytesToSize(this.maxSize * 1024) + '。'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClick() {
|
||||||
|
//手动上传
|
||||||
|
this.$refs.upload.handleClick()
|
||||||
|
},
|
||||||
|
|
||||||
|
upload(file) {
|
||||||
|
//手动传file
|
||||||
|
this.$refs.upload.upload(file);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,13 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="message-view">
|
<div class="message-view" :data-id="msgData.id">
|
||||||
|
|
||||||
<div v-if="msgData.type == 'text'" class="message-content" v-html="textMsg(msgData.msg.text)"></div>
|
<!--文本-->
|
||||||
<div v-else class="message-content message-unknown">{{$L("未知的消息类型")}}</div>
|
<div v-if="msgData.type === 'text'" class="message-content" v-html="textMsg(msgInfo.text)"></div>
|
||||||
|
<!--等待-->
|
||||||
|
<div v-else-if="msgData.type === 'loading'" class="message-content loading"><Loading/></div>
|
||||||
|
<!--文件-->
|
||||||
|
<div v-else-if="msgData.type === 'file'" :class="['message-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 class="file-box">
|
||||||
|
<img class="file-thumb" :src="msgInfo.thumb"/>
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name">{{msgInfo.name}}</div>
|
||||||
|
<div class="file-size">{{$A.bytesToSize(msgInfo.size)}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<!--未知-->
|
||||||
|
<div v-else class="message-content unknown">{{$L("未知的消息类型")}}</div>
|
||||||
|
|
||||||
|
<!--时间/阅读-->
|
||||||
<div v-if="msgData.created_at" class="message-foot">
|
<div v-if="msgData.created_at" class="message-foot">
|
||||||
<div class="time">{{formatTime(msgData.created_at)}}</div>
|
<div class="time">{{formatTime(msgData.created_at)}}</div>
|
||||||
<Icon v-if="msgData.read" class="done-all" type="md-done-all" />
|
<Poptip
|
||||||
<Icon v-else class="done" type="md-checkmark" />
|
v-if="msgData.send > 1 || dialogType == 'group'"
|
||||||
|
class="percent"
|
||||||
|
placement="left-end"
|
||||||
|
transfer
|
||||||
|
:width="360"
|
||||||
|
:offset="8"
|
||||||
|
@on-popper-show="popperShow">
|
||||||
|
<div slot="content" class="message-readbox">
|
||||||
|
<ul class="read">
|
||||||
|
<li class="read-title"><em>{{readList.length}}</em>{{$L('已读')}}</li>
|
||||||
|
<li v-for="item in readList"><UserAvatar :userid="item.userid" :size="26" show-name/></li>
|
||||||
|
</ul>
|
||||||
|
<ul class="unread">
|
||||||
|
<li class="read-title"><em>{{unreadList.length}}</em>{{$L('未读')}}</li>
|
||||||
|
<li v-for="item in unreadList"><UserAvatar :userid="item.userid" :size="26" show-name/></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<WCircle :percent="msgData.percentage" :size="14"/>
|
||||||
|
</Poptip>
|
||||||
|
<Icon v-else-if="msgData.percentage === 100" class="done" type="md-done-all"/>
|
||||||
|
<Icon v-else class="done" type="md-checkmark"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="message-foot"><Loading/></div>
|
<div v-else class="message-foot"><Loading/></div>
|
||||||
|
|
||||||
@ -16,9 +54,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
|
import WCircle from "../../../components/WCircle";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "MessageView",
|
name: "MessageView",
|
||||||
|
components: {WCircle},
|
||||||
props: {
|
props: {
|
||||||
msgData: {
|
msgData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -26,32 +66,66 @@ export default {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dialogType: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
msgInfo: {},
|
||||||
|
read_list: []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.readMarking()
|
this.parsingData()
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['userId']),
|
...mapState(['userId']),
|
||||||
|
|
||||||
|
readList() {
|
||||||
|
return this.read_list.filter(({read_at}) => read_at)
|
||||||
|
},
|
||||||
|
|
||||||
|
unreadList() {
|
||||||
|
return this.read_list.filter(({read_at}) => !read_at)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
msgData() {
|
msgData() {
|
||||||
this.readMarking()
|
this.parsingData()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
readMarking() {
|
popperShow() {
|
||||||
if (this.msgData.read === 0 && this.msgData.userid != this.userId) {
|
$A.apiAjax({
|
||||||
this.$store.commit('wsSend', {
|
url: 'dialog/msg/readlist',
|
||||||
type: 'readMsg',
|
data: {
|
||||||
data: {
|
msg_id: this.msgData.id,
|
||||||
id: this.msgData.id
|
},
|
||||||
|
success: ({ret, data, msg}) => {
|
||||||
|
if (ret === 1) {
|
||||||
|
this.read_list = data;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
this.$store.commit('wsSend', {
|
||||||
|
type: 'readMsg',
|
||||||
|
data: {id}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime(date) {
|
formatTime(date) {
|
||||||
@ -74,6 +148,30 @@ export default {
|
|||||||
text = text.trim().replace(/(\n\x20*){3,}/g, "<br/><br/>");
|
text = text.trim().replace(/(\n\x20*){3,}/g, "<br/><br/>");
|
||||||
text = text.trim().replace(/\n/g, "<br/>");
|
text = text.trim().replace(/\n/g, "<br/>");
|
||||||
return text;
|
return text;
|
||||||
|
},
|
||||||
|
|
||||||
|
imageStyle(info) {
|
||||||
|
const {width, height} = info;
|
||||||
|
if (width && height) {
|
||||||
|
let maxWidth = 220,
|
||||||
|
maxHeight = 220,
|
||||||
|
tempWidth = width,
|
||||||
|
tempHeight = height;
|
||||||
|
if (width > maxWidth || height > maxHeight) {
|
||||||
|
if (width > height) {
|
||||||
|
tempWidth = maxWidth;
|
||||||
|
tempHeight = height * (maxWidth / width);
|
||||||
|
} else {
|
||||||
|
tempWidth = width * (maxHeight / height);
|
||||||
|
tempHeight = maxHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: tempWidth + 'px',
|
||||||
|
height: tempHeight + 'px',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<Tooltip theme="light" :always="searchText!=''" transfer>
|
<Tooltip theme="light" :always="searchText!=''" transfer>
|
||||||
<Icon type="ios-search" />
|
<Icon type="ios-search" />
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<Input v-model="searchText" placeholder="Search task..." clearable autofocus/>
|
<Input v-model="searchText" :placeholder="$L('名称、描述...')" clearable autofocus/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</li>
|
</li>
|
||||||
@ -99,11 +99,11 @@
|
|||||||
<div v-else class="project-table">
|
<div v-else class="project-table">
|
||||||
<div class="project-table-head">
|
<div class="project-table-head">
|
||||||
<Row class="project-row">
|
<Row class="project-row">
|
||||||
<Col span="12"># Task name</Col>
|
<Col span="12"># {{$L('任务名称')}}</Col>
|
||||||
<Col span="3">Task Column</Col>
|
<Col span="3">{{$L('列表')}}</Col>
|
||||||
<Col span="3">Priority</Col>
|
<Col span="3">{{$L('优先级')}}</Col>
|
||||||
<Col span="3">Member</Col>
|
<Col span="3">{{$L('负责人')}}</Col>
|
||||||
<Col span="3">Expiration</Col>
|
<Col span="3">{{$L('到期时间')}}</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
<!--我的任务-->
|
<!--我的任务-->
|
||||||
@ -112,7 +112,7 @@
|
|||||||
<Row class="project-row">
|
<Row class="project-row">
|
||||||
<Col span="12" class="row-title">
|
<Col span="12" class="row-title">
|
||||||
<i class="iconfont"></i>
|
<i class="iconfont"></i>
|
||||||
<div class="row-h1">My task</div>
|
<div class="row-h1">{{$L('我的任务')}}</div>
|
||||||
<div class="row-num">({{myList.length}})</div>
|
<div class="row-num">({{myList.length}})</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span="3"></Col>
|
<Col span="3"></Col>
|
||||||
@ -170,7 +170,7 @@
|
|||||||
<Row class="project-row">
|
<Row class="project-row">
|
||||||
<Col span="12" class="row-title">
|
<Col span="12" class="row-title">
|
||||||
<i class="iconfont"></i>
|
<i class="iconfont"></i>
|
||||||
<div class="row-h1">Undone</div>
|
<div class="row-h1">{{$L('未完成任务')}}</div>
|
||||||
<div class="row-num">({{undoneList.length}})</div>
|
<div class="row-num">({{undoneList.length}})</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span="3"></Col>
|
<Col span="3"></Col>
|
||||||
@ -217,7 +217,7 @@
|
|||||||
<Row class="project-row">
|
<Row class="project-row">
|
||||||
<Col span="12" class="row-title">
|
<Col span="12" class="row-title">
|
||||||
<i class="iconfont"></i>
|
<i class="iconfont"></i>
|
||||||
<div class="row-h1">Completed</div>
|
<div class="row-h1">{{$L('已完成任务')}}</div>
|
||||||
<div class="row-num">({{completedList.length}})</div>
|
<div class="row-num">({{completedList.length}})</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span="3"></Col>
|
<Col span="3"></Col>
|
||||||
@ -975,6 +975,15 @@ export default {
|
|||||||
$A.messageSuccess(msg);
|
$A.messageSuccess(msg);
|
||||||
this.$store.commit('getProjectDetail', this.addData.project_id);
|
this.$store.commit('getProjectDetail', this.addData.project_id);
|
||||||
this.addShow = false;
|
this.addShow = false;
|
||||||
|
this.addData = {
|
||||||
|
owner: 0,
|
||||||
|
column_id: 0,
|
||||||
|
times: [],
|
||||||
|
subtasks: [],
|
||||||
|
p_level: 0,
|
||||||
|
p_name: '',
|
||||||
|
p_color: '',
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
$A.modalError(msg);
|
$A.modalError(msg);
|
||||||
}
|
}
|
||||||
@ -1166,7 +1175,7 @@ export default {
|
|||||||
let minutes = Math.floor(((second % 86400) % 3600) / 60);
|
let minutes = Math.floor(((second % 86400) % 3600) / 60);
|
||||||
let seconds = Math.floor(((second % 86400) % 3600) % 60);
|
let seconds = Math.floor(((second % 86400) % 3600) % 60);
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
if (hours > 0) duration = days + "d," + this.formatBit(hours) + "h.";
|
if (hours > 0) duration = days + "d," + this.formatBit(hours) + "h";
|
||||||
else if (minutes > 0) duration = days + "d," + this.formatBit(minutes) + "min";
|
else if (minutes > 0) duration = days + "d," + this.formatBit(minutes) + "min";
|
||||||
else if (seconds > 0) duration = days + "d," + this.formatBit(seconds) + "s";
|
else if (seconds > 0) duration = days + "d," + this.formatBit(seconds) + "s";
|
||||||
else duration = days + "d";
|
else duration = days + "d";
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="$store.state.projectChatShow" class="project-message">
|
<div
|
||||||
|
v-if="$store.state.projectChatShow"
|
||||||
|
class="project-message"
|
||||||
|
@drop.prevent="chatPasteDrag($event, 'drag')"
|
||||||
|
@dragover.prevent="chatDragOver(true)"
|
||||||
|
@dragleave.prevent="chatDragOver(false)">
|
||||||
<div class="group-member">
|
<div class="group-member">
|
||||||
<div class="member-head">
|
<div class="member-head">
|
||||||
<div class="member-title">{{$L('项目成员')}}<span>({{projectDetail.project_user.length}})</span></div>
|
<div class="member-title">{{$L('项目成员')}}<span>({{projectDetail.project_user.length}})</span></div>
|
||||||
@ -12,7 +17,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="group-title">{{$L('群聊')}}</div>
|
<div class="group-title">{{$L('群聊')}}</div>
|
||||||
<ScrollerY ref="groupChat" class="group-chat message-scroller" @on-scroll="groupChatScroll">
|
<ScrollerY ref="groupChat" class="group-chat message-scroller" @on-scroll="chatScroll">
|
||||||
<div ref="manageList" class="message-list">
|
<div ref="manageList" class="message-list">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-if="dialogLoad > 0" class="loading"><Loading/></li>
|
<li v-if="dialogLoad > 0" class="loading"><Loading/></li>
|
||||||
@ -21,13 +26,24 @@
|
|||||||
<div class="message-avatar">
|
<div class="message-avatar">
|
||||||
<UserAvatar :userid="item.userid" :size="30"/>
|
<UserAvatar :userid="item.userid" :size="30"/>
|
||||||
</div>
|
</div>
|
||||||
<MessageView :msg-data="item"/>
|
<MessageView :msg-data="item" dialog-type="group"/>
|
||||||
</li>
|
</li>
|
||||||
|
<li ref="bottom" class="bottom"></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</ScrollerY>
|
</ScrollerY>
|
||||||
<div class="group-footer">
|
<div :class="['group-footer', msgNew > 0 ? 'newmsg' : '']">
|
||||||
<DragInput class="group-input" v-model="msgText" type="textarea" :rows="1" :autosize="{ minRows: 1, maxRows: 3 }" :maxlength="255" @on-keydown="groupKeydown" @on-input-paste="groupPasteDrag" :placeholder="$L('输入消息...')" />
|
<div class="group-newmsg" @click="goNewBottom">{{$L('有' + msgNew + '条新消息')}}</div>
|
||||||
|
<DragInput class="group-input" v-model="msgText" type="textarea" :rows="1" :autosize="{ minRows: 1, maxRows: 3 }" :maxlength="255" @on-keydown="chatKeydown" @on-input-paste="pasteDrag" :placeholder="$L('输入消息...')" />
|
||||||
|
<MessageUpload
|
||||||
|
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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -115,8 +131,64 @@
|
|||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
.group-footer {
|
.group-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
padding: 0 28px;
|
padding: 0 28px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
.group-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;
|
||||||
|
.group-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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,24 +199,34 @@ import DragInput from "../../../components/DragInput";
|
|||||||
import ScrollerY from "../../../components/ScrollerY";
|
import ScrollerY from "../../../components/ScrollerY";
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
import MessageView from "./message-view";
|
import MessageView from "./message-view";
|
||||||
|
import MessageUpload from "./message-upload";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ProjectMessage",
|
name: "ProjectMessage",
|
||||||
components: {MessageView, ScrollerY, DragInput},
|
components: {MessageUpload, MessageView, ScrollerY, DragInput},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
autoBottom: true,
|
autoBottom: true,
|
||||||
|
autoInterval: null,
|
||||||
|
|
||||||
memberShowAll: false,
|
memberShowAll: false,
|
||||||
|
|
||||||
dialogId: 0,
|
dialogId: 0,
|
||||||
|
dialogDrag: false,
|
||||||
|
|
||||||
msgText: '',
|
msgText: '',
|
||||||
|
msgLength: 0,
|
||||||
|
msgNew: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.groupChatGoAuto();
|
this.goBottom();
|
||||||
this.groupChatGoBottom();
|
this.autoInterval = setInterval(this.goBottom, 200)
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
clearInterval(this.autoInterval)
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@ -158,21 +240,31 @@ export default {
|
|||||||
|
|
||||||
dialogId(id) {
|
dialogId(id) {
|
||||||
this.$store.commit('getDialogMsg', id);
|
this.$store.commit('getDialogMsg', id);
|
||||||
|
},
|
||||||
|
|
||||||
|
dialogList(list) {
|
||||||
|
if (!this.autoBottom) {
|
||||||
|
let length = list.length - this.msgLength;
|
||||||
|
if (length > 0) {
|
||||||
|
this.msgNew+= length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.msgLength = list.length;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
sendMsg() {
|
sendMsg() {
|
||||||
let mid = $A.randomString(16);
|
let tempId = $A.randomString(16);
|
||||||
this.dialogList.push({
|
this.dialogList.push({
|
||||||
id: mid,
|
id: tempId,
|
||||||
userid: this.userId,
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
userid: this.userId,
|
||||||
msg: {
|
msg: {
|
||||||
text: this.msgText,
|
text: this.msgText,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.groupChatGoBottom(true);
|
this.goBottom();
|
||||||
//
|
//
|
||||||
$A.apiAjax({
|
$A.apiAjax({
|
||||||
url: 'dialog/msg/sendtext',
|
url: 'dialog/msg/sendtext',
|
||||||
@ -181,24 +273,26 @@ export default {
|
|||||||
text: this.msgText,
|
text: this.msgText,
|
||||||
},
|
},
|
||||||
error:() => {
|
error:() => {
|
||||||
this.dialogList = this.dialogList.filter(({id}) => id != mid);
|
this.$store.commit('spliceDialogMsg', {id: tempId});
|
||||||
},
|
},
|
||||||
success: ({ret, data, msg}) => {
|
success: ({ret, data, msg}) => {
|
||||||
if (ret === 1) {
|
if (ret !== 1) {
|
||||||
if (!this.dialogList.find(({id}) => id == data.id)) {
|
$A.modalWarning({
|
||||||
let index = this.dialogList.findIndex(({id}) => id == mid);
|
title: '发送失败',
|
||||||
if (index > -1) this.dialogList.splice(index, 1, data);
|
content: msg
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.dialogList = this.dialogList.filter(({id}) => id != mid);
|
this.$store.commit('spliceDialogMsg', {
|
||||||
|
id: tempId,
|
||||||
|
data: ret === 1 ? data : null
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//
|
//
|
||||||
this.msgText = '';
|
this.msgText = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
groupKeydown(e) {
|
chatKeydown(e) {
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
return;
|
return;
|
||||||
@ -208,46 +302,86 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
groupPasteDrag(e, type) {
|
pasteDrag(e, type) {
|
||||||
const files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
|
const files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
|
||||||
const postFiles = Array.prototype.slice.call(files);
|
const postFiles = Array.prototype.slice.call(files);
|
||||||
if (postFiles.length > 0) {
|
if (postFiles.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
postFiles.forEach((file) => {
|
postFiles.forEach((file) => {
|
||||||
// 上传文件
|
this.$refs.chatUpload.upload(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
groupChatScroll(res) {
|
chatDragOver(show) {
|
||||||
if (res.directionreal === 'up') {
|
let random = (this.__dialogDrag = $A.randomString(8));
|
||||||
if (res.scrollE < 10) {
|
if (!show) {
|
||||||
this.autoBottom = true;
|
setTimeout(() => {
|
||||||
}
|
if (random === this.__dialogDrag) {
|
||||||
} else if (res.directionreal === 'down') {
|
this.dialogDrag = show;
|
||||||
this.autoBottom = false;
|
}
|
||||||
|
}, 150);
|
||||||
|
} else {
|
||||||
|
this.dialogDrag = show;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
groupChatGoAuto() {
|
chatPasteDrag(e, type) {
|
||||||
clearTimeout(this.groupChatGoTimeout);
|
this.dialogDrag = false;
|
||||||
this.groupChatGoTimeout = setTimeout(() => {
|
const files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
|
||||||
if (this.autoBottom) {
|
const postFiles = Array.prototype.slice.call(files);
|
||||||
this.groupChatGoBottom(true);
|
if (postFiles.length > 0) {
|
||||||
}
|
e.preventDefault();
|
||||||
this.groupChatGoAuto();
|
postFiles.forEach((file) => {
|
||||||
}, 1000);
|
this.$refs.chatUpload.upload(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
groupChatGoBottom(animation = false) {
|
chatFile(type, file) {
|
||||||
this.$nextTick(() => {
|
switch (type) {
|
||||||
if (typeof this.$refs.groupChat !== "undefined") {
|
case 'progress':
|
||||||
if (this.$refs.groupChat.getScrollInfo().scrollE > 0) {
|
this.dialogList.push({
|
||||||
this.$refs.groupChat.scrollTo(this.$refs.manageList.clientHeight, animation);
|
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;
|
||||||
}
|
}
|
||||||
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) {
|
formatTime(date) {
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
.project-message {
|
.project-message {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 30%;
|
width: 35%;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
@ -54,9 +54,9 @@ export default [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'manage-project-detail',
|
name: 'manage-project',
|
||||||
path: 'project/:id',
|
path: 'project/:id',
|
||||||
component: () => import('./pages/manage/project-detail.vue'),
|
component: () => import('./pages/manage/project.vue'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -192,6 +192,80 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话消息
|
||||||
|
* @param state
|
||||||
|
* @param dialog_id
|
||||||
|
*/
|
||||||
|
getDialogMsg(state, dialog_id) {
|
||||||
|
if (state.method.runNum(dialog_id) === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.method.isArray(state.cacheDialog[dialog_id])) {
|
||||||
|
state.dialogList = state.cacheDialog[dialog_id]
|
||||||
|
} else {
|
||||||
|
state.dialogList = [];
|
||||||
|
}
|
||||||
|
state.dialogId = dialog_id;
|
||||||
|
//
|
||||||
|
if (state.cacheDialog[dialog_id + "::load"]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.cacheDialog[dialog_id + "::load"] = true;
|
||||||
|
//
|
||||||
|
state.dialogLoad++;
|
||||||
|
$A.apiAjax({
|
||||||
|
url: 'dialog/msg/lists',
|
||||||
|
data: {
|
||||||
|
dialog_id: dialog_id,
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
state.dialogLoad--;
|
||||||
|
state.cacheDialog[dialog_id + "::load"] = false;
|
||||||
|
},
|
||||||
|
success: ({ret, data, msg}) => {
|
||||||
|
if (ret === 1) {
|
||||||
|
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);
|
||||||
|
if (index === -1) {
|
||||||
|
state.dialogList.push(item);
|
||||||
|
} else {
|
||||||
|
state.dialogList.splice(index, 1, item);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据消息ID 删除 或 替换 对话数据
|
||||||
|
* @param state
|
||||||
|
* @param params {id, data}
|
||||||
|
*/
|
||||||
|
spliceDialogMsg(state, params) {
|
||||||
|
let {id, data} = params;
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.method.isJson(data)) {
|
||||||
|
if (data.id && state.dialogList.find(m => m.id == data.id)) {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let index = state.dialogList.findIndex(m => m.id == id);
|
||||||
|
if (index > -1) {
|
||||||
|
if (data) {
|
||||||
|
state.dialogList.splice(index, 1, state.method.cloneJSON(data));
|
||||||
|
} else {
|
||||||
|
state.dialogList.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化 websocket
|
* 初始化 websocket
|
||||||
* @param state
|
* @param state
|
||||||
@ -291,7 +365,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {type, data, callback} = params;
|
const {type, data, callback} = params;
|
||||||
let msgId = params.msgId || 0;
|
let msgId = params.msgId;
|
||||||
if (!state.ws) {
|
if (!state.ws) {
|
||||||
typeof callback === "function" && callback(null, false)
|
typeof callback === "function" && callback(null, false)
|
||||||
return;
|
return;
|
||||||
@ -332,53 +406,4 @@ export default {
|
|||||||
wsClose(state) {
|
wsClose(state) {
|
||||||
state.ws && state.ws.close();
|
state.ws && state.ws.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取对话消息
|
|
||||||
* @param state
|
|
||||||
* @param dialog_id
|
|
||||||
*/
|
|
||||||
getDialogMsg(state, dialog_id) {
|
|
||||||
if (state.method.runNum(dialog_id) === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state.method.isArray(state.cacheDialog[dialog_id])) {
|
|
||||||
state.dialogList = state.cacheDialog[dialog_id]
|
|
||||||
} else {
|
|
||||||
state.dialogList = [];
|
|
||||||
}
|
|
||||||
state.dialogId = dialog_id;
|
|
||||||
//
|
|
||||||
if (state.cacheDialog[dialog_id + "::load"]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.cacheDialog[dialog_id + "::load"] = true;
|
|
||||||
//
|
|
||||||
state.dialogLoad++;
|
|
||||||
$A.apiAjax({
|
|
||||||
url: 'dialog/msg/lists',
|
|
||||||
data: {
|
|
||||||
dialog_id: dialog_id,
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
state.dialogLoad--;
|
|
||||||
state.cacheDialog[dialog_id + "::load"] = false;
|
|
||||||
},
|
|
||||||
success: ({ret, data, msg}) => {
|
|
||||||
if (ret === 1) {
|
|
||||||
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);
|
|
||||||
if (index === -1) {
|
|
||||||
state.dialogList.push(item);
|
|
||||||
} else {
|
|
||||||
state.dialogList.splice(index, 1, item);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,13 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ivu-modal-confirm {
|
||||||
|
.ivu-modal-confirm-body {
|
||||||
|
> div {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-info-show {
|
.modal-info-show {
|
||||||
@ -464,24 +471,37 @@ body {
|
|||||||
|
|
||||||
.common-avatar {
|
.common-avatar {
|
||||||
position: relative;
|
position: relative;
|
||||||
.common-avatar-text {
|
.avatar-wrapper {
|
||||||
background-color: #87d068;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
&:before {
|
.avatar-box {
|
||||||
content: "";
|
position: relative;
|
||||||
position: absolute;
|
.avatar-text {
|
||||||
right: 0;
|
background-color: #87d068;
|
||||||
bottom: 0;
|
}
|
||||||
width: 9px;
|
&:before {
|
||||||
height: 9px;
|
content: "";
|
||||||
border-radius: 50%;
|
position: absolute;
|
||||||
background-color: #ff0000;
|
right: 0;
|
||||||
border: 1px solid #ffffff;
|
bottom: 0;
|
||||||
z-index: 1;
|
width: 9px;
|
||||||
}
|
height: 9px;
|
||||||
&.online {
|
border-radius: 50%;
|
||||||
&:before {
|
background-color: #ff0000;
|
||||||
background-color: #87d068;
|
border: 1px solid #ffffff;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
&.online {
|
||||||
|
&:before {
|
||||||
|
background-color: #87d068;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.avatar-name {
|
||||||
|
padding-left: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -693,16 +713,72 @@ body {
|
|||||||
background-color: #F4F5F7;
|
background-color: #F4F5F7;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 6px 6px 6px 0;
|
border-radius: 6px 6px 6px 0;
|
||||||
}
|
&.loading {
|
||||||
.message-unknown {
|
display: flex;
|
||||||
text-decoration: underline;
|
.common-loading {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.file {
|
||||||
|
display: inline-block;
|
||||||
|
.file-box {
|
||||||
|
background-color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 220px;
|
||||||
|
.file-thumb {
|
||||||
|
width: 36px;
|
||||||
|
}
|
||||||
|
.file-info {
|
||||||
|
margin-left: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
.file-name {
|
||||||
|
color: #333333;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
word-break: break-all;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
.file-size {
|
||||||
|
padding-top: 4px;
|
||||||
|
color: #666666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.img {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
max-width: 220px;
|
||||||
|
max-height: 220px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
.file-img {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.unknown {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.message-foot {
|
.message-foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 3px;
|
padding-top: 4px;
|
||||||
height: 21px;
|
height: 21px;
|
||||||
line-height: 21px;
|
line-height: 1;
|
||||||
.common-loading {
|
.common-loading {
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@ -712,14 +788,18 @@ body {
|
|||||||
color: #bbbbbb;
|
color: #bbbbbb;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.done,
|
.done {
|
||||||
.done-all {
|
|
||||||
display: none;
|
display: none;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #87d068;
|
color: #87d068;
|
||||||
}
|
}
|
||||||
|
.percent {
|
||||||
|
display: none;
|
||||||
|
margin-left: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.loading {
|
&.loading {
|
||||||
@ -738,6 +818,11 @@ body {
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
|
&.bottom {
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
&.self {
|
&.self {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
.message-view {
|
.message-view {
|
||||||
@ -747,10 +832,19 @@ body {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: #2d8cf0;
|
background-color: #2d8cf0;
|
||||||
border-radius: 6px 6px 0 6px;
|
border-radius: 6px 6px 0 6px;
|
||||||
|
&.file {
|
||||||
|
background-color: #F4F5F7;
|
||||||
|
}
|
||||||
|
&.img {
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.message-foot {
|
.message-foot {
|
||||||
.done,
|
.done {
|
||||||
.done-all {
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.percent {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -760,3 +854,46 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-readbox {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
.read,
|
||||||
|
.unread {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
> li {
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.common-avatar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
&.read-title {
|
||||||
|
> em {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unread {
|
||||||
|
> li {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background-color: #F4F4F5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BIN
resources/assets/statics/public/images/ext/ai.png
Normal file
After Width: | Height: | Size: 575 B |
BIN
resources/assets/statics/public/images/ext/avi.png
Normal file
After Width: | Height: | Size: 633 B |
BIN
resources/assets/statics/public/images/ext/bmp.png
Normal file
After Width: | Height: | Size: 644 B |
BIN
resources/assets/statics/public/images/ext/cdr.png
Normal file
After Width: | Height: | Size: 642 B |
BIN
resources/assets/statics/public/images/ext/doc.png
Normal file
After Width: | Height: | Size: 621 B |
BIN
resources/assets/statics/public/images/ext/document.png
Normal file
After Width: | Height: | Size: 505 B |
BIN
resources/assets/statics/public/images/ext/eps.png
Normal file
After Width: | Height: | Size: 610 B |
BIN
resources/assets/statics/public/images/ext/exe.png
Normal file
After Width: | Height: | Size: 589 B |
BIN
resources/assets/statics/public/images/ext/file.png
Normal file
After Width: | Height: | Size: 481 B |
BIN
resources/assets/statics/public/images/ext/flow.png
Normal file
After Width: | Height: | Size: 526 B |
BIN
resources/assets/statics/public/images/ext/folder.png
Normal file
After Width: | Height: | Size: 682 B |
BIN
resources/assets/statics/public/images/ext/gif.png
Normal file
After Width: | Height: | Size: 553 B |
BIN
resources/assets/statics/public/images/ext/html.png
Normal file
After Width: | Height: | Size: 576 B |
BIN
resources/assets/statics/public/images/ext/mind.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
resources/assets/statics/public/images/ext/mov.png
Normal file
After Width: | Height: | Size: 701 B |
BIN
resources/assets/statics/public/images/ext/mp3.png
Normal file
After Width: | Height: | Size: 655 B |
BIN
resources/assets/statics/public/images/ext/mp4.png
Normal file
After Width: | Height: | Size: 619 B |
BIN
resources/assets/statics/public/images/ext/pdf.png
Normal file
After Width: | Height: | Size: 585 B |
BIN
resources/assets/statics/public/images/ext/ppt.png
Normal file
After Width: | Height: | Size: 538 B |
BIN
resources/assets/statics/public/images/ext/pr.png
Normal file
After Width: | Height: | Size: 558 B |
BIN
resources/assets/statics/public/images/ext/psd.png
Normal file
After Width: | Height: | Size: 637 B |
BIN
resources/assets/statics/public/images/ext/rar.png
Normal file
After Width: | Height: | Size: 644 B |
BIN
resources/assets/statics/public/images/ext/sheet.png
Normal file
After Width: | Height: | Size: 587 B |
BIN
resources/assets/statics/public/images/ext/svg.png
Normal file
After Width: | Height: | Size: 678 B |
BIN
resources/assets/statics/public/images/ext/tif.png
Normal file
After Width: | Height: | Size: 450 B |
BIN
resources/assets/statics/public/images/ext/txt.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
resources/assets/statics/public/images/ext/xls.png
Normal file
After Width: | Height: | Size: 637 B |
BIN
resources/assets/statics/public/images/ext/zip.png
Normal file
After Width: | Height: | Size: 553 B |