Merge branch 'pro' into kuanfan-pro

This commit is contained in:
weifashi 2023-12-18 14:53:33 +08:00
commit fad98dcc9d
77 changed files with 4873 additions and 1002 deletions

View File

@ -134,6 +134,37 @@ class DialogController extends AbstractController
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/dialog/search/tag 02. 搜索标注会话
*
* @apiDescription 根据消息关键词搜索相关会话需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName search__tag
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function search__tag()
{
$user = User::auth();
// 搜索会话
$msgs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->join('web_socket_dialog_msgs as m', 'web_socket_dialogs.id', '=', 'm.dialog_id')
->where('u.userid', $user->userid)
->where('m.tag', '>', 0)
->orderByDesc('m.id')
->take(50)
->get();
$msgs->transform(function (WebSocketDialog $item) use ($user) {
return $item->formatData($user->userid);
});
//
return Base::retSuccess('success', $msgs->toArray());
}
/**
* @api {get} api/dialog/one 03. 获取单个会话信息
*
@ -1327,9 +1358,11 @@ class DialogController extends AbstractController
* @apiGroup dialog
* @apiName msg__forward
*
* @apiParam {Number} msg_id 消息ID
* @apiParam {Array} dialogids 转发给的对话ID
* @apiParam {Array} userids 转发给的成员ID
* @apiParam {Number} msg_id 消息ID
* @apiParam {Array} dialogids 转发给的对话ID
* @apiParam {Array} userids 转发给的成员ID
* @apiParam {Number} show_source 是否显示原发送者信息
* @apiParam {Array} leave_message 转发留言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -1342,6 +1375,8 @@ class DialogController extends AbstractController
$msg_id = intval(Request::input("msg_id"));
$dialogids = Request::input('dialogids');
$userids = Request::input('userids');
$show_source = intval(Request::input("show_source"));
$leave_message = Request::input('leave_message');
//
if (empty($dialogids) && empty($userids)) {
return Base::retError("请选择转发对话或成员");
@ -1353,7 +1388,7 @@ class DialogController extends AbstractController
}
WebSocketDialog::checkDialog($msg->dialog_id);
//
return $msg->forwardMsg($dialogids, $userids, $user);
return $msg->forwardMsg($dialogids, $userids, $user, $show_source, $leave_message);
}
/**
@ -1956,4 +1991,179 @@ class DialogController extends AbstractController
}
return Base::retSuccess('success', $dialog);
}
/**
* @api {post} api/dialog/msg/wordchain 44. 发送接龙消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__wordchain
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} uuid 接龙ID
* @apiParam {String} text 接龙内容
* @apiParam {Array} list 接龙列表
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__wordchain()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$uuid = trim(Request::input('uuid'));
$text = trim(Request::input('text'));
$list = Request::input('list');
//
WebSocketDialog::checkDialog($dialog_id);
$strlen = mb_strlen($text);
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
if ($strlen < 1) {
return Base::retError('内容不能为空');
}
if ($noimglen > 200000) {
return Base::retError('内容最大不能超过200000字');
}
//
$userid = $user->userid;
if ($uuid) {
$dialogMsg = WebSocketDialogMsg::whereDialogId($dialog_id)
->whereType('word-chain')
->orderByDesc('created_at')
->where('msg', 'like', "%$uuid%")
->value('msg');
$list = array_reverse(array_merge($dialogMsg['list'] ?? [], $list));
$list = array_reduce($list, function ($result, $item) {
$fieldValue = $item['id']; // 指定字段名
if (!isset($result[$fieldValue])) {
$result[$fieldValue] = $item;
}
return $result;
}, []);
$list = array_reverse(array_values($list));
}
//
$msgData = [
'text' => $text,
'list' => $list,
'userid' => $userid,
'uuid' => $uuid ?: Base::generatePassword(36),
];
return WebSocketDialogMsg::sendMsg(null, $dialog_id, 'word-chain', $msgData, $user->userid);
}
/**
* @api {post} api/dialog/msg/vote 45. 发起投票
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__vote
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} text 投票内容
* @apiParam {Array} type 投票类型
* @apiParam {String} [uuid] 投票ID
* @apiParam {Array} [list] 投票列表
* @apiParam {Number} [multiple] 多选
* @apiParam {Number} [anonymous] 匿名
* @apiParam {Array} [vote] 投票
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__vote()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$uuid = trim(Request::input('uuid'));
$text = trim(Request::input('text'));
$type = trim(Request::input('type', 'create'));
$multiple = intval(Request::input('multiple')) ?: 0;
$anonymous = intval(Request::input('anonymous')) ?: 0;
$list = Request::input('list');
$vote = Request::input('vote') ?: [];
$votes = is_array($vote) ? $vote : [$vote];
//
WebSocketDialog::checkDialog($dialog_id);
//
$action = null;
$userid = $user->userid;
$result = [];
if ($type != 'create') {
if ($type == 'vote' && empty($votes)) {
return Base::retError('参数错误');
}
if (empty($uuid)) {
return Base::retError('参数错误');
}
$dialogMsgs = WebSocketDialogMsg::whereDialogId($dialog_id)
->whereType('vote')
->orderByDesc('created_at')
->where('msg', 'like', "%$uuid%")
->get();
//
if ($type == 'again') {
$res = WebSocketDialogMsg::sendMsg(null, $dialog_id, 'vote', $dialogMsgs[0]->msg, $user->userid);
if (Base::isError($res)) {
return $res;
}
$result[] = $res['data'];
} else {
foreach ($dialogMsgs as $dialogMsg) {
$action = "change-{$dialogMsg->id}";
$msgData = $dialogMsg->msg;
if ($type == 'finish') {
$msgData['state'] = 0;
} else {
$msgDataVotes = $msgData['votes'] ?? [];
if (in_array($userid, array_column($msgDataVotes, 'userid'))) {
return Base::retError('不能重复投票');
}
$msgDataVotes[] = [
'userid' => $userid,
'votes' => $votes,
];
$msgData['votes'] = $msgDataVotes;
}
$res = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'vote', $msgData, $user->userid);
if (Base::isError($res)) {
return $res;
}
$result[] = $res['data'];
}
}
} else {
$strlen = mb_strlen($text);
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
if ($strlen < 1) {
return Base::retError('内容不能为空');
}
if ($noimglen > 200000) {
return Base::retError('内容最大不能超过200000字');
}
$msgData = [
'text' => $text,
'list' => $list,
'userid' => $userid,
'uuid' => $uuid ?: Base::generatePassword(36),
'multiple' => $multiple,
'anonymous' => $anonymous,
'votes' => [],
'state' => 1
];
$res = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'vote', $msgData, $user->userid);
if (Base::isError($res)) {
return $res;
}
$result[] = $res['data'];
}
return Base::retSuccess('发送成功', $result);
}
}

View File

@ -2,10 +2,10 @@
namespace App\Http\Controllers\Api;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialog;
use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Tasks\FilePackTask;
use App\Models\File;
use App\Models\FileContent;
use App\Models\FileLink;
@ -480,6 +480,8 @@ class FileController extends AbstractController
$only_update_at = Request::input('only_update_at', 'no');
$history_id = intval(Request::input('history_id'));
//
Base::checkClientVersion('0.31.75');
//
if (Base::isNumber($id)) {
$user = User::auth();
$file = File::permissionFind(intval($id), $user, $down == 'yes' ? 1 : 0);
@ -1018,15 +1020,15 @@ class FileController extends AbstractController
}
$zip = new \ZipArchive();
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName;
$zipPath = storage_path('app/'.$zipName);
$zipName = 'tmp/file/' . date("Ym") . '/' . $user->userid . '/' . $downName;
$zipPath = public_path($zipName);
Base::makeDir(dirname($zipPath));
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
return Base::retError('创建压缩文件失败');
}
go(function() use ($zip, $files, $downName) {
go(function() use ($user, $zip, $files, $downName, $zipName) {
Coroutine::sleep(0.1);
// 压缩进度
$progress = 0;
@ -1049,6 +1051,19 @@ class FileController extends AbstractController
'progress' => 100
]);
}
//
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return;
}
if ($dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
$text = "<b>文件下载打包已完成。</b>";
$text .= "\n\n";
$text .= "文件名:{$downName}";
$text .= "\n";
$text .= "下载地址:".Base::fillUrl($zipName);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
}
});
return Base::retSuccess('success');
@ -1072,8 +1087,8 @@ class FileController extends AbstractController
{
$user = User::auth();
$downName = Request::input('name');
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName;
$zipPath = storage_path('app/'.$zipName);
$zipName = 'tmp/file/' . date("Ym") . '/' . $user->userid . '/' . $downName;
$zipPath = public_path($zipName);
if (!file_exists($zipPath)) {
abort(403, "The file does not exist.");
}

View File

@ -12,6 +12,7 @@ use App\Module\Doo;
use App\Models\File;
use App\Models\User;
use App\Module\Base;
use Swoole\Coroutine;
use App\Models\Deleted;
use App\Models\Project;
use App\Module\TimeRange;
@ -30,7 +31,9 @@ use App\Models\ProjectTaskFile;
use App\Models\ProjectTaskUser;
use App\Models\WebSocketDialog;
use App\Exceptions\ApiException;
use App\Models\ProjectPermission;
use App\Module\BillMultipleExport;
use App\Models\WebSocketDialogMsg;
use Illuminate\Support\Facades\DB;
use App\Models\ProjectTaskFlowChange;
@ -575,6 +578,8 @@ class ProjectController extends AbstractController
$project = Project::userProject($project_id);
//
if ($only_column) {
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_SORT);
// 排序列表
$index = 0;
foreach ($sort as $item) {
@ -760,6 +765,8 @@ class ProjectController extends AbstractController
// 项目
$project = Project::userProject($project_id);
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_ADD);
//
if (empty($name)) {
return Base::retError('列表名称不能为空');
}
@ -809,7 +816,9 @@ class ProjectController extends AbstractController
return Base::retError('列表不存在');
}
// 项目
Project::userProject($column->project_id);
$project = Project::userProject($column->project_id);
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_UPDATE);
//
if (Arr::exists($data, 'name') && $column->name != $data['name']) {
$column->addLog("修改列表名称:{$column->name} => {$data['name']}");
@ -849,7 +858,9 @@ class ProjectController extends AbstractController
return Base::retError('列表不存在');
}
// 项目
Project::userProject($column->project_id, true, true);
$project = Project::userProject($column->project_id);
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_REMOVE);
//
$column->deleteColumn();
return Base::retSuccess('删除成功', ['id' => $column->id]);
@ -876,10 +887,10 @@ class ProjectController extends AbstractController
public function column__one()
{
User::auth();
//
//
$column_id = intval(Request::input('column_id'));
$deleted = Request::input('deleted', 'no');
//
//
$builder = ProjectColumn::whereId($column_id);
if ($deleted == 'all') {
$builder->withTrashed();
@ -890,10 +901,10 @@ class ProjectController extends AbstractController
if (empty($column)) {
return Base::retError('列表不存在');
}
//
//
return Base::retSuccess('success', $column);
}
/**
* @api {get} api/project/task/lists 19. 任务列表
@ -1180,170 +1191,182 @@ class ProjectController extends AbstractController
if (Carbon::parse($time[1])->timestamp - Carbon::parse($time[0])->timestamp > 90 * 86400) {
return Base::retError('时间范围限制最大90天');
}
$headings = [];
$headings[] = '任务ID';
$headings[] = '父级任务ID';
$headings[] = '所属项目';
$headings[] = '任务标题';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '完成时间';
$headings[] = '归档时间';
$headings[] = '任务计划用时';
$headings[] = '实际完成用时';
$headings[] = '超时时间';
$headings[] = '开发用时';
$headings[] = '验收/测试用时';
$headings[] = '负责人';
$headings[] = '创建人';
$headings[] = '状态';
$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(), $type);
$builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use (&$datas) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get();
$testTime = 0;//验收/测试时间
$taskStartTime = $task->start_at ? Carbon::parse($task->start_at)->timestamp : Carbon::parse($task->created_at)->timestamp;
$taskCompleteTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time();
$totalTime = $taskCompleteTime - $taskStartTime; //开发测试总用时
foreach ($flowChanges as $change) {
if (!str_contains($change->before_flow_item_name, 'end')) {
$upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first();
if ($upOne) {
if (str_contains($change->before_flow_item_name, 'test') || str_contains($change->before_flow_item_name, '测试') || strpos($change->before_flow_item_name, '验收') !== false) {
$testCtime = Carbon::parse($change->created_at)->timestamp;
$tTime = Carbon::parse($upOne->created_at)->timestamp;
$tMinusNum = $testCtime - $tTime;
$testTime += $tMinusNum;
go(function() use ($user, $userid, $time, $type) {
Coroutine::sleep(0.1);
$headings = [];
$headings[] = '任务ID';
$headings[] = '父级任务ID';
$headings[] = '所属项目';
$headings[] = '任务标题';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '完成时间';
$headings[] = '归档时间';
$headings[] = '任务计划用时';
$headings[] = '实际完成用时';
$headings[] = '超时时间';
$headings[] = '开发用时';
$headings[] = '验收/测试用时';
$headings[] = '负责人';
$headings[] = '创建人';
$headings[] = '状态';
$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(), $type);
$builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use (&$datas) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get();
$testTime = 0;//验收/测试时间
$taskStartTime = $task->start_at ? Carbon::parse($task->start_at)->timestamp : Carbon::parse($task->created_at)->timestamp;
$taskCompleteTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time();
$totalTime = $taskCompleteTime - $taskStartTime; //开发测试总用时
foreach ($flowChanges as $change) {
if (!str_contains($change->before_flow_item_name, 'end')) {
$upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first();
if ($upOne) {
if (str_contains($change->before_flow_item_name, 'test') || str_contains($change->before_flow_item_name, '测试') || strpos($change->before_flow_item_name, '验收') !== false) {
$testCtime = Carbon::parse($change->created_at)->timestamp;
$tTime = Carbon::parse($upOne->created_at)->timestamp;
$tMinusNum = $testCtime - $tTime;
$testTime += $tMinusNum;
}
}
}
}
}
if (!$task->complete_at) {
$lastChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderByDesc('id')->first();
$nowTime = time();
$unFinishTime = $nowTime - Carbon::parse($lastChange->created_at)->timestamp;
if (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) {
$testTime += $unFinishTime;
if (!$task->complete_at) {
$lastChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderByDesc('id')->first();
$nowTime = time();
$unFinishTime = $nowTime - Carbon::parse($lastChange->created_at)->timestamp;
if (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) {
$testTime += $unFinishTime;
}
}
}
$developTime = $totalTime - $testTime;//开发时间
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
if ($task->end_at) {
$startTime = Carbon::parse($task->start_at)->timestamp;
$endTime = Carbon::parse($task->end_at)->timestamp;
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Base::timeFormat(abs($residueTime));
$developTime = $totalTime - $testTime;//开发时间
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
if ($task->end_at) {
$startTime = Carbon::parse($task->start_at)->timestamp;
$endTime = Carbon::parse($task->end_at)->timestamp;
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Base::timeFormat(abs($residueTime));
}
$planTime = Base::timeDiff($startTime, $endTime);
}
$planTime = Base::timeDiff($startTime, $endTime);
}
$actualTime = $task->complete_at ? $totalTime : 0;//实际完成用时
$statusText = '未完成';
if ($task->flow_item_name) {
if (str_contains($task->flow_item_name, '已取消')) {
$statusText = '已取消';
$actualTime = 0;
$testTime = 0;
$developTime = 0;
$overTime = '-';
} elseif (str_contains($task->flow_item_name, '已完成')) {
$actualTime = $task->complete_at ? $totalTime : 0;//实际完成用时
$statusText = '未完成';
if ($task->flow_item_name) {
if (str_contains($task->flow_item_name, '已取消')) {
$statusText = '已取消';
$actualTime = 0;
$testTime = 0;
$developTime = 0;
$overTime = '-';
} elseif (str_contains($task->flow_item_name, '已完成')) {
$statusText = '已完成';
}
} elseif ($task->complete_at) {
$statusText = '已完成';
}
} elseif ($task->complete_at) {
$statusText = '已完成';
if (!isset($datas[$task->ownerid])) {
$datas[$task->ownerid] = [
'index' => 1,
'nickname' => Base::filterEmoji(User::userid2nickname($task->ownerid)),
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
'data' => [],
];
}
$datas[$task->ownerid]['index']++;
if ($statusText === '未完成') {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; // 未完成
} elseif ($statusText === '已完成' && $task->end_at && Carbon::parse($task->complete_at)->gt($task->end_at)) {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; // 已完成超期
}
$datas[$task->ownerid]['data'][] = [
$task->id,
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->start_at ?: '-',
$task->end_at ?: '-',
$task->complete_at ?: '-',
$task->archived_at ?: '-',
$planTime ?: '-',
$actualTime ? Base::timeFormat($actualTime) : '-',
$overTime,
$developTime > 0 ? Base::timeFormat($developTime) : '-',
$testTime > 0 ? Base::timeFormat($testTime) : '-',
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
$statusText
];
}
if (!isset($datas[$task->ownerid])) {
$datas[$task->ownerid] = [
'index' => 1,
'nickname' => Base::filterEmoji(User::userid2nickname($task->ownerid)),
});
if (empty($datas)) {
return Base::retError('没有任何数据');
}
//
$sheets = [];
foreach ($userid as $ownerid) {
$data = $datas[$ownerid] ?? [
'nickname' => Base::filterEmoji(User::userid2nickname($ownerid)),
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
'data' => [],
];
$title = (count($sheets) + 1) . "." . ($data['nickname'] ?: $ownerid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']);
}
//
$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());
$export = new BillMultipleExport($sheets);
$res = $export->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 (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('task::export:userid', $user->userid);
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return;
}
$datas[$task->ownerid]['index']++;
if ($statusText === '未完成') {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; // 未完成
} elseif ($statusText === '已完成' && $task->end_at && Carbon::parse($task->complete_at)->gt($task->end_at)) {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; // 已完成超期
if ($dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
$text = "<b>导出任务统计已完成。</b>";
$text .= "\n\n";
$text .= "文件名:{$fileName}";
$text .= "\n";
$text .= "文件大小:".Base::twoFloat(filesize($zipPath) / 1024, true)."KB";
$text .= "\n";
$text .= "下载地址:".Base::fillUrl('api/project/task/down?key=' . urlencode($base64));
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
}
$datas[$task->ownerid]['data'][] = [
$task->id,
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->start_at ?: '-',
$task->end_at ?: '-',
$task->complete_at ?: '-',
$task->archived_at ?: '-',
$planTime ?: '-',
$actualTime ? Base::timeFormat($actualTime) : '-',
$overTime,
$developTime > 0 ? Base::timeFormat($developTime) : '-',
$testTime > 0 ? Base::timeFormat($testTime) : '-',
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
$statusText
];
}
});
if (empty($datas)) {
return Base::retError('没有任何数据');
}
//
$sheets = [];
foreach ($userid as $ownerid) {
$data = $datas[$ownerid] ?? [
'nickname' => Base::filterEmoji(User::userid2nickname($ownerid)),
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
'data' => [],
];
$title = (count($sheets) + 1) . "." . ($data['nickname'] ?: $ownerid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']);
}
//
$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());
$export = new BillMultipleExport($sheets);
$res = $export->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 (\Throwable) {
}
//
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('打包失败,请稍后再试...');
}
return Base::retSuccess('success',['msg' => '正在打包,请留意系统消息']);
}
/**
@ -1507,7 +1530,7 @@ class ProjectController extends AbstractController
$archived = Request::input('archived', 'no');
//
$isArchived = str_replace(['all', 'yes', 'no'], [null, false, true], $archived);
$task = ProjectTask::userTask($task_id, $isArchived, true, false, ['taskUser', 'taskTag']);
$task = ProjectTask::userTask($task_id, $isArchived, true, ['taskUser', 'taskTag']);
// 项目可见性
$project_userid = ProjectUser::whereProjectId($task->project_id)->whereOwner(1)->value('userid'); // 项目负责人
if ($task->visibility != 1 && $user->userid != $project_userid) {
@ -1603,7 +1626,9 @@ class ProjectController extends AbstractController
return Base::retError('文件不存在或已被删除');
}
//
$task = ProjectTask::userTask($file->task_id, true, true, true);
$task = ProjectTask::userTask($file->task_id);
//
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_UPDATE, $task);
//
$task->pushMsg('filedelete', $file);
$file->delete();
@ -1732,6 +1757,8 @@ class ProjectController extends AbstractController
$column_id = $data['column_id'];
// 项目
$project = Project::userProject($project_id);
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_ADD);
// 列表
$column = null;
$newColumn = null;
@ -1808,11 +1835,13 @@ class ProjectController extends AbstractController
$task_id = intval(Request::input('task_id'));
$name = Request::input('name');
//
$task = ProjectTask::userTask($task_id, true, true, true);
$task = ProjectTask::userTask($task_id);
if ($task->complete_at) {
return Base::retError('主任务已完成无法添加子任务');
}
//
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_ADD);
//
$task = ProjectTask::addTask([
'name' => $name,
'parent_id' => $task->id,
@ -1867,11 +1896,18 @@ class ProjectController extends AbstractController
$param = Request::input();
$task_id = intval($param['task_id']);
//
$task = ProjectTask::userTask($task_id, true, true, 2);
$task = ProjectTask::userTask($task_id);
//
$project = Project::userProject($task->project_id);
if (Arr::exists($param, 'flow_item_id')) {
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_STATUS, $task);
}else{
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_UPDATE, $task);
}
//
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($task_id)->get();
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray(); // 负责人
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray(); // 协助人
// 更新任务
$updateMarking = [];
$task->updateTask($param, $updateMarking);
@ -2003,12 +2039,15 @@ class ProjectController extends AbstractController
$task_id = intval(Request::input('task_id'));
$type = Request::input('type', 'add');
//
$task = ProjectTask::userTask($task_id, $type == 'add', true, true);
$task = ProjectTask::userTask($task_id, $type == 'add');
//
if ($task->parent_id > 0) {
return Base::retError('子任务不支持此功能');
}
//
$project = Project::userProject($task->project_id);
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_ARCHIVED, $task);
//
if ($type == 'recovery') {
$task->archivedTask(null);
} elseif ($type == 'add') {
@ -2045,7 +2084,11 @@ class ProjectController extends AbstractController
$task_id = intval(Request::input('task_id'));
$type = Request::input('type', 'delete');
//
$task = ProjectTask::userTask($task_id, null, $type !== 'recovery', true);
$task = ProjectTask::userTask($task_id, null, $type !== 'recovery');
//
$project = Project::userProject($task->project_id);
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_REMOVE, $task);
//
if ($type == 'recovery') {
$task->restoreTask();
return Base::retSuccess('操作成功', ['id' => $task->id]);
@ -2080,7 +2123,7 @@ class ProjectController extends AbstractController
return Base::retError('记录不存在');
}
//
$task = ProjectTask::userTask($projectLog->task_id, true, true, true);
$task = ProjectTask::userTask($projectLog->task_id);
//
$record = $projectLog->record;
if ($record['flow'] && is_array($record['flow'])) {
@ -2123,6 +2166,7 @@ class ProjectController extends AbstractController
* @apiName task__flow
*
* @apiParam {Number} task_id 任务ID
* @apiParam {Number} project_id 项目ID - 存在时只返回这个项目的
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -2133,17 +2177,23 @@ class ProjectController extends AbstractController
User::auth();
//
$task_id = intval(Request::input('task_id'));
$project_id = intval(Request::input('project_id'));
//
$projectTask = ProjectTask::select(['id', 'project_id', 'complete_at', 'flow_item_id', 'flow_item_name'])->withTrashed()->find($task_id);
if (empty($projectTask)) {
return Base::retError('任务不存在', [ 'task_id' => $task_id ], -4002);
return Base::retError('任务不存在', ['task_id' => $task_id], -4002);
}
//
$projectFlowItem = $projectTask->flow_item_id ? ProjectFlowItem::with(['projectFlow'])->find($projectTask->flow_item_id) : null;
if ($projectFlowItem?->projectFlow) {
$projectFlow = $projectFlowItem->projectFlow;
$projectFlowItem = null;
if ($project_id) {
$projectFlow = ProjectFlow::whereProjectId($project_id)->orderByDesc('id')->first();
} else {
$projectFlow = ProjectFlow::whereProjectId($projectTask->project_id)->orderByDesc('id')->first();
$projectFlowItem = $projectTask->flow_item_id ? ProjectFlowItem::with(['projectFlow'])->find($projectTask->flow_item_id) : null;
if ($projectFlowItem?->projectFlow) {
$projectFlow = $projectFlowItem->projectFlow;
} else {
$projectFlow = ProjectFlow::whereProjectId($projectTask->project_id)->orderByDesc('id')->first();
}
}
if (empty($projectFlow)) {
return Base::retSuccess('success', [
@ -2207,6 +2257,9 @@ class ProjectController extends AbstractController
* @apiParam {Number} task_id 任务ID
* @apiParam {Number} project_id 项目ID
* @apiParam {Number} column_id 列ID
* @apiParam {Number} flow_item_id 工作流id
* @apiParam {Array} owner 负责人
* @apiParam {Array} assist 协助人
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -2219,22 +2272,36 @@ class ProjectController extends AbstractController
$task_id = intval(Request::input('task_id'));
$project_id = intval(Request::input('project_id'));
$column_id = intval(Request::input('column_id'));
//
$task = ProjectTask::userTask($task_id, true, true, 2);
//
$flow_item_id = intval(Request::input('flow_item_id'));
$owner = Request::input('owner', []);
$assist = Request::input('assist', []);
//
$task = ProjectTask::userTask($task_id);
//
$project = Project::userProject($task->project_id);
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_MOVE, $task);
//
if( $task->project_id == $project_id && $task->column_id == $column_id){
return Base::retSuccess('移动成功', ['id' => $task_id]);
}
//
//
$project = Project::userProject($project_id);
$column = ProjectColumn::whereProjectId($project->id)->whereId($column_id)->first();
if (empty($column)) {
return Base::retError('列表不存在');
}
//
$task->moveTask($project_id,$column_id);
//
return Base::retSuccess('移动成功', ['id' => $task_id]);
if($flow_item_id){
$flowItem = projectFlowItem::whereProjectId($project->id)->whereId($flow_item_id)->first();
if (empty($flowItem)) {
return Base::retError('任务状态不存在');
}
}
//
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist);
//
$task = ProjectTask::userTask($task_id);
//
return Base::retSuccess('移动成功', $task);
}
/**
@ -2411,4 +2478,74 @@ class ProjectController extends AbstractController
'top_at' => $projectUser->top_at?->toDateTimeString(),
]);
}
/**
* @api {get} api/project/permission 43. 获取项目权限设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName permission
*
* @apiParam {Number} project_id 项目ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function permission()
{
$user = User::auth();
$projectId = intval(Request::input('project_id'), 0);
$projectUser = ProjectUser::whereUserid($user->userid)->whereProjectId($projectId)->first();
if (!$projectUser) {
return Base::retError("项目不存在");
}
$projectPermission = ProjectPermission::initPermissions($projectId);
return Base::retSuccess("success", $projectPermission);
}
/**
* @api {get} api/project/permission/update 44. 项目权限设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName permission__update
*
* @apiParam {Number} project_id 项目ID
* @apiParam {Array} task_add 添加任务权限
* @apiParam {Array} task_update 修改任务权限
* @apiParam {Array} task_remove 删除任务权限
* @apiParam {Array} task_update_complete 标记完成权限
* @apiParam {Array} task_archived 归档任务权限
* @apiParam {Array} task_move 移动任务权限
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function permission__update()
{
$user = User::auth();
$projectId = intval(Request::input('project_id'), 0);
$projectUser = ProjectUser::whereUserid($user->userid)->whereProjectId($projectId)->first();
if (!$projectUser) {
return Base::retError("项目不存在");
}
$permissions = Request::only([
ProjectPermission::TASK_LIST_ADD,
ProjectPermission::TASK_LIST_UPDATE,
ProjectPermission::TASK_LIST_REMOVE,
ProjectPermission::TASK_LIST_SORT,
ProjectPermission::TASK_ADD,
ProjectPermission::TASK_UPDATE,
ProjectPermission::TASK_REMOVE,
ProjectPermission::TASK_STATUS,
ProjectPermission::TASK_ARCHIVED,
ProjectPermission::TASK_MOVE,
]);
$projectPermission = ProjectPermission::updatePermissions($projectId, Base::newArrayRecursive('intval', $permissions));
return Base::retSuccess("success", $projectPermission);
}
}

View File

@ -24,6 +24,7 @@ use Request;
* @property int|null $dialog_id 聊天会话ID
* @property string|null $archived_at 归档时间
* @property int|null $archived_userid 归档会员
* @property int|null $is_fixed 是否固定
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
@ -47,6 +48,7 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDesc($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereIsFixed($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project wherePersonal($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUpdatedAt($value)

View File

@ -0,0 +1,202 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
/**
* App\Models\ProjectPermission
*
* @property int $id
* @property int|null $project_id 项目ID
* @property string|null $permissions 权限
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project|null $project
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission wherePermissions($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereUpdatedAt($value)
* @mixin \Eloquent
*/
class ProjectPermission extends AbstractModel
{
const TASK_LIST_ADD = 'task_list_add'; // 添加列
const TASK_LIST_UPDATE = 'task_list_update'; // 修改列
const TASK_LIST_REMOVE = 'task_list_remove'; // 删除列
const TASK_LIST_SORT = 'task_list_sort'; // 列表排序
const TASK_ADD = 'task_add'; // 任务添加
const TASK_UPDATE = 'task_update'; // 任务更新
const TASK_STATUS = 'task_status'; // 任务状态
const TASK_REMOVE = 'task_remove'; // 任务删除
const TASK_ARCHIVED = 'task_archived'; // 任务归档
const TASK_MOVE = 'task_move'; // 任务移动
// 权限列表
const PERMISSIONS = [
'project_leader' => 1, // 项目负责人
'project_member' => 2, // 项目成员
'task_leader' => 3, // 任务负责人
'task_assist' => 4, // 任务协助人
];
// 权限描述
const PERMISSIONS_DESC = [
1 => "项目负责人",
2 => "项目成员",
3 => "任务负责人",
4 => "任务协助人",
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['project_id', 'permissions'];
/**
* 权限
* @param $value
* @return string
*/
public function getPermissionsAttribute($value)
{
return Base::json2array($value);
}
/**
* 获取权限值
*
* @param int $projectId
* @param string $key
* @return object|array
*/
public static function getPermission($projectId, $key = '')
{
$projectPermission = self::initPermissions($projectId);
$currentPermissions = $projectPermission->permissions;
if ($key) {
if (!isset($currentPermissions[$key])) {
throw new ApiException('项目权限设置不存在');
}
return $currentPermissions[$key];
}
return $projectPermission;
}
/**
* 初始化项目权限
*
* @param int $projectId
* @return ProjectPermission
*/
public static function initPermissions($projectId)
{
$permissions = [
self::TASK_LIST_ADD => $projectTaskList = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['project_member']],
self::TASK_LIST_UPDATE => $projectTaskList,
self::TASK_LIST_REMOVE => [self::PERMISSIONS['project_leader']],
self::TASK_LIST_SORT => $projectTaskList,
self::TASK_ADD => $projectTaskList,
self::TASK_UPDATE => [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader'], self::PERMISSIONS['task_assist']],
self::TASK_STATUS => $taskStatus = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader']],
self::TASK_REMOVE => $taskStatus,
self::TASK_ARCHIVED => $taskStatus,
self::TASK_MOVE => $taskStatus
];
return self::firstOrCreate(
['project_id' => $projectId],
['permissions' => Base::array2json($permissions)]
);
}
/**
* 更新项目权限
*
* @param int $projectId
* @param array $permissions
* @return ProjectPermission
*/
public static function updatePermissions($projectId, $newPermissions)
{
$projectPermission = self::initPermissions($projectId);
$currentPermissions = $projectPermission->permissions;
$mergedPermissions = empty($newPermissions) ? $currentPermissions : array_merge($currentPermissions, $newPermissions);
$projectPermission->permissions = Base::array2json($mergedPermissions);
$projectPermission->save();
return $projectPermission;
}
/**
* 检查用户是否有执行特定动作的权限
* @param string $action 动作名称
* @param Project $project 项目实例
* @param ProjectTask $task 任务实例
* @return bool
*/
public static function userTaskPermission(Project $project, $action, ProjectTask $task = null)
{
$userid = User::userid();
$permissions = self::getPermission($project->id, $action);
switch ($action) {
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
case self::TASK_LIST_ADD:
case self::TASK_LIST_UPDATE:
case self::TASK_LIST_REMOVE:
case self::TASK_LIST_SORT:
case self::TASK_ADD:
case self::TASK_UPDATE:
case self::TASK_STATUS:
case self::TASK_REMOVE:
case self::TASK_ARCHIVED:
case self::TASK_MOVE:
$verify = false;
// 项目负责人
if (in_array(self::PERMISSIONS['project_leader'], $permissions)) {
if ($project->owner) {
$verify = true;
}
}
// 项目成员
if (!$verify && in_array(self::PERMISSIONS['project_member'], $permissions)) {
$user = ProjectUser::whereProjectId($project->id)->whereUserid(intval($userid))->first();
if (!empty($user)) {
$verify = true;
}
}
// 任务负责人
if (!$verify && $task && in_array(self::PERMISSIONS['task_leader'], $permissions)) {
if ($task->isOwner()) {
$verify = true;
}
}
// 任务协助人
if (!$verify && $task && in_array(self::PERMISSIONS['task_assist'], $permissions)) {
if ($task->isAssister()) {
$verify = true;
}
}
//
if (!$verify) {
$desc = [];
rsort($permissions);
foreach ($permissions as $permission) {
$desc[] = self::PERMISSIONS_DESC[$permission];
}
$desc = array_reverse($desc);
throw new ApiException(sprintf("仅限%s操作", implode('、', $desc)));
}
break;
}
return true;
}
}

View File

@ -32,7 +32,8 @@ use Request;
* @property int|null $archived_follow 跟随项目归档(项目取消归档时任务也取消归档)
* @property string|null $complete_at 完成时间
* @property int|null $userid 创建人
* @property int|null $is_all_visible 是否所有人可见
* @property int|null $visibility 任务可见性1-项目人员 2-任务人员 3-指定成员
* @property int|null $is_default 是否默认任务
* @property int|null $p_level 优先级
* @property string|null $p_name 优先级名称
* @property string|null $p_color 优先级颜色
@ -81,7 +82,7 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereIsAllVisible($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereIsDefault($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoop($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereName($value)
@ -94,6 +95,7 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereStartAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereVisibility($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withoutTrashed()
* @mixin \Eloquent
@ -549,15 +551,12 @@ class ProjectTask extends AbstractModel
*/
public function updateTask($data, &$updateMarking = [])
{
//
AbstractModel::transaction(function () use ($data, &$updateMarking) {
// 主任务
$mainTask = $this->parent_id > 0 ? self::find($this->parent_id) : null;
// 工作流
if (Arr::exists($data, 'flow_item_id')) {
$isProjectOwner = $this->useridInTheProject(User::userid()) === 2;
if (!$isProjectOwner && !$this->isOwner()) {
throw new ApiException('仅限项目或任务负责人修改任务状态');
}
if ($this->flow_item_id == $data['flow_item_id']) {
throw new ApiException('任务状态未发生改变');
}
@ -578,6 +577,7 @@ class ProjectTask extends AbstractModel
throw new ApiException("当前状态[{$currentFlowItem->name}]不可流转到[{$newFlowItem->name}]");
}
if ($currentFlowItem->userlimit) {
$isProjectOwner = $this->useridInTheProject(User::userid()) === 2;
if (!$isProjectOwner && !in_array(User::userid(), $currentFlowItem->userids)) {
throw new ApiException("当前状态[{$currentFlowItem->name}]仅限状态负责人或项目负责人修改");
}
@ -1667,13 +1667,21 @@ class ProjectTask extends AbstractModel
/**
* 移动任务
* @param int $project_id
* @param int $column_id
* @param int $project_id
* @param int $column_id
* @param int $flowItemId
* @param array $owner
* @param array $assist
* @return bool
*/
public function moveTask(int $projectId, int $columnId)
public function moveTask(int $projectId, int $columnId,int $flowItemId = 0,array $owner = [], array $assist = [])
{
AbstractModel::transaction(function () use($projectId, $columnId) {
AbstractModel::transaction(function () use($projectId, $columnId, $flowItemId, $owner, $assist) {
$newTaskUser = array_merge($owner, $assist);
//
$this->project_id = $projectId;
$this->column_id = $columnId;
$this->flow_item_id = $flowItemId;
// 任务内容
if($this->content){
$this->content->project_id = $projectId;
@ -1690,15 +1698,24 @@ class ProjectTask extends AbstractModel
$taskTag->save();
}
// 任务用户
$this->updateTask(['owner' => $owner]);
$this->updateTask(['assist' => $assist]);
foreach ($this->taskUser as $taskUser){
$taskUser->project_id = $projectId;
$taskUser->save();
if( in_array($taskUser->id, $newTaskUser) ){
$taskUser->project_id = $projectId;
$taskUser->save();
}
}
//
$this->project_id = $projectId;
$this->column_id = $columnId;
//
if($flowItemId){
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
$this->flow_item_name = $flowItem->status . "|" . $flowItem->name;
}else{
$this->flow_item_name = '';
}
//
$this->save();
//
//
$this->addLog("移动{任务}");
});
$this->pushMsg('update');
@ -1728,14 +1745,10 @@ class ProjectTask extends AbstractModel
* @param int $task_id
* @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param bool $trashed true:仅限未删除, false:仅限已删除, null:不限制
* @param int|bool $permission
* - 0|false 限制:项目成员、任务成员、任务群聊成员(任务成员 = 任务创建人+任务协助人+任务负责人)
* - 1|true 限制:项目负责人、任务成员
* - 2 已有负责人才限制true (子任务时如果是主任务负责人也可以)
* @param array $with
* @return self
*/
public static function userTask($task_id, $archived = true, $trashed = true, $permission = false, $with = [])
public static function userTask($task_id, $archived = true, $trashed = true, $with = [])
{
$builder = self::with($with)->allData()->where("project_tasks.id", intval($task_id));
if ($trashed === false) {
@ -1758,7 +1771,7 @@ class ProjectTask extends AbstractModel
try {
$project = Project::userProject($task->project_id);
} catch (\Throwable $e) {
if ($task->owner !== null || (!$permission && $task->permission(4))) {
if ($task->owner !== null || $task->permission(4)) {
$project = Project::find($task->project_id);
if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
@ -1768,13 +1781,6 @@ class ProjectTask extends AbstractModel
}
}
//
if ($permission >= 2) {
$permission = $task->hasOwner() ? 1 : 0;
}
if ($permission && !$project->owner && !$task->permission(3)) {
throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作');
}
//
return $task;
}
}

View File

@ -60,6 +60,8 @@ class ProjectTaskContent extends AbstractModel
*/
public static function saveContent($task_id, $content)
{
@ini_set("pcre.backtrack_limit", 999999999);
//
$oldContent = $content;
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
//
@ -81,9 +83,6 @@ class ProjectTaskContent extends AbstractModel
Base::makeDir(dirname($publicPath));
$result = file_put_contents($publicPath, $content);
if(!$result && $oldContent){
info("保存任务详情至文件失败");
info($publicPath);
info($oldContent);
throw new ApiException("保存任务详情至文件失败,请重试");
}
//

View File

@ -72,6 +72,7 @@ class WebSocketDialogMsg extends AbstractModel
protected $appends = [
'percentage',
'reply_data',
'forward_data',
];
protected $hidden = [
@ -114,6 +115,21 @@ class WebSocketDialogMsg extends AbstractModel
return $this->appendattrs['reply_data'];
}
/**
* 转发消息详情
* @return WebSocketDialogMsg|null
*/
public function getForwardDataAttribute()
{
if (!isset($this->appendattrs['forward_data'])) {
$this->appendattrs['forward_data'] = null;
if ($this->forward_id > 0) {
$this->appendattrs['forward_data'] = self::find($this->forward_id, ['id', 'userid', 'type', 'msg'])?->cancelAppend() ?: null;
}
}
return $this->appendattrs['forward_data'];
}
/**
* 消息格式化
* @param $value
@ -369,11 +385,13 @@ class WebSocketDialogMsg extends AbstractModel
* @param array|int $dialogids
* @param array|int $userids
* @param User $user 发送的会员
* @param int $showSource 是否显示原发送者信息
* @param string $leaveMessage 转发留言
* @return mixed
*/
public function forwardMsg($dialogids, $userids, $user)
public function forwardMsg($dialogids, $userids, $user, $showSource = 1, $leaveMessage = '')
{
return AbstractModel::transaction(function() use ($dialogids, $user, $userids) {
return AbstractModel::transaction(function() use ($dialogids, $user, $userids, $showSource, $leaveMessage) {
$originalMsg = Base::json2array($this->getRawOriginal('msg'));
$msgs = [];
$already = [];
@ -382,11 +400,14 @@ class WebSocketDialogMsg extends AbstractModel
$dialogids = [$dialogids];
}
foreach ($dialogids as $dialogid) {
$res = self::sendMsg(null, $dialogid, $this->type, $originalMsg, $user->userid);
$res = self::sendMsg('forward-'.( $showSource ? 1 : 0).'-'.($this->forward_id ?: $this->id), $dialogid, $this->type, $originalMsg, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
$already[] = $dialogid;
}
if ($leaveMessage) {
self::sendMsg(null, $dialogid, 'text', ['text' => $leaveMessage], $user->userid);
}
}
}
if ($userids) {
@ -399,10 +420,13 @@ class WebSocketDialogMsg extends AbstractModel
}
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog && !in_array($dialog->id, $already)) {
$res = self::sendMsg(null, $dialog->id, $this->type, $originalMsg, $user->userid);
$res = self::sendMsg('forward-'.( $showSource ? 1 : 0).'-'.($this->forward_id ?: $this->id), $dialog->id, $this->type, $originalMsg, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
if ($leaveMessage) {
self::sendMsg(null, $dialog->id, 'text', ['text' => $leaveMessage], $user->userid);
}
}
}
}
@ -497,6 +521,8 @@ class WebSocketDialogMsg extends AbstractModel
}
switch ($data['type']) {
case 'text':
case 'word-chain':
case 'vote':
return $this->previewTextMsg($data['msg']['text'], $preserveHtml);
case 'record':
return "[语音]";
@ -772,6 +798,7 @@ class WebSocketDialogMsg extends AbstractModel
* - reply-98回复消息ID=98
* - update-99更新消息ID=99(标记修改)
* - change-99更新消息ID=99(不标记修改)
* - forward-99转发消息ID=99
* @param int $dialog_id 会话ID 聊天室ID
* @param string $type 消息类型
* @param array $msg 发送的消息
@ -812,6 +839,7 @@ class WebSocketDialogMsg extends AbstractModel
$update_id = preg_match("/^update-(\d+)$/", $action, $match) ? $match[1] : 0;
$change_id = preg_match("/^change-(\d+)$/", $action, $match) ? $match[1] : 0;
$reply_id = preg_match("/^reply-(\d+)$/", $action, $match) ? $match[1] : 0;
$forward_id = preg_match("/^forward-(\d+)-(\d+)$/", $action, $match) ? $match[2] : 0;
$sender = $sender === null ? User::userid() : $sender;
//
$dialog = WebSocketDialog::find($dialog_id);
@ -833,10 +861,10 @@ class WebSocketDialogMsg extends AbstractModel
if (empty($dialogMsg)) {
throw new ApiException('消息不存在');
}
if ($dialogMsg->type !== 'text') {
if ($dialogMsg->type !== 'text' && $dialogMsg->type !== 'vote') {
throw new ApiException('此消息不支持此操作');
}
if ($dialogMsg->userid != $sender) {
if ($dialogMsg->userid != $sender && $dialogMsg->type !== 'vote') {
throw new ApiException('仅支持修改自己的消息');
}
//
@ -862,6 +890,10 @@ class WebSocketDialogMsg extends AbstractModel
if ($reply_id && !self::whereId($reply_id)->increment('reply_num')) {
throw new ApiException('回复的消息不存在');
}
// 转发
if ($forward_id && !self::whereId($forward_id)->increment('forward_num')) {
throw new ApiException('转发的消息不存在');
}
//
$dialogMsg = self::createInstance([
'dialog_id' => $dialog_id,
@ -873,6 +905,8 @@ class WebSocketDialogMsg extends AbstractModel
'link' => $link,
'msg' => $msg,
'read' => 0,
'forward_id' => $forward_id,
'forward_show' => $forward_id ? $match[1] : 1,
]);
AbstractModel::transaction(function () use ($dialog, $dialogMsg) {
$dialog->last_at = Carbon::now();

View File

@ -687,6 +687,22 @@ class Base
return $string;
}
/**
* 递归处理数组
*
* @param string $callback 如:'intval''trim''addslashes''stripslashes''htmlspecialchars'
* @param array $array
* @return array
*/
public static function newArrayRecursive($callback, $array)
{
$func = function ($item) use (&$func, &$callback) {
return is_array($item) ? array_map($func, $item) : call_user_func($callback, $item);
};
return array_map($func, $array);
}
/**
* 重MD5加密
* @param $text

View File

@ -7,6 +7,7 @@ use App\Models\TaskWorker;
use App\Models\Tmp;
use App\Models\WebSocketTmpMsg;
use Carbon\Carbon;
use Illuminate\Support\Facades\File as SupportFile;
/**
* 删除过期临时数据任务
@ -102,10 +103,12 @@ class DeleteTmpTask extends AbstractTask
*/
case 'file_pack':
{
$path = storage_path('app/temp/download/');
$path = public_path('tmp/file/');
if (!SupportFile::exists($path)) {
return;
}
$dirIterator = new \RecursiveDirectoryIterator($path);
$iterator = new \RecursiveIteratorIterator($dirIterator);
foreach ($iterator as $file) {
if ($file->isFile()) {
$time = $file->getMTime();

View File

@ -38,12 +38,17 @@ class WebSocketDialogMsgTask extends AbstractTask
{
parent::__construct(...func_get_args());
$this->id = $id;
$this->ignoreFd = $ignoreFd === null ? Request::header('fd') : $ignoreFd;
$this->client = [
'version' => Base::headerOrInput('version'),
'language' => Base::headerOrInput('language'),
'platform' => Base::headerOrInput('platform'),
];
// 判断是否有Request方法兼容go协程请求
$this->ignoreFd = $ignoreFd;
$this->client = [];
if (method_exists(new Request,"header")) {
$this->ignoreFd = $ignoreFd === null ? Request::header('fd') : $ignoreFd;
$this->client = [
'version' => Base::headerOrInput('version'),
'language' => Base::headerOrInput('language'),
'platform' => Base::headerOrInput('platform'),
];
}
}
/**

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectPermissionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('project_permissions'))
return;
Schema::create('project_permissions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
$table->text('permissions')->nullable()->comment('权限');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_permissions');
}
}

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class WebSocketDialogMsgsAddForwardId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'forward_id')) {
$table->bigInteger('forward_id')->nullable()->default(0)->after('reply_id')->comment('转发ID');
$table->bigInteger('forward_num')->nullable()->default(0)->after('forward_id')->comment('被转发多少次');
$table->boolean('forward_show')->nullable()->default(1)->after('forward_num')->comment('是否显示转发的来源');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->dropColumn("forward_id");
$table->dropColumn("forward_num");
$table->dropColumn("forward_show");
});
}
}

View File

@ -150,7 +150,7 @@ services:
approve:
container_name: "dootask-approve-${APP_ID}"
image: "kuaifan/dooapprove:0.0.8"
image: "kuaifan/dooapprove:0.0.9"
environment:
TZ: "${TIMEZONE:-PRC}"
MYSQL_HOST: "${DB_HOST}"

View File

@ -480,3 +480,5 @@ Api接口文档
保存任务详情至文件失败
保存任务详情至文件失败,请重试
移动成功
不能重复投票

View File

@ -1252,6 +1252,7 @@ Markdown 格式发送
OKR管理
OKR结果
OKR结果分析
计划时间冲突提示
忽略并继续
你确定要清除缓存吗?
@ -1412,4 +1413,70 @@ APP推送
你确定取消待办吗?
取消成功
请等待打包完成
选择一个项目查看更多任务
首页
无相关数据
当前环境
权限设置
任务列权限
添加列
修改列
删除列
排序列
任务权限
修改任务
修改状态
移动任务
任务协助人
搜索项目名称
服务器版本过低,请升级服务器。
不显示原发送者信息
转发给:
留言
多选
@我的
移动前
移动后
状态
协助人
未变更移动项
接龙
参与接龙
发起接龙
发起接龙,参与接龙目前共(*)人
请输入接龙主题
请输入接龙内容
可填写接龙格式
重复内容将不再计入接龙结果
返回编辑
继续发送
接龙结果
选择群组发起接龙
来自
发起投票
投票结果
发起
请输入投票主题
请输入选项内容
允许多选
匿名投票
投票
匿名
实名
单选
多选
请选择后投票
立即投票
再次发送
再次发送投票?
结束投票
确定结束投票?
已发送
选择群组发起投票
以下为新消息

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#87D068;}
.st1{fill:#FFFFFF;}
</style>
<g>
<g>
<path class="st0" d="M36,48H12C5.4,48,0,42.6,0,36V12C0,5.4,5.4,0,12,0h24c6.6,0,12,5.4,12,12v24C48,42.6,42.6,48,36,48z"/>
</g>
<g>
<path class="st1" d="M34.4,33.5H25l9.3-9.3c1.2-1.2,1.2-3.3,0-4.5l-6.8-6.8c-1.2-1.2-3.3-1.2-4.5,0L12.9,23.1
c-1.2,1.2-1.2,3.3,0,4.5l6,6h-6c-0.5,0-0.9,0.4-0.9,0.9c0,0.5,0.4,0.9,0.9,0.9H22c0,0,0,0,0,0c0,0,0,0,0,0h12.4
c0.5,0,0.9-0.4,0.9-0.9C35.3,33.9,34.9,33.5,34.4,33.5z M17.1,25.5c0.3-0.3,0.9-0.3,1.2,0l3.4,3.4c0.1,0.1,0.3,0.1,0.4,0l6.7-6.7
c0.3-0.3,0.9-0.3,1.2,0c0.3,0.3,0.3,0.9,0,1.2l-6.7,6.7c-0.4,0.4-0.9,0.6-1.5,0.6c-0.5,0-1.1-0.2-1.5-0.6l-3.4-3.4
C16.8,26.4,16.8,25.9,17.1,25.5L17.1,25.5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#87D068;}
.st1{clip-path:url(#SVGID_00000018941778426702510620000006481262296390689922_);}
.st2{fill:#FFFFFF;}
</style>
<g>
<g>
<path class="st0" d="M36,48H12C5.4,48,0,42.6,0,36V12C0,5.4,5.4,0,12,0h24c6.6,0,12,5.4,12,12v24C48,42.6,42.6,48,36,48z"/>
</g>
<g>
<defs>
<rect id="SVGID_1_" x="12" y="12" width="24" height="24"/>
</defs>
<clipPath id="SVGID_00000141444668484078751200000003089482387513471677_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000141444668484078751200000003089482387513471677_);">
<g>
<path class="st2" d="M28.5,21c-4.1,0-7.5,3.4-7.5,7.5c0,4.1,3.4,7.5,7.5,7.5c4.1,0,7.5-3.4,7.5-7.5C36,24.4,32.6,21,28.5,21z
M30.6,29.4h-1.2v1.2c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.9-0.4-0.9-0.9v-1.2h-1.2c-0.5,0-0.9-0.4-0.9-0.9c0-0.5,0.4-0.9,0.9-0.9
h1.2v-1.2c0-0.5,0.4-0.9,0.9-0.9c0.5,0,0.9,0.4,0.9,0.9v1.2h1.2c0.5,0,0.9,0.4,0.9,0.9C31.5,29,31,29.4,30.6,29.4z"/>
</g>
<g>
<path class="st2" d="M15.3,23.9c0-4.8,3.9-8.7,8.7-8.7c0.6,0,1.2,0.1,1.8,0.2c-1.3-2.1-3.7-3.4-6.3-3.4c-4.1,0-7.5,3.4-7.5,7.5
c0,2.7,1.4,5,3.5,6.3C15.3,25.2,15.3,24.6,15.3,23.9z"/>
</g>
<g>
<path class="st2" d="M19.8,28.5c0-4.8,3.9-8.7,8.7-8.7c0.6,0,1.3,0.1,1.9,0.2c-1.3-2.1-3.7-3.6-6.4-3.6c-4.1,0-7.5,3.4-7.5,7.5
c0,2.7,1.4,5,3.5,6.3C19.8,29.7,19.8,29.1,19.8,28.5z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

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

@ -1 +1 @@
["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""]
["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""]

View File

@ -470,6 +470,11 @@ img {
grid-template-columns: 480px 266px 266px 266px;
}
.grid-5 {
display: grid !important;
grid-template-columns: 336px 236px 236px 236px 236px;
}
/* padding样式 */
.pl-26 {
padding-left: 26px;

View File

@ -139,18 +139,26 @@
}
.plans-ol-item-h6{
color: var(--txt-4ca5);
line-height: normal;
}
.plans-ol-item-icon{
display: block;
height: 24px;
}
.plans-ol-item-icon2{
display: block;
height: 24px;
opacity: 0.5;
}
.plans-ul-b{
display: grid;
grid-template-columns: 506px 266px 266px 266px;
}
.plans-ul-b-item-btn{
display: inline-block;
width: 218px;
width: 160px;
}
.flex-s-c{
display: flex;
@ -202,6 +210,13 @@
box-sizing: border-box;
box-shadow: 0 4px 12px rgba(0,0,0,.15);
}
@media screen and (max-width: 600px) {
.BulletBox1{
width: 80%;
}
}
.BulletBox1 .top{
width: 382px;
line-height: 20px;
@ -225,13 +240,12 @@
color: rgb(96, 98, 102);
}
.BulletBox1 .bottom{
width: 370px;
height: 34px;
display: flex;
justify-content: end;
padding: 20px 30px 22px 30px;
position: relative;
}
.BulletBox1 .bottom .BulletBox1Btn{
position: absolute;
min-width: 100px;
right: 20px;
height: 34px;
@ -243,3 +257,11 @@
border-radius: 8px;
padding: 5px;
}
.price-ceiling {
position: sticky;
top: 80px;
background-color: #fff;
border-bottom: 1px solid var(--border-color);
z-index: 9999;
}

View File

@ -80,6 +80,9 @@
.grid-4{
grid-template-columns: 32vw 18vw 18vw 18vw !important;
}
.grid-5{
grid-template-columns: 26vw 18.5vw 18.5vw 18.5vw 18.5vw !important;
}
.plans-ul-b{
grid-template-columns: calc(34vw + 4px) 18vw 18vw 18vw !important;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1026_5003)">
<path d="M4 4L15.4141 15.4141" stroke="#727570" stroke-width="2" stroke-linecap="round"/>
<path d="M15.4141 4L4 15.4141" stroke="#727570" stroke-width="2" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_1026_5003">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 429 B

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ const isElectron = !!(window && window.process && window.process.type);
const isEEUiApp = window && window.navigator && /eeui/i.test(window.navigator.userAgent);
import microappInit from "./microapp"
import {switchLanguage as $L} from "./language";
import {switchLanguage as $L, setLanguage, getLanguage} from "./language";
import './functions/common'
import './functions/eeui'
@ -91,17 +91,18 @@ if (!isElectron && !isEEUiApp) {
ViewUI.LoadingBar._load = true;
ViewUI.LoadingBar.start();
}, 300)
//
if (to.query?.theme) {
store.dispatch("setTheme", typeof to.query?.theme == 'string' ? to.query?.theme : to.query?.theme[0])
}
if (to.query?.lang) {
let lang = typeof to.query?.lang == 'string' ? to.query?.lang : to.query?.lang[0]
if (window.localStorage.getItem("__language:type__") != lang) {
window.localStorage.setItem("__language:type__", to.query?.lang);
window.location.reload();
return false;
if(lang && lang != getLanguage()){
setLanguage(lang, true)
return;
}
}
//
next();
});
router.afterEach(() => {
@ -157,6 +158,7 @@ $A.Platform = "web";
$A.isMainElectron = false;
$A.isSubElectron = false;
$A.isEEUiApp = isEEUiApp;
$A.isElectron = isElectron;
$A.openLog = false;
if (isElectron) {
$A.Electron = electron;

View File

@ -163,7 +163,9 @@ export default {
}
const documentKey = this.documentKey();
if (documentKey && documentKey.then) {
documentKey.then(this.loadFile);
documentKey.then(this.loadFile).catch(({msg})=>{
$A.modalError({content: msg});
});
} else {
this.loadFile();
}
@ -280,6 +282,14 @@ export default {
}
//
}).catch(({msg}) => {
if (msg.indexOf("404 not found") !== -1) {
$A.modalInfo({
language: false,
title: '版本过低',
content: '服务器版本过低,请升级服务器。',
})
return;
}
$A.modalError({content: msg});
});
})

View File

@ -19,15 +19,20 @@
<!-- 顶部 -->
<template #header>
<div v-if="isFullscreen" class="user-modal-header">
<div class="user-modal-close" @click="showModal=false">{{$L('关闭')}}</div>
<div class="user-modal-close" @click="[ !multipleChoice && showMultiple ? showMultiple=false : showModal=false]">
{{ !multipleChoice && showMultiple ? $L('取消') : $L('关闭') }}
</div>
<div class="user-modal-title"><span>{{localTitle}}</span></div>
<div class="user-modal-submit" @click="onSubmit">
<div v-if="showMultiple" class="user-modal-submit" @click="onSubmit(1)">
<div v-if="submittIng > 0" class="submit-loading"><Loading /></div>
{{$L('确定')}}
<template v-if="selects.length > 0">
({{selects.length}}<span v-if="multipleMax">/{{multipleMax}}</span>)
</template>
</div>
<div v-else-if="!forcedRadio" class="user-modal-submit" @click="showMultiple = true">
{{$L('多选')}}
</div>
</div>
<div v-else class="ivu-modal-header-inner">{{localTitle}}</div>
</template>
@ -37,7 +42,7 @@
<!-- 搜索 -->
<div class="user-modal-search">
<Scrollbar ref="selected" class="search-selected" v-if="selects.length > 0" enable-x :enable-y="false">
<Scrollbar ref="selected" class="search-selected" v-if="showMultiple && selects.length > 0" enable-x :enable-y="false">
<ul>
<li v-for="item in formatSelect(selects)" :data-id="item.userid" @click.stop="onRemoveItem(item.userid)">
<template v-if="item.type=='group'">
@ -76,15 +81,19 @@
v-for="item in lists"
:class="selectClass(item.userid_list)"
@click="onSelectProject(item.userid_list)">
<Icon class="user-modal-icon" :type="selectIcon(item.userid_list)" />
<template v-if="showMultiple">
<Icon class="user-modal-icon" :type="selectIcon(item.userid_list)" />
</template>
<div class="user-modal-avatar">
<i class="taskfont icon-avatar">&#xe6f9;</i>
<div class="project-name">
<div class="label">{{item.name}}</div>
<div class="subtitle">
{{item.userid_list.length}} {{$L('项目成员')}}
<em class="all">{{$L('已全选')}}</em>
<em class="some">{{$L('已选部分')}}</em>
<template v-if="showMultiple">
<em class="all">{{$L('已全选')}}</em>
<em class="some">{{$L('已选部分')}}</em>
</template>
</div>
</div>
</div>
@ -93,7 +102,7 @@
<!-- 会员会话 -->
<ul v-else>
<li
v-if="showSelectAll"
v-if="showMultiple && showSelectAll"
:class="selectClass('all')"
@click="onSelectAll">
<Icon class="user-modal-icon" :type="selectIcon('all')" />
@ -106,8 +115,10 @@
disabled: isUncancelable(item.userid) || isDisabled(item.userid)
}"
@click="onSelectItem(item)">
<Icon v-if="selects.includes(item.userid)" class="user-modal-icon" type="ios-checkmark-circle" />
<Icon v-else class="user-modal-icon" type="ios-radio-button-off" />
<template v-if="showMultiple">
<Icon v-if="selects.includes(item.userid)" class="user-modal-icon" type="ios-checkmark-circle" />
<Icon v-else class="user-modal-icon" type="ios-radio-button-off" />
</template>
<div v-if="item.type=='group'" class="user-modal-avatar">
<EAvatar v-if="item.avatar" class="img-avatar" :src="item.avatar" :size="40"></EAvatar>
<i v-else-if="item.group_type=='department'" class="taskfont icon-avatar department">&#xe75c;</i>
@ -134,12 +145,57 @@
<!-- 底部 -->
<template #footer>
<Button type="primary" :loading="submittIng > 0" @click="onSubmit">
<Button v-if="!forcedRadio && !multipleChoice && showMultiple" @click="showMultiple = false">
{{$L('取消')}}
</Button>
<Button v-if="!forcedRadio && showMultiple" type="primary" :loading="submittIng > 0" @click="onSubmit(1)">
{{$L('确定')}}
<template v-if="selects.length > 0">
({{selects.length}}<span v-if="multipleMax">/{{multipleMax}}</span>)
</template>
</Button>
<Button v-else-if="!forcedRadio" type="primary" @click="showMultiple = true">
{{$L('多选')}}
</Button>
</template>
</Modal>
<!-- 二次确认 -->
<Modal
v-model="showAffirmModal"
:title="twiceAffirmTitle"
class-name="common-user-select-modal twice-affirm-modal"
:mask-closable="false"
width="420">
<div class="user-modal-search twice-affirm">
<Scrollbar class="search-selected" v-if="selects?.length > 0" enable-x :enable-y="false">
<ul>
<li v-for="item in formatSelect(selects)" :data-id="item.userid">
<div v-if="item.type=='group'" class="user-modal-avatar">
<EAvatar v-if="item.avatar" class="img-avatar" :src="item.avatar" :size="32"></EAvatar>
<i v-else-if="item.group_type=='department'" class="taskfont icon-avatar department">&#xe75c;</i>
<i v-else-if="item.group_type=='project'" class="taskfont icon-avatar project">&#xe6f9;</i>
<i v-else-if="item.group_type=='task'" class="taskfont icon-avatar task">&#xe6f4;</i>
<i v-else-if="item.group_type=='okr'" class="taskfont icon-avatar task">&#xe6f4;</i>
<Icon v-else class="icon-avatar" type="ios-people" />
<div v-if="selects?.length == 1" class="avatar-name">
<span>{{item.name}}</span>
</div>
</div>
<UserAvatar v-else :userid="item.userid" :size="32" :show-name="selects?.length == 1" tooltip-disabled />
</li>
</ul>
</Scrollbar>
</div>
<div class="twice-affirm-body-extend">
<slot name="twice-affirm-body-extend"></slot>
</div>
<template #footer>
<slot name="twice-affirm-footer-extend"></slot>
<Button type="primary" :loading="submittIng > 0" @click="onSubmit(2)">
{{$L('确定')}}
<template v-if="selects.length > 0">({{selects.length}})</template>
</Button>
</template>
</Modal>
</div>
@ -260,22 +316,52 @@ export default {
default: false
},
//
submitBtnTwoText: {
type: String,
default: ''
},
//
twiceAffirm: {
type: Boolean,
default: false
},
//
twiceAffirmTitle: {
type: String,
default: ''
},
//
multipleChoice: {
type: Boolean,
default: true
},
//
forcedRadio: {
type: Boolean,
default: false
},
//
group: {
type: Boolean,
default: false
},
//
beforeSubmit: Function
},
data() {
return {
switchItems: [
{key: 'recent', label: '最近'},
{key: 'contact', label: '通讯录'},
{key: 'project', label: '项目成员'},
{ key: 'recent', label: '最近' },
{ key: 'contact', label: '通讯录' },
{ key: 'project', label: '项目成员' },
],
switchActive: 'recent',
loadIng: 0, //
waitIng: 0, //
submittIng: 0, //
loadIng: 0,
waitIng: 0,
submittIng: 0,
values: [],
selects: [],
@ -287,7 +373,10 @@ export default {
searchKey: null,
searchCache: [],
}
showAffirmModal: false,
showMultiple: true,
};
},
watch: {
value: {
@ -305,7 +394,7 @@ export default {
isWhole: {
handler(value) {
if (value) {
if (value || this.group) {
this.switchActive = 'recent'
} else {
this.switchActive = 'contact'
@ -317,10 +406,14 @@ export default {
showModal(value) {
if (value) {
this.searchBefore()
this.showMultiple = this.multipleChoice
if(this.forcedRadio){
this.showMultiple = false
}
} else {
this.searchKey = ""
}
this.$emit("on-show-change",value,this.values)
this.$emit("on-show-change", value, this.values)
},
searchKey() {
@ -336,15 +429,15 @@ export default {
'cacheDialogs',
]),
isFullscreen({windowWidth}) {
isFullscreen({ windowWidth }) {
return windowWidth < 576
},
isWhole({projectId, noProjectId, dialogId}) {
return projectId === 0 && noProjectId === 0 && dialogId === 0
isWhole({ projectId, noProjectId, dialogId }) {
return projectId === 0 && noProjectId === 0 && dialogId === 0 && !this.group
},
lists({switchActive, searchKey, recents, contacts, projects}) {
lists({ switchActive, searchKey, recents, contacts, projects }) {
switch (switchActive) {
case 'recent':
if (searchKey) {
@ -363,7 +456,7 @@ export default {
return []
},
isSelectAll({lists, selects}) {
isSelectAll({ lists, selects }) {
return lists.length > 0 && lists.filter(item => selects.includes(item.userid)).length === lists.length;
},
@ -375,14 +468,14 @@ export default {
}
},
addStyle({avatarSize}) {
addStyle({ avatarSize }) {
return {
width: avatarSize + 'px',
height: avatarSize + 'px',
}
},
localTitle({title}) {
localTitle({ title }) {
if (title === undefined) {
return this.$L('选择会员')
} else {
@ -390,7 +483,7 @@ export default {
}
},
localPlaceholder({placeholder}) {
localPlaceholder({ placeholder }) {
if (placeholder === undefined) {
return this.$L('搜索')
} else {
@ -466,6 +559,9 @@ export default {
searchRecent() {
this.recents = this.cacheDialogs.filter(dialog => {
if(this.group && dialog.type != 'group'){
return false
}
if (dialog.name === undefined || dialog.dialog_delete === 1) {
return false
}
@ -481,7 +577,7 @@ export default {
return b.todo_num - a.todo_num;
}
return $A.Date(b.last_at) - $A.Date(a.last_at);
}).map(({id, name, type, group_type, avatar, dialog_user}) => {
}).map(({ id, name, type, group_type, avatar, dialog_user }) => {
return {
name,
type,
@ -521,18 +617,18 @@ export default {
},
take: 50
},
}).then(({data}) => {
data = data.map(item => Object.assign(item, {type: 'user'}))
}).then(({ data }) => {
data = data.map(item => Object.assign(item, { type: 'user' }))
this.contacts = data
//
const index = this.searchCache.findIndex(item => item.key == key);
const tmpData = {type: 'contact', key, data, time: $A.Time()};
const tmpData = { type: 'contact', key, data, time: $A.Time() };
if (index > -1) {
this.searchCache.splice(index, 1, tmpData)
} else {
this.searchCache.push(tmpData)
}
}).catch(({msg}) => {
}).catch(({ msg }) => {
this.contacts = []
$A.messageWarning(msg)
}).finally(_ => {
@ -568,18 +664,18 @@ export default {
getuserid: 'yes',
getstatistics: 'no'
},
}).then(({data}) => {
data = data.data.map(item => Object.assign(item, {type: 'project'}))
this.projects = data
}).then(({ data }) => {
data = data.data.map(item => Object.assign(item, { type: 'project' }))
this.projects = data;
//
const index = this.searchCache.findIndex(item => item.key == key);
const tmpData = {type: 'project', key, data, time: $A.Time()};
const tmpData = { type: 'project', key, data, time: $A.Time() };
if (index > -1) {
this.searchCache.splice(index, 1, tmpData)
} else {
this.searchCache.push(tmpData)
}
}).catch(({msg}) => {
}).catch(({ msg }) => {
this.projects = []
$A.messageWarning(msg)
}).finally(_ => {
@ -590,7 +686,7 @@ export default {
},
onSelection() {
if(this.disable){
if (this.disable) {
return
}
this.$nextTick(_ => {
@ -618,7 +714,10 @@ export default {
})
},
onSelectItem({userid}) {
onSelectItem({ userid }) {
if (!this.showMultiple) {
this.selects = []
}
if (this.selects.includes(userid)) {
if (this.isUncancelable(userid)) {
return
@ -635,12 +734,18 @@ export default {
this.selects.push(userid)
//
this.$nextTick(() => {
$A.scrollIntoViewIfNeeded(this.$refs.selected.querySelector(`li[data-id="${userid}"]`))
$A.scrollIntoViewIfNeeded(this.$refs.selected?.querySelector(`li[data-id="${userid}"]`))
})
}
if(!this.showMultiple){
this.onSubmit(1)
}
},
onSelectProject(userid_list) {
if (!this.showMultiple) {
this.selects = []
}
switch (this.selectIcon(userid_list)) {
case 'ios-checkmark-circle':
//
@ -664,6 +769,9 @@ export default {
}
break;
}
if (!this.showMultiple) {
this.onSubmit(1)
}
},
onRemoveItem(userid) {
@ -673,17 +781,28 @@ export default {
this.selects = this.selects.filter(value => value != userid)
},
onSubmit() {
onSubmit(index) {
if (this.submittIng > 0) {
return
}
const clone = $A.cloneJSON(this.values)
this.values = $A.cloneJSON(this.selects)
//
if (index !=2 && this.twiceAffirm) {
if (this.values.length < 1) {
$A.messageError("请选择对话或成员")
return
}
this.showAffirmModal = true
return
}
//
this.$emit('input', this.values)
this.$emit('onSubmit', this.values)
if (!this.beforeSubmit) {
this.showModal = false
this.showAffirmModal = false
return
}
const before = this.beforeSubmit();
@ -691,6 +810,7 @@ export default {
this.submittIng++
before.then(() => {
this.showModal = false
this.showAffirmModal = false
}).catch(() => {
this.values = clone
this.$emit('input', this.values)
@ -699,6 +819,7 @@ export default {
})
} else {
this.showModal = false
this.showAffirmModal = false
}
},
}

View File

@ -735,8 +735,9 @@
// 处理内容连接
if (/https*:\/\//.test(text)) {
const urlMatch = $.apiUrl('../').match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n]+)/im);
// const theme = window.localStorage.getItem("__theme:mode__")
// const lang = window.localStorage.getItem("__language:type__")
const isMentionFile = text.indexOf('class="mention file"') !== -1 && ($A.isEEUiApp || $A.isElectron)
const theme = isMentionFile ? $A.dark.isDarkEnabled() ? 'dark' : 'light' : '';
const lang = isMentionFile ? window.localStorage.getItem("__language:type__") : ''
text = text.split(/(<[^>]*>)/g).map(string => {
if (string && !/<[^>]*>/.test(string)) {
string = string.replace(/(^|[^'"])((https*:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+))/g, "$1<a href=\"$2\" target=\"_blank\">$2</a>")
@ -745,8 +746,8 @@
const href = string.match(/href="([^"]+)"/)?.[1] || ''
if (urlMatch?.[1] && href.indexOf(urlMatch[1]) !== -1) {
const searchParams = new URLSearchParams()
// href.indexOf("theme=") === -1 && searchParams.append('theme', theme);
// href.indexOf("lang=") === -1 && searchParams.append('lang', lang);
theme && href.indexOf("theme=") === -1 && searchParams.append('theme', theme);
lang && href.indexOf("lang=") === -1 && searchParams.append('lang', lang);
const prefix = searchParams.toString() ? (href.indexOf("?") === -1 ? '?' : '&') : '';
string = string.replace(/(href="[^"]*)/g, '$1' + prefix + searchParams.toString())
}
@ -819,6 +820,10 @@
switch (data.type) {
case 'text':
return $A.getMsgTextPreview(data.msg.text, imgClassName)
case 'word-chain':
return `[${$A.L('接龙')}]` + $A.getMsgTextPreview(data.msg.text, imgClassName)
case 'vote':
return `[${$A.L('投票')}]` + $A.getMsgTextPreview(data.msg.text, imgClassName)
case 'record':
return `[${$A.L('语音')}]`
case 'meeting':

View File

@ -30,20 +30,33 @@ function addLanguage(data) {
/**
* 设置语言
* @param language
* @param silence
*/
function setLanguage(language) {
function setLanguage(language, silence = false) {
if (language === undefined) {
return
}
$A.modalConfirm({
content: '切换语言需要刷新后生效,是否确定刷新?',
cancelText: '取消',
okText: '确定',
onOk: () => {
window.localStorage.setItem("__language:type__", language)
$A.reloadUrl()
}
})
if(silence){
window.localStorage.setItem("__language:type__", language)
$A.reloadUrl()
}else{
$A.modalConfirm({
content: '切换语言需要刷新后生效,是否确定刷新?',
cancelText: '取消',
okText: '确定',
onOk: () => {
window.localStorage.setItem("__language:type__", language)
$A.reloadUrl()
}
})
}
}
/**
* 获取最新语言
*/
function getLanguage() {
return utils.getLanguage();
}
/**
@ -111,4 +124,4 @@ function switchLanguage(text) {
return text
}
export { languageType, languageList, addLanguage, setLanguage, switchLanguage }
export { languageType, languageList, addLanguage, setLanguage, getLanguage, switchLanguage }

View File

@ -188,6 +188,19 @@
</div>
</Modal>
<!-- 发起接龙 -->
<UserSelect
ref="wordChainAndVoteRef"
v-model="sendData"
:multiple-max="50"
:title="sendType == 'vote' ? $L('选择群组发起投票') : $L('选择群组发起接龙')"
:before-submit="goWordChainAndVote"
:show-select-all="false"
:forced-radio="true"
:group="true"
show-dialog
module/>
</div>
</template>
@ -278,6 +291,9 @@ export default {
scanLoginShow: false,
scanLoginLoad: false,
scanLoginCode: '',
//
sendData: [],
sendType: '',
}
},
activated() {
@ -302,43 +318,52 @@ export default {
},
methods: {
initList() {
const applyList = [
{value: "approve", label: "审批中心"},
{value: "okr", label: "OKR管理"},
{value: "report", label: "工作报告"},
{value: "robot", label: "AI机器人"},
{value: "signin", label: "签到"},
{value: "meeting", label: "会议"}
let applyList = [
{ value: "approve", label: "审批中心", sort: 3 },
{ value: "report", label: "工作报告", sort: 5 },
{ value: "okr", label: "OKR管理", sort: 4 },
{ value: "robot", label: "AI机器人", sort: 6 },
{ value: "signin", label: "签到", sort: 7 },
{ value: "meeting", label: "会议", sort: 8 },
{ value: "word-chain", label: "接龙", sort: 9 },
{ value: "vote", label: "投票", sort: 10 },
];
// wap
if (this.windowOrientation == 'landscape') {
//
applyList.push({value: "scan", label: "扫一扫", show: $A.isEEUiApp})
applyList.push({ value: "scan", label: "扫一扫", show: $A.isEEUiApp, sort: 13 })
} else {
//
applyList.unshift(...[
{value: "calendar", label: "日历"},
{value: "file", label: "文件"}
])
applyList.push(...[
{value: "addProject", label: "创建项目"},
{value: "addTask", label: "添加任务"},
{value: "scan", label: "扫一扫", show: $A.isEEUiApp},
{value: "setting", label: "设置"}
{ value: "calendar", label: "日历", sort: 1 },
{ value: "file", label: "文件", sort: 2 },
{ value: "addProject", label: "创建项目", sort: 11 },
{ value: "addTask", label: "添加任务", sort: 12 },
{ value: "scan", label: "扫一扫", show: $A.isEEUiApp, sort: 13 },
{ value: "setting", label: "设置", sort: 14 }
])
}
//
const adminApplyList = !this.userIsAdmin ? [] : [
{value: "okrAnalyze", label: "OKR结果"},
{value: "ldap", label: "LDAP"},
{value: "mail", label: "邮件通知"},
{value: "appPush", label: "APP推送"},
{value: "allUser", label: "团队管理"}
let adminApplyList = !this.userIsAdmin ? [] : [
{ value: "okrAnalyze", label: "OKR结果", sort: 15 },
{ value: "ldap", label: "LDAP", sort: 16 },
{ value: "mail", label: "邮件通知", sort: 17 },
{ value: "appPush", label: "APP推送", sort: 18 },
{ value: "allUser", label: "团队管理", sort: 19 }
].map((h) => {
h.type = 'admin';
return h;
});
//
this.applyList = [...applyList, ...adminApplyList];
this.applyList = [...applyList, ...adminApplyList].sort((a, b) => {
if (a.sort < b.sort) {
return -1;
} else if (a.sort > b.sort) {
return 1;
} else {
return 0;
}
});
},
getLogoPath(name) {
name = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
@ -402,6 +427,12 @@ export default {
case 'scan':
$A.eeuiAppScan(this.scanResult);
return;
case 'word-chain':
case 'vote':
this.sendData = [];
this.sendType = item.value;
this.$refs.wordChainAndVoteRef.onSelection()
return;
}
this.$emit("on-click", item.value)
},
@ -520,7 +551,7 @@ export default {
this.scanLoginLoad = false
});
},
//
//
openDetail(desc){
$A.modalInfo({
content: desc,
@ -541,6 +572,26 @@ export default {
})
},
});
},
//
goWordChainAndVote(){
const dialog_id = Number(this.sendData[0].replace('d:', ''))
if(this.windowPortrait){
this.$store.dispatch("openDialog", dialog_id ).then(() => {
this.$store.state[ this.sendType == 'word-chain' ?'dialogDroupWordChain' : 'dialogGroupVote'] = {
type: 'create',
dialog_id: dialog_id
}
})
}else{
this.goForward({ name: 'manage-messenger', params: { dialog_id: dialog_id}});
setTimeout(()=>{
this.$store.state[ this.sendType == 'word-chain' ?'dialogDroupWordChain' : 'dialogGroupVote'] = {
type: 'create',
dialog_id: dialog_id
}
},100)
}
}
}
}

View File

@ -4,7 +4,7 @@
<div class="calendar-head">
<div class="calendar-titbox">
<div class="calendar-title">
<div class="common-nav-back portrait" @click="goForward({name: 'manage-application'},true)"><i class="taskfont">&#xe676;</i></div>
<div class="common-nav-back" @click="goForward({name: 'manage-application'},true)"><i class="taskfont">&#xe676;</i></div>
<h1>{{rangeText}}</h1>
</div>
<ButtonGroup class="calendar-arrow" size="small">
@ -44,10 +44,6 @@
</template>
<script>
import 'tui-date-picker/dist/tui-date-picker.css';
import 'tui-time-picker/dist/tui-time-picker.css';
import 'tui-calendar-hi/dist/tui-calendar-hi.css'
import {mapState, mapGetters} from "vuex";
import Calendar from "./components/Calendar";
import moment from "moment";
@ -363,7 +359,10 @@ export default {
if (!data) {
return;
}
if (changes.start || changes.end) {
if(changes?.start?.getTime() == schedule?.start?.getTime() && changes?.end?.getTime() == schedule?.end?.getTime()){
return;
}
if (changes?.start || changes?.end) {
const cal = this.$refs.cal.getInstance();
cal.updateSchedule(schedule.id, schedule.calendarId, changes);
//

View File

@ -1,7 +1,11 @@
<template>
<div ref="tuiCalendar" class="calendar-wrapper"></div>
</template>
<script>
import 'tui-date-picker/dist/tui-date-picker.css';
import 'tui-time-picker/dist/tui-time-picker.css';
import 'tui-calendar-hi/dist/tui-calendar-hi.css'
import Calendar from 'tui-calendar-hi';
export default {
@ -148,7 +152,13 @@ export default {
},
isReadOnly(newValue) {
this.calendarInstance.setOptions({isReadOnly: newValue});
}
},
windowPortrait: {
handler(v) {
this.resetRender()
},
immediate: true
},
},
mounted() {
this.calendarInstance = new Calendar(this.$refs.tuiCalendar, {
@ -170,10 +180,13 @@ export default {
});
this.addEventListeners();
this.reflectSchedules();
//
window.addEventListener('resize',this.resetRender);
},
beforeDestroy() {
this.calendarInstance.off();
this.calendarInstance.destroy();
window.removeEventListener('resize',this.resetRender);
},
methods: {
addEventListeners() {
@ -193,8 +206,10 @@ export default {
return this.calendarInstance;
},
resetRender() {
this.calendarInstance.clear();
this.reflectSchedules();
if(this.calendarInstance){
this.calendarInstance.clear();
this.reflectSchedules();
}
},
invoke(methodName, ...args) {
let result;

View File

@ -102,6 +102,14 @@
<i class="taskfont">&#xe690;</i>
{{$L('匿名消息')}}
</div>
<div v-if="dialogData.type == 'group'" class="chat-input-popover-item" @click="onToolbar('word-chain')">
<i class="taskfont">&#xe807;</i>
{{$L('发起接龙')}}
</div>
<div v-if="dialogData.type == 'group'" class="chat-input-popover-item" @click="onToolbar('vote')">
<i class="taskfont">&#xe806;</i>
{{$L('发起投票')}}
</div>
<div class="chat-input-popover-item" @click="onToolbar('full')">
<i class="taskfont">&#xe6a7;</i>
{{$L('全屏输入')}}
@ -1224,6 +1232,21 @@ export default {
case 'anon':
this.$emit('on-more', action)
break;
case 'word-chain':
this.$store.state.dialogDroupWordChain = {
type: 'create',
dialog_id: this.dialogId
}
break;
case 'vote':
this.$store.state.dialogGroupVote = {
type: 'create',
dialog_id: this.dialogId
}
break;
}
},

View File

@ -0,0 +1,225 @@
<template>
<Modal class-name="dialog-droup-word-chain"
v-model="show"
:mask-closable="false"
:title="dialogGroupVote.type == 'create' ? $L('发起投票') : $L('投票结果')"
:closable="!isFullscreen"
:fullscreen="isFullscreen"
:footer-hide="isFullscreen">
<!-- 顶部 -->
<template #header>
<div v-if="isFullscreen" class="chain-modal-header">
<div class="chain-modal-close" @click="show = false">
{{ $L('取消') }}
</div>
<div class="chain-modal-title">
{{ dialogGroupVote.type == 'create' ? $L('发起投票') : $L('投票结果') }}
</div>
<div class="chain-modal-submit" :class="{'disabled': !isEdit}" @click="onSend" >
<div v-if="loadIng > 0" class="submit-loading"><Loading /></div>
{{$L('发送')}}
</div>
</div>
</template>
<template #close>
<i class="ivu-icon ivu-icon-ios-close"></i>
</template>
<div ref="wordChainBodyRef" class="word-chain-body">
<div class="source" v-if="dialogGroupVote.type == 'create'">
{{$L('来自')}}
<span>{{ dialog.name }}</span>
</div>
<div class="initiate">
<span>{{ $L('由') }}</span>
<UserAvatar :userid="createId" :size="22" :showName="true" tooltipDisabled/>
<span> {{ $L('发起') }}</span>
</div>
<div class="textarea">
<Input ref="wordChainTextareaRef"
v-model="value"
type="textarea"
:placeholder="$L('请输入投票主题')"
:autosize="{minRows: 3,maxRows: 5}"
:disabled="dialogGroupVote.type != 'create'" />
</div>
<ul ref="wordChainListRef">
<li v-for="(item,index) in list">
<i class="taskfont" :class="{'disabled': list.length <= 2}" @click="onDel(index)">&#xe680;</i>
<Input v-model="item.text" :placeholder="$L('请输入选项内容')"/>
</li>
<li class="add">
<i class="taskfont" @click="onAdd">&#xe78c;</i>
</li>
</ul>
<div class="switch-row" v-if="dialogGroupVote.type == 'create'">
<span class="label">{{ $L('允许多选') }}</span>
<iSwitch v-model="multiple" :true-value="1" :false-value="0"/>
</div>
<div class="switch-row" v-if="dialogGroupVote.type == 'create'">
<span class="label">{{ $L('匿名投票') }}</span>
<iSwitch v-model="anonymous" :true-value="1" :false-value="0"/>
</div>
</div>
<div slot="footer">
<Button type="default" @click="show=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="loadIng > 0" @click="onSend" :disabled="!isEdit">{{$L('发送')}}</Button>
</div>
</Modal>
</template>
<script>
import {mapState} from "vuex";
export default {
name: 'DialogGroupVote',
data() {
return {
show: false,
createId: 0,
value: "",
list: [],
multiple: 0,
anonymous: 0,
oldData: '',
loadIng: 0,
}
},
computed: {
...mapState(['dialogGroupVote', 'userInfo', 'dialogMsgs', 'cacheDialogs']),
isFullscreen({ windowWidth }) {
return windowWidth < 576;
},
allList(){
const msg = this.dialogGroupVote.msgData?.msg || {};
let list = JSON.parse(JSON.stringify(msg.list || []));
this.dialogMsgs.filter(h=>{
return h.type == "word-chain" && h.msg?.uuid == msg.uuid
}).forEach((h)=>{
(h.msg.list || []).forEach(k=>{
if(list.map(j=>j.id).indexOf(k.id) == -1){
list.push(k)
}
})
});
return list;
},
isEdit(){
return this.oldData != JSON.stringify(this.list);
},
dialog(){
return this.cacheDialogs.find(h=>h.id == this.dialogGroupVote.dialog_id) || {}
},
},
watch: {
show(val){
if(!val){
this.value = "";
this.list = [];
}else{
if(this.dialogGroupVote.type == 'create'){
this.$nextTick(()=>{
this.$refs.wordChainTextareaRef.focus()
})
}
this.scrollTo();
}
},
dialogGroupVote(data) {
if(data.type == 'create' && data.dialog_id){
this.show = true;
this.createId = this.userId;
this.list.push({
id: Date.now(),
text: ""
});
this.list.push({
id: Date.now() + 1,
text: ""
});
}
if(data.type == 'participate' && data.dialog_id && data.msgData){
this.show = true;
this.createId = data.msgData.msg.userid;
this.value = data.msgData.msg.text;
this.list = this.allList;
this.oldData = JSON.stringify(this.list);
}
}
},
methods: {
onAdd(){
this.list.push({
id: Date.now(),
text: "",
});
this.scrollTo();
},
onDel(index){
if( this.list.length > 2 ){
this.list.splice(index, 1);
}
},
scrollTo(){
this.$nextTick(()=>{
this.$refs.wordChainListRef.scrollTo(0, 99999);
});
},
onSend() {
if(!this.isEdit){
return;
}
//
if(!this.value){
$A.messageError("请输入投票主题");
return;
}
if(this.list.find(h=> !h.text)){
$A.messageError("请输入选项内容");
return;
}
//
this.loadIng++;
this.$store.dispatch("call", {
url: 'dialog/msg/vote',
method: 'post',
data: {
dialog_id: this.dialogGroupVote.dialog_id,
text: this.value,
list: this.list,
uuid: this.dialogGroupVote.msgData?.msg?.uuid || '',
multiple: this.multiple,
anonymous: this.anonymous
}
}).then(({data}) => {
this.show = false;
this.$store.dispatch("saveDialogMsg", data);
}).catch(({msg}) => {
if( msg.indexOf("System error") !== -1){
$A.modalInfo({
language: false,
title: '版本过低',
content: '服务器版本过低,请升级服务器。',
})
return;
}
$A.modalError(msg);
}).finally(_ => {
this.loadIng--;
});
}
}
}
</script>

View File

@ -0,0 +1,248 @@
<template>
<Modal class-name="dialog-droup-word-chain"
v-model="show"
:mask-closable="false"
:title="dialogDroupWordChain.type == 'create' ? $L('发起接龙') : $L('接龙结果')"
:closable="!isFullscreen"
:fullscreen="isFullscreen"
:footer-hide="isFullscreen">
<!-- 顶部 -->
<template #header>
<div v-if="isFullscreen" class="chain-modal-header">
<div class="chain-modal-close" @click="show = false">
{{ $L('取消') }}
</div>
<div class="chain-modal-title">
{{ dialogDroupWordChain.type == 'create' ? $L('发起接龙') : $L('接龙结果') }}
</div>
<div class="chain-modal-submit" :class="{'disabled': !isEdit}" @click="onSend" >
<div v-if="loadIng > 0" class="submit-loading"><Loading /></div>
{{$L('发送')}}
</div>
</div>
</template>
<template #close>
<i class="ivu-icon ivu-icon-ios-close"></i>
</template>
<div ref="wordChainBodyRef" class="word-chain-body">
<div class="source" v-if="dialogDroupWordChain.type == 'create'">
{{$L('来自')}}
<span>{{ dialog.name }}</span>
</div>
<div class="initiate">
<span>{{ $L('由') }}</span>
<UserAvatar :userid="createId" :size="22" :showName="true" tooltipDisabled/>
<span> {{ $L('发起,参与接龙目前共'+num+'人') }}</span>
</div>
<div class="textarea">
<Input ref="wordChainTextareaRef"
v-model="value"
type="textarea"
:autosize="{minRows: 3,maxRows: 5}"
:disabled="dialogDroupWordChain.type != 'create'"
:placeholder="$L('请输入接龙主题')"
/>
</div>
<ul ref="wordChainListRef">
<li v-for="(item) in list" v-if="item.type == 'case' && (dialogDroupWordChain.type == 'create' || item.text)">
<span>{{ $L('例') }}</span>
<Input v-model="item.text" :placeholder="$L('可填写接龙格式')" :disabled="dialogDroupWordChain.type != 'create'" />
</li>
<li v-for="(item,index) in list.filter(h=>h.type != 'case')">
<span>{{index + 1}}</span>
<Input v-model="item.text" :disabled="item.userid != userId" :placeholder="$L('请输入接龙内容')"/>
</li>
<li class="add">
<i class="taskfont" @click="onAdd">&#xe78c;</i>
</li>
</ul>
</div>
<div slot="footer">
<Button type="default" @click="show=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="loadIng > 0" @click="onSend" :disabled="!isEdit">{{$L('发送')}}</Button>
</div>
</Modal>
</template>
<script>
import {mapState} from "vuex";
export default {
name: 'DialogDroupWordChain',
data() {
return {
show: false,
createId: 0,
value: "#" + this.$L('接龙') + " \n",
list: [],
oldData: '',
loadIng: 0,
}
},
computed: {
...mapState(['dialogDroupWordChain', 'userInfo', 'dialogMsgs', 'cacheDialogs']),
isFullscreen({ windowWidth }) {
return windowWidth < 576;
},
num(){
return this.list.filter(h=>h.type != 'case')?.length || 0;
},
allList(){
const msg = this.dialogDroupWordChain.msgData?.msg || {};
let list = JSON.parse(JSON.stringify(msg.list || []));
this.dialogMsgs.filter(h=>{
return h.type == "word-chain" && h.msg?.uuid == msg.uuid
}).forEach((h)=>{
(h.msg.list || []).forEach(k=>{
if( k.type != 'case' && list.map(j=>j.id).indexOf(k.id) == -1 ){
list.push(k)
}
})
});
return list;
},
isEdit(){
return this.oldData != JSON.stringify(this.list);
},
dialog(){
return this.cacheDialogs.find(h=>h.id == this.dialogDroupWordChain.dialog_id) || {}
},
},
watch: {
show(val){
if(!val){
this.value = "#" + this.$L('接龙') + " \n";
this.list = [];
}else{
if(this.dialogDroupWordChain.type == 'create'){
this.$nextTick(()=>{
this.$refs.wordChainTextareaRef.focus()
})
}
this.scrollTo();
}
},
dialogDroupWordChain(data) {
if(data.type == 'create' && data.dialog_id){
this.show = true;
this.createId = this.userId;
this.list.push({
id: Date.now(),
type: "case",
userid: this.userId,
text: ""
});
this.list.push({
id: Date.now() + 1,
type: "text",
userid: this.userId,
text: this.userInfo.nickname
});
}
if(data.type == 'participate' && data.dialog_id && data.msgData){
this.show = true;
this.createId = data.msgData.msg.userid;
this.value = data.msgData.msg.text;
this.list = this.allList;
this.oldData = JSON.stringify(this.list);
}
}
},
methods: {
onAdd(){
this.list.push({
id: Date.now(),
type: 'text',
userid: this.userId,
text: this.userInfo.nickname,
});
this.scrollTo();
},
scrollTo(){
this.$nextTick(()=>{
this.$refs.wordChainListRef.scrollTo(0, 99999);
});
},
onSend() {
if( !this.isEdit ){
return;
}
//
if(!this.value){
$A.messageError("请输入接龙主题");
return;
}
if( this.list.find(h=> !h.text && h.type != "case") ){
$A.messageError("请输入接龙内容");
return;
}
//
const texts = this.list.map(h=> h.text);
if( texts.length != [...new Set(texts)].length ){
$A.modalConfirm({
content: '重复内容将不再计入接龙结果',
cancelText: '返回编辑',
okText: '继续发送',
onOk: () => {
this.send()
}
})
return;
}
this.send();
},
/**
* 发送消息
*/
send() {
const list = [];
this.list.forEach(h=>{
if(h.text && list.map(h=> h.text).indexOf(h.text) == -1){
list.push(h);
}
});
//
this.loadIng++;
this.$store.dispatch("call", {
url: 'dialog/msg/wordchain',
method: 'post',
data: {
dialog_id: this.dialogDroupWordChain.dialog_id,
text: this.value,
list: list,
uuid: this.dialogDroupWordChain.msgData?.msg?.uuid || ''
}
}).then(({data}) => {
this.show = false;
this.$store.dispatch("saveDialogMsg", data);
}).catch(({msg}) => {
if( msg.indexOf("System error") !== -1){
$A.modalInfo({
language: false,
title: '版本过低',
content: '服务器版本过低,请升级服务器。',
})
return;
}
$A.modalError(msg);
}).finally(_ => {
this.loadIng--;
});
}
}
}
</script>

View File

@ -14,6 +14,10 @@
<UserAvatar :userid="msgData.reply_data.userid" :show-icon="false" :show-name="true"/>
<div class="reply-desc" v-html="$A.getMsgSimpleDesc(msgData.reply_data, 'image-preview')"></div>
</div>
<!--转发-->
<div v-if="msgData.forward_show && msgData.forward_data && msgData.forward_data.userid" class="dialog-reply no-dark-content" @click="openDialog(msgData.forward_data.userid)">
<UserAvatar :userid="msgData.forward_data.userid" :show-icon="false" :show-name="true" :tooltip-disabled="true"/>
</div>
<!--详情-->
<div ref="content" class="dialog-content" :class="contentClass">
<!--文本-->
@ -62,6 +66,72 @@
</li>
</ul>
</div>
<!--接龙-->
<div v-else-if="msgData.type === 'word-chain'" class="content-text content-word-chain no-dark-content">
<pre v-html="$A.formatTextMsg(msgData.msg.text, userId)"></pre>
<ul>
<li v-for="(item) in (msgData.msg.list || []).filter(h=>h.type == 'case')">
{{ $L('例') }} {{ item.text }}
</li>
<li v-for="(item,index) in (msgData.msg.list || []).filter(h=>h.type != 'case')">
<span class="expand" v-if="index == 2 && msgData.msg.list.length > 4" @click="unfoldWordChain">
...{{$L('展开')}}...
</span>
<span :class="{'shrink': index >= 2 && msgData.msg.list.length > 4 } ">
{{index + 1}}. {{item.text}}
</span>
</li>
<li @click="onWordChain" class="participate">{{ $L('参与接龙') }}<span>></span></li>
</ul>
</div>
<!--投票-->
<div v-else-if="msgData.type === 'vote'" class="content-text content-word-vote no-dark-content">
<div class="vote-msg-head">
<i class="taskfont">&#xe7fd;</i> {{ $L('投票') }}
<span>{{ msgData.msg.multiple == 1 ? $L('多选') : $L('单选')}}</span>
<span>{{ msgData.msg.multiple == 1 ? $L('匿名') : $L('实名')}}</span>
</div>
<pre v-html="$A.formatTextMsg(msgData.msg.text, userId)"></pre>
<template v-if="(msgData.msg.votes || []).filter(h=>h.userid == userId).length == 0">
<RadioGroup v-if="msgData.msg.multiple == 0" v-model="msgData.msg._vote" vertical>
<Radio v-for="(item,index) in (msgData.msg.list || [])" :label="item.id" :key="index">
{{item.text}}
</Radio>
</RadioGroup>
<CheckboxGroup v-else v-model="msgData.msg._vote">
<Checkbox v-for="(item,index) in (msgData.msg.list || [])" :label="item.id" :key="index">
{{item.text}}
</Checkbox>
</CheckboxGroup>
<div class="btn-row no-dark-content">
<Button v-if="(msgData.msg._vote || []).length == 0" class="ivu-btn" disabled>{{$L("请选择后投票")}}</Button>
<Button v-else class="ivu-btn" :loading="msgData.msg._loadIng > 0" @click="onVote('vote',msgData)">{{$L("立即投票")}}</Button>
</div>
</template>
<template v-else>
<div class="vote-result-body">
<ul>
<li v-for="item in (msgData.msg.list || [])">
<div class="vote-option-title">{{ item.text }}</div>
<div class="ticket-num">
<span>{{ getVoteProgress(msgData.msg,item.id).num }}{{$L('票')}}</span>
<span>{{ getVoteProgress(msgData.msg,item.id).progress + '%' }}</span>
</div>
<Progress :percent="Number(getVoteProgress(msgData.msg,item.id).progress)" :stroke-width="5" hide-info/>
<div v-if="msgData.msg.anonymous" class="avatar-row">
<template v-for="votes in (msgData.msg.votes || []).filter(h=>h.votes.indexOf(item.id) != -1)">
<UserAvatar :userid="votes.userid" :size="18" />
</template>
</div>
</li>
</ul>
</div>
<div class="btn-row no-dark-content" v-if="msgData.msg.state == 1 && msgData.msg.userid == userId">
<Button class="ivu-btn" :loading="msgData.msg._loadIng > 0" @click="onVote('again',msgData)">{{$L("再次发送")}}</Button>
<Button class="ivu-btn" :loading="msgData.msg._loadIng > 0" @click="onVote('finish',msgData)">{{$L("结束投票")}}</Button>
</div>
</template>
</div>
<!--等待-->
<div v-else-if="msgData.type === 'loading'" class="content-loading">
<Icon v-if="msgData.error === true" type="ios-alert-outline" />
@ -453,6 +523,14 @@ export default {
});
},
openDialog(userid) {
this.$store.dispatch("openDialogUserid", userid).then(_ => {
this.goForward({name: 'manage-messenger'})
}).catch(({msg}) => {
$A.modalError(msg)
});
},
viewReply() {
this.$emit("on-view-reply", {
msg_id: this.msgData.id,
@ -492,6 +570,67 @@ export default {
onShowEmojiUser(item) {
this.$emit("on-show-emoji-user", item)
},
onWordChain(){
this.$store.state.dialogDroupWordChain = {
type: 'participate',
dialog_id: this.msgData.dialog_id,
msgData: this.msgData,
}
},
unfoldWordChain(e){
e.target.parentNode?.parentNode?.classList.add('expand')
},
onVote(type,msgData){
if(type != 'vote'){
$A.modalConfirm({
content: type == 'finish' ? '确定结束投票?': '再次发送投票?',
cancelText: '取消',
okText: '确定',
onOk: () => {
this.vote(type,msgData);
}
});
return;
}
this.vote(type,msgData);
},
vote(type,msgData){
this.$set(msgData.msg,'_loadIng',1)
this.$store.dispatch("call", {
url: 'dialog/msg/vote',
method: 'post',
data: {
dialog_id: msgData.dialog_id,
uuid: msgData.msg.uuid,
vote: msgData.msg._vote || [],
type: type
}
}).then(({data}) => {
if(type == 'again'){
$A.messageSuccess("已发送");
}
data.forEach(d => {
this.$store.dispatch("saveDialogMsg", d );
});
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(_ => {
this.$set(msgData.msg,'_loadIng',0)
});
},
getVoteProgress(msgData, id){
const num = msgData.votes.filter(h=>(h.votes || '').indexOf(id) != -1).length
let progress = '0.00';
if(num){
progress = (msgData.votes.length / num * 100).toFixed(2)
}
return {num, progress};
}
}
}
</script>

View File

@ -257,7 +257,7 @@
<i class="taskfont" v-html="item.icon"></i>
<span>{{ $L(item.label) }}</span>
</li>
<li @click="onOperate('forward')">
<li v-if="operateItem.type !== 'word-chain' && operateItem.type !== 'vote'" @click="onOperate('forward')">
<i class="taskfont">&#xe638;</i>
<span>{{ $L('转发') }}</span>
</li>
@ -392,10 +392,33 @@
v-model="forwardData"
:multiple-max="50"
:title="$L('转发')"
:twice-affirm="true"
:twice-affirm-title="$L('转发给:')"
:before-submit="onForward"
:show-select-all="false"
:multiple-choice="false"
show-dialog
module/>
module>
<template #twice-affirm-body-extend>
<div class="dialog-wrapper-forward-body">
<div class="dialog-wrapper ">
<div class="dialog-scroller">
<DialogItem :source="operateItem" simpleView :dialogAvatar="false"/>
</div>
</div>
<div class="leave-message">
<Input type="textarea" :autosize="{minRows: 1,maxRows: 3}" v-model="forwardLeaveMessage" :placeholder="$L('留言')" clearable />
</div>
</div>
</template>
<template #twice-affirm-footer-extend>
<div class="dialog-wrapper-forward-footer" :class="{'selected': !forwardShowOriginal}" @click="forwardShowOriginal = !forwardShowOriginal">
<Icon v-if="!forwardShowOriginal" class="user-modal-icon" type="ios-checkmark-circle" />
<Icon v-else class="user-modal-icon" type="ios-radio-button-off" />
{{$L('不显示原发送者信息')}}
</div>
</template>
</UserSelect>
<!-- 设置待办 -->
<Modal
@ -508,6 +531,13 @@
<DrawerOverlay v-model="approveDetailsShow" placement="right" :size="600">
<ApproveDetails v-if="approveDetailsShow" :data="approveDetails" style="height: 100%;border-radius: 10px;"></ApproveDetails>
</DrawerOverlay>
<!-- 群接龙 -->
<DialogGroupWordChain/>
<!-- 群投票 -->
<DialogGroupVote/>
</div>
</template>
@ -528,6 +558,9 @@ import {choiceEmojiOne} from "./ChatInput/one";
import ApproveDetails from "../../../pages/manage/approve/details.vue";
import UserSelect from "../../../components/UserSelect.vue";
import UserAvatarTip from "../../../components/UserAvatar/tip.vue";
import DialogGroupWordChain from "./DialogGroupWordChain";
import DialogGroupVote from "./DialogGroupVote";
export default {
name: "DialogWrapper",
@ -542,7 +575,9 @@ export default {
DialogGroupInfo,
DrawerOverlay,
DialogUpload,
ApproveDetails
ApproveDetails,
DialogGroupWordChain,
DialogGroupVote,
},
props: {
@ -598,6 +633,8 @@ export default {
modifyLoad: 0,
forwardData: [],
forwardShowOriginal: true,
forwardLeaveMessage: '',
openId: 0,
dialogDrag: false,
@ -1138,6 +1175,9 @@ export default {
},
allMsgList(newList, oldList) {
if(JSON.stringify(newList) == JSON.stringify(oldList)){
return;
}
const {tail} = this.scrollInfo();
if ($A.isIos() && newList.length !== oldList.length) {
// iOS
@ -2249,7 +2289,9 @@ export default {
data: {
dialogids,
userids,
msg_id: this.operateItem.id
msg_id: this.operateItem.id,
show_source: this.forwardShowOriginal ? 1 : 0,
leave_message: this.forwardLeaveMessage
}
}).then(({data, msg}) => {
this.$store.dispatch("saveDialogMsg", data.msgs);
@ -2382,14 +2424,21 @@ export default {
value: $A.thumbRestore(event.target.currentSrc),
})
} else if (event.target.nodeName === 'A') {
let href = event.target.href;
if (event.target.classList.contains("mention") && event.target.classList.contains("file")) {
this.findOperateFile(this.operateItem.id, event.target.href)
if(this.isEEUiApp || this.$Electron){
const url = new URL(href);
const params = new URLSearchParams(url.search);
params.delete('theme'); params.delete('lang');
href = url.origin + url.pathname + (params.toString() ? ('?' + params.toString()) : '');
}
this.findOperateFile(this.operateItem.id, href)
}
this.operateCopys.push({
type: 'link',
icon: '&#xe7cb;',
label: '复制链接',
value: event.target.href,
value: href,
})
}
if (msgData.type === 'text') {
@ -2454,6 +2503,8 @@ export default {
case "forward":
this.forwardData = [];
this.forwardLeaveMessage = '';
this.forwardShowOriginal = true;
this.$refs.forwardSelect.onSelection()
break;

View File

@ -599,7 +599,7 @@ export default {
},
documentKey() {
return new Promise(resolve => {
return new Promise((resolve,reject) => {
this.$store.dispatch("call", {
url: 'file/content',
data: {
@ -608,8 +608,8 @@ export default {
},
}).then(({data}) => {
resolve(`${data.id}-${$A.Time(data.update_at)}`)
}).catch(() => {
resolve(0)
}).catch((res) => {
reject(res)
});
})
},

View File

@ -144,7 +144,7 @@ export default {
},
documentKey() {
return new Promise(resolve => {
return new Promise((resolve,reject) => {
this.$store.dispatch("call", {
url: 'file/content',
data: {
@ -153,8 +153,8 @@ export default {
},
}).then(({data}) => {
resolve(`${data.id}-${$A.Time(data.update_at)}`)
}).catch(() => {
resolve(0)
}).catch((res) => {
reject(res)
});
})
},

View File

@ -52,6 +52,7 @@
<Icon class="menu-icon" type="ios-more" />
<EDropdownMenu v-if="projectData.owner_userid === userId" slot="dropdown">
<EDropdownItem command="setting">{{$L('项目设置')}}</EDropdownItem>
<EDropdownItem command="permissions">{{$L('权限设置')}}</EDropdownItem>
<EDropdownItem command="workflow">{{$L('工作流设置')}}</EDropdownItem>
<EDropdownItem command="user" divided>{{$L('成员管理')}}</EDropdownItem>
<EDropdownItem command="invite">{{$L('邀请链接')}}</EDropdownItem>
@ -80,7 +81,7 @@
<div v-if="completedCount > 0" class="project-checkbox">
<Checkbox :value="projectData.cacheParameter.completedTask" @on-change="toggleCompleted">{{$L('显示已完成')}}</Checkbox>
</div>
<div v-if="flowList.length > 0" class="project-select">
<div class="project-select">
<Cascader ref="flow" :data="flowData" @on-change="flowChange" transfer-class-name="project-panel-flow-cascader" transfer>
<span :class="`project-flow ${flowInfo.status || ''}`">{{ flowTitle }}</span>
</Cascader>
@ -236,11 +237,11 @@
<Scrollbar v-else-if="tabTypeActive === 'table'" class="project-table" enable-x>
<div class="project-table-head">
<Row class="task-row">
<Col span="12"># {{$L('任务名称')}}</Col>
<Col span="3">{{$L('列表')}}</Col>
<Col span="12"><span class="head-title"># {{$L('任务名称')}}</span></Col>
<Col span="3"><span class="head-title">{{$L('列表')}}</span></Col>
<Col span="3">
<div class="sort" @click="onSort('level')">
{{$L('优先级')}}
<span class="head-title">{{$L('优先级')}}</span>
<div class="task-sort">
<Icon :class="{on:sortField=='level' && sortType=='asc'}" type="md-arrow-dropup" />
<Icon :class="{on:sortField=='level' && sortType=='desc'}" type="md-arrow-dropdown" />
@ -250,7 +251,7 @@
<Col span="3">{{$L('负责人')}}</Col>
<Col span="3">
<div class="sort" @click="onSort('end_at')">
{{$L('到期时间')}}
<span class="head-title">{{$L('到期时间')}}</span>
<div class="task-sort">
<Icon :class="{on:sortField=='end_at' && sortType=='asc'}" type="md-arrow-dropup" />
<Icon :class="{on:sortField=='end_at' && sortType=='desc'}" type="md-arrow-dropdown" />
@ -344,6 +345,14 @@
</div>
</Modal>
<!--项目权限-->
<DrawerOverlay
v-model="permissionShow"
placement="right"
:size="650">
<ProjectPermission ref="permission" v-if="permissionShow" @close="()=>{ this.permissionShow = false }" :project-id="projectId"/>
</DrawerOverlay>
<!--成员管理-->
<Modal
v-model="userShow"
@ -474,6 +483,7 @@ import TaskArchived from "./TaskArchived";
import ProjectLog from "./ProjectLog";
import DrawerOverlay from "../../../components/DrawerOverlay";
import ProjectWorkflow from "./ProjectWorkflow";
import ProjectPermission from "./ProjectPermission";
import TaskMenu from "./TaskMenu";
import TaskDeleted from "./TaskDeleted";
import ProjectGantt from "./ProjectGantt";
@ -489,6 +499,7 @@ export default {
MarkdownPreviewNostyle,
TaskMenu,
ProjectWorkflow,
ProjectPermission,
DrawerOverlay,
ProjectLog, TaskArchived, TaskRow, Draggable, TaskAddSimple, TaskPriority, TaskDeleted, ProjectGantt},
data() {
@ -516,6 +527,10 @@ export default {
settingData: {},
settingLoad: 0,
permissionShow: false,
permissionShowData: {},
permissionShowLoad: 0,
userShow: false,
userData: {},
userLoad: 0,
@ -846,7 +861,7 @@ export default {
//
const {project_user} = this.projectData;
if ($A.isArray(project_user)) {
const userItems = project_user.map((item, index) => {
let userItems = project_user.map((item, index) => {
const userInfo = cacheUserBasic.find(({userid}) => userid === item.userid) || {}
const length = allTask.filter(({task_user, complete_at}) => {
if (!this.projectData.cacheParameter.completedTask) {
@ -859,12 +874,18 @@ export default {
return {
value: `user:${userInfo.userid}`,
label: `${userInfo.nickname} (${length})`,
class: `user-${index}`,
userid: userInfo.userid || 0,
length,
}
}).filter(({userid, length}) => userid > 0 && length > 0)
if (userItems.length > 0) {
userItems.sort((a, b) => {
return a.userid == this.userId ? -1 : 1
})
userItems = userItems.map((item, index)=>{
item.class = `user-${index}`
return item;
})
list.push(...userItems)
}
}
@ -1253,6 +1274,16 @@ export default {
});
break;
case "permissions":
// this.$set(this.settingData, 'name', this.projectData.name);
// this.$set(this.settingData, 'desc', this.projectData.desc);
this.permissionShow = true;
// this.$nextTick(() => {
// this.$refs.projectName.focus()
// setTimeout(this.$refs.projectDesc.resizeTextarea, 0)
// });
break;
case "user":
if (this.projectData.owner_userid !== this.userId) {
return;

View File

@ -0,0 +1,177 @@
<template>
<div class="project-permission">
<div class="permission-title">
{{$L('权限设置')}}
<div class="title-icon">
<Loading v-if="loadIng > 0"/>
<Icon v-else type="ios-refresh" @click="getData()"/>
</div>
</div>
<div class="permission-content">
<Form :model="formData" label-width="90" label-position="right">
<!-- 项目权限 -->
<div class="project-permission-title" >{{$L('任务列权限')}}:</div>
<FormItem :label="$L('添加列')">
<CheckboxGroup v-model="formData.task_list_add">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
<FormItem :label="$L('修改列')">
<CheckboxGroup v-model="formData.task_list_update">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
<FormItem :label="$L('删除列')">
<CheckboxGroup v-model="formData.task_list_remove">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
<FormItem :label="$L('排序列')">
<CheckboxGroup v-model="formData.task_list_sort">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
<!-- 任务权限 -->
<div class="project-permission-title" >{{$L('任务权限')}}:</div>
<FormItem :label="$L('添加任务')">
<CheckboxGroup v-model="formData.task_add">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
<FormItem :label="$L('修改任务')">
<CheckboxGroup v-model="formData.task_update">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="3">{{ $L('任务负责人') }}</Checkbox>
<Checkbox :label="4">{{ $L('任务协助人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
<FormItem :label="$L('修改状态')">
<CheckboxGroup v-model="formData.task_status">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="3">{{ $L('任务负责人') }}</Checkbox>
<Checkbox :label="4">{{ $L('任务协助人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
<FormItem :label="$L('归档任务')">
<CheckboxGroup v-model="formData.task_archived">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="3">{{ $L('任务负责人') }}</Checkbox>
<Checkbox :label="4">{{ $L('任务协助人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
<FormItem :label="$L('删除任务')">
<CheckboxGroup v-model="formData.task_remove">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="3">{{ $L('任务负责人') }}</Checkbox>
<Checkbox :label="4">{{ $L('任务协助人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
<FormItem :label="$L('移动任务')">
<CheckboxGroup v-model="formData.task_move">
<Checkbox :label="1" disabled>{{ $L('项目负责人') }}</Checkbox>
<Checkbox :label="3">{{ $L('任务负责人') }}</Checkbox>
<Checkbox :label="4">{{ $L('任务协助人') }}</Checkbox>
<Checkbox :label="2">{{ $L('项目成员') }}</Checkbox>
</CheckboxGroup>
</FormItem>
</Form>
</div>
<div slot="footer" class="project-permission-footer">
<Button type="default" @click="onClose()">{{$L('取消')}}</Button>
<Button type="primary" @click="updateData()" :loading="loadIng > 0">{{$L('修改')}}</Button>
</div>
</div>
</template>
<script>
export default {
name: "ProjectPermission",
props: {
projectId: {
type: Number,
default: 0
},
},
data() {
return {
loadIng: 0,
formData: {
project_task_list: [],
task_add: [],
task_update: [],
task_status: [],
task_archived: [],
task_remove: [],
task_move: []
}
}
},
mounted() {
},
watch: {
projectId: {
handler(val) {
if (val) {
this.getData()
}
},
immediate: true
},
},
methods: {
getData() {
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/permission',
data: {
project_id: this.projectId,
},
}).then(({data}) => {
this.formData = data.permissions;
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(_ => {
this.loadIng--;
});
},
updateData() {
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/permission/update',
method: 'post',
data: {
project_id: this.projectId,
...this.formData
},
}).then(({data}) => {
this.formData = data.permissions;
this.$Message.success(this.$L('修改成功'));
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(_ => {
this.loadIng--;
});
},
onClose() {
this.$emit('close')
},
}
}
</script>

View File

@ -230,10 +230,33 @@
</EDropdown>
</div>
<div class="item-content user">
<span @click="showCisibleDropdown" v-if="taskDetail.visibility == 1" class="visibility-text">{{$L('项目人员可见')}}</span>
<span @click="showCisibleDropdown" v-else-if="taskDetail.visibility == 2" class="visibility-text">{{$L('任务人员可见')}}</span>
<UserSelect
v-else
<EDropdown v-if="taskDetail.visibility == 1 || taskDetail.visibility == 2" trigger="click" placement="bottom" @command="dropVisible">
<span class="visibility-text">{{ taskDetail.visibility == 1 ? $L('项目人员可见') : $L('任务人员可见') }}</span>
<EDropdownMenu slot="dropdown">
<EDropdownItem :command="1">
<div class="task-menu-icon" >
<Icon v-if="taskDetail.visibility == 1" class="completed" :type="'md-checkmark-circle'"/>
<Icon v-else class="uncomplete" :type="'md-radio-button-off'"/>
{{$L('项目人员')}}
</div>
</EDropdownItem>
<EDropdownItem :command="2">
<div class="task-menu-icon" >
<Icon v-if="taskDetail.visibility == 2" class="completed" :type="'md-checkmark-circle'"/>
<Icon v-else class="uncomplete" :type="'md-radio-button-off'"/>
{{$L('任务人员')}}
</div>
</EDropdownItem>
<EDropdownItem :command="3">
<div class="task-menu-icon" >
<Icon v-if="taskDetail.visibility == 3" class="completed" :type="'md-checkmark-circle'"/>
<Icon v-else class="uncomplete" :type="'md-radio-button-off'"/>
{{$L('指定成员')}}
</div>
</EDropdownItem>
</EDropdownMenu>
</EDropdown>
<UserSelect v-else
ref="visibleUserSelectRef"
v-model="taskDetail.visibility_appointor"
:avatar-size="28"
@ -1096,6 +1119,10 @@ export default {
}
this.$set(this.taskDetail, 'content', contentSave)
action = 'content';
if (content == this.taskContent.replace(/original-width="[^"]*"/g, "").replace(/original-height="[^"]*"/g, "").replace(/\" \//g, "\" /")) {
return;
}
this.$set(this.taskDetail, 'content', content)
successCallback = () => {
this.$store.dispatch("saveTaskContent", {
task_id: this.taskId,

View File

@ -102,9 +102,7 @@ export default {
data: this.formData,
}).then(({data}) => {
this.show = false;
this.$store.dispatch('downUrl', {
url: data.url
});
$A.messageSuccess(data.msg);
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(_ => {

View File

@ -28,6 +28,10 @@ export default {
type: Boolean,
default: true
},
operationShow: {
type: Boolean,
default: true
},
updateBefore: {
type: Boolean,
default: false
@ -48,6 +52,10 @@ export default {
type: String,
default: 'md-checkmark-circle'
},
projectId:{
type: Number,
default: 0
}
},
computed: {
...mapState(['loads', 'taskFlows']),
@ -67,9 +75,11 @@ export default {
task: this.task,
loadStatus: this.loadStatus,
colorShow: this.colorShow,
operationShow: this.operationShow,
updateBefore: this.updateBefore,
disabled: this.disabled,
size: this.size,
projectId: this.projectId,
onUpdate: data => {
this.$emit("on-update", data)
}

View File

@ -1,14 +1,91 @@
<template>
<div class="task-add">
<div class="task-move">
<Cascader
v-model="cascader"
:data="cascaderData"
:clearable="false"
:placeholder="$L('请选择项目')"
:load-data="cascaderLoadData"
@on-input-change="cascaderInputChange"
@on-visible-change="cascaderShow=!cascaderShow"
filterable/>
v-model="cascader"
:data="cascaderData"
:clearable="false"
:placeholder="$L('请选择项目')"
:load-data="cascaderLoadData"
@on-visible-change="cascaderShow=!cascaderShow"
filterable/>
<div class="task-move-content">
<div class="task-move-content-old">
<div class="task-move-title">{{ $L('移动前') }}</div>
<div class="task-move-row">
<span class="label">{{$L('状态')}}:</span>
<div v-if="task.flow_item_name" class="flow">
<span :class="task.flow_item_status">{{task.flow_item_name}}</span>
</div>
</div>
<div class="task-move-row" :class="{'not-flex': windowPortrait}">
<span class="label">{{$L('负责人')}}:</span>
<UserSelect class="item-content user"
v-model="ownerUserids"
:avatar-size="28"
:project-id="task.project_id"
:add-icon="false"
disable
/>
</div>
<div class="task-move-row" :class="{'not-flex': windowPortrait}">
<span class="label">{{$L('协助人')}}:</span>
<UserSelect class="item-content user"
v-model="assistUserids"
:avatar-size="28"
:project-id="task.project_id"
:add-icon="false"
disable
/>
</div>
</div>
<div class="task-move-content-new">
<div class="task-move-title">{{ $L('移动后') }}</div>
<div class="task-move-row">
<span class="label"> {{$L('状态:')}} </span>
<TaskMenu
:ref="`taskMenu_${task.id}`"
:task="tasks"
:project-id="cascader[0]"
:color-show="false"
:operation-show="false"
:load-status="task.loading === true"
@on-update="onStatusUpdate"
/>
<div v-if="updateData.flow.flow_item_name" class="flow">
<span :class="updateData.flow.flow_item_status" @click.stop="openMenu($event, tasks)">{{updateData.flow.flow_item_name}}</span>
</div>
</div>
<div class="task-move-row" :class="{'not-flex': windowPortrait}">
<span class="label">{{$L('负责人:')}}</span>
<div>
<UserSelect
class="item-content user"
v-model="updateData.owner_userids"
:multiple-max="10"
:avatar-size="28"
:project-id="cascader[0]"
:add-icon="false"
/>
</div>
</div>
<div class="task-move-row" :class="{'not-flex': windowPortrait}">
<span class="label">{{$L('协助人:')}}</span>
<div>
<UserSelect
class="item-content user"
v-model="updateData.assist_userids"
:multiple-max="10"
:avatar-size="28"
:project-id="cascader[0]"
:add-icon="false"
/>
</div>
</div>
</div>
</div>
<div class="ivu-modal-footer">
<div class="adaption">
<Button type="default" @click="close">{{$L('取消')}}</Button>
@ -20,9 +97,15 @@
<script>
import {mapState} from "vuex";
import TaskMenu from "./TaskMenu";
import UserSelect from "../../../components/UserSelect.vue";
export default {
name: "TaskMove",
components: {
TaskMenu,
UserSelect,
},
props: {
value: {
type: Boolean,
@ -36,6 +119,7 @@ export default {
},
data() {
return {
tasks: {},
cascader: [],
cascaderShow: false,
cascaderData: [],
@ -44,19 +128,21 @@ export default {
cascaderAlready: [],
loadIng: 0,
beforeClose: [],
flowItemId: 0,
ownerUserids: [],
assistUserids: [],
updateData:{
flow: {},
owner_userids: [],
assist_userids: []
}
}
},
async mounted() {
this.initCascaderData();
},
beforeDestroy() {
this.beforeClose.some(func => {
typeof func === "function" && func()
})
this.beforeClose = [];
this.initData();
},
computed: {
@ -64,20 +150,63 @@ export default {
},
watch: {
task: {
handler: function (val) {
this.cascader = [val.project_id, val.column_id];
},
deep: true,
immediate: true
cascader(val){
this.tasks.flow_item_id = this.flowItemId;
if(val[0] != this.task.project_id){
this.updateData.flow.flow_item_id = 0;
this.updateData.flow.flow_item_name = '';
this.updateData.flow.flow_item_status = '';
}else{
this.updateData.flow.flow_item_id = this.flowItemId;
this.updateData.flow.flow_item_name = this.task.flow_item_name;
this.updateData.flow.flow_item_status = this.task.flow_item_status;
}
//
const projectUserIds = this.cacheProjects.find(project => project.id == val[0])?.project_user?.map(h=>{
return h.userid
}) || [];
//
this.updateData.owner_userids = (this.task.task_user || []).filter(h=>{
return h.owner && projectUserIds.indexOf(h.userid) !== -1
}).sort((a, b) => {
return a.id - b.id;
}).map(h=>{
return h.userid
});
//
this.updateData.assist_userids = (this.task.task_user || []).filter(h=>{
return !h.owner && projectUserIds.indexOf(h.userid) !== -1
}).sort((a, b) => {
return a.id - b.id;
}).map(h=>{
return h.userid
});
},
},
methods: {
/**
* 初始化级联数据
* 初始化数据
*/
initCascaderData() {
initData() {
this.flowItemId = this.task.flow_item_id;
this.cascader = [this.task.project_id, this.task.column_id];
this.ownerUserids = (this.task.task_user || []).filter(h=>{
return h.owner
}).sort((a, b) => {
return a.id - b.id;
}).map(h=>{
return h.userid
});
this.assistUserids = (this.task.task_user || []).filter(h=>{
return !h.owner
}).sort((a, b) => {
return a.id - b.id;
}).map(h=>{
return h.userid
});
this.tasks = JSON.parse(JSON.stringify(this.task));
//
const data = $A.cloneJSON(this.cacheProjects).sort((a, b) => {
if (a.top_at || b.top_at) {
return $A.Date(b.top_at) - $A.Date(a.top_at);
@ -120,32 +249,15 @@ export default {
});
},
cascaderInputChange(key) {
this.cascaderValue = key || "";
//
if (this.cascaderAlready[this.cascaderValue] === true) {
async onConfirm() {
if( this.task.project_id == this.cascader[0] && this.task.column_id == this.cascader[1]){
$A.messageError("未变更移动项");
return;
}
if( !this.updateData.flow.flow_item_id ){
$A.messageError("请选择移动后状态");
return;
}
this.cascaderAlready[this.cascaderValue] = true;
//
setTimeout(() => {
this.cascaderLoading++;
}, 1000)
this.$store.dispatch("getProjects", {
keys: {
name: this.cascaderValue,
},
getcolumn: 'yes'
}).then(() => {
this.cascaderLoading--;
this.initCascaderData();
}).catch(() => {
this.cascaderLoading--;
});
},
async onConfirm() {
this.loadIng++;
this.$store.dispatch("call", {
url: "project/task/move",
@ -153,14 +265,13 @@ export default {
task_id: this.task.id,
project_id: this.cascader[0],
column_id: this.cascader[1],
flow_item_id: this.updateData.flow.flow_item_id,
owner: this.updateData.owner_userids,
assist: this.updateData.assist_userids,
}
}).then(({msg}) => {
}).then(({data,msg}) => {
this.loadIng--;
this.$store.dispatch("saveTask", {
id: this.task.id,
project_id: this.cascader[0],
column_id: this.cascader[1],
});
this.$store.dispatch("saveTask", data);
$A.messageSuccess(msg);
this.close()
}).catch(({msg}) => {
@ -172,6 +283,16 @@ export default {
close() {
this.$emit("input", !this.value)
},
openMenu(event, task) {
const el = this.$refs[`taskMenu_${task.id}`];
el && el.handleClick(event)
},
onStatusUpdate(val) {
this.tasks.flow_item_id = val.flow_item_id;
this.updateData.flow = val
}
}
}
</script>

View File

@ -42,21 +42,23 @@
</template>
<template v-if="task.parent_id === 0">
<EDropdownItem :divided="turns.length > 0" command="archived">
<div class="item">
<Icon type="ios-filing" />{{$L(task.archived_at ? '还原归档' : '归档')}}
</div>
</EDropdownItem>
<EDropdownItem command="move">
<div class="item">
<i class="taskfont movefont">&#xe7fc;</i>{{$L('移动')}}
</div>
</EDropdownItem>
<EDropdownItem command="remove">
<div class="item">
<Icon type="md-trash" />{{$L('删除')}}
</div>
</EDropdownItem>
<template v-if="operationShow">
<EDropdownItem :divided="turns.length > 0" command="archived">
<div class="item">
<Icon type="ios-filing" />{{$L(task.archived_at ? '还原归档' : '归档')}}
</div>
</EDropdownItem>
<EDropdownItem command="move">
<div class="item">
<i class="taskfont movefont">&#xe7fc;</i>{{$L('移动')}}
</div>
</EDropdownItem>
<EDropdownItem command="remove">
<div class="item hover-del">
<Icon type="md-trash" />{{$L('删除')}}
</div>
</EDropdownItem>
</template>
<template v-if="colorShow">
<EDropdownItem v-for="(c, k) in taskColorList" :key="'c_' + k" :divided="k==0" :command="c">
<div class="item">
@ -65,7 +67,7 @@
</EDropdownItem>
</template>
</template>
<EDropdownItem v-else command="remove" :divided="turns.length > 0">
<EDropdownItem v-else-if="operationShow" command="remove" :divided="turns.length > 0">
<div class="item">
<Icon type="md-trash" />{{$L('删除')}}
</div>
@ -104,9 +106,11 @@ export default {
task: {},
loadStatus: false,
colorShow: true,
operationShow: true,
updateBefore: false,
disabled: false,
size: 'small',
projectId: 0,
onUpdate: null,
element: null,
@ -164,15 +168,17 @@ export default {
this.task = data.task;
this.loadStatus = typeof data.loadStatus === "undefined" ? false : data.loadStatus;
this.colorShow = typeof data.colorShow === "undefined" ? true : data.colorShow;
this.operationShow = typeof data.operationShow === "undefined" ? true : data.operationShow;
this.updateBefore = typeof data.updateBefore === "undefined" ? false : data.updateBefore;
this.disabled = typeof data.disabled === "undefined" ? false : data.disabled;
this.size = typeof data.size === "undefined" ? "small" : data.size;
this.projectId = typeof data.projectId === "undefined" ? 0 : data.projectId;
this.onUpdate = typeof data.onUpdate === "function" ? data.onUpdate : null;
//
this.$refs.icon.focus();
this.updatePopper();
this.show();
this.$store.dispatch("getTaskFlow", this.task.id).finally(this.updatePopper)
this.$store.dispatch("getTaskFlow", {task_id: this.task.id, project_id: this.projectId}).finally(this.updatePopper)
this.setupEventListeners(data.event)
} else {
this.hide();
@ -223,7 +229,9 @@ export default {
}
}
this.updateTask({
flow_item_id
flow_item_id,
flow_item_status: updateFlow.status,
flow_item_name: updateFlow.name
}).then(() => {
if (isComplete) {
completeTemp(true)
@ -291,11 +299,19 @@ export default {
return;
}
//
Object.keys(updata).forEach(key => this.$set(this.task, key, updata[key]));
//
const updateData = Object.assign(updata, {
task_id: this.task.id,
});
if(!this.operationShow){
if (typeof this.onUpdate === "function") {
this.onUpdate(updateData)
}
reject()
return;
}
//
Object.keys(updata).forEach(key => this.$set(this.task, key, updata[key]));
//
this.$store.dispatch("taskUpdate", updateData).then(({data, msg}) => {
$A.messageSuccess(msg);
resolve()

View File

@ -58,65 +58,70 @@
@touchstart.native="listTouch"
@on-scroll="listScroll">
<ul v-if="tabActive==='dialog'" ref="ul" class="dialog">
<li
v-if="dialogList.length > 0"
v-for="(dialog, key) in dialogList"
:ref="`dialog_${dialog.id}`"
:key="key"
:data-id="dialog.id"
:class="dialogClass(dialog)"
@click="openDialog({
dialog_id: dialog.id,
search_msg_id: dialog.search_msg_id
})"
v-longpress="handleLongpress"
:style="{'background-color':dialog.color}">
<template v-if="dialog.type=='group'">
<EAvatar v-if="dialog.avatar" class="img-avatar" :src="dialog.avatar" :size="42"></EAvatar>
<i v-else-if="dialog.group_type=='department'" class="taskfont icon-avatar department">&#xe75c;</i>
<i v-else-if="dialog.group_type=='project'" class="taskfont icon-avatar project">&#xe6f9;</i>
<i v-else-if="dialog.group_type=='task'" class="taskfont icon-avatar task">&#xe6f4;</i>
<i v-else-if="dialog.group_type=='okr'" class="taskfont icon-avatar task">&#xe6f4;</i>
<Icon v-else class="icon-avatar" type="ios-people" />
</template>
<div v-else-if="dialog.dialog_user" class="user-avatar"><UserAvatar :userid="dialog.dialog_user.userid" :size="42"/></div>
<Icon v-else class="icon-avatar" type="md-person" />
<div class="dialog-box">
<div class="dialog-title">
<div v-if="dialog.todo_num" class="todo">[{{$L('待办')}}{{formatTodoNum(dialog.todo_num)}}]</div>
<div v-if="$A.getDialogMention(dialog) > 0" class="mention">[@{{$A.getDialogMention(dialog)}}]</div>
<div v-if="dialog.bot" class="taskfont bot">&#xe68c;</div>
<template v-for="tag in $A.dialogTags(dialog)" v-if="tag.color != 'success'">
<Tag :color="tag.color" :fade="false" @on-click="openDialog(dialog.id)">{{$L(tag.text)}}</Tag>
</template>
<span>{{dialog.name}}</span>
<Icon v-if="dialog.type == 'user' && lastMsgReadDone(dialog.last_msg) && dialog.dialog_user.userid != userId" :type="lastMsgReadDone(dialog.last_msg)"/>
<em v-if="dialog.last_at">{{$A.formatTime(dialog.last_at)}}</em>
</div>
<div class="dialog-text no-dark-content">
<template v-if="dialog.extra_draft_has && dialog.id != dialogId">
<div class="last-draft">[{{$L('草稿')}}]</div>
<div class="last-text"><span>{{formatDraft(dialog.extra_draft_content)}}</span></div>
</template>
<template v-else>
<template v-if="dialog.type=='group' && dialog.last_msg && dialog.last_msg.userid">
<div v-if="dialog.last_msg.userid == userId" class="last-self">{{$L('')}}</div>
<UserAvatar v-else :userid="dialog.last_msg.userid" :show-name="true" :show-icon="false"/>
<template v-if="dialogList.length > 0">
<li
v-for="(dialog, key) in dialogList"
:ref="`dialog_${dialog.id}`"
:key="key"
:data-id="dialog.id"
:class="dialogClass(dialog)"
@click="openDialog({
dialog_id: dialog.id,
dialog_msg_id: dialog.search_msg_id,
search_msg_id: dialog.search_msg_id,
})"
v-longpress="handleLongpress"
:style="{'background-color':dialog.color}">
<template v-if="dialog.type=='group'">
<EAvatar v-if="dialog.avatar" class="img-avatar" :src="dialog.avatar" :size="42"></EAvatar>
<i v-else-if="dialog.group_type=='department'" class="taskfont icon-avatar department">&#xe75c;</i>
<i v-else-if="dialog.group_type=='project'" class="taskfont icon-avatar project">&#xe6f9;</i>
<i v-else-if="dialog.group_type=='task'" class="taskfont icon-avatar task">&#xe6f4;</i>
<i v-else-if="dialog.group_type=='okr'" class="taskfont icon-avatar task">&#xe6f4;</i>
<Icon v-else class="icon-avatar" type="ios-people" />
</template>
<div v-else-if="dialog.dialog_user" class="user-avatar"><UserAvatar :userid="dialog.dialog_user.userid" :size="42"/></div>
<Icon v-else class="icon-avatar" type="md-person" />
<div class="dialog-box">
<div class="dialog-title">
<div v-if="dialog.todo_num" class="todo">[{{$L('待办')}}{{formatTodoNum(dialog.todo_num)}}]</div>
<div v-if="$A.getDialogMention(dialog) > 0" class="mention">[@{{$A.getDialogMention(dialog)}}]</div>
<div v-if="dialog.bot" class="taskfont bot">&#xe68c;</div>
<template v-for="tag in $A.dialogTags(dialog)" v-if="tag.color != 'success'">
<Tag :color="tag.color" :fade="false" @on-click="openDialog(dialog.id)">{{$L(tag.text)}}</Tag>
</template>
<div class="last-text">
<em v-if="formatMsgEmojiDesc(dialog.last_msg)">{{formatMsgEmojiDesc(dialog.last_msg)}}</em>
<span>{{$A.getMsgSimpleDesc(dialog.last_msg)}}</span>
</div>
</template>
<div v-if="dialog.silence" class="taskfont last-silence">&#xe7d7;</div>
<span>{{dialog.name}}</span>
<Icon v-if="dialog.type == 'user' && lastMsgReadDone(dialog.last_msg) && dialog.dialog_user.userid != userId" :type="lastMsgReadDone(dialog.last_msg)"/>
<em v-if="dialog.last_at">{{$A.formatTime(dialog.last_at)}}</em>
</div>
<div class="dialog-text no-dark-content">
<template v-if="dialog.extra_draft_has && dialog.id != dialogId">
<div class="last-draft">[{{$L('草稿')}}]</div>
<div class="last-text"><span>{{formatDraft(dialog.extra_draft_content)}}</span></div>
</template>
<template v-else>
<template v-if="dialog.type=='group' && dialog.last_msg && dialog.last_msg.userid">
<div v-if="dialog.last_msg.userid == userId" class="last-self">{{$L('')}}</div>
<UserAvatar v-else :userid="dialog.last_msg.userid" :show-name="true" :show-icon="false"/>
</template>
<div class="last-text">
<em v-if="formatMsgEmojiDesc(dialog.last_msg)">{{formatMsgEmojiDesc(dialog.last_msg)}}</em>
<span>{{$A.getMsgSimpleDesc(dialog.last_msg)}}</span>
</div>
</template>
<div v-if="dialog.silence" class="taskfont last-silence">&#xe7d7;</div>
</div>
</div>
</div>
<Badge class="dialog-num" :type="dialog.silence ? 'normal' : 'error'" :overflow-count="999" :count="$A.getDialogUnread(dialog, true)"/>
<div class="dialog-line"></div>
</li>
<li v-else-if="dialogSearchLoad === 0" class="nothing">
<Badge class="dialog-num" :type="dialog.silence ? 'normal' : 'error'" :overflow-count="999" :count="$A.getDialogUnread(dialog, true)"/>
<div class="dialog-line"></div>
</li>
</template>
<li v-else-if="dialogSearchLoad === 0 && dialogMarkLoad === 0" class="nothing">
{{$L(dialogSearchKey ? `没有任何与"${dialogSearchKey}"相关的会话` : `没有任何会话`)}}
</li>
<li v-else class="nothing">
<Loading/>
</li>
</ul>
<ul v-else class="contacts">
<template v-if="contactsFilter.length === 0">
@ -235,6 +240,8 @@ export default {
{type: 'user', name: '单聊'},
{type: 'group', name: '群聊'},
{type: 'bot', name: '机器人'},
{type: 'mark', name: '标注'},
{type: '@', name: '@我的'},
],
dialogHistory: MessengerObject.menuHistory,
@ -251,6 +258,8 @@ export default {
operateVisible: false,
clickAgainSubscribe: null,
dialogMarkLoad: 0,
}
},
@ -291,7 +300,7 @@ export default {
},
computed: {
...mapState(['cacheDialogs', 'loadDialogs', 'dialogId', 'messengerSearchKey', 'appNotificationPermission', 'taskColorList']),
...mapState(['cacheDialogs', 'loadDialogs', 'dialogId', 'dialogMsgId', 'dialogMsgs', 'messengerSearchKey', 'appNotificationPermission', 'taskColorList']),
routeName() {
return this.$route.name
@ -337,6 +346,18 @@ export default {
if (dialogActive == '' && dialogSearchKey == '') {
return this.cacheDialogs.filter(dialog => this.filterDialog(dialog)).sort(this.dialogSort);
}
if(dialogActive == 'mark' && !dialogSearchKey){
const lists = [];
this.dialogMsgs.filter(h=>h.tag).forEach(h=>{
let dialog = $A.cloneJSON(this.cacheDialogs).find(p=>p.id == h.dialog_id)
if(dialog){
dialog.last_msg = h;
dialog.search_msg_id = h.id;
lists.push(dialog);
}
});
return lists;
}
const list = this.cacheDialogs.filter(dialog => {
if (!this.filterDialog(dialog)) {
return false;
@ -381,6 +402,11 @@ export default {
return false;
}
break;
case '@':
if (!$A.getDialogMention(dialog)) {
return false;
}
break;
default:
return false;
}
@ -547,6 +573,13 @@ export default {
},
immediate: true
},
dialogActive(){
this.dialogSearchList = [];
if(this.dialogActive == 'mark' && !this.dialogSearchKey){
this.searchTagDialog()
}
}
},
methods: {
@ -623,7 +656,7 @@ export default {
}
return {
top: dialog.top_at,
active: dialog.id == this.dialogId,
active: dialog.id == this.dialogId && (dialog.search_msg_id == this.dialogMsgId || !this.dialogMsgId),
operate: this.operateVisible && dialog.id == this.operateItem.id,
completed: $A.dialogCompleted(dialog)
}
@ -769,6 +802,29 @@ export default {
});
},
searchTagDialog() {
//
this.dialogMarkLoad++;
this.$store.dispatch("call", {
url: 'dialog/search/tag',
}).then(({data}) => {
const msgIds = [];
const lists = [];
this.dialogList.forEach(h=>{
lists.push(h);
msgIds.push(h.search_msg_id)
});
data.some(item => {
if (!item.last_msg || !msgIds.includes(item.search_msg_id)) {
lists.push(Object.assign(item, {is_search: true}))
}
})
this.dialogSearchList = lists;
}).finally(_ => {
this.dialogMarkLoad--;
});
},
getContactsList(page) {
this.contactsLoad++;
const key = this.contactsKey

View File

@ -4,7 +4,7 @@
<FormItem label="License" prop="license">
<Input v-model="formData.license" type="textarea" :autosize="{minRows: 2,maxRows: 5}" :placeholder="$L('请输入License...')" />
</FormItem>
<FormItem :label="$L('详细信息')">
<FormItem>
<div class="license-box">
<ul v-if="formData.info.sn">
<li>
@ -51,7 +51,6 @@
<Icon class="information" type="ios-information-circle-outline" />
</ETooltip>
</li>
<li v-for="tip in formData.error" class="warning">{{tip}}</li>
</ul>
<ul v-else>
<li>
@ -60,6 +59,21 @@
</ul>
</div>
</FormItem>
<FormItem :label="$L('当前环境')" v-if="formData.error?.length > 0">
<div class="license-box">
<ul>
<li>
<em>SN:</em>
<span>{{formData.doo_sn}}</span>
</li>
<li>
<em>MAC:</em>
<span>{{infoJoin(formData.macs)}}</span>
</li>
<li v-for="tip in formData.error" class="warning">{{tip}}</li>
</ul>
</div>
</FormItem>
</Form>
<div class="setting-footer">
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{$L('提交')}}</Button>

View File

@ -168,7 +168,7 @@ export default {
},
documentKey() {
return new Promise(resolve => {
return new Promise((resolve,reject) => {
this.$store.dispatch("call", {
url: 'dialog/msg/detail',
data: {
@ -177,8 +177,8 @@ export default {
},
}).then(({data}) => {
resolve(`${data.id}-${$A.Time(data.update_at)}`)
}).catch(() => {
resolve(0)
}).catch((res) => {
reject(res)
});
});
},

View File

@ -155,7 +155,7 @@ export default {
});
},
documentKey() {
return new Promise(resolve => {
return new Promise((resolve,reject) => {
this.$store.dispatch("call", {
url: 'project/task/filedetail',
data: {
@ -164,8 +164,8 @@ export default {
},
}).then(({data}) => {
resolve(`${data.id}-${$A.Time(data.update_at)}`)
}).catch(() => {
resolve(0)
}).catch((res) => {
reject(res)
});
})
}

View File

@ -1920,6 +1920,8 @@ export default {
}
});
});
} else {
state.taskOperation = {};
}
},
@ -2126,14 +2128,16 @@ export default {
* @param state
* @param dispatch
* @param task_id
* @param project_id
* @returns {Promise<unknown>}
*/
getTaskFlow({state, dispatch}, task_id) {
getTaskFlow({state, dispatch}, {task_id, project_id}) {
return new Promise(function (resolve, reject) {
dispatch("call", {
url: 'project/task/flow',
data: {
task_id: task_id
task_id: task_id,
project_id: project_id || 0
},
}).then(result => {
let task = state.cacheTasks.find(({id}) => id == task_id)
@ -2500,13 +2504,16 @@ export default {
openDialog({state, dispatch}, dialog_id) {
return new Promise(resolve => {
let search_msg_id;
let dialog_msg_id;
if ($A.isJson(dialog_id)) {
search_msg_id = dialog_id.search_msg_id;
dialog_msg_id = dialog_id.dialog_msg_id;
dialog_id = dialog_id.dialog_id;
}
//
requestAnimationFrame(_ => {
state.dialogSearchMsgId = /^\d+$/.test(search_msg_id) ? search_msg_id : 0;
state.dialogMsgId = /^\d+$/.test(dialog_msg_id) ? dialog_msg_id : 0;
state.dialogId = /^\d+$/.test(dialog_id) ? dialog_id : 0;
resolve()
})

View File

@ -105,6 +105,7 @@ export default {
// 会话聊天
dialogId: 0,
dialogMsgId: 0,
dialogSearchMsgId: 0,
dialogIns: [],
dialogMsgs: [],
@ -113,6 +114,8 @@ export default {
dialogDraftTimer: {},
dialogMsgTransfer: {time: 0},
dialogSseList: [],
dialogDroupWordChain: {},
dialogGroupVote: {},
// 搜索关键词(主要用于移动端判断滑动返回)
messengerSearchKey: {dialog: '', contacts: ''},

View File

@ -172,6 +172,21 @@
}
}
}
&.twice-affirm{
padding-bottom: 20px;
.search-selected{
max-width: 100%;
}
.user-modal-avatar{
display: flex;
align-items: center;
gap: 5px;
.avatar-name{
max-width: 90%;
}
}
}
}
.user-modal-switch {
@ -416,12 +431,19 @@
border-radius: 14px;
}
}
.twice-affirm-body-extend{
margin: 0 24px;
}
}
.ivu-modal-footer {
border-top: 1px solid #f2f2f2 !important;
padding: 12px 0 !important;
margin: 0 24px !important;
display: flex;
justify-content: flex-end;
gap: 20px;
}
&.ivu-modal-fullscreen {
@ -462,4 +484,10 @@
}
}
}
&.twice-affirm-modal{
.ivu-modal {
max-width: 90%;
margin: 10px auto;
}
}
}

View File

@ -13,6 +13,7 @@
@import "project-management";
@import "project-panel";
@import "project-workflow";
@import "project-permission";
@import "task-add";
@import "task-add-simple";
@import "task-archived";
@ -21,6 +22,9 @@
@import "task-menu";
@import "task-operation";
@import "task-priority";
@import "task-move";
@import "team-management";
@import "update-log";
@import "task-exist-tips";
@import "calendar";
@import "dialog-droup-word-chain";

View File

@ -0,0 +1,162 @@
.calendar-wrapper {
flex: 1;
position: relative;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background-color: #ffffff;
z-index: 1;
}
.tui-full-calendar-popup {
box-shadow: none;
.tui-full-calendar-section-header {
.tui-full-calendar-ic-checkbox-checked {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAhFBMVEUAAACLz3CLz3CLz3CKzm6Gy2+Lz3CLz3CL0HCLz3CLz3CLz3CLz3CMz3GLz3CKz3CLz3CL0HCJ0G+KznCN0HCL0HCLz3CKz3CLz3CLz3CLz3CMz3CLz3CLz3GL0XCL0HCN0XKLz3CLz3CMz3CLz3CM0HCM0G+FzHCLz3CKz3CMz3CLz3Bod5CFAAAAK3RSTlMA18RAOQ3s8+Pc0rmyq3tpiUwTgBnovyDMjmNSRjUvJQX5yKB0WisKppuUFLaY7gAAAotJREFUeNrtm+FymkAUhc8KqIAgSkyUtkmsmqa97/9+HWeSudpCd8qZ7E0m+73A9/1gxmXx4IK0nbipvDFTN2lT9JDVToLh6gx/sHQSFLfEFZUEp8IFczFgrv5CTChe/TsxYvfy/IkZy7M/c2KGywDUYkgNIBdDciARUxI0YkqDiZgygRNTHKZiyhRiTAyIATEgBsSAGBADYsAnCJgdmtIywKUAOrsA94QzJyKA95/pbALcPV7piADGryzDB+Rnv3IkAhi/UhMBjF9pQwbk3/A3JRXA+7GjAng/DqEC5v3+dMYE8P6nQsIEzFOPnw7g/QpC+R8K4QJ4f5iAzaA/TMAm6fdvZQgY+wW2fn9A0a66Bee/2xLH8kp/Rr1MB/3jAxaP0ALCPzagzPBCM9r/XcYH3K4BLfD4vw75xwdMbqB4CmaD/vEB2ztc0RD+MQHFg+c85fffDPv9AbPUd6Lz+ydCBDyjh1//6WcCGngK1L8a8lMBJTwFfj8XIC16+SnX7Af9bIDUngLSrwFEwaD/VogA5YheDj7/Wv1MgL9g/8XjpwNk+c8Cyq8BYwqePX46QOkGCmi/BowrqFi/Bvg4wYv6y7e4Kz4Rfj5AnzY/WSlcwHAB5+cD9ivKzwfIbEX4qQA99RB+NkDfO4b5sRAigHjzpP0C6u6D9wuo2z/eL+DvXx8Zv4C4gVd/qABxCeFnApQT5+cDpOP8fICcLv2VxbfjlQZUNh+vU/XbBIj6jQLm9wDWlZgFyOaYHJ3Ix/gDQwyIATEgBsSAGODBfuBgPvEwH7m0YkqLVExJYfsQuPcwdjOf+5kPHoFKjKio0e3Hn90WUCyGt7v3Nf0GsjqXYOR1hh6SJsz8v0mg/AZRXmaRKXtJBwAAAABJRU5ErkJggg==);
}
}
.tui-full-calendar-popup-container {
word-break: break-all;
border: 0;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
border-radius: 6px;
}
.tui-full-calendar-arrow-top .tui-full-calendar-popup-arrow-border {
top: -8px;
border-bottom-color: rgba(217, 217, 217, .5);
}
}
.tui-full-calendar-dropdown-menu {
border-color: #e8e8e8;
width: calc(100% - 14px);
}
.tui-full-calendar-popup-creation {
.tui-full-calendar-icon {
&.tui-full-calendar-ic-title,
&.tui-full-calendar-calendar-dot {
display: none;
}
&.tui-full-calendar-ic-date {
background-image: url("data:image/svg+xml;base64,PHN2ZyB0PSIxNjIzODU5NjcwNjA3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE2Mzg4IiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PHBhdGggZD0iTTk2MCAxMjhIODMzYzAtNTMtNDMtOTYtOTYtOTZoLTE2Yy01MyAwLTk2IDQzLTk2IDk2SDQwMGMwLTI2LjUtMTAuNy01MC41LTI4LjEtNjcuOUMzNTQuNSA0Mi43IDMzMC41IDMyIDMwNCAzMmgtMTZjLTUzIDAtOTYgNDMtOTYgOTZINjRjLTM1LjMgMC02NCAyOC42LTY0IDY0djczNmMwIDM1LjMgMjguNyA2NCA2NCA2NGg4OTZjMzUuMyAwIDY0LTI4LjcgNjQtNjRWMTkyYzAtMzUuNC0yOC43LTY0LTY0LTY0eiBtLTI3MSA4YzAtMjIuMSAxNy45LTQwIDQwLTQwczQwIDE3LjkgNDAgNDB2ODBjMCAyMi4xLTE3LjkgNDAtNDAgNDAtMTEgMC0yMS00LjUtMjguMy0xMS43QzY5My41IDIzNyA2ODkgMjI3IDY4OSAyMTZ2LTgweiBtLTQzMyAwYzAtMjIuMSAxNy45LTQwIDQwLTQwczQwIDE3LjkgNDAgNDB2ODBjMCAyMi4xLTE3LjkgNDAtNDAgNDAtMTEgMC0yMS00LjUtMjguMy0xMS43QzI2MC41IDIzNyAyNTYgMjI3IDI1NiAyMTZ2LTgweiBtNzA0IDc2MGMwIDE3LjctMTQuMyAzMi0zMiAzMkg5NmMtMTcuNyAwLTMyLTE0LjMtMzItMzJWNDQ4aDg5NnY0NDh6IiBwLWlkPSIxNjM4OSIgZmlsbD0iIzUxNTE1MSI+PC9wYXRoPjwvc3ZnPg==");
background-size: contain;
}
}
.tui-full-calendar-content {
padding-left: 0;
}
.tui-full-calendar-popup-section {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
.tui-full-calendar-popup-section-item {
height: 36px;
line-height: 34px;
border-color: #e8e8e8;
border-radius: 4px;
}
.tui-full-calendar-popup-section-item input {
height: 34px;
}
}
.tui-full-calendar-section-title {
width: 100%;
input {
width: 100%;
}
}
.tui-full-calendar-section-start-date,
.tui-full-calendar-section-end-date {
width: 210px;
.tui-full-calendar-content {
padding-left: 8px;
}
}
.tui-full-calendar-popup-location,
.tui-full-calendar-section-private,
.tui-full-calendar-section-allday,
.tui-full-calendar-section-state {
display: none;
}
}
.tui-full-calendar-popup-task {
.priority {
color: #ffffff;
padding: 2px 4px;
border-radius: 4px;
margin-right: 6px;
}
.overdue {
color: #f5222d;
background: #fff1f0;
border: 1px solid #ffa39e;
padding: 1px 3px;
border-radius: 4px;
margin-right: 6px;
}
.tui-full-calendar-calendar-dot,
.tui-full-calendar-ic-priority {
opacity: 0;
}
.tui-full-calendar-ic-edit {
top: -2px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB0PSIxNjIzODU5MzY4MTg5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjExMTkiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNODMzLjQyODU3MTY4IDYySDE5MC41NzE0MjgzMmExMjguNTcxNDI4MzIgMTI4LjU3MTQyODMyIDAgMCAwLTEyOC41NzE0MjgzMiAxMjguNTcxNDI4MzJ2NjQyLjg1NzE0MzM2YTEyOC41NzE0MjgzMiAxMjguNTcxNDI4MzIgMCAwIDAgMTI4LjU3MTQyODMyIDEyOC41NzE0MjgzMmg2NDIuODU3MTQzMzZhMTI4LjU3MTQyODMyIDEyOC41NzE0MjgzMiAwIDAgMCAxMjguNTcxNDI4MzItMTI4LjU3MTQyODMyVjE5MC41NzE0MjgzMmExMjguNTcxNDI4MzIgMTI4LjU3MTQyODMyIDAgMCAwLTEyOC41NzE0MjgzMi0xMjguNTcxNDI4MzJ6IG02NC4yODU3MTQxNiA3NzEuNDI4NTcxNjhhNjQuMjg1NzE0MTYgNjQuMjg1NzE0MTYgMCAwIDEtNjQuMjg1NzE0MTcgNjQuMjg1NzE0MTZIMTkwLjU3MTQyODMyYTY0LjI4NTcxNDE2IDY0LjI4NTcxNDE2IDAgMCAxLTY0LjI4NTcxNDE2LTY0LjI4NTcxNDE2VjE5MC41NzE0MjgzMmE2NC4yODU3MTQxNiA2NC4yODU3MTQxNiAwIDAgMSA2NC4yODU3MTQxNy02NC4yODU3MTQxNmg2NDIuODU3MTQzMzVhNjQuMjg1NzE0MTYgNjQuMjg1NzE0MTYgMCAwIDEgNjQuMjg1NzE0MTYgNjQuMjg1NzE0MTd6IiBwLWlkPSIxMTIwIiBmaWxsPSIjNTE1MTUxIj48L3BhdGg+PHBhdGggZD0iTTE5MC41NzE0MjgzMiAyNTQuODU3MTQyNDhoNjQuMjg1NzE0MTZ2NjQuMjg1NzE1MDRIMTkwLjU3MTQyODMyek0zMTkuMTQyODU3NTIgMjU0Ljg1NzE0MjQ4aDQ1MHY2NC4yODU3MTUwNEgzMTkuMTQyODU3NTJ6TTE5MC41NzE0MjgzMiA0NDcuNzE0Mjg1ODRoNjQuMjg1NzE0MTZ2NjQuMjg1NzE0MTZIMTkwLjU3MTQyODMyek0zMTkuMTQyODU3NTIgNDQ3LjcxNDI4NTg0aDQ1MHY2NC4yODU3MTQxNkgzMTkuMTQyODU3NTJ6TTE5MC41NzE0MjgzMiA2NDAuNTcxNDI4MzJoNjQuMjg1NzE0MTZ2NjQuMjg1NzE0MTZIMTkwLjU3MTQyODMyek0zMTkuMTQyODU3NTIgNjQwLjU3MTQyODMyaDMyMS40Mjg1NzA4djY0LjI4NTcxNDE2SDMxOS4xNDI4NTc1MnoiIHAtaWQ9IjExMjEiIGZpbGw9IiM1MTUxNTEiPjwvcGF0aD48L3N2Zz4=");
}
.tui-full-calendar-ic-delete {
top: -2px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB0PSIxNjIzODU5MzMwMTc2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijc5MiIgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiPjxwYXRoIGQ9Ik04OTIuMjg4IDI1NmgtMTkxLjE2OEEyMDIuMjQgMjAyLjI0IDAgMCAwIDUwOS42MzIgNjIuMDggMjAxLjIxNiAyMDEuMjE2IDAgMCAwIDMxOC44NDggMjU2SDEyOGMtMTguNjg4IDAtNjYuMDQ4LTQuMjI0LTY2LjA0OCAyNC43NjhDNjEuOTUyIDMyNy43NDQgMTA5LjM3NiAzMjAgMTI4IDMyMGg2NHY1MTJhMTQ2LjQ5NiAxNDYuNDk2IDAgMCAwIDEyNy40MjQgMTI4aDM4Mi4yNzJBMTUwLjAxNiAxNTAuMDE2IDAgMCAwIDgzMiA4MzJsLTMuMzkyLTUxMmg2NGMxOC4zNjggMCA2NS4wMjQgMS40NzIgNjUuMDI0LTM5Ljc0NEE3Mi4zODQgNzIuMzg0IDAgMCAwIDg5Mi4yODggMjU2ek01MDkuNjMyIDEyOC41MTJBMTM4LjE3NiAxMzguMTc2IDAgMCAxIDYzNy40NCAyNTZIMzgyLjU5MmExMzcuOTIgMTM3LjkyIDAgMCAxIDEyNy4wNC0xMjcuNDg4ek03NjggODMyYTk3Ljk4NCA5Ny45ODQgMCAwIDEtNjYuODggNjRIMzE4Ljg0OGE5My41NjggOTMuNTY4IDAgMCAxLTY0LTY0VjMyMEg3Njh2NTEyeiBtLTM4NS40MDgtNjRWNTEyYzAtMTguNDk2IDAuOTYtNjAuOTkyIDM2LjczNi02MC45OTIgMjcuMzI4IDAgMjYuNDk2IDQzLjAwOCAyNi45NDQgNjAuOTkydjI1NmMwIDE4LjQ5Ni02LjQgMjAuMDMyLTI0Ljk2IDIwLjAzMnMtMzguNzItMS41MzYtMzguNzItMjAuMDMyeiBtMTkxLjE2OCAwVjUxMmE2NCA2NCAwIDAgMSAyMy44MDgtNjAuOTkyYzQyLjQzMiAwIDM5LjM2IDQzLjAwOCAzOS44NzIgNjAuOTkydjI1NmMwIDE4LjQ5Ni0xOS41ODQgMjAuMDMyLTM3Ljk1MiAyMC4wMzJzLTI1Ljc5Mi0xLjUzNi0yNS43OTItMjAuMDMyeiIgcC1pZD0iNzkzIiBmaWxsPSIjNTE1MTUxIj48L3BhdGg+PC9zdmc+");
}
.tui-full-calendar-popup-detail-item-separate {
padding-left: 22px;
}
}
.tui-datepicker {
border-color: #e8e8e8;
.tui-calendar {
th,
td {
height: 32px;
}
.tui-calendar-prev-month.tui-calendar-date,
.tui-calendar-next-month.tui-calendar-date {
visibility: visible;
}
}
.tui-datepicker-body .tui-timepicker,
.tui-datepicker-footer .tui-timepicker {
padding: 16px 46px 16px 47px;
}
}
.tui-full-calendar-week-container{
min-height: 100px;
}
}
body.window-portrait {
.calendar-wrapper {
.tui-full-calendar-popup {
font-weight: normal;
}
.tui-full-calendar-section-button {
> button {
.tui-full-calendar-icon {
width: 14px;
height: 14px;
background-size: 14px;
}
.tui-full-calendar-content {
font-size: 14px;
}
}
}
}
@media (max-width: 640px) {
.calendar-wrapper {
.tui-full-calendar-popup-arrow {
display: none;
}
}
}
}

View File

@ -0,0 +1,156 @@
.dialog-droup-word-chain {
.ivu-modal-body{
max-height: calc(100vh - 260px);
overflow: auto;
padding-top: 0 !important;
padding-right: 0 !important;
}
.chain-modal-header {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
font-weight: 500;
.chain-modal-close {
color: $primary-text-color;
}
.chain-modal-title {
position: absolute;
top: 0;
bottom: 0;
left: 100px;
right: 100px;
display: flex;
justify-content: center;
align-items: center;
> span {
font-size: 16px;
color: $primary-title-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.chain-modal-submit {
color: $primary-color;
display: flex;
align-items: center;
.submit-loading {
width: 14px;
height: 14px;
margin-right: 6px;
}
&.disabled{
color: #9c9c9c;
}
}
}
.word-chain-body{
display: flex;
flex-direction: column;
height: 100%;
max-height: calc(100vh - 265px);
.source{
margin-right: 32px;
span{
color: #84C56A;
}
}
.initiate{
gap: 5px;
white-space: nowrap;
text-overflow: ellipsis;
overflow-y: auto;
min-height: 26px;
margin: 10px 32px 20px 0;
.ivu-input{
border-color: #fff !important;
}
.avatar-wrapper{
margin: 0 4px 4px;
}
>span,>div{
float: left;
}
}
.textarea{
padding-right: 32px;
}
ul{
margin: 20px 0;
list-style-type:none;
flex: 1;
overflow: auto;
padding-right: 32px;
li{
display: flex;
gap: 10px;
padding: 5px 0;
color: #7f7f7f;
span{
min-width: 28px;
height: 28px;
line-height: 28px;
margin-top: 2px;
background-color: #f2f2f2;
border-radius: 100%;
text-align: center;
font-size: 12px;
}
.taskfont{
font-size: 28px;
cursor: pointer;
line-height: 34px;
user-select: none;
transform: scale(0.92);
&.disabled{
opacity: 0.5;
cursor: no-drop;
}
}
&.add{
.taskfont{
line-height: 32px;
transform: scale(1);
}
}
}
}
.switch-row{
padding: 10px 5px;
margin: 0 32px 0 0;
display: flex;
border-top: 1px solid #f0f0f0;
span.label{
flex: 1;
}
&:last-child{
border-bottom: 1px solid #f0f0f0;
}
}
}
}
body.window-portrait {
.dialog-droup-word-chain {
.ivu-modal-fullscreen{
.ivu-modal-body{
padding-top: 10px !important;
max-height: 100%;
}
.word-chain-body{
max-height: 100%;
ul{
flex: none;
}
}
}
}
}

View File

@ -447,7 +447,8 @@
.dialog-tag,
.dialog-todo,
.dialog-notice {
.dialog-notice,
.dialog-new {
font-size: 12px;
max-width: 80%;
margin: 0 auto;
@ -963,6 +964,120 @@
text-decoration: underline;
color: $primary-title-color;
}
.content-word-chain{
ul{
list-style-type:none;
margin-top: 20px;
li{
margin-top: 5px;
.expand{
cursor: pointer;
color: #0bc037;
}
.shrink{
display: none;
}
}
li.participate{
cursor: pointer;
margin-top: 10px;
color: #0bc037;
>span{
font-size: 12px;
margin-left: 2px;
}
}
&.expand,li:nth-last-child(2){
.expand{
display: none;
}
.shrink{
display: block;
}
}
}
}
.content-word-vote{
min-width: 200px;
max-width: 260px;
.vote-msg-head{
margin-bottom: 8px;
color: #0bc037;
line-height: 18px;
span{
padding: 2px 4px;
border-radius: 3px;
background-color: #dee2fa;
margin: 0 4px;
font-size: 12px;
color: #7076e4;
}
}
.ivu-checkbox-group,.ivu-radio-group{
margin-top: 10px;
width: 100%;
.ivu-checkbox-wrapper,.ivu-radio-wrapper{
display: block;
width: 100%;
height: 34px;
line-height: 34px;
.ivu-checkbox-inner{
border-radius: 100%;
}
}
}
.vote-result-body{
font-size: 12px;
margin-top: 14px;
ul{
list-style-type:none;
li{
margin-bottom: 14px;
.vote-option-title{
margin-bottom: 3px;
}
.ivu-progress-inner{
background-color: #e2e2e2;
}
.avatar-row{
gap: 2px;
margin-top: 2px;
display: flex;
overflow: auto;
padding-bottom: 4px;
&::-webkit-scrollbar {
background: none;
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: #d4d4d4;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
}
}
}
li:last-child{
margin-bottom: 10px;
}
}
>span,.ticket-num span{
margin-right: 10px;
}
}
.btn-row{
display: flex;
text-align: center;
padding: 10px 0 5px 0;
gap: 10px;
.ivu-btn{
flex: 1;
}
}
}
}
.dialog-emoji {
@ -1237,6 +1352,20 @@
.content-unknown {
color: #ffffff;
}
.content-word-chain{
ul{
li.participate, li .expand{
color: #23241f;
}
}
}
.content-word-vote{
.vote-msg-head{
color: #23241f;
}
}
}
.dialog-emoji {
@ -1301,6 +1430,9 @@
height: 14px;
font-size: 14px;
line-height: 14px;
&.down{
transform: rotate(-180deg);
}
}
}
}
@ -1485,6 +1617,73 @@
}
}
.dialog-wrapper-forward-body{
.dialog-wrapper{
position: relative !important;
.dialog-scroller{
padding: 0 !important;
.dialog-item{
.dialog-view{
width: 100%;
max-width: 100% !important;
margin: 0 !important;
.dialog-head{
width: 100%;
border-radius: 8px !important;
}
.dialog-foot{
display: none !important;
}
}
&.self {
.dialog-head{
background-color: #F4F5F7;
.content-text{
color: #303133 !important;
.markdown-body{
color: #303133 !important;
}
}
.dialog-reply{
&:after{
background-color: rgba(132, 197, 106, 0.7);
}
.bot,.common-avatar{
color: #84C56A !important;
}
}
}
}
}
}
}
.leave-message{
padding-bottom: 16px;
textarea{
background: #f7f7f7;
}
}
}
.dialog-wrapper-forward-footer{
display: flex;
line-height: 34px;
cursor: pointer;
.user-modal-icon {
flex-shrink: 0;
font-size: 22px;
margin-right: 5px;
color: rgba($primary-desc-color, 0.7);
margin-top: 6px;
}
&.selected {
.user-modal-icon {
color: $primary-color;
}
}
}
.dialog-wrapper-read-poptip {
width: 360px;
max-width: 72%;

View File

@ -659,6 +659,9 @@
align-items: center;
cursor: pointer;
}
.head-title{
white-space: nowrap;
}
.task-sort {
display: inline-block;
width: 14px;

View File

@ -0,0 +1,77 @@
.project-permission {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
.permission-title {
color: $primary-title-color;
font-size: 20px;
font-weight: 500;
line-height: 1;
padding: 20px 20px 24px;
display: flex;
align-items: center;
.title-icon {
display: flex;
align-items: center;
width: 14px;
height: 14px;
margin-left: 4px;
margin-top: 2px;
> i {
cursor: pointer;
}
}
}
.permission-content {
flex: 1;
padding: 0 25px;
overflow: auto;
margin-bottom: 20px;
height: 100%;
position: relative;
.project-permission-title{
font-weight: 500;
padding:20px 0 10px 0;
}
.ivu-form-item {
margin-bottom: 5px;
}
.form-placeholder {
font-size: 12px;
color: #999;
}
}
.project-permission-footer{
flex-shrink: 0;
position: static;
padding: 16px 26px;
border-top: 1px solid #F4F4F5;
display: flex;
align-items: center;
gap: 16px;
button {
min-width: 120px;
height: 38px;
line-height: 36px;
}
}
}
body.window-portrait {
.project-permission {
.project-permission-footer{
button {
width: 50%;
}
}
}
}

View File

@ -0,0 +1,105 @@
.task-move {
.task-move-content{
display: flex;
gap: 10px;
margin-top: 16px;
>div{
flex: 1;
padding: 5px;
border-radius: 5px;
.task-move-title{
margin-bottom: 10px;
font-weight: bold;
}
.task-move-row{
padding: 5px 0;
display: flex;
line-height: 36px;
&.not-flex{
display: block;
}
.label{
width: 60px;
min-width: 60px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.task-menu-icon{
padding-right: 5px;
}
.flow {
cursor: pointer;
> span{
font-size: 12px;
height: 18px;
min-width: 20px;
line-height: 16px;
padding: 0 2px;
border-radius: 3px;
color: $primary-color;
border: 1px solid $primary-color;
display: inline-block;
vertical-align: top;
margin-top: 8px;
text-align: center;
&.start {
background-color: rgba($flow-status-start-color, 0.1);
border-color: rgba($flow-status-start-color, 0.1);
color: $flow-status-start-color;
}
&.progress {
background-color: rgba($flow-status-progress-color, 0.1);
border-color: rgba($flow-status-progress-color, 0.1);
color: $flow-status-progress-color;
}
&.test {
background-color: rgba($flow-status-test-color, 0.1);
border-color: rgba($flow-status-test-color, 0.1);
color: $flow-status-test-color;
}
&.end {
background-color: rgba($flow-status-end-color, 0.1);
border-color: rgba($flow-status-end-color, 0.1);
color: $flow-status-end-color;
}
}
}
}
&.task-move-content-old{
.task-move-row{
>div{
opacity: 0.5;
}
.common-user-select > ul > li, .flow{
cursor: initial;
}
}
}
}
}
.ivu-modal-footer {
padding: 26px 0 22px !important;
}
}
body.window-portrait {
.task-move {
.ivu-select-dropdown{
max-width: 100%;
overflow: auto;
.ivu-cascader-menu:last-child{
margin-right: 0px;
}
}
.task-move-row{
.label{
width: auto;
min-width: 50px;
}
}
}
}

View File

@ -5,7 +5,7 @@
flex-shrink: 0;
display: flex;
align-items: flex-start;
margin: 32px 32px 16px;
margin: 32px 20px 16px;
border-bottom: 1px solid #F4F4F5;
.calendar-titbox {
flex: 1;
@ -50,137 +50,6 @@
flex-direction: column;
padding: 0 48px 6px;
overflow: hidden;
.calendar-wrapper {
flex: 1;
position: relative;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background-color: #ffffff;
z-index: 1;
}
.tui-full-calendar-popup {
box-shadow: none;
.tui-full-calendar-section-header {
.tui-full-calendar-ic-checkbox-checked {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAhFBMVEUAAACLz3CLz3CLz3CKzm6Gy2+Lz3CLz3CL0HCLz3CLz3CLz3CLz3CMz3GLz3CKz3CLz3CL0HCJ0G+KznCN0HCL0HCLz3CKz3CLz3CLz3CLz3CMz3CLz3CLz3GL0XCL0HCN0XKLz3CLz3CMz3CLz3CM0HCM0G+FzHCLz3CKz3CMz3CLz3Bod5CFAAAAK3RSTlMA18RAOQ3s8+Pc0rmyq3tpiUwTgBnovyDMjmNSRjUvJQX5yKB0WisKppuUFLaY7gAAAotJREFUeNrtm+FymkAUhc8KqIAgSkyUtkmsmqa97/9+HWeSudpCd8qZ7E0m+73A9/1gxmXx4IK0nbipvDFTN2lT9JDVToLh6gx/sHQSFLfEFZUEp8IFczFgrv5CTChe/TsxYvfy/IkZy7M/c2KGywDUYkgNIBdDciARUxI0YkqDiZgygRNTHKZiyhRiTAyIATEgBsSAGBADYsAnCJgdmtIywKUAOrsA94QzJyKA95/pbALcPV7piADGryzDB+Rnv3IkAhi/UhMBjF9pQwbk3/A3JRXA+7GjAng/DqEC5v3+dMYE8P6nQsIEzFOPnw7g/QpC+R8K4QJ4f5iAzaA/TMAm6fdvZQgY+wW2fn9A0a66Bee/2xLH8kp/Rr1MB/3jAxaP0ALCPzagzPBCM9r/XcYH3K4BLfD4vw75xwdMbqB4CmaD/vEB2ztc0RD+MQHFg+c85fffDPv9AbPUd6Lz+ydCBDyjh1//6WcCGngK1L8a8lMBJTwFfj8XIC16+SnX7Af9bIDUngLSrwFEwaD/VogA5YheDj7/Wv1MgL9g/8XjpwNk+c8Cyq8BYwqePX46QOkGCmi/BowrqFi/Bvg4wYv6y7e4Kz4Rfj5AnzY/WSlcwHAB5+cD9ivKzwfIbEX4qQA99RB+NkDfO4b5sRAigHjzpP0C6u6D9wuo2z/eL+DvXx8Zv4C4gVd/qABxCeFnApQT5+cDpOP8fICcLv2VxbfjlQZUNh+vU/XbBIj6jQLm9wDWlZgFyOaYHJ3Ix/gDQwyIATEgBsSAGODBfuBgPvEwH7m0YkqLVExJYfsQuPcwdjOf+5kPHoFKjKio0e3Hn90WUCyGt7v3Nf0GsjqXYOR1hh6SJsz8v0mg/AZRXmaRKXtJBwAAAABJRU5ErkJggg==);
}
}
.tui-full-calendar-popup-container {
border: 0;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
border-radius: 6px;
}
.tui-full-calendar-arrow-top .tui-full-calendar-popup-arrow-border {
top: -8px;
border-bottom-color: rgba(217, 217, 217, .5);
}
}
.tui-full-calendar-dropdown-menu {
border-color: #e8e8e8;
width: calc(100% - 14px);
}
.tui-full-calendar-popup-creation {
.tui-full-calendar-icon {
&.tui-full-calendar-ic-title,
&.tui-full-calendar-calendar-dot {
display: none;
}
&.tui-full-calendar-ic-date {
background-image: url("data:image/svg+xml;base64,PHN2ZyB0PSIxNjIzODU5NjcwNjA3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE2Mzg4IiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PHBhdGggZD0iTTk2MCAxMjhIODMzYzAtNTMtNDMtOTYtOTYtOTZoLTE2Yy01MyAwLTk2IDQzLTk2IDk2SDQwMGMwLTI2LjUtMTAuNy01MC41LTI4LjEtNjcuOUMzNTQuNSA0Mi43IDMzMC41IDMyIDMwNCAzMmgtMTZjLTUzIDAtOTYgNDMtOTYgOTZINjRjLTM1LjMgMC02NCAyOC42LTY0IDY0djczNmMwIDM1LjMgMjguNyA2NCA2NCA2NGg4OTZjMzUuMyAwIDY0LTI4LjcgNjQtNjRWMTkyYzAtMzUuNC0yOC43LTY0LTY0LTY0eiBtLTI3MSA4YzAtMjIuMSAxNy45LTQwIDQwLTQwczQwIDE3LjkgNDAgNDB2ODBjMCAyMi4xLTE3LjkgNDAtNDAgNDAtMTEgMC0yMS00LjUtMjguMy0xMS43QzY5My41IDIzNyA2ODkgMjI3IDY4OSAyMTZ2LTgweiBtLTQzMyAwYzAtMjIuMSAxNy45LTQwIDQwLTQwczQwIDE3LjkgNDAgNDB2ODBjMCAyMi4xLTE3LjkgNDAtNDAgNDAtMTEgMC0yMS00LjUtMjguMy0xMS43QzI2MC41IDIzNyAyNTYgMjI3IDI1NiAyMTZ2LTgweiBtNzA0IDc2MGMwIDE3LjctMTQuMyAzMi0zMiAzMkg5NmMtMTcuNyAwLTMyLTE0LjMtMzItMzJWNDQ4aDg5NnY0NDh6IiBwLWlkPSIxNjM4OSIgZmlsbD0iIzUxNTE1MSI+PC9wYXRoPjwvc3ZnPg==");
background-size: contain;
}
}
.tui-full-calendar-content {
padding-left: 0;
}
.tui-full-calendar-popup-section {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
.tui-full-calendar-popup-section-item {
height: 36px;
line-height: 34px;
border-color: #e8e8e8;
border-radius: 4px;
}
.tui-full-calendar-popup-section-item input {
height: 34px;
}
}
.tui-full-calendar-section-title {
width: 100%;
input {
width: 100%;
}
}
.tui-full-calendar-section-start-date,
.tui-full-calendar-section-end-date {
width: 210px;
.tui-full-calendar-content {
padding-left: 8px;
}
}
.tui-full-calendar-popup-location,
.tui-full-calendar-section-private,
.tui-full-calendar-section-allday,
.tui-full-calendar-section-state {
display: none;
}
}
.tui-full-calendar-popup-task {
.priority {
color: #ffffff;
padding: 2px 4px;
border-radius: 4px;
margin-right: 6px;
}
.overdue {
color: #f5222d;
background: #fff1f0;
border: 1px solid #ffa39e;
padding: 1px 3px;
border-radius: 4px;
margin-right: 6px;
}
.tui-full-calendar-calendar-dot,
.tui-full-calendar-ic-priority {
opacity: 0;
}
.tui-full-calendar-ic-edit {
top: -2px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB0PSIxNjIzODU5MzY4MTg5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjExMTkiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNODMzLjQyODU3MTY4IDYySDE5MC41NzE0MjgzMmExMjguNTcxNDI4MzIgMTI4LjU3MTQyODMyIDAgMCAwLTEyOC41NzE0MjgzMiAxMjguNTcxNDI4MzJ2NjQyLjg1NzE0MzM2YTEyOC41NzE0MjgzMiAxMjguNTcxNDI4MzIgMCAwIDAgMTI4LjU3MTQyODMyIDEyOC41NzE0MjgzMmg2NDIuODU3MTQzMzZhMTI4LjU3MTQyODMyIDEyOC41NzE0MjgzMiAwIDAgMCAxMjguNTcxNDI4MzItMTI4LjU3MTQyODMyVjE5MC41NzE0MjgzMmExMjguNTcxNDI4MzIgMTI4LjU3MTQyODMyIDAgMCAwLTEyOC41NzE0MjgzMi0xMjguNTcxNDI4MzJ6IG02NC4yODU3MTQxNiA3NzEuNDI4NTcxNjhhNjQuMjg1NzE0MTYgNjQuMjg1NzE0MTYgMCAwIDEtNjQuMjg1NzE0MTcgNjQuMjg1NzE0MTZIMTkwLjU3MTQyODMyYTY0LjI4NTcxNDE2IDY0LjI4NTcxNDE2IDAgMCAxLTY0LjI4NTcxNDE2LTY0LjI4NTcxNDE2VjE5MC41NzE0MjgzMmE2NC4yODU3MTQxNiA2NC4yODU3MTQxNiAwIDAgMSA2NC4yODU3MTQxNy02NC4yODU3MTQxNmg2NDIuODU3MTQzMzVhNjQuMjg1NzE0MTYgNjQuMjg1NzE0MTYgMCAwIDEgNjQuMjg1NzE0MTYgNjQuMjg1NzE0MTd6IiBwLWlkPSIxMTIwIiBmaWxsPSIjNTE1MTUxIj48L3BhdGg+PHBhdGggZD0iTTE5MC41NzE0MjgzMiAyNTQuODU3MTQyNDhoNjQuMjg1NzE0MTZ2NjQuMjg1NzE1MDRIMTkwLjU3MTQyODMyek0zMTkuMTQyODU3NTIgMjU0Ljg1NzE0MjQ4aDQ1MHY2NC4yODU3MTUwNEgzMTkuMTQyODU3NTJ6TTE5MC41NzE0MjgzMiA0NDcuNzE0Mjg1ODRoNjQuMjg1NzE0MTZ2NjQuMjg1NzE0MTZIMTkwLjU3MTQyODMyek0zMTkuMTQyODU3NTIgNDQ3LjcxNDI4NTg0aDQ1MHY2NC4yODU3MTQxNkgzMTkuMTQyODU3NTJ6TTE5MC41NzE0MjgzMiA2NDAuNTcxNDI4MzJoNjQuMjg1NzE0MTZ2NjQuMjg1NzE0MTZIMTkwLjU3MTQyODMyek0zMTkuMTQyODU3NTIgNjQwLjU3MTQyODMyaDMyMS40Mjg1NzA4djY0LjI4NTcxNDE2SDMxOS4xNDI4NTc1MnoiIHAtaWQ9IjExMjEiIGZpbGw9IiM1MTUxNTEiPjwvcGF0aD48L3N2Zz4=");
}
.tui-full-calendar-ic-delete {
top: -2px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB0PSIxNjIzODU5MzMwMTc2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijc5MiIgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiPjxwYXRoIGQ9Ik04OTIuMjg4IDI1NmgtMTkxLjE2OEEyMDIuMjQgMjAyLjI0IDAgMCAwIDUwOS42MzIgNjIuMDggMjAxLjIxNiAyMDEuMjE2IDAgMCAwIDMxOC44NDggMjU2SDEyOGMtMTguNjg4IDAtNjYuMDQ4LTQuMjI0LTY2LjA0OCAyNC43NjhDNjEuOTUyIDMyNy43NDQgMTA5LjM3NiAzMjAgMTI4IDMyMGg2NHY1MTJhMTQ2LjQ5NiAxNDYuNDk2IDAgMCAwIDEyNy40MjQgMTI4aDM4Mi4yNzJBMTUwLjAxNiAxNTAuMDE2IDAgMCAwIDgzMiA4MzJsLTMuMzkyLTUxMmg2NGMxOC4zNjggMCA2NS4wMjQgMS40NzIgNjUuMDI0LTM5Ljc0NEE3Mi4zODQgNzIuMzg0IDAgMCAwIDg5Mi4yODggMjU2ek01MDkuNjMyIDEyOC41MTJBMTM4LjE3NiAxMzguMTc2IDAgMCAxIDYzNy40NCAyNTZIMzgyLjU5MmExMzcuOTIgMTM3LjkyIDAgMCAxIDEyNy4wNC0xMjcuNDg4ek03NjggODMyYTk3Ljk4NCA5Ny45ODQgMCAwIDEtNjYuODggNjRIMzE4Ljg0OGE5My41NjggOTMuNTY4IDAgMCAxLTY0LTY0VjMyMEg3Njh2NTEyeiBtLTM4NS40MDgtNjRWNTEyYzAtMTguNDk2IDAuOTYtNjAuOTkyIDM2LjczNi02MC45OTIgMjcuMzI4IDAgMjYuNDk2IDQzLjAwOCAyNi45NDQgNjAuOTkydjI1NmMwIDE4LjQ5Ni02LjQgMjAuMDMyLTI0Ljk2IDIwLjAzMnMtMzguNzItMS41MzYtMzguNzItMjAuMDMyeiBtMTkxLjE2OCAwVjUxMmE2NCA2NCAwIDAgMSAyMy44MDgtNjAuOTkyYzQyLjQzMiAwIDM5LjM2IDQzLjAwOCAzOS44NzIgNjAuOTkydjI1NmMwIDE4LjQ5Ni0xOS41ODQgMjAuMDMyLTM3Ljk1MiAyMC4wMzJzLTI1Ljc5Mi0xLjUzNi0yNS43OTItMjAuMDMyeiIgcC1pZD0iNzkzIiBmaWxsPSIjNTE1MTUxIj48L3BhdGg+PC9zdmc+");
}
.tui-full-calendar-popup-detail-item-separate {
padding-left: 22px;
}
}
.tui-datepicker {
border-color: #e8e8e8;
.tui-calendar {
th,
td {
height: 32px;
}
.tui-calendar-prev-month.tui-calendar-date,
.tui-calendar-next-month.tui-calendar-date {
visibility: visible;
}
}
.tui-datepicker-body .tui-timepicker,
.tui-datepicker-footer .tui-timepicker {
padding: 16px 46px 16px 47px;
}
}
}
}
.calendar-menu {
position: absolute;

View File

@ -240,6 +240,7 @@
flex: 1;
padding: 24px 40px;
overflow: auto;
padding-bottom: 0;
}
.ivu-tabs {
flex: 1;

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#87D068;}
.st1{fill:#FFFFFF;}
</style>
<g>
<g>
<path class="st0" d="M36,48H12C5.4,48,0,42.6,0,36V12C0,5.4,5.4,0,12,0h24c6.6,0,12,5.4,12,12v24C48,42.6,42.6,48,36,48z"/>
</g>
<g>
<path class="st1" d="M34.4,33.5H25l9.3-9.3c1.2-1.2,1.2-3.3,0-4.5l-6.8-6.8c-1.2-1.2-3.3-1.2-4.5,0L12.9,23.1
c-1.2,1.2-1.2,3.3,0,4.5l6,6h-6c-0.5,0-0.9,0.4-0.9,0.9c0,0.5,0.4,0.9,0.9,0.9H22c0,0,0,0,0,0c0,0,0,0,0,0h12.4
c0.5,0,0.9-0.4,0.9-0.9C35.3,33.9,34.9,33.5,34.4,33.5z M17.1,25.5c0.3-0.3,0.9-0.3,1.2,0l3.4,3.4c0.1,0.1,0.3,0.1,0.4,0l6.7-6.7
c0.3-0.3,0.9-0.3,1.2,0c0.3,0.3,0.3,0.9,0,1.2l-6.7,6.7c-0.4,0.4-0.9,0.6-1.5,0.6c-0.5,0-1.1-0.2-1.5-0.6l-3.4-3.4
C16.8,26.4,16.8,25.9,17.1,25.5L17.1,25.5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#87D068;}
.st1{clip-path:url(#SVGID_00000018941778426702510620000006481262296390689922_);}
.st2{fill:#FFFFFF;}
</style>
<g>
<g>
<path class="st0" d="M36,48H12C5.4,48,0,42.6,0,36V12C0,5.4,5.4,0,12,0h24c6.6,0,12,5.4,12,12v24C48,42.6,42.6,48,36,48z"/>
</g>
<g>
<defs>
<rect id="SVGID_1_" x="12" y="12" width="24" height="24"/>
</defs>
<clipPath id="SVGID_00000141444668484078751200000003089482387513471677_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000141444668484078751200000003089482387513471677_);">
<g>
<path class="st2" d="M28.5,21c-4.1,0-7.5,3.4-7.5,7.5c0,4.1,3.4,7.5,7.5,7.5c4.1,0,7.5-3.4,7.5-7.5C36,24.4,32.6,21,28.5,21z
M30.6,29.4h-1.2v1.2c0,0.5-0.4,0.9-0.9,0.9c-0.5,0-0.9-0.4-0.9-0.9v-1.2h-1.2c-0.5,0-0.9-0.4-0.9-0.9c0-0.5,0.4-0.9,0.9-0.9
h1.2v-1.2c0-0.5,0.4-0.9,0.9-0.9c0.5,0,0.9,0.4,0.9,0.9v1.2h1.2c0.5,0,0.9,0.4,0.9,0.9C31.5,29,31,29.4,30.6,29.4z"/>
</g>
<g>
<path class="st2" d="M15.3,23.9c0-4.8,3.9-8.7,8.7-8.7c0.6,0,1.2,0.1,1.8,0.2c-1.3-2.1-3.7-3.4-6.3-3.4c-4.1,0-7.5,3.4-7.5,7.5
c0,2.7,1.4,5,3.5,6.3C15.3,25.2,15.3,24.6,15.3,23.9z"/>
</g>
<g>
<path class="st2" d="M19.8,28.5c0-4.8,3.9-8.7,8.7-8.7c0.6,0,1.3,0.1,1.9,0.2c-1.3-2.1-3.7-3.6-6.4-3.6c-4.1,0-7.5,3.4-7.5,7.5
c0,2.7,1.4,5,3.5,6.3C19.8,29.7,19.8,29.1,19.8,28.5z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB