no message
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogMsgRead;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Module\Base;
|
||||
use Request;
|
||||
@ -44,6 +45,10 @@ class DialogController extends AbstractController
|
||||
}
|
||||
//
|
||||
$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);
|
||||
}
|
||||
@ -94,4 +99,128 @@ class DialogController extends AbstractController
|
||||
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/dialog/msg/sendfile/',
|
||||
];
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Tasks\PushTask;
|
||||
use App\Tasks\WebSocketDialogMsgTask;
|
||||
use Carbon\Carbon;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
|
||||
/**
|
||||
* Class WebSocketDialogMsg
|
||||
@ -15,11 +17,13 @@ use Carbon\Carbon;
|
||||
* @property int|null $userid 发送会员ID
|
||||
* @property string|null $type 消息类型
|
||||
* @property array|mixed $msg 详细消息
|
||||
* @property int|null $read 是否已读
|
||||
* @property int|null $read 已阅数量
|
||||
* @property int|null $send 发送数量
|
||||
* @property int|null $extra_int 额外数字参数
|
||||
* @property string|null $extra_str 额外字符参数
|
||||
* @property \Illuminate\Support\Carbon|null $created_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 newQuery()
|
||||
* @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 whereMsg($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 whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUserid($value)
|
||||
@ -37,12 +42,32 @@ use Carbon\Carbon;
|
||||
*/
|
||||
class WebSocketDialogMsg extends AbstractModel
|
||||
{
|
||||
protected $appends = [
|
||||
'percentage',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'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
|
||||
* @return array|mixed
|
||||
*/
|
||||
@ -51,27 +76,54 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
if (is_array($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)) {
|
||||
$this->read = 1;
|
||||
$this->save();
|
||||
PushTask::push([
|
||||
'userid' => $this->userid,
|
||||
'msg' => [
|
||||
'type' => 'dialog',
|
||||
'data' => $this->toArray(),
|
||||
]
|
||||
]);
|
||||
if (empty($userid)) {
|
||||
return false;
|
||||
}
|
||||
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->save();
|
||||
$userids = WebSocketDialogUser::whereDialogId($dialog->id)->where('userid', '!=', $dialogMsg->userid)->pluck('userid')->toArray();
|
||||
$dialogMsg->send = count($userids);
|
||||
$dialogMsg->dialog_id = $dialog->id;
|
||||
$dialogMsg->save();
|
||||
//
|
||||
$userids = WebSocketDialogUser::whereDialogId($dialog->id)->where('userid', '!=', $dialogMsg->userid)->pluck('userid')->toArray();
|
||||
if ($userids) {
|
||||
PushTask::push([
|
||||
'userid' => $userids,
|
||||
'msg' => [
|
||||
'type' => 'dialog',
|
||||
'data' => $dialogMsg->toArray(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
$task = new WebSocketDialogMsgTask($userids, $dialogMsg->toArray());
|
||||
Task::deliver($task);
|
||||
//
|
||||
return Base::retSuccess('发送成功', $dialogMsg);
|
||||
});
|
||||
}
|
||||
@ -146,16 +193,13 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
$dialog->last_at = Carbon::now();
|
||||
$dialog->save();
|
||||
$dialogMsg->send = 1;
|
||||
$dialogMsg->dialog_id = $dialog->id;
|
||||
$dialogMsg->save();
|
||||
//
|
||||
PushTask::push([
|
||||
'userid' => $userid,
|
||||
'msg' => [
|
||||
'type' => 'dialog',
|
||||
'data' => $dialogMsg->toArray(),
|
||||
]
|
||||
]);
|
||||
$task = new WebSocketDialogMsgTask($userid, $dialogMsg->toArray());
|
||||
Task::deliver($task);
|
||||
//
|
||||
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;
|
||||
} else {
|
||||
return url($str);
|
||||
try {
|
||||
return url($str);
|
||||
} catch (\Throwable $e) {
|
||||
return self::getSchemeAndHost() . "/" . $str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -761,7 +765,22 @@ class Base
|
||||
}
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return intval(self::getContentValue($key, $default));
|
||||
return intval(self::getPostValue($key, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2301,7 +2324,7 @@ class Base
|
||||
$array['thumb'] = $array['path'];
|
||||
if ($param['autoThumb'] === "false") $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";
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ class WebSocketService implements WebSocketHandlerInterface
|
||||
*/
|
||||
case 'readMsg':
|
||||
$dialogMsg = WebSocketDialogMsg::whereId(intval($data['id']))->first();
|
||||
$dialogMsg && $dialogMsg->readSuccess();
|
||||
$dialogMsg && $dialogMsg->readSuccess($this->getUserid($frame->fd));
|
||||
return;
|
||||
}
|
||||
//
|
||||
@ -178,4 +178,14 @@ class WebSocketService implements WebSocketHandlerInterface
|
||||
{
|
||||
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>
|
||||
<Tooltip v-if="user"
|
||||
:class="['common-avatar', user.online ? 'online' : '']"
|
||||
class="common-avatar"
|
||||
:delay="600"
|
||||
:transfer="transfer">
|
||||
<div slot="content">
|
||||
<p>{{$L('昵称')}}: {{user.nickname}}</p>
|
||||
<p>{{$L('职位/职称')}}: {{user.profession || '-'}}</p>
|
||||
</div>
|
||||
<Avatar v-if="showImg" :src="user.userimg" :size="size"/>
|
||||
<Avatar v-else :size="size" class="common-avatar-text">{{nickname}}</Avatar>
|
||||
<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>
|
||||
</div>
|
||||
<div v-if="showName" class="avatar-name">{{user.nickname}}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
@ -24,6 +29,10 @@
|
||||
type: [String, Number],
|
||||
default: 'default'
|
||||
},
|
||||
showName: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
transfer: {
|
||||
type: Boolean,
|
||||
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格式的转成指定格式
|
||||
* @param myObj
|
||||
* @param format 默认格式:Y-m-d
|
||||
* @param params
|
||||
* @param format 默认格式:Y-m-d H:i:s
|
||||
* @returns {*}
|
||||
*/
|
||||
date2string(myObj, format) {
|
||||
if (myObj === null) {
|
||||
return myObj;
|
||||
date2string(params, format) {
|
||||
if (params === null) {
|
||||
return params;
|
||||
}
|
||||
if (typeof format === "undefined") {
|
||||
format = "Y-m-d";
|
||||
format = "Y-m-d H:i:s";
|
||||
}
|
||||
if (typeof myObj === "object") {
|
||||
if (myObj instanceof Date) {
|
||||
return $A.formatDate(format, myObj);
|
||||
if (params instanceof Date) {
|
||||
params = $A.formatDate(format, params);
|
||||
} 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)=>{
|
||||
myObj[key] = $A.date2string(val, format);
|
||||
} else if ($A.isArray(params)) {
|
||||
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.header === 'undefined') params.header = {};
|
||||
params.url = this.apiUrl(params.url);
|
||||
params.data = this.date2string(params.data);
|
||||
params.header['Content-Type'] = 'application/json';
|
||||
params.header['language'] = $A.getLanguage();
|
||||
params.header['token'] = $A.store.state.userToken;
|
||||
|
@ -8,15 +8,15 @@
|
||||
<ul>
|
||||
<li @click="toggleRoute('dashboard')" :class="classNameRoute('dashboard')">
|
||||
<Icon type="md-speedometer" />
|
||||
<div class="menu-title">Dashboard</div>
|
||||
<div class="menu-title">{{$L('仪表板')}}</div>
|
||||
</li>
|
||||
<li @click="toggleRoute('setting/personal')" :class="classNameRoute('setting')">
|
||||
<Icon type="md-cog" />
|
||||
<div class="menu-title">Setting</div>
|
||||
<div class="menu-title">{{$L('设置')}}</div>
|
||||
</li>
|
||||
<li @click="toggleRoute('calendar')" :class="classNameRoute('calendar')">
|
||||
<Icon type="md-calendar" />
|
||||
<div class="menu-title">Calendar</div>
|
||||
<div class="menu-title">{{$L('日历')}}</div>
|
||||
</li>
|
||||
<li class="menu-project">
|
||||
<ul>
|
||||
@ -25,7 +25,7 @@
|
||||
<Loading v-if="loadIng > 0"/>
|
||||
</li>
|
||||
</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 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>
|
||||
<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 class="time">{{formatTime(msgData.created_at)}}</div>
|
||||
<Icon v-if="msgData.read" class="done-all" type="md-done-all" />
|
||||
<Icon v-else class="done" type="md-checkmark" />
|
||||
<Poptip
|
||||
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 v-else class="message-foot"><Loading/></div>
|
||||
|
||||
@ -16,9 +54,11 @@
|
||||
|
||||
<script>
|
||||
import {mapState} from "vuex";
|
||||
import WCircle from "../../../components/WCircle";
|
||||
|
||||
export default {
|
||||
name: "MessageView",
|
||||
components: {WCircle},
|
||||
props: {
|
||||
msgData: {
|
||||
type: Object,
|
||||
@ -26,32 +66,66 @@ export default {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
dialogType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
msgInfo: {},
|
||||
read_list: []
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.readMarking()
|
||||
this.parsingData()
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['userId']),
|
||||
|
||||
readList() {
|
||||
return this.read_list.filter(({read_at}) => read_at)
|
||||
},
|
||||
|
||||
unreadList() {
|
||||
return this.read_list.filter(({read_at}) => !read_at)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
msgData() {
|
||||
this.readMarking()
|
||||
this.parsingData()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
readMarking() {
|
||||
if (this.msgData.read === 0 && this.msgData.userid != this.userId) {
|
||||
this.$store.commit('wsSend', {
|
||||
type: 'readMsg',
|
||||
data: {
|
||||
id: this.msgData.id
|
||||
popperShow() {
|
||||
$A.apiAjax({
|
||||
url: 'dialog/msg/readlist',
|
||||
data: {
|
||||
msg_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) {
|
||||
@ -74,6 +148,30 @@ export default {
|
||||
text = text.trim().replace(/(\n\x20*){3,}/g, "<br/><br/>");
|
||||
text = text.trim().replace(/\n/g, "<br/>");
|
||||
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>
|
||||
<Icon type="ios-search" />
|
||||
<div slot="content">
|
||||
<Input v-model="searchText" placeholder="Search task..." clearable autofocus/>
|
||||
<Input v-model="searchText" :placeholder="$L('名称、描述...')" clearable autofocus/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</li>
|
||||
@ -99,11 +99,11 @@
|
||||
<div v-else class="project-table">
|
||||
<div class="project-table-head">
|
||||
<Row class="project-row">
|
||||
<Col span="12"># Task name</Col>
|
||||
<Col span="3">Task Column</Col>
|
||||
<Col span="3">Priority</Col>
|
||||
<Col span="3">Member</Col>
|
||||
<Col span="3">Expiration</Col>
|
||||
<Col span="12"># {{$L('任务名称')}}</Col>
|
||||
<Col span="3">{{$L('列表')}}</Col>
|
||||
<Col span="3">{{$L('优先级')}}</Col>
|
||||
<Col span="3">{{$L('负责人')}}</Col>
|
||||
<Col span="3">{{$L('到期时间')}}</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<!--我的任务-->
|
||||
@ -112,7 +112,7 @@
|
||||
<Row class="project-row">
|
||||
<Col span="12" class="row-title">
|
||||
<i class="iconfont"></i>
|
||||
<div class="row-h1">My task</div>
|
||||
<div class="row-h1">{{$L('我的任务')}}</div>
|
||||
<div class="row-num">({{myList.length}})</div>
|
||||
</Col>
|
||||
<Col span="3"></Col>
|
||||
@ -170,7 +170,7 @@
|
||||
<Row class="project-row">
|
||||
<Col span="12" class="row-title">
|
||||
<i class="iconfont"></i>
|
||||
<div class="row-h1">Undone</div>
|
||||
<div class="row-h1">{{$L('未完成任务')}}</div>
|
||||
<div class="row-num">({{undoneList.length}})</div>
|
||||
</Col>
|
||||
<Col span="3"></Col>
|
||||
@ -217,7 +217,7 @@
|
||||
<Row class="project-row">
|
||||
<Col span="12" class="row-title">
|
||||
<i class="iconfont"></i>
|
||||
<div class="row-h1">Completed</div>
|
||||
<div class="row-h1">{{$L('已完成任务')}}</div>
|
||||
<div class="row-num">({{completedList.length}})</div>
|
||||
</Col>
|
||||
<Col span="3"></Col>
|
||||
@ -975,6 +975,15 @@ export default {
|
||||
$A.messageSuccess(msg);
|
||||
this.$store.commit('getProjectDetail', this.addData.project_id);
|
||||
this.addShow = false;
|
||||
this.addData = {
|
||||
owner: 0,
|
||||
column_id: 0,
|
||||
times: [],
|
||||
subtasks: [],
|
||||
p_level: 0,
|
||||
p_name: '',
|
||||
p_color: '',
|
||||
};
|
||||
} else {
|
||||
$A.modalError(msg);
|
||||
}
|
||||
@ -1166,7 +1175,7 @@ export default {
|
||||
let minutes = Math.floor(((second % 86400) % 3600) / 60);
|
||||
let seconds = Math.floor(((second % 86400) % 3600) % 60);
|
||||
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 (seconds > 0) duration = days + "d," + this.formatBit(seconds) + "s";
|
||||
else duration = days + "d";
|
||||
|
@ -1,5 +1,10 @@
|
||||
<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="member-head">
|
||||
<div class="member-title">{{$L('项目成员')}}<span>({{projectDetail.project_user.length}})</span></div>
|
||||
@ -12,7 +17,7 @@
|
||||
</ul>
|
||||
</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">
|
||||
<ul>
|
||||
<li v-if="dialogLoad > 0" class="loading"><Loading/></li>
|
||||
@ -21,13 +26,24 @@
|
||||
<div class="message-avatar">
|
||||
<UserAvatar :userid="item.userid" :size="30"/>
|
||||
</div>
|
||||
<MessageView :msg-data="item"/>
|
||||
<MessageView :msg-data="item" dialog-type="group"/>
|
||||
</li>
|
||||
<li ref="bottom" class="bottom"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</ScrollerY>
|
||||
<div class="group-footer">
|
||||
<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-footer', msgNew > 0 ? 'newmsg' : '']">
|
||||
<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>
|
||||
</template>
|
||||
@ -115,8 +131,64 @@
|
||||
margin-top: 18px;
|
||||
}
|
||||
.group-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
padding: 0 28px;
|
||||
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 {mapState} from "vuex";
|
||||
import MessageView from "./message-view";
|
||||
import MessageUpload from "./message-upload";
|
||||
|
||||
export default {
|
||||
name: "ProjectMessage",
|
||||
components: {MessageView, ScrollerY, DragInput},
|
||||
components: {MessageUpload, MessageView, ScrollerY, DragInput},
|
||||
data() {
|
||||
return {
|
||||
autoBottom: true,
|
||||
autoInterval: null,
|
||||
|
||||
memberShowAll: false,
|
||||
|
||||
dialogId: 0,
|
||||
dialogDrag: false,
|
||||
|
||||
msgText: '',
|
||||
msgLength: 0,
|
||||
msgNew: 0,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.groupChatGoAuto();
|
||||
this.groupChatGoBottom();
|
||||
this.goBottom();
|
||||
this.autoInterval = setInterval(this.goBottom, 200)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
clearInterval(this.autoInterval)
|
||||
},
|
||||
|
||||
computed: {
|
||||
@ -158,21 +240,31 @@ export default {
|
||||
|
||||
dialogId(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: {
|
||||
sendMsg() {
|
||||
let mid = $A.randomString(16);
|
||||
let tempId = $A.randomString(16);
|
||||
this.dialogList.push({
|
||||
id: mid,
|
||||
userid: this.userId,
|
||||
id: tempId,
|
||||
type: 'text',
|
||||
userid: this.userId,
|
||||
msg: {
|
||||
text: this.msgText,
|
||||
},
|
||||
});
|
||||
this.groupChatGoBottom(true);
|
||||
this.goBottom();
|
||||
//
|
||||
$A.apiAjax({
|
||||
url: 'dialog/msg/sendtext',
|
||||
@ -181,24 +273,26 @@ export default {
|
||||
text: this.msgText,
|
||||
},
|
||||
error:() => {
|
||||
this.dialogList = this.dialogList.filter(({id}) => id != mid);
|
||||
this.$store.commit('spliceDialogMsg', {id: tempId});
|
||||
},
|
||||
success: ({ret, data, msg}) => {
|
||||
if (ret === 1) {
|
||||
if (!this.dialogList.find(({id}) => id == data.id)) {
|
||||
let index = this.dialogList.findIndex(({id}) => id == mid);
|
||||
if (index > -1) this.dialogList.splice(index, 1, data);
|
||||
return;
|
||||
}
|
||||
if (ret !== 1) {
|
||||
$A.modalWarning({
|
||||
title: '发送失败',
|
||||
content: msg
|
||||
});
|
||||
}
|
||||
this.dialogList = this.dialogList.filter(({id}) => id != mid);
|
||||
this.$store.commit('spliceDialogMsg', {
|
||||
id: tempId,
|
||||
data: ret === 1 ? data : null
|
||||
});
|
||||
}
|
||||
});
|
||||
//
|
||||
this.msgText = '';
|
||||
},
|
||||
|
||||
groupKeydown(e) {
|
||||
chatKeydown(e) {
|
||||
if (e.keyCode === 13) {
|
||||
if (e.shiftKey) {
|
||||
return;
|
||||
@ -208,46 +302,86 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
groupPasteDrag(e, type) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
groupChatScroll(res) {
|
||||
if (res.directionreal === 'up') {
|
||||
if (res.scrollE < 10) {
|
||||
this.autoBottom = true;
|
||||
}
|
||||
} else if (res.directionreal === 'down') {
|
||||
this.autoBottom = false;
|
||||
chatDragOver(show) {
|
||||
let random = (this.__dialogDrag = $A.randomString(8));
|
||||
if (!show) {
|
||||
setTimeout(() => {
|
||||
if (random === this.__dialogDrag) {
|
||||
this.dialogDrag = show;
|
||||
}
|
||||
}, 150);
|
||||
} else {
|
||||
this.dialogDrag = show;
|
||||
}
|
||||
},
|
||||
|
||||
groupChatGoAuto() {
|
||||
clearTimeout(this.groupChatGoTimeout);
|
||||
this.groupChatGoTimeout = setTimeout(() => {
|
||||
if (this.autoBottom) {
|
||||
this.groupChatGoBottom(true);
|
||||
}
|
||||
this.groupChatGoAuto();
|
||||
}, 1000);
|
||||
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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
groupChatGoBottom(animation = false) {
|
||||
this.$nextTick(() => {
|
||||
if (typeof this.$refs.groupChat !== "undefined") {
|
||||
if (this.$refs.groupChat.getScrollInfo().scrollE > 0) {
|
||||
this.$refs.groupChat.scrollTo(this.$refs.manageList.clientHeight, animation);
|
||||
chatFile(type, file) {
|
||||
switch (type) {
|
||||
case 'progress':
|
||||
this.dialogList.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;
|
||||
}
|
||||
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) {
|
||||
|
@ -20,7 +20,7 @@
|
||||
.project-message {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 30%;
|
||||
width: 35%;
|
||||
min-width: 320px;
|
||||
max-width: 520px;
|
||||
flex-shrink: 0;
|
@ -54,9 +54,9 @@ export default [
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'manage-project-detail',
|
||||
name: 'manage-project',
|
||||
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
|
||||
* @param state
|
||||
@ -291,7 +365,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
const {type, data, callback} = params;
|
||||
let msgId = params.msgId || 0;
|
||||
let msgId = params.msgId;
|
||||
if (!state.ws) {
|
||||
typeof callback === "function" && callback(null, false)
|
||||
return;
|
||||
@ -332,53 +406,4 @@ export default {
|
||||
wsClose(state) {
|
||||
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 {
|
||||
@ -464,24 +471,37 @@ body {
|
||||
|
||||
.common-avatar {
|
||||
position: relative;
|
||||
.common-avatar-text {
|
||||
background-color: #87d068;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background-color: #ff0000;
|
||||
border: 1px solid #ffffff;
|
||||
z-index: 1;
|
||||
}
|
||||
&.online {
|
||||
&:before {
|
||||
background-color: #87d068;
|
||||
.avatar-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.avatar-box {
|
||||
position: relative;
|
||||
.avatar-text {
|
||||
background-color: #87d068;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background-color: #ff0000;
|
||||
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;
|
||||
padding: 8px;
|
||||
border-radius: 6px 6px 6px 0;
|
||||
}
|
||||
.message-unknown {
|
||||
text-decoration: underline;
|
||||
&.loading {
|
||||
display: flex;
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 3px;
|
||||
padding-top: 4px;
|
||||
height: 21px;
|
||||
line-height: 21px;
|
||||
line-height: 1;
|
||||
.common-loading {
|
||||
margin: 0 2px;
|
||||
width: 10px;
|
||||
@ -712,14 +788,18 @@ body {
|
||||
color: #bbbbbb;
|
||||
font-size: 12px;
|
||||
}
|
||||
.done,
|
||||
.done-all {
|
||||
.done {
|
||||
display: none;
|
||||
margin-left: 4px;
|
||||
transform: scale(0.9);
|
||||
font-size: 12px;
|
||||
color: #87d068;
|
||||
}
|
||||
.percent {
|
||||
display: none;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.loading {
|
||||
@ -738,6 +818,11 @@ body {
|
||||
transform: translate(-50%, -50%);
|
||||
color: #999999;
|
||||
}
|
||||
&.bottom {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
&.self {
|
||||
flex-direction: row-reverse;
|
||||
.message-view {
|
||||
@ -747,10 +832,19 @@ body {
|
||||
color: #ffffff;
|
||||
background-color: #2d8cf0;
|
||||
border-radius: 6px 6px 0 6px;
|
||||
&.file {
|
||||
background-color: #F4F5F7;
|
||||
}
|
||||
&.img {
|
||||
border-radius: 6px;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.message-foot {
|
||||
.done,
|
||||
.done-all {
|
||||
.done {
|
||||
display: inline-block;
|
||||
}
|
||||
.percent {
|
||||
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 |