Compare commits

...

37 Commits

Author SHA1 Message Date
kuaifan
9eaa575d1a build 2022-03-04 08:53:42 +08:00
kuaifan
996ed78a0e perf: 优化仪表盘角标数 2022-03-04 08:51:22 +08:00
kuaifan
1759572b2e perf: 优化客户端任务详情按command+s保存 2022-03-04 08:50:22 +08:00
kuaifan
0a9f9eea90 build 2022-03-04 00:00:26 +08:00
kuaifan
223fb540b1 perf: 优化文件重命名,支持按esc取消编辑 2022-03-03 23:57:58 +08:00
kuaifan
c5f1c95f7b perf: 任务详情打开操作菜单时按esc任务窗口隐藏了但是菜单还看见 2022-03-03 23:06:54 +08:00
kuaifan
3bbcbca926 build 2022-03-03 16:21:08 +08:00
kuaifan
63c1deb630 fix: md编辑器出现toc混乱的情况 2022-03-03 16:17:49 +08:00
kuaifan
424e2428fe pref: 上传文件名称过程显示错位的问题 2022-03-03 15:11:50 +08:00
kuaifan
2fdef45156 pref: 退出登录返回登录页而不是注册页 2022-03-03 15:11:34 +08:00
kuaifan
4cd4550a36 pref: 网络异常的情况下需提示网络异常而不是系统出错 2022-03-03 14:53:01 +08:00
kuaifan
16af625aef no msg 2022-03-03 14:35:21 +08:00
kuaifan
8e72794c07 pref: 任务详情当任务倒计时结束时显示"超期未完成"标签 2022-03-03 14:35:12 +08:00
kuaifan
d9cf6d7e1b pref: 优化脚本,支持部分服务器是docker compose命令 2022-03-03 14:22:25 +08:00
kuaifan
f4e4252227 perf: 优化已读回执 2022-03-03 11:41:54 +08:00
kuaifan
7107409b1b no message 2022-03-02 08:46:29 +08:00
kuaifan
6c086fab6f no message 2022-03-02 08:32:38 +08:00
kuaifan
d027d67e08 pref: 倒计时刚到到达0时会显示自定义才继续显示计时,且未显示超时标签 2022-03-02 08:25:38 +08:00
kuaifan
a7d9e635eb pref: 鼠标滑动至仪表盘中的待完成任务卡片时,卡片周围未显示光晕,且未显示为手指样式 2022-03-02 08:19:09 +08:00
kuaifan
e7196efaea pref: 优化任务详细描述显示 2022-03-02 08:17:30 +08:00
kuaifan
0a2a903c74 fix: 修复登录页设置下拉显示不全的情况 2022-03-02 07:59:53 +08:00
kuaifan
8104c26b19 处理任务简介出现"的情况 2022-03-02 07:48:29 +08:00
kuaifan
101d5c7eb0 build 2022-02-28 00:23:20 +08:00
kuaifan
cad253b85f feat: 导出任务功能 2022-02-28 00:21:48 +08:00
kuaifan
f17009a988 优化左上角菜单集合 2022-02-27 22:54:48 +08:00
kuaifan
1f76278d2b 优化客户端升级提示 2022-02-27 22:53:54 +08:00
kuaifan
e032d29c91 修复数据库字段填写错误 2022-02-27 21:12:11 +08:00
kuaifan
31d1b0c994 nomsg 2022-02-27 15:03:39 +08:00
kuaifan
611c6d415c perf: 记录任务工作流变化 2022-02-27 14:12:15 +08:00
kuaifan
e3b7ac00fd perf: 优化修改工作流的过程 2022-02-27 14:11:50 +08:00
kuaifan
7c952822db perf: 优化任务排序 2022-02-27 11:06:18 +08:00
kuaifan
b9e435c0e2 perf: 支持nodejs16+ 2022-02-27 10:59:38 +08:00
kuaifan
356d40e640 feat: 文件支持拖动到列表上传 2022-02-25 22:49:56 +08:00
kuaifan
123ffd4e66 no message 2022-02-25 22:47:56 +08:00
kuaifan
c952659620 fix: 修复无法预览pdf文件 2022-02-25 22:15:25 +08:00
kuaifan
ea8e1e9c57 优化样式 2022-02-25 15:14:20 +08:00
kuaifan
478d63893b fix: 无法浏览聊天图片的问题 2022-02-25 14:38:40 +08:00
70 changed files with 1380 additions and 538 deletions

View File

@ -17,10 +17,13 @@ use App\Models\ProjectUser;
use App\Models\User;
use App\Models\WebSocketDialog;
use App\Module\Base;
use App\Module\BillExport;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Madzipper;
use Request;
use Response;
use Session;
/**
* @apiDefine project
@ -604,13 +607,14 @@ class ProjectController extends AbstractController
if (!is_array($item['task'])) continue;
$index = 0;
foreach ($item['task'] as $task_id) {
ProjectTask::whereId($task_id)->whereProjectId($project->id)->update([
if (ProjectTask::whereId($task_id)->whereProjectId($project->id)->whereCompleteAt(null)->update([
'column_id' => $item['id'],
'sort' => $index
]);
])) {
ProjectTask::whereParentId($task_id)->whereProjectId($project->id)->update([
'column_id' => $item['id'],
]);
}
$index++;
}
}
@ -976,6 +980,154 @@ class ProjectController extends AbstractController
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/project/task/export 18. 导出任务(限管理员)
*
* @apiDescription 导出指定范围任务已完成、未完成、已归档返回下载地址需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__export
*
* @apiParam {Array} [userid] 指定会员,如:[1, 2]
* @apiParam {Array} [time] 指定时间范围,如:['2020-12-12', '2020-12-30']
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__export()
{
$user = User::auth('admin');
//
$userid = Base::arrayRetainInt(Request::input('userid'), true);
$time = Request::input('time');
if (empty($userid) || empty($time)) {
return Base::retError('参数错误');
}
if (count($userid) > 20) {
return Base::retError('导出会员限制最多20个');
}
if (!(is_array($time) && Base::isDateOrTime($time[0]) && Base::isDateOrTime($time[1]))) {
return Base::retError('时间选择错误');
}
if (Carbon::parse($time[1])->timestamp - Carbon::parse($time[0])->timestamp > 90 * 86400) {
return Base::retError('时间范围限制最大90天');
}
//
$headings = [];
$headings[] = '任务ID';
$headings[] = '任务标题';
$headings[] = '负责人';
$headings[] = '创建人';
$headings[] = '是否完成';
$headings[] = '完成时间';
$headings[] = '是否归档';
$headings[] = '归档时间';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '结束剩余';
$headings[] = '所属项目';
$headings[] = '父级任务ID';
$datas = [];
//
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
->where('project_task_users.owner', 1)
->whereIn('project_task_users.userid', $userid)
->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay());
$builder->orderByDesc('project_tasks.id')->chunk(100, function($tasks) use (&$datas) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
if ($task->complete_at) {
$a = Carbon::parse($task->complete_at)->timestamp;
$b = Carbon::parse($task->end_at)->timestamp;
if ($b > $a) {
$endSurplus = Base::timeDiff($a, $b);
} else {
$endSurplus = "-" . Base::timeDiff($b, $a);
}
} else {
$endSurplus = '-';
}
$datas[] = [
$task->id,
Base::filterEmoji($task->name),
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
$task->complete_at ? '已完成' : '-',
$task->complete_at ?: '-',
$task->archived_at ? '已归档' : '-',
$task->archived_at ?: '-',
$task->start_at ?: '-',
$task->end_at ?: '-',
$endSurplus,
Base::filterEmoji($task->project?->name) ?: '-',
$task->parent_id ?: '-',
];
}
});
//
$fileName = User::userid2nickname($userid[0]) ?: $userid[0];
if (count($userid) > 1) {
$fileName .= "" . count($userid) . "位成员";
}
$fileName .= '任务统计_' . Base::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Base::time());
$res = BillExport::create()->setHeadings($headings)->setData($datas)->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls'). ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Exception) { }
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('task::export:userid', $user->userid);
return Base::retSuccess('success', [
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
'url' => Base::fillUrl('api/project/task/down?key=' . urlencode($base64)),
]);
} else {
return Base::retError('打包失败,请稍后再试...');
}
}
/**
* @api {get} api/project/task/down 18. 导出任务(限管理员)
*
* @apiDescription 导出指定范围任务已完成、未完成、已归档返回下载地址需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__down
*
* @apiParam {String} key 通过export接口得到的下载钥匙
*
* @apiSuccess {File} 文件下载
*/
public function task__down()
{
$userid = Session::get('task::export:userid');
if (empty($userid)) {
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
}
//
$array = Base::string2array(base64_decode(urldecode(Request::input('key'))));
$file = $array['file'];
if (empty($file) || !file_exists(storage_path($file))) {
return Base::ajaxError("文件不存在!", [], 0, 502);
}
return response()->download(storage_path($file));
}
/**
* @api {get} api/project/task/one 19. 获取单个任务信息
*

View File

@ -153,9 +153,10 @@ class AbstractModel extends Model
* @param $where
* @param array $update 存在时更新的内容
* @param array $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
*/
public static function updateInsert($where, $update = [], $insert = [])
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true)
{
$row = static::where($where)->first();
if (empty($row)) {
@ -165,8 +166,10 @@ class AbstractModel extends Model
unset($array[$row->primaryKey]);
}
$row->updateInstance($array);
$isInsert = true;
} elseif ($update) {
$row->updateInstance($update);
$isInsert = false;
}
if (!$row->save()) {
return null;

View File

@ -275,15 +275,16 @@ class File extends AbstractModel
/**
* 格式化内容数据
* @param array $data [path, ext, size, name]
* @param array $data [path, size, ext, name]
* @return array
*/
public static function formatFileData(array $data)
{
$filePath = $data['path'];
$fileExt = $data['ext'];
$fileSize = $data['size'];
$fileName = $data['name'];
$fileExt = $data['ext'];
$fileDotExt = '.' . $fileExt;
$fileName = Base::rightDelete($data['name'], $fileDotExt) . $fileDotExt;
$publicPath = public_path($filePath);
//
switch ($fileExt) {

View File

@ -376,6 +376,7 @@ class Project extends AbstractModel
$idc = [];
$hasStart = false;
$hasEnd = false;
$upTaskList = [];
foreach ($flows as $item) {
$id = intval($item['id']);
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
@ -403,7 +404,7 @@ class Project extends AbstractModel
'userids' => $userids,
'usertype' => trim($item['usertype']),
'userlimit' => $userlimit,
]);
], [], $isInsert);
if ($flow) {
$ids[] = $flow->id;
if ($flow->id != $id) {
@ -415,6 +416,9 @@ class Project extends AbstractModel
if ($flow->status == 'end') {
$hasEnd = true;
}
if (!$isInsert) {
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name;
}
}
}
if (!$hasStart) {
@ -429,6 +433,12 @@ class Project extends AbstractModel
}
});
//
foreach ($upTaskList as $id => $value) {
ProjectTask::whereFlowItemId($id)->update([
'flow_item_name' => $value
]);
}
//
$projectFlow = ProjectFlow::with(['projectFlowItem'])->whereProjectId($this->id)->find($projectFlow->id);
$itemIds = $projectFlow->projectFlowItem->pluck('id')->toArray();
foreach ($projectFlow->projectFlowItem as $item) {

View File

@ -585,6 +585,14 @@ class ProjectTask extends AbstractModel
'flow' => $flowData,
'change' => [$currentFlowItem?->name, $newFlowItem->name]
]);
ProjectTaskFlowChange::createInstance([
'task_id' => $this->id,
'userid' => User::userid(),
'before_flow_item_id' => $flowData['flow_item_id'],
'before_flow_item_name' => $flowData['flow_item_name'],
'after_flow_item_id' => $this->flow_item_id,
'after_flow_item_name' => $this->flow_item_name,
])->save();
}
// 状态
if (Arr::exists($data, 'complete_at')) {

View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
/**
* App\Models\ProjectTaskFlowChange
*
* @property int $id
* @property int|null $task_id 任务ID
* @property int|null $userid 会员ID
* @property int|null $before_flow_item_id 变化前工作流状态ID
* @property string|null $before_flow_item_name (变化前)工作流状态名称
* @property int|null $after_flow_item_id 变化后工作流状态ID
* @property string|null $after_flow_item_name (变化后)工作流状态名称
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereAfterFlowItemId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereAfterFlowItemName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereBeforeFlowItemId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereBeforeFlowItemName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereUserid($value)
* @mixin \Eloquent
*/
class ProjectTaskFlowChange extends AbstractModel
{
}

View File

@ -11,7 +11,7 @@ use App\Module\Base;
* @property int|null $project_id 项目ID
* @property int|null $userid 成员ID
* @property int|null $owner 是否负责人
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
* @property string|null $top_at 置顶时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project|null $project
@ -22,6 +22,7 @@ use App\Module\Base;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereOwner($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereTopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUserid($value)
* @mixin \Eloquent

View File

@ -70,11 +70,7 @@ class WebSocketDialogMsg extends AbstractModel
public function getPercentageAttribute()
{
if (!isset($this->appendattrs['percentage'])) {
if ($this->read > $this->send || empty($this->send)) {
$this->appendattrs['percentage'] = 100;
} else {
$this->appendattrs['percentage'] = intval($this->read / $this->send * 100);
}
$this->generatePercentage();
}
return $this->appendattrs['percentage'];
}
@ -98,6 +94,22 @@ class WebSocketDialogMsg extends AbstractModel
return $value;
}
/**
* 获取占比
* @param bool $increment 是否新增阅读数
* @return int
*/
public function generatePercentage($increment = false) {
if ($increment) {
$this->increment('read');
}
if ($this->read > $this->send || empty($this->send)) {
return $this->appendattrs['percentage'] = 100;
} else {
return $this->appendattrs['percentage'] = intval($this->read / $this->send * 100);
}
}
/**
* 标记已送达 同时 告诉发送人已送达
* @param $userid
@ -127,13 +139,17 @@ class WebSocketDialogMsg extends AbstractModel
if (!$msgRead->read_at) {
$msgRead->read_at = Carbon::now();
$msgRead->save();
$this->increment('read');
$this->generatePercentage(true);
PushTask::push([
'userid' => $this->userid,
'msg' => [
'type' => 'dialog',
'mode' => 'update',
'data' => $this->toArray(),
'mode' => 'readed',
'data' => [
'id' => $this->id,
'read' => $this->read,
'percentage' => $this->percentage,
],
]
]);
}

View File

@ -8,7 +8,7 @@ namespace App\Models;
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $userid 会员ID
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
* @property string|null $top_at 置顶时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newModelQuery()
@ -17,6 +17,7 @@ namespace App\Models;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereTopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUserid($value)
* @mixin \Eloquent

View File

@ -342,19 +342,15 @@ class Base
{
if (strtolower($charset) == 'utf-8') {
if (Base::getStrlen($string) <= $length) return $string;
$strcut = str_replace(array('&amp;', '&quot;', '&lt;', '&gt;'), array('&', '"', '<', '>'), $string);
$strcut = Base::utf8Substr($strcut, $length, $start);
$strcut = str_replace(array('&', '"', '<', '>'), array('&amp;', '&quot;', '&lt;', '&gt;'), $strcut);
$strcut = Base::utf8Substr($string, $length, $start);
return $strcut . $dot;
} else {
$length = $length * 2;
if (strlen($string) <= $length) return $string;
$string = str_replace(array('&amp;', '&quot;', '&lt;', '&gt;'), array('&', '"', '<', '>'), $string);
$strcut = '';
for ($i = 0; $i < $length; $i++) {
$strcut .= ord($string[$i]) > 127 ? $string[$i] . $string[++$i] : $string[$i];
}
$strcut = str_replace(array('&', '"', '<', '>'), array('&amp;', '&quot;', '&lt;', '&gt;'), $strcut);
}
return $strcut . $dot;
}
@ -2967,4 +2963,19 @@ class Base
$matrix = array_unique($matrix, SORT_REGULAR);
return array_merge($matrix);
}
/**
* 去除emoji表情
* @param $str
* @return string|string[]|null
*/
public static function filterEmoji($str)
{
return preg_replace_callback(
'/./u',
function (array $match) {
return strlen($match[0]) >= 4 ? '' : $match[0];
},
$str);
}
}

144
app/Module/BillExport.php Normal file
View File

@ -0,0 +1,144 @@
<?php
namespace App\Module;
use Excel;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Writer\Exception;
class BillExport implements WithHeadings, WithEvents, FromCollection, WithTitle, WithStrictNullComparison
{
public $title;
public $headings = [];
public $data = [];
public $typeLists = [];
public $typeNumber = 0;
public function __construct($title, array $data)
{
$this->title = $title;
$this->data = $data;
}
public static function create($data = [], $title = "Sheet1") {
if (is_string($data)) {
list($title, $data) = [$data, $title];
}
if (!is_array($data)) {
$data = [];
}
return new BillExport($title, $data);
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setHeadings(array $headings) {
$this->headings = $headings;
return $this;
}
public function setData(array $data) {
$this->data = $data;
return $this;
}
public function setTypeList(array $typeList, $number = 0) {
$this->typeLists = $typeList;
$this->typeNumber = $number;
return $this;
}
public function store($fileName = '') {
if (empty($fileName)) {
$fileName = date("YmdHis") . '.xls';
}
try {
return Excel::store($this, $fileName);
} catch (Exception $e) {
return "导出错误:" . $e->getMessage();
} catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
return "导出错误:" . $e->getMessage();
}
}
public function download($fileName = '') {
if (empty($fileName)) {
$fileName = date("YmdHis") . '.xls';
}
try {
return Excel::download($this, $fileName);
} catch (Exception $e) {
return "导出错误:" . $e->getMessage();
} catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
return "导出错误:" . $e->getMessage();
}
}
/**
* 导出的文件标题
* @return string
*/
public function title(): string
{
return $this->title;
}
/**
* 标题行
* @return array
*/
public function headings(): array
{
return $this->headings;
}
/**
* 导出的内容
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return collect($this->data);
}
/**
* 设置单元格事件
* @return array
*/
public function registerEvents(): array
{
return [
AfterSheet::Class => function (AfterSheet $event) {
$count = count($this->data);
foreach ($this->typeLists AS $cell => $typeList) {
if ($cell && $typeList) {
$p = $this->headings ? 1 : 0;
for ($i = 1 + $p; $i <= max($count, $this->typeNumber) + $p; $i++) {
$validation = $event->sheet->getDelegate()->getCell($cell . $i)->getDataValidation();
$validation->setType(DataValidation::TYPE_LIST);
$validation->setErrorStyle(DataValidation::STYLE_WARNING);
$validation->setAllowBlank(false);
$validation->setShowDropDown(true);
$validation->setShowInputMessage(true);
$validation->setShowErrorMessage(true);
$validation->setErrorTitle('输入的值不合法');
$validation->setError('选择的值不在列表中,请选择列表中的值');
$validation->setPromptTitle('从列表中选择');
$validation->setPrompt('请选择下拉列表中的值');
$validation->setFormula1('"' . implode(',', $typeList) . '"');
}
}
}
}
];
}
}

16
app/Module/BillImport.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Module;
use Maatwebsite\Excel\Concerns\ToArray;
class BillImport implements ToArray
{
public function Array(Array $tables)
{
return $tables;
}
}

View File

@ -127,9 +127,11 @@ class WebSocketService implements WebSocketHandlerInterface
case 'readMsg':
$ids = is_array($data['id']) ? $data['id'] : [$data['id']];
$userid = $this->getUserid($frame->fd);
$list = WebSocketDialogMsg::whereIn('id', $ids)->get();
$list->transform(function(WebSocketDialogMsg $item) use ($userid) {
WebSocketDialogMsg::whereIn('id', $ids)->chunkById(20, function($list) use ($userid) {
/** @var WebSocketDialogMsg $item */
foreach ($list as $item) {
$item->readSuccess($userid);
}
});
return;

33
cmd
View File

@ -13,6 +13,7 @@ Error="${Red}[错误]${Font}"
cur_path="$(pwd)"
cur_arg=$@
COMPOSE="docker-compose"
judge() {
if [[ 0 -eq $? ]]; then
@ -55,20 +56,24 @@ check_docker() {
echo -e "${Error} ${RedBG} 未安装 Docker${Font}"
exit 1
fi
docker-compose --version &> /dev/null
docker-compose version &> /dev/null
if [ $? -ne 0 ]; then
docker compose version &> /dev/null
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 未安装 Docker-compose${Font}"
exit 1
fi
if [[ -n `docker-compose --version | grep "docker-compose" | grep -E "\sv*1"` ]]; then
docker-compose --version
COMPOSE="docker compose"
fi
if [[ -n `$COMPOSE version | grep -E "\sv*1"` ]]; then
$COMPOSE version
echo -e "${Error} ${RedBG} Docker-compose 版本过低请升级至v2+${Font}"
exit 1
fi
}
check_node() {
npm --version > /dev/null
npm --version &> /dev/null
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 未安装nodejs${Font}"
exit 1
@ -76,7 +81,7 @@ check_node() {
}
docker_name() {
echo `docker-compose ps | awk '{print $1}' | grep "\-$1\-"`
echo `$COMPOSE ps | awk '{print $1}' | grep "\-$1\-"`
}
run_compile() {
@ -272,7 +277,7 @@ if [ $# -gt 0 ]; then
chmod -R 775 "${cur_path}/docker/mysql/data"
# 启动容器
[[ "$(arg_get port)" -gt 0 ]] && env_set APP_PORT "$(arg_get port)"
docker-compose up php -d
$COMPOSE up php -d
# 安装composer依赖
run_exec php "composer install"
if [ ! -f "${cur_path}/vendor/autoload.php" ]; then
@ -300,7 +305,7 @@ if [ $# -gt 0 ]; then
run_exec php "php artisan migrate --seed"
# 设置初始化密码
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
docker-compose up -d
$COMPOSE up -d
supervisorctl_restart php
echo -e "${OK} ${GreenBG} 安装完成 ${Font}"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
@ -314,7 +319,7 @@ if [ $# -gt 0 ]; then
run_exec php "composer update"
run_exec php "php artisan migrate"
supervisorctl_restart php
docker-compose up -d
$COMPOSE up -d
elif [[ "$1" == "uninstall" ]]; then
shift 1
read -rp "确定要卸载(含:删除容器、数据库、日志)吗?(y/n): " uninstall
@ -328,7 +333,7 @@ if [ $# -gt 0 ]; then
exit 2
;;
esac
docker-compose down
$COMPOSE down
rm -rf "./docker/mysql/data"
rm -rf "./docker/log/supervisor"
find "./storage/logs" -name "*.log" | xargs rm -rf
@ -341,7 +346,7 @@ if [ $# -gt 0 ]; then
elif [[ "$1" == "port" ]]; then
shift 1
env_set APP_PORT "$1"
docker-compose up -d
$COMPOSE up -d
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
elif [[ "$1" == "repassword" ]]; then
@ -414,11 +419,11 @@ if [ $# -gt 0 ]; then
e="./vendor/bin/phpunit $@" && run_exec php "$e"
elif [[ "$1" == "restart" ]]; then
shift 1
docker-compose stop "$@"
docker-compose start "$@"
$COMPOSE stop "$@"
$COMPOSE start "$@"
else
docker-compose "$@"
$COMPOSE "$@"
fi
else
docker-compose ps
$COMPOSE ps
fi

View File

@ -17,7 +17,7 @@ class CreateProjectLogsTable extends Migration
$table->bigIncrements('id');
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
$table->bigInteger('column_id')->nullable()->default(0)->comment('列表ID');
$table->bigInteger('task_id')->nullable()->default(0)->comment('项目ID');
$table->bigInteger('task_id')->nullable()->default(0)->comment('任务ID');
$table->bigInteger('userid')->nullable()->default(0)->comment('会员ID');
$table->string('detail', 500)->nullable()->default('')->comment('详细信息');
$table->timestamps();

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectTaskFlowChangesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_task_flow_changes', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('task_id')->nullable()->default(0)->comment('任务ID');
$table->bigInteger('userid')->nullable()->default(0)->comment('会员ID');
$table->bigInteger('before_item_id')->nullable()->default(0)->comment('变化前工作流状态ID');
$table->string('before_item_name', 50)->nullable()->default('')->comment('(变化前)工作流状态名称');
$table->bigInteger('after_item_id')->nullable()->default(0)->comment('变化后工作流状态ID');
$table->string('after_item_name', 50)->nullable()->default('')->comment('(变化后)工作流状态名称');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_task_flow_changes');
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RenamePreProjectTaskFlowChangesItem extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_task_flow_changes', function (Blueprint $table) {
if (Schema::hasColumn('project_task_flow_changes', 'before_item_id')) {
$table->renameColumn('before_item_id', 'before_flow_item_id');
$table->renameColumn('before_item_name', 'before_flow_item_name');
$table->renameColumn('after_item_id', 'after_flow_item_id');
$table->renameColumn('after_item_name', 'after_flow_item_name');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_task_flow_changes', function (Blueprint $table) {
//
});
}
}

View File

@ -1,6 +1,6 @@
{
"name": "DooTask",
"version": "0.9.68",
"version": "0.10.5",
"description": "DooTask is task management system.",
"main": "electron.js",
"license": "MIT",

View File

@ -1,6 +1,6 @@
{
"name": "DooTask",
"version": "0.9.68",
"version": "0.10.5",
"description": "DooTask is task management system.",
"scripts": {
"start": "./cmd dev",
@ -53,7 +53,7 @@
"less-loader": "^10.2.0",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"node-sass": "^4.11.0",
"node-sass": "^6.0.1",
"notification-koro1": "^1.1.1",
"postcss": "^8.4.5",
"resolve-url-loader": "^4.0.0",

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/build/304.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/build/422.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -21,6 +21,7 @@ export default {
},
mounted() {
tocObj.reset()
this.init();
this.createEditor();
},

View File

@ -1,8 +1,20 @@
<template>
<div class="quick-edit" :class="[alwaysIcon ? 'quick-always' : '']">
<div v-if="isEdit" v-clickoutside="onClickOut" class="quick-input">
<TagInput v-if="isTag" ref="input" v-model="content" :disabled="isLoad" @on-enter="onEnter" @on-blur="onBlur"/>
<Input v-else ref="input" v-model="content" :disabled="isLoad" @on-enter="onEnter" @on-blur="onBlur"/>
<TagInput
v-if="isTag"
ref="input"
v-model="content"
:disabled="isLoad"
@on-keyup="onKeyup"
@on-blur="onBlur"/>
<Input
v-else
ref="input"
v-model="content"
:disabled="isLoad"
@on-keyup="onKeyup"
@on-blur="onBlur"/>
<div v-if="isLoad" class="quick-loading"><Loading/></div>
</div>
<template v-else>
@ -54,9 +66,6 @@ export default {
},
watch: {
isEdit(val) {
this.$emit("on-edit-change", val);
},
autoEdit(val) {
if (val === true) {
setTimeout(this.onEdit, 0)
@ -65,9 +74,14 @@ export default {
},
methods: {
onEditChange(val) {
this.isEdit = val;
this.$emit("on-edit-change", val);
},
onEdit() {
this.content = this.value;
this.isEdit = true;
this.onEditChange(true);
this.$nextTick(() => {
this.$refs.input.focus({
cursor: 'all'
@ -75,9 +89,18 @@ export default {
})
},
onKeyup(e) {
if (e.keyCode === 13) {
this.onEnter();
} else if (e.keyCode === 27) {
this.isEdit = false;
this.isLoad = false;
}
},
onEnter() {
if (this.content == this.value) {
this.isEdit = false;
this.onEditChange(false);
return;
}
if (this.isLoad) {
@ -86,7 +109,7 @@ export default {
this.isLoad = true;
this.$emit("input", this.content);
this.$emit("on-update", this.content, () => {
this.isEdit = false;
this.onEditChange(false);
this.isLoad = false;
})
},
@ -99,7 +122,7 @@ export default {
},
onBlur() {
if (this.clickOutSide) {
if (this.clickOutSide || !this.isEdit) {
return;
}
this.onEnter();

View File

@ -5,7 +5,7 @@
{{ $L('使用 SSO 登录') }}
</div>
<template v-if="showDown">
<div v-if="$Electron" class="common-right-bottom-link" @click="releasesNotification">
<div v-if="$Electron" class="common-right-bottom-link" @click="updateWinShow=true">
<Icon type="md-download"/>
{{ $L(repoTitle) }}
</div>
@ -14,20 +14,32 @@
{{ $L(repoTitle) }}
</a>
</template>
<Modal
v-model="updateWinShow"
:ok-text="$L('立即升级')"
:closable="false"
:mask-closable="false"
@on-ok="installApplication"
@on-cancel="repoStatus=2"
class-name="common-right-bottom-notification">
<div slot="header" class="notification-head">
<div class="notification-title">{{$L('发现新版本')}}</div>
<Tag color="volcano">{{repoReleases.tag_name}}</Tag>
</div>
<MarkdownPreview class="notification-body overlay-y" :initialValue="repoReleases.body"/>
</Modal>
</div>
</template>
<script>
import Vue from 'vue'
import MarkdownPreview from "./MDEditor/components/preview";
import axios from "axios";
Vue.component('MarkdownPreview', MarkdownPreview)
import {mapState} from "vuex";
import {Store} from "le5le-store";
export default {
name: 'RightBottom',
components: {MarkdownPreview},
data() {
return {
loadIng: 0,
@ -37,6 +49,7 @@ export default {
repoStatus: 0, // 0 12
repoReleases: {},
updateWinShow: false,
downloadResult: {},
subscribe: null,
@ -46,14 +59,15 @@ export default {
this.getReleases();
//
this.subscribe = Store.subscribe('releasesNotification', () => {
this.releasesNotification();
this.updateWinShow = true;
});
//
if (this.$Electron) {
this.$Electron.registerMsgListener('downloadDone', ({result}) => {
if (result.name == this.repoData.name && this.repoStatus !== 2) {
this.$store.state.clientNewVersion = this.repoReleases.tag_name
this.downloadResult = result;
this.releasesNotification()
this.updateWinShow = true;
}
})
}
@ -213,44 +227,6 @@ export default {
}
},
releasesNotification() {
const {tag_name, body} = this.repoReleases;
this.$store.state.clientNewVersion = tag_name
$A.modalConfirm({
okText: this.$L('立即更新'),
onOk: () => {
this.installApplication();
},
onCancel: () => {
this.repoStatus = 2;
},
render: (h) => {
return h('div', {
class: 'common-right-bottom-notification'
}, [
h('div', {
class: "notification-head"
}, [
h('div', {
class: "notification-title"
}, this.$L('发现新版本')),
h('Tag', {
props: {
color: 'volcano'
}
}, tag_name)
]),
h('MarkdownPreview', {
class: 'notification-body',
props: {
initialValue: body
}
}),
])
}
});
},
installApplication() {
if (!this.$Electron) {
return;

View File

@ -2,7 +2,7 @@
<div class="teditor-wrapper">
<div class="teditor-box" :class="[!inline && spinShow ? 'teditor-loadstyle' : 'teditor-loadedstyle']">
<template v-if="inline">
<div ref="myTextarea" :id="id" v-html="content"></div>
<div ref="myTextarea" :id="id" v-html="spinShow ? '' : content"></div>
<Icon v-if="spinShow" type="ios-loading" :size="18" class="icon-loading icon-inline"></Icon>
</template>
<template v-else>
@ -181,11 +181,7 @@
newValue = "";
}
if (!this.isTyping) {
if (this.getEditor() !== null) {
this.getEditor().setContent(newValue);
} else{
this.content = newValue;
}
this.setContent(newValue);
}
},
readOnly(value) {
@ -459,6 +455,14 @@
return this.getEditor().getContent();
},
setContent(content) {
if (this.getEditor() === null) {
this.content = content;
} else if (content != this.getEditor().getContent()){
this.getEditor().setContent(content);
}
},
insertImage(src) {
this.insertContent('<img src="' + src + '">');
},

View File

@ -11,7 +11,7 @@
:placeholder="tis || placeholderText"
@keydown.enter="downEnter($event)"
@keydown.delete="delTag(false)"
@keyup="addTag($event, content)"
@keyup="onKeyup"
@focus="onFocus"
@blur="onBlur"
:disabled="disabled"
@ -158,19 +158,22 @@
this.addTag(false, this.content)
this.$emit("on-blur", e)
},
addTag(e, content) {
if (e === false || e.keyCode === 13) {
if (content.trim() != '' && this.disSource.indexOf(content.trim()) === -1) {
this.disSource.push(content.trim());
}
this.content = '';
onKeyup(e) {
this.addTag(e, this.content);
//
this.$emit("on-keyup", e)
if (e.keyCode === 13) {
this.$nextTick(() => {
this.$emit("on-enter", e)
})
}
},
addTag(e, content) {
if (e === false || e.keyCode === 13) {
if (content.trim() != '' && this.disSource.indexOf(content.trim()) === -1) {
this.disSource.push(content.trim());
}
this.content = '';
return;
}
if (this.max > 0 && this.disSource.length >= this.max) {

View File

@ -102,7 +102,7 @@
* @returns {*|string}
*/
formatTime(date) {
let time = Math.round($A.Date(date).getTime() / 1000),
let time = $A.Date(date, true),
string = '';
if ($A.formatDate('Ymd') === $A.formatDate('Ymd', time)) {
string = $A.formatDate('H:i', time)
@ -157,8 +157,10 @@
let time = Math.round(this.Date(date).getTime() / 1000) - nowTime;
if (time < 86400 * 7 && time > 0 ) {
return this.formatSeconds(time);
} else if (time <= 0) {
} else if (time < 0) {
return '-' + this.formatSeconds(time * -1);
} else if (time == 0) {
return 0 + 's';
}
return this.formatTime(date)
},

View File

@ -37,7 +37,7 @@
<i class="taskfont">&#xe689;</i>
</div>
<Dropdown-menu slot="list" class="login-setting-menu">
<Dropdown placement="right-start" @on-click="setTheme">
<Dropdown placement="right" @on-click="setTheme">
<DropdownItem>
<div class="login-setting-item">
{{$L('主题皮肤')}}
@ -48,7 +48,7 @@
<Dropdown-item v-for="(item, key) in themeList" :key="key" :name="item.value" :selected="themeMode === item.value">{{$L(item.name)}}</Dropdown-item>
</DropdownMenu>
</Dropdown>
<Dropdown placement="right-start" @on-click="setLanguage">
<Dropdown placement="right" @on-click="setLanguage">
<DropdownItem divided>
<div class="login-setting-item">
{{currentLanguage}}
@ -114,6 +114,9 @@ export default {
this.subscribe = null;
}
},
activated() {
this.loginType = 'login'
},
deactivated() {
this.loginJump = false;
this.password = "";

View File

@ -19,46 +19,93 @@
</div>
</div>
<DropdownMenu slot="list">
<DropdownItem
v-for="(item, key) in menu"
v-if="item.visible !== false"
:key="key"
:divided="!!item.divided"
:name="item.path">
{{$L(item.name)}}
<Badge v-if="item.path === 'version'" class="manage-menu-report-badge" :text="clientNewVersion"/>
<Badge v-if="item.path === 'workReport'" class="manage-menu-report-badge" :count="reportUnreadNumber"/>
</DropdownItem>
<Dropdown placement="right-start" @on-click="setTheme">
<template v-for="item in menu">
<!-- 团队管理 -->
<Dropdown
v-if="item.path === 'team'"
placement="right-start">
<DropdownItem divided>
<div class="manage-menu-language">
{{$L('主题皮肤')}}
<div class="manage-menu-flex">
{{$L(item.name)}}
<Badge v-if="reportUnreadNumber > 0" class="manage-menu-report-badge" :count="reportUnreadNumber"/>
<Icon v-else type="ios-arrow-forward"></Icon>
</div>
</DropdownItem>
<DropdownMenu slot="list">
<DropdownItem name="allUser">{{$L('团队管理')}}</DropdownItem>
<DropdownItem name="workReport">
<div class="manage-menu-flex">
{{$L('工作报告')}}
<Badge v-if="reportUnreadNumber > 0" class="manage-menu-report-badge" :count="reportUnreadNumber"/>
</div>
</DropdownItem>
<DropdownItem name="exportTask">{{$L('导出任务统计')}}</DropdownItem>
</DropdownMenu>
</Dropdown>
<!-- 主题皮肤 -->
<Dropdown
v-else-if="item.path === 'theme'"
placement="right-start"
@on-click="setTheme">
<DropdownItem divided>
<div class="manage-menu-flex">
{{$L(item.name)}}
<Icon type="ios-arrow-forward"></Icon>
</div>
</DropdownItem>
<DropdownMenu slot="list">
<Dropdown-item v-for="(item, key) in themeList" :key="key" :name="item.value" :selected="themeMode === item.value">{{$L(item.name)}}</Dropdown-item>
<DropdownItem
v-for="(item, key) in themeList"
:key="key"
:name="item.value"
:selected="themeMode === item.value">{{$L(item.name)}}</DropdownItem>
</DropdownMenu>
</Dropdown>
<Dropdown placement="right-start" @on-click="setLanguage">
<!-- 语言设置 -->
<Dropdown
v-else-if="item.path === 'language'"
placement="right-start"
@on-click="setLanguage">
<DropdownItem divided>
<div class="manage-menu-language">
<div class="manage-menu-flex">
{{currentLanguage}}
<Icon type="ios-arrow-forward"></Icon>
</div>
</DropdownItem>
<DropdownMenu slot="list">
<Dropdown-item v-for="(item, key) in languageList" :key="key" :name="key" :selected="getLanguage() === key">{{item}}</Dropdown-item>
<DropdownItem
v-for="(item, key) in languageList"
:key="key"
:name="key"
:selected="getLanguage() === key">{{item}}</DropdownItem>
</DropdownMenu>
</Dropdown>
<DropdownItem divided name="signout" style="color:#f40">{{$L('退出登录')}}</DropdownItem>
<!-- 其他菜单 -->
<DropdownItem
v-else-if="item.visible !== false"
:divided="!!item.divided"
:name="item.path"
:style="item.style || {}">
{{$L(item.name)}}
<Badge
v-if="item.path === 'version'"
class="manage-menu-report-badge"
:text="clientNewVersion"/>
<Badge
v-else-if="item.path === 'workReport' && reportUnreadNumber > 0"
class="manage-menu-report-badge"
:count="reportUnreadNumber"/>
</DropdownItem>
</template>
</DropdownMenu>
</Dropdown>
<ul :class="overlayClass" @scroll="handleClickTopOperateOutside">
<li @click="toggleRoute('dashboard')" :class="classNameRoute('dashboard')">
<i class="taskfont">&#xe6fb;</i>
<div class="menu-title">{{$L('仪表盘')}}</div>
<Badge class="menu-badge" :type="dashboardTask.overdue.length > 0 ? 'error' : 'primary'" :count="dashboardTotal"></Badge>
<Badge v-if="dashboardTask.overdue.length > 0" class="menu-badge" type="error" :count="dashboardTask.overdue.length"/>
<Badge v-else-if="dashboardTask.today.length > 0" class="menu-badge" type="info" :count="dashboardTask.today.length"/>
<Badge v-else-if="dashboardTask.all.length > 0" class="menu-badge" type="primary" :count="dashboardTask.all.length"/>
</li>
<li @click="toggleRoute('calendar')" :class="classNameRoute('calendar')">
<i class="taskfont">&#xe6f5;</i>
@ -67,7 +114,7 @@
<li @click="toggleRoute('messenger')" :class="classNameRoute('messenger')">
<i class="taskfont">&#xe6eb;</i>
<div class="menu-title">{{$L('消息')}}</div>
<Badge class="menu-badge" :count="msgAllUnread"></Badge>
<Badge class="menu-badge" :count="msgAllUnread"/>
</li>
<li @click="toggleRoute('file')" :class="classNameRoute('file')">
<i class="taskfont">&#xe6f3;</i>
@ -184,18 +231,42 @@
<TaskAdd ref="addTask" v-model="addTaskShow"/>
</Modal>
<!--导出任务统计-->
<Modal
v-model="exportTaskShow"
:title="$L('导出任务统计')"
:mask-closable="false">
<Form ref="exportTask" :model="exportData" label-width="auto" @submit.native.prevent>
<FormItem :label="$L('导出会员')">
<UserInput v-model="exportData.userid" :multiple-max="20" :placeholder="$L('请选择会员')"/>
</FormItem>
<FormItem :label="$L('时间范围')">
<DatePicker
v-model="exportData.time"
type="daterange"
format="yyyy/MM/dd"
style="width:100%"
:placeholder="$L('请选择时间')"/>
</FormItem>
</Form>
<div slot="footer" class="adaption">
<Button type="default" @click="exportTaskShow=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="exportLoadIng > 0" @click="onExportTask">{{$L('导出')}}</Button>
</div>
</Modal>
<!--任务详情-->
<Modal
:value="taskId > 0"
:mask-closable="false"
:styles="{
width: '90%',
maxWidth: taskData.dialog_id ? '1200px' : '700px'
}"
@on-visible-change="taskVisibleChange"
footer-hide>
:mask-closable="false"
:footer-hide="true"
@on-visible-change="taskVisibleChange">
<div class="page-manage-task-modal" :style="taskStyle">
<TaskDetail :task-id="taskId" :open-task="taskData"/>
<TaskDetail ref="taskDetail" :task-id="taskId" :open-task="taskData"/>
</div>
</Modal>
@ -248,20 +319,27 @@
import { mapState, mapGetters } from 'vuex'
import TaskDetail from "./manage/components/TaskDetail";
import ProjectArchived from "./manage/components/ProjectArchived";
import notificationKoro from "notification-koro1";
import TeamManagement from "./manage/components/TeamManagement";
import ProjectManagement from "./manage/components/ProjectManagement";
import DrawerOverlay from "../components/DrawerOverlay";
import DragBallComponent from "../components/DragBallComponent";
import TaskAdd from "./manage/components/TaskAdd";
import Report from "./manage/components/Report";
import notificationKoro from "notification-koro1";
import {Store} from "le5le-store";
import UserInput from "../components/UserInput";
export default {
components: {
UserInput,
TaskAdd,
TaskDetail,
Report,
DragBallComponent, DrawerOverlay, ProjectManagement, TeamManagement, ProjectArchived, TaskDetail},
DragBallComponent,
DrawerOverlay,
ProjectManagement,
TeamManagement,
ProjectArchived},
data() {
return {
loadIng: 0,
@ -280,6 +358,13 @@ export default {
addTaskShow: false,
addTaskSubscribe: null,
exportTaskShow: false,
exportLoadIng: 0,
exportData: {
userid: [],
time: [],
},
dialogMsgSubscribe: null,
projectKeyValue: '',
@ -380,12 +465,8 @@ export default {
return num;
},
dashboardTotal() {
return this.dashboardTask.today.length + this.dashboardTask.overdue.length
},
unreadTotal() {
return this.msgAllUnread + this.dashboardTotal + this.reportUnreadNumber;
return this.msgAllUnread + this.dashboardTask.overdue.length + this.reportUnreadNumber;
},
currentLanguage() {
@ -399,21 +480,37 @@ export default {
{path: 'personal', name: '个人设置'},
{path: 'password', name: '密码设置'},
{path: 'clearCache', name: '清除缓存'},
{path: 'system', name: '系统设置', divided: true},
{path: 'version', name: '更新版本', visible: !!this.clientNewVersion},
{path: 'workReport', name: '工作报告', divided: true},
{path: 'allUser', name: '团队管理'},
{path: 'allProject', name: '所有项目'},
{path: 'archivedProject', name: '已归档的项目'}
{path: 'allProject', name: '所有项目', divided: true},
{path: 'archivedProject', name: '已归档的项目'},
{path: 'team', name: '团队管理', divided: true},
{path: 'theme', name: '主题皮肤', divided: true},
{path: 'language', name: this.currentLanguage, divided: true},
{path: 'logout', name: '退出登录', style: {color: '#f40'}, divided: true},
]
} else {
return [
{path: 'personal', name: '个人设置'},
{path: 'password', name: '密码设置'},
{path: 'clearCache', name: '清除缓存'},
{path: 'version', name: '更新版本', divided: true, visible: !!this.clientNewVersion},
{path: 'workReport', name: '工作报告', divided: true},
{path: 'archivedProject', name: '已归档的项目'}
{path: 'archivedProject', name: '已归档的项目'},
{path: 'theme', name: '主题皮肤', divided: true},
{path: 'language', name: this.currentLanguage, divided: true},
{path: 'logout', name: '退出登录', style: {color: '#f40'}, divided: true},
]
}
},
@ -429,7 +526,7 @@ export default {
projectLists() {
const {projectKeyValue, cacheProjects} = this;
const data = cacheProjects.sort((a, b) => {
const data = $A.cloneJSON(cacheProjects).sort((a, b) => {
if (a.top_at || b.top_at) {
return $A.Date(b.top_at) - $A.Date(a.top_at);
}
@ -575,6 +672,9 @@ export default {
case 'archivedProject':
this.archivedProjectShow = true;
return;
case 'exportTask':
this.exportTaskShow = true;
return;
case 'workReport':
if (this.reportUnreadNumber > 0) {
this.reportTabs = "receive";
@ -592,7 +692,7 @@ export default {
window.location.reload()
});
return;
case 'signout':
case 'logout':
$A.modalConfirm({
title: '退出登录',
content: '你确定要登出系统?',
@ -685,10 +785,13 @@ export default {
},
shortcutEvent(e) {
if (e.keyCode === 75 || e.keyCode === 78) {
if (e.metaKey || e.ctrlKey) {
if (e.keyCode === 75 || e.keyCode === 78) {
e.preventDefault();
this.onAddTask(0)
} else if (e.keyCode === 83 && this.taskId > 0) {
e.preventDefault();
this.$refs.taskDetail.checkUpdate(true)
}
}
},
@ -756,11 +859,60 @@ export default {
this.$store.dispatch("call", {
url: 'report/unread',
}).then(({data}) => {
this.reportUnreadNumber = data.total ? data.total : 0;
this.reportUnreadNumber = data.total || 0;
}).catch(() => {});
}, typeof timeout === "number" ? timeout : 1000)
},
handleRightClick(event, item) {
this.handleClickTopOperateOutside();
this.topOperateItem = item;
this.$nextTick(() => {
const projectWrap = this.$refs.projectWrapper;
const projectBounding = projectWrap.getBoundingClientRect();
this.topOperateStyles = {
left: `${event.clientX - projectBounding.left}px`,
top: `${event.clientY - projectBounding.top}px`
};
this.topOperateVisible = true;
})
},
handleClickTopOperateOutside() {
this.topOperateVisible = false;
},
handleTopClick() {
this.$store.dispatch("call", {
url: 'project/top',
data: {
project_id: this.topOperateItem.id,
},
}).then(() => {
this.$store.dispatch("getProjects").catch(() => {});
}).catch(({msg}) => {
$A.modalError(msg, 301);
});
},
onExportTask() {
if (this.exportLoadIng > 0) {
return;
}
this.exportLoadIng++;
this.$store.dispatch("call", {
url: 'project/task/export',
data: this.exportData,
}).then(({data}) => {
this.exportLoadIng--;
this.exportTaskShow = false;
$A.downFile(data.url);
}).catch(({msg}) => {
this.exportLoadIng--;
$A.modalError(msg);
});
},
notificationInit() {
this.notificationClass = new notificationKoro(this.$L("打开通知成功"));
if (this.notificationClass.support) {
@ -821,37 +973,6 @@ export default {
}
document.addEventListener(visibilityChangeEvent, visibilityChangeListener);
},
handleRightClick(event, item) {
this.handleClickTopOperateOutside();
this.topOperateItem = item;
this.$nextTick(() => {
const projectWrap = this.$refs.projectWrapper;
const projectBounding = projectWrap.getBoundingClientRect();
this.topOperateStyles = {
left: `${event.clientX - projectBounding.left}px`,
top: `${event.clientY - projectBounding.top}px`
};
this.topOperateVisible = true;
})
},
handleClickTopOperateOutside() {
this.topOperateVisible = false;
},
handleTopClick() {
this.$store.dispatch("call", {
url: 'project/top',
data: {
project_id: this.topOperateItem.id,
},
}).then(() => {
this.$store.dispatch("getProjects").catch(() => {});
}).catch(({msg}) => {
$A.modalError(msg, 301);
});
},
}
}
</script>

View File

@ -46,6 +46,7 @@
<div class="time" :title="msgData.created_at">{{$A.formatTime(msgData.created_at)}}</div>
<Poptip
v-if="msgData.send > 1 || dialogType == 'group'"
ref="percent"
class="percent"
placement="left-end"
transfer
@ -134,13 +135,13 @@ export default {
}
this.msgData._r = true;
//
this.$nextTick(() => {
setTimeout(() => {
if (!this.$el.offsetParent) {
this.msgData._r = false;
return
}
this.$store.dispatch("dialogMsgRead", this.msgData);
})
}, 50)
},
popperShow() {
@ -151,6 +152,7 @@ export default {
},
}).then(({data}) => {
this.read_list = data;
this.$refs.percent.updatePopper();
}).catch(() => {
this.read_list = [];
});

View File

@ -90,6 +90,7 @@
<div class="drag-text">{{$L('拖动到这里发送')}}</div>
</div>
<!--拖动发送提示-->
<Modal
v-model="pasteShow"
:title="$L(pasteTitle)"

View File

@ -623,10 +623,8 @@ export default {
column.tasks = this.transforTasks(allTask.filter(task => {
return task.column_id == column.id;
})).sort((a, b) => {
let at1 = $A.Date(a.complete_at),
at2 = $A.Date(b.complete_at);
if (at1 || at2) {
return at1 - at2;
if (a.complete_at || b.complete_at) {
return $A.Date(a.complete_at) - $A.Date(b.complete_at);
}
if (a.sort != b.sort) {
return a.sort - b.sort;

View File

@ -238,7 +238,7 @@ export default {
const {times} = this.addData;
let temp = $A.date2string(times, "Y-m-d H:i");
if (temp[0] && temp[1]) {
let d = Math.ceil(($A.Date(temp[1]).getTime() - $A.Date(temp[0]).getTime()) / 86400000);
let d = Math.ceil(($A.Date(temp[1], true) - $A.Date(temp[0], true)) / 86400);
if (d > 0) {
return d;
}

View File

@ -3,6 +3,7 @@
<li v-if="ready && taskDetail.parent_id > 0">
<div class="subtask-icon">
<TaskMenu
v-if="taskId > 0"
:ref="`taskMenu_${taskDetail.id}`"
:task="taskDetail"
:load-status="taskDetail.loading === true"
@ -72,6 +73,7 @@
<div v-show="taskDetail.id > 0" class="task-info">
<div class="head">
<TaskMenu
v-if="taskId > 0"
:ref="`taskMenu_${taskDetail.id}`"
:task="taskDetail"
class="icon"
@ -121,6 +123,7 @@
</ETooltip>
<div class="menu">
<TaskMenu
v-if="taskId > 0"
:task="taskDetail"
icon="ios-more"
completed-icon="ios-more"
@ -150,7 +153,6 @@
:option-full="taskOptionFull"
:placeholder="$L('详细描述...')"
@on-blur="updateData('content')"
@editorSave="updateData('content')"
inline/>
</div>
<Form class="items" label-position="left" label-width="auto" @submit.native.prevent>
@ -261,7 +263,7 @@
<div @click="openTime" class="time">{{taskDetail.end_at ? cutTime : '--'}}</div>
<template v-if="!taskDetail.complete_at && taskDetail.end_at">
<Tag v-if="within24Hours(taskDetail.end_at)" color="blue"><i class="taskfont">&#xe71d;</i>{{expiresFormat(taskDetail.end_at)}}</Tag>
<Tag v-if="taskDetail.overdue" color="red">{{$L('超期未完成')}}</Tag>
<Tag v-if="isOverdue(taskDetail)" color="red">{{$L('超期未完成')}}</Tag>
</template>
</div>
</DatePicker>
@ -307,7 +309,13 @@
<i class="taskfont">&#xe6f0;</i>{{$L('子任务')}}
</div>
<ul class="item-content subtask">
<TaskDetail v-for="(task, key) in subList" :key="key" :task-id="task.id" :open-task="task" :main-end-at="taskDetail.end_at"/>
<TaskDetail
v-for="(task, key) in subList"
:ref="`subTask_${task.id}`"
:key="key"
:task-id="task.id"
:open-task="task"
:main-end-at="taskDetail.end_at"/>
</ul>
<ul :class="['item-content', subList.length === 0 ? 'nosub' : '']">
<li>
@ -399,7 +407,10 @@
@on-input-paste="msgPasteDrag"/>
<div class="no-send" @click="msgDialog">
<Loading v-if="sendLoad > 0"/>
<Icon v-else type="md-send" />
<template v-else>
<Badge :count="taskDetail.msg_num"/>
<Icon type="md-send" />
</template>
</div>
</div>
<div v-if="dialogDrag" class="drag-over" @click="dialogDrag=false">
@ -613,8 +624,8 @@ export default {
cutTime() {
const {taskDetail} = this;
let start_at = Math.round($A.Date(taskDetail.start_at).getTime() / 1000);
let end_at = Math.round($A.Date(taskDetail.end_at).getTime() / 1000);
let start_at = $A.Date(taskDetail.start_at, true);
let end_at = $A.Date(taskDetail.end_at, true);
let string = "";
if ($A.formatDate('Y/m/d', start_at) == $A.formatDate('Y/m/d', end_at)) {
string = $A.formatDate('Y/m/d H:i', start_at) + " ~ " + $A.formatDate('H:i', end_at)
@ -722,29 +733,27 @@ export default {
},
methods: {
initLanguage() {
},
innerHeightListener() {
this.innerHeight = Math.min(1100, window.innerHeight);
},
within24Hours(date) {
return Math.round($A.Date(date).getTime() / 1000) - this.nowTime < 86400
return $A.Date(date, true) - this.nowTime < 86400
},
expiresFormat(date) {
return $A.countDownFormat(date, this.nowTime)
},
onNameKeydown(e) {
if (e.keyCode === 83) {
if (e.metaKey || e.ctrlKey) {
e.preventDefault();
this.updateData('name');
isOverdue(taskDetail) {
if (taskDetail.overdue) {
return true;
}
} else if (e.keyCode === 13) {
return $A.Date(taskDetail.end_at, true) < this.nowTime;
},
onNameKeydown(e) {
if (e.keyCode === 13) {
if (!e.shiftKey) {
e.preventDefault();
this.updateData('name');
@ -752,6 +761,40 @@ export default {
}
},
checkUpdate(update) {
let isModify = false;
if (this.openTask.name != this.taskDetail.name) {
isModify = true;
if (update) {
this.updateData('name');
} else if (isModify) {
return true
}
}
if (this.$refs.desc && this.$refs.desc.getContent() != this.taskContent) {
isModify = true;
if (update) {
this.updateData('content');
} else if (isModify) {
return true
}
}
if (this.addsubShow && this.addsubName) {
isModify = true;
if (update) {
this.onAddsub();
} else if (isModify) {
return true
}
}
this.subList.some(({id}) => {
if (this.$refs[`subTask_${id}`][0].checkUpdate(update)) {
isModify = true;
}
})
return isModify;
},
updateData(action, params) {
switch (action) {
case 'priority':

View File

@ -15,7 +15,7 @@
</template>
</div>
</slot>
<EDropdownMenu slot="dropdown" class="task-menu-more-dropdown">
<EDropdownMenu ref="dropdownMenu" slot="dropdown" class="task-menu-more-dropdown">
<li class="task-menu-more-warp" :class="size">
<ul>
<EDropdownItem v-if="!flow" class="load-flow" disabled>
@ -218,7 +218,9 @@ export default {
visibleChange(visible) {
if (visible) {
this.$store.dispatch("getTaskFlow", this.task.id).catch(() => {})
this.$store.dispatch("getTaskFlow", this.task.id)
.then(this.$refs.dropdownMenu.updatePopper)
.catch(this.$refs.dropdownMenu.updatePopper)
}
},

View File

@ -266,7 +266,7 @@ export default {
},
completeAtFormat(date) {
let time = Math.round($A.Date(date).getTime() / 1000);
let time = $A.Date(date, true);
if ($A.formatDate('Y') === $A.formatDate('Y', time)) {
return $A.formatDate('m-d H:i', time)
} else {

View File

@ -57,6 +57,11 @@
</div>
</div>
<div
class="file-drag"
@drop.prevent="filePasteDrag($event, 'drag')"
@dragover.prevent="fileDragOver(true, $event)"
@dragleave.prevent="fileDragOver(false, $event)">
<div v-if="tableMode" class="file-table" @contextmenu.prevent="handleRightClick">
<Table
:columns="columns"
@ -116,7 +121,7 @@
size="small"
:disabled="!!item._load"
@on-blur="onBlur(item)"
@on-enter="onEnter(item)"/>
@on-keyup="onKeyup($event, item)"/>
<div v-if="item._load" class="file-load"><Loading/></div>
</div>
<div v-else class="file-name" :title="item.name">{{formatName(item)}}</div>
@ -124,6 +129,10 @@
</ul>
</div>
</template>
<div v-if="dialogDrag" class="drag-over" @click="dialogDrag=false">
<div class="drag-text">{{$L('拖动到这里发送')}}</div>
</div>
</div>
<div class="file-menu" :style="contextMenuStyles">
<Dropdown
@ -323,6 +332,21 @@
<FileContent v-else v-model="fileShow" :file="fileInfo"/>
</DrawerOverlay>
<!--拖动上传提示-->
<Modal
v-model="pasteShow"
:title="$L(pasteTitle)"
:cancel-text="$L('取消')"
:ok-text="$L('发送')"
:enter-ok="true"
@on-ok="pasteSend">
<div class="dialog-wrapper-paste">
<template v-for="item in pasteItem">
<img v-if="item.type == 'image'" :src="item.result"/>
<div v-else>{{$L('文件')}}: {{item.name}} ({{$A.bytesToSize(item.size)}})</div>
</template>
</div>
</Modal>
</div>
</template>
@ -457,6 +481,11 @@ export default {
shearIds: [],
selectIds: [],
dialogDrag: false,
pasteShow: false,
pasteFile: [],
pasteItem: [],
}
},
@ -537,6 +566,18 @@ export default {
const {navigator} = this;
return !!navigator.find(({share}) => share);
},
pasteTitle() {
const {pasteItem} = this;
let hasImage = pasteItem.find(({type}) => type == 'image')
let hasFile = pasteItem.find(({type}) => type != 'image')
if (hasImage && hasFile) {
return '上传文件/图片'
} else if (hasImage) {
return '上传图片'
}
return '上传文件'
}
},
watch: {
@ -918,7 +959,6 @@ export default {
break;
case 'rename':
this.$set(item, 'newname', item.name);
this.setEdit(item.id, true)
this.autoBlur(item.id)
break;
@ -1113,9 +1153,21 @@ export default {
},
onBlur(item) {
if (this.files.find(({id, _edit}) => id == item.id && !_edit)) {
return;
}
this.onEnter(item);
},
onKeyup(e, item) {
if (e.keyCode === 13) {
this.onEnter(item);
} else if (e.keyCode === 27) {
this.setLoad(item.id, false)
this.setEdit(item.id, false)
}
},
onEnter(item) {
let isCreate = !/^\d+$/.test(item.id);
if (!item.newname) {
@ -1163,6 +1215,9 @@ export default {
let item = this.$store.state.files.find(({id}) => id == fileId)
if (item) {
this.$set(item, '_edit', is);
if (is) {
this.$set(item, 'newname', item.name);
}
}
},
@ -1281,6 +1336,61 @@ export default {
this.selectIds = [];
},
/********************拖动上传部分************************/
pasteDragNext(e, type) {
let files = type === 'drag' ? e.dataTransfer.files : e.clipboardData.files;
files = Array.prototype.slice.call(files);
if (files.length > 0) {
e.preventDefault();
if (files.length > 0) {
this.pasteFile = [];
this.pasteItem = [];
files.some(file => {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = ({target}) => {
this.pasteFile.push(file)
this.pasteItem.push({
type: $A.getMiddle(file.type, null, '/'),
name: file.name,
size: file.size,
result: target.result
})
this.pasteShow = true
}
});
}
}
},
filePasteDrag(e, type) {
this.dialogDrag = false;
this.pasteDragNext(e, type);
},
fileDragOver(show, e) {
let random = (this.__dialogDrag = $A.randomString(8));
if (!show) {
setTimeout(() => {
if (random === this.__dialogDrag) {
this.dialogDrag = show;
}
}, 150);
} else {
if (e.dataTransfer.effectAllowed === 'move') {
return;
}
this.dialogDrag = true;
}
},
pasteSend() {
this.pasteFile.some(file => {
this.$refs.fileUpload.upload(file)
});
},
/********************文件上传部分************************/
uploadUpdate(fileList) {

View File

@ -120,7 +120,7 @@ export default {
},
previewUrl() {
return $A.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.msgDetail.url))
return $A.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.msgDetail.content.url))
}
},
methods: {

View File

@ -120,7 +120,7 @@ export default {
},
previewUrl() {
return $A.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.fileDetail.url))
return $A.apiUrl("../fileview/onlinePreview?url=" + encodeURIComponent(this.fileDetail.content.url))
}
},
methods: {

View File

@ -74,8 +74,12 @@ export default {
}
}
};
params.error = () => {
params.error = (xhr, status) => {
if (window.navigator.onLine === false || (status === 0 && xhr.readyState === 4)) {
reject({data: {}, msg: $A.L('网络异常,请稍后再试!')})
} else {
reject({data: {}, msg: "System error"})
}
};
//
if (params.websocket === true || params.ws === true) {
@ -1628,6 +1632,7 @@ export default {
task_id: task_id
},
}).then(result => {
let task = state.cacheTasks.find(({id}) => id == task_id)
let {data} = result
data.turns.some(item => {
let index = state.taskFlowItems.findIndex(({id}) => id == item.id);
@ -1636,6 +1641,16 @@ export default {
} else {
state.taskFlowItems.push(item);
}
if (task
&& task.flow_item_id == item.id
&& task.flow_item_name != item.name) {
state.cacheTasks.filter(({flow_item_id})=> flow_item_id == item.id).some(task => {
dispatch("saveTask", {
id: task.id,
flow_item_name: `${item.status}|${item.name}`,
})
})
}
})
//
delete data.turns;
@ -2035,7 +2050,7 @@ export default {
}
});
state.wsReadWaitList = [];
}, 20);
}, 50);
},
/**
@ -2163,6 +2178,12 @@ export default {
// 更新最后消息
dispatch("updateDialogLastMsg", data);
break;
case 'readed':
// 已读回执
if (state.dialogMsgs.find(({id}) => id == data.id)) {
dispatch("saveDialogMsg", data)
}
break;
}
})(msgDetail);
break;

View File

@ -44,7 +44,7 @@
max-height: 210px;
overflow-x: hidden;
overflow-y: auto;
margin: 18px 0;
margin-bottom: 16px;
.markdown-preview {
margin: -20px -12px;
h2 {

View File

@ -476,7 +476,7 @@
.dialog-send {
position: absolute;
top: 0;
right: 28px;
right: 14px;
bottom: 0;
font-size: 18px;
width: 46px;
@ -604,10 +604,6 @@
.dialog-footer {
padding: 0 20px;
margin-bottom: 16px;
.dialog-send {
right: 20px;
}
}
}
}

View File

@ -478,9 +478,9 @@
padding: 0;
margin: 10px 0 0 0;
line-height: 20px;
word-break: break-all;
white-space: pre-wrap;
word-wrap: break-word;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.task-tags {

View File

@ -77,6 +77,13 @@
img {
max-width: 100%;
}
pre {
padding: 14px;
margin: 7px 0;
overflow: auto;
background: #f5f2f0;
border-radius: 5px;
}
&[data-mce-placeholder]:not(.mce-visualblocks)::before {
color: #bbbbbb;
}

View File

@ -163,6 +163,13 @@
img {
max-width: 100%;
}
pre {
padding: 14px;
margin: 7px 0;
overflow: auto;
background: #f5f2f0;
border-radius: 5px;
}
&[data-mce-placeholder]:not(.mce-visualblocks)::before {
color: #bbbbbb;
}
@ -433,6 +440,7 @@
align-items: center;
margin-top: 6px;
height: 32px;
white-space: nowrap;
> i {
font-size: 14px;
padding-right: 8px;
@ -554,6 +562,12 @@
}
.no-send {
display: none;
.ivu-badge {
position: absolute;
transform: scale(0.6);
top: 5px;
left: 4px;
}
}
}
.drag-over {

View File

@ -108,8 +108,8 @@
padding: 8px;
.load-flow-warp {
width: 20px;
height: 20px;
width: 18px;
height: 18px;
}
}
}

View File

@ -51,8 +51,6 @@
&:last-child {
background-color: #98de6e;
margin-right: 0;
cursor: default;
box-shadow: none;
}
.block-title {
color: rgba(255, 255, 255, 0.6);

View File

@ -90,24 +90,6 @@
}
}
}
.file-no {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 5%;
opacity: 0.8;
> i {
font-size: 64px;
}
> p {
margin-top: 18px;
font-size: 14px;
font-weight: 500;
line-height: 1;
}
}
.file-navigator {
display: flex;
align-items: center;
@ -254,6 +236,30 @@
}
}
}
.file-drag {
flex: 1;
height: 0;
display: flex;
flex-direction: column;
position: relative;
.file-no {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 5%;
opacity: 0.8;
> i {
font-size: 64px;
}
> p {
margin-top: 18px;
font-size: 14px;
font-weight: 500;
line-height: 1;
}
}
.file-table {
flex: 1;
cursor: default;
@ -443,6 +449,35 @@
}
}
}
.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;
margin: 16px 32px 32px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2px dashed #7b7b7b;
border-radius: 12px;
}
.drag-text {
padding: 12px;
font-size: 18px;
color: #666666;
}
}
}
.file-menu {
position: absolute;
}
@ -450,7 +485,7 @@
.file-upload-list {
display: flex;
width: 380px;
padding: 14px 26px 14px 13px;
padding: 14px 26px 14px 26px;
border-radius: 8px;
border: 1px solid #ebeef5;
position: fixed;
@ -462,8 +497,7 @@
overflow: hidden;
.upload-wrap {
flex: 1;
margin-left: 13px;
margin-right: 8px;
width: 100%;
.title {
font-weight: 700;
font-size: 16px;
@ -481,16 +515,18 @@
.content {
font-size: 14px;
line-height: 21px;
margin: 12px -16px 0 0;
margin: 12px 0 0;
color: #606266;
max-height: 500px;
max-width: 100%;
overflow: auto;
> li {
list-style: none;
padding: 4px 16px 4px 0;
padding: 4px 0;
position: relative;
.file-name {
line-height: 18px;
padding-right: 16px;
}
.file-error {
font-size: 12px;
@ -498,8 +534,9 @@
}
.file-close {
position: absolute;
font-size: 14px;
top: 7px;
right: 0;
right: -1px;
display: none;
cursor: pointer;
}
@ -620,9 +657,11 @@
.file-navigator {
margin: 0 24px 0;
}
.file-drag {
.file-table {
margin: 16px 24px 24px;
}
}
}
}
}

View File

@ -87,7 +87,7 @@
transform: scale(0.9);
vertical-align: top;
}
.manage-menu-language {
.manage-menu-flex {
display: flex;
align-items: center;
justify-content: space-between;