Compare commits

..

No commits in common. "master" and "v0.7.73" have entirely different histories.

952 changed files with 282379 additions and 31269 deletions

View File

@ -19,7 +19,7 @@ jobs:
uses: loopwerk/tag-changelog@v1
with:
token: ${{ secrets.GH_PAT }}
exclude_types: other,chore,build
exclude_types: other,chore
- name: Create release
uses: actions/create-release@latest

1
.gitignore vendored
View File

@ -19,6 +19,7 @@ Homestead.yaml
npm-debug.log
yarn-error.log
test.*
composer.lock
package-lock.json
laravels-timer-process.pid
.DS_Store

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "resources/drawio"]
path = resources/drawio
url = https://github.com/jgraph/drawio.git

View File

@ -21,9 +21,9 @@ Group No.: `546574618`
# 1、Clone the repository
# Clone projects on github
git clone --depth=1 https://github.com/kuaifan/dootask.git
git clone https://github.com/kuaifan/dootask.git
# Or you can use gitee
git clone --depth=1 https://gitee.com/aipaw/dootask.git
git clone https://gitee.com/aipaw/dootask.git
# 2、Enter directory
cd dootask

View File

@ -21,9 +21,9 @@
# 1、克隆项目到您的本地或服务器
# 通过github克隆项目
git clone --depth=1 https://github.com/kuaifan/dootask.git
git clone https://github.com/kuaifan/dootask.git
# 或者你也可以使用gitee
git clone --depth=1 https://gitee.com/aipaw/dootask.git
git clone https://gitee.com/aipaw/dootask.git
# 2、进入目录
cd dootask

View File

@ -2,16 +2,13 @@
namespace App\Http\Controllers\Api;
use App\Models\File;
use App\Models\ProjectTask;
use App\Models\ProjectTaskFile;
use App\Models\User;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use App\Models\WebSocketDialogUser;
use App\Module\Base;
use Carbon\Carbon;
use Request;
use Response;
@ -41,10 +38,9 @@ class DialogController extends AbstractController
{
$user = User::auth();
//
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at'])
$list = WebSocketDialog::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('u.userid', $user->userid)
->orderByDesc('u.top_at')
->orderByDesc('web_socket_dialogs.last_at')
->paginate(Base::getPaginate(200, 100));
$list->transform(function (WebSocketDialog $item) use ($user) {
@ -192,7 +188,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendtext 06. 发送消息
* @api {get} api/dialog/msg/sendtext 06. 发送消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -208,7 +204,6 @@ class DialogController extends AbstractController
*/
public function msg__sendtext()
{
Base::checkClientVersion('0.8.1');
$user = User::auth();
//
$chat_nickname = Base::settingFind('system', 'chat_nickname');
@ -219,8 +214,8 @@ class DialogController extends AbstractController
}
}
//
$dialog_id = Base::getPostInt('dialog_id');
$text = trim(Base::getPostValue('text'));
$dialog_id = intval(Request::input('dialog_id'));
$text = trim(Request::input('text'));
//
if (mb_strlen($text) < 1) {
return Base::retError('消息内容不能为空');
@ -230,21 +225,11 @@ class DialogController extends AbstractController
//
WebSocketDialog::checkDialog($dialog_id);
//
if (mb_strlen($text) > 2000) {
$array = mb_str_split($text, 2000);
} else {
$array = [$text];
}
$msg = [
'text' => $text
];
//
$list = [];
foreach ($array as $item) {
$res = WebSocketDialogMsg::sendMsg($dialog_id, 'text', ['text' => $item], $user->userid);
if (Base::isSuccess($res)) {
$list[] = $res['data'];
}
}
//
return Base::retSuccess('发送成功', $list);
return WebSocketDialogMsg::sendMsg($dialog_id, 'text', $msg, $user->userid);
}
/**
@ -272,7 +257,7 @@ class DialogController extends AbstractController
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
$path = "uploads/chat/" . $user->userid . "/";
$image64 = Base::getPostValue('image64');
$fileName = Base::getPostValue('filename');
if ($image64) {
@ -284,7 +269,7 @@ class DialogController extends AbstractController
} else {
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'more',
"type" => 'file',
"path" => $path,
"fileName" => $fileName,
]);
@ -364,9 +349,6 @@ class DialogController extends AbstractController
* @apiName msg__detail
*
* @apiParam {Number} msg_id 消息ID
* @apiParam {String} only_update_at 仅获取update_at字段
* - no (默认)
* - yes
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -377,30 +359,39 @@ class DialogController extends AbstractController
User::auth();
//
$msg_id = intval(Request::input('msg_id'));
$only_update_at = Request::input('only_update_at', 'no');
//
$dialogMsg = WebSocketDialogMsg::whereId($msg_id)->first();
if (empty($dialogMsg)) {
return Base::retError("文件不存在");
}
//
if ($only_update_at == 'yes') {
return Base::retSuccess('success', [
'id' => $dialogMsg->id,
'update_at' => Carbon::parse($dialogMsg->updated_at)->toDateTimeString()
]);
}
//
$data = $dialogMsg->toArray();
//
if ($data['type'] == 'file') {
$codeExt = ['txt'];
$officeExt = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
$localExt = ['jpg', 'jpeg', 'png', 'gif'];
$msg = Base::json2array($dialogMsg->getRawOriginal('msg'));
$msg = File::formatFileData($msg);
$data['content'] = $msg['content'];
$data['file_mode'] = $msg['file_mode'];
$filePath = public_path($msg['path']);
if (in_array($msg['ext'], $codeExt) && $msg['size'] < 2 * 1024 * 1024) {
// 文本预览限制2M内的文件
$data['content'] = file_get_contents($filePath);
$data['file_mode'] = 1;
} elseif (in_array($msg['ext'], $officeExt)) {
// office预览
$data['file_mode'] = 2;
} else {
// 其他预览
if (in_array($msg['ext'], $localExt)) {
$url = Base::fillUrl($msg['path']);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $msg['path'];
}
$data['url'] = base64_encode($url);
$data['file_mode'] = 3;
}
}
//
return Base::retSuccess('success', $data);
return Base::retSuccess("success", $data);
}
/**
@ -460,31 +451,4 @@ class DialogController extends AbstractController
$msg->deleteMsg();
return Base::retSuccess("success");
}
/**
* @api {get} api/dialog/top 12. 会话置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName top
*
* @apiParam {Number} dialog_id 会话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function top()
{
$user = User::auth();
$dialogId = intval(Request::input('dialog_id'));
$dialogUser = WebSocketDialogUser::whereUserid($user->userid)->whereDialogId($dialogId)->first();
if (!$dialogUser) {
return Base::retError("会话不存在");
}
$dialogUser->top_at = $dialogUser->top_at ? null : Carbon::now();
$dialogUser->save();
return Base::retSuccess("success", $dialogId);
}
}

View File

@ -9,11 +9,12 @@ use App\Models\FileContent;
use App\Models\FileLink;
use App\Models\FileUser;
use App\Models\User;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Ihttp;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Request;
use Response;
/**
* @apiDefine file
@ -206,30 +207,21 @@ class FileController extends AbstractController
'folder',
'document',
'mind',
'drawio',
'flow',
'word',
'excel',
'ppt',
])) {
return Base::retError('类型错误');
}
$ext = str_replace([
'folder',
'document',
'mind',
'drawio',
$ext = '';
if (in_array($type, [
'word',
'excel',
'ppt',
], [
'',
'md',
'mind',
'drawio',
'docx',
'xlsx',
'pptx',
], $type);
])) {
$ext = str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $type);
}
//
$userid = $user->userid;
if ($pid > 0) {
@ -302,19 +294,9 @@ class FileController extends AbstractController
'userid' => $userid,
'created_id' => $user->userid,
]);
$data = AbstractModel::transaction(function() use ($file) {
$content = FileContent::select(['content', 'text', 'size'])->whereFid($file->cid)->orderByDesc('id')->first();
$file->size = $content?->size ?: 0;
$file->save();
if ($content) {
$content = $content->toArray();
$content['fid'] = $file->id;
$content['userid'] = $file->userid;
FileContent::createInstance($content)->save();
}
return File::find($file->id);
});
$file->save();
//
$data = File::find($file->id);
$data->pushMsg('add', $data);
return Base::retSuccess('复制成功', $data);
}
@ -327,7 +309,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName move
*
* @apiParam {Numbers} ids 文件ID(格式:[id1, id2]
* @apiParam {Number} id 文件ID
* @apiParam {Number} pid 移动到的文件夹ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@ -338,45 +320,30 @@ class FileController extends AbstractController
{
$user = User::auth();
//
$ids = Request::input('ids');
$id = intval(Request::input('id'));
$pid = intval(Request::input('pid'));
//
if (!is_array($ids) || empty($ids)) {
return Base::retError('请选择移动的文件或文件夹');
}
if (count($ids) > 100) {
return Base::retError('一次最多只能移动100个文件或文件夹');
}
$file = File::permissionFind($id, 1000);
//
if ($pid > 0) {
File::permissionFind($pid, 1);
if (!File::whereUserid($user->userid)->whereId($pid)->exists()) {
return Base::retError('参数错误');
}
$arr = [];
$tid = $pid;
while ($tid > 0) {
$arr[] = $tid;
$tid = intval(File::whereId($tid)->value('pid'));
}
if (in_array($id, $arr)) {
return Base::retError('位置错误');
}
}
//
$files = [];
AbstractModel::transaction(function() use ($pid, $ids, &$files) {
foreach ($ids as $id) {
$file = File::permissionFind($id, 1000);
//
if ($pid > 0) {
$arr = [];
$tid = $pid;
while ($tid > 0) {
$arr[] = $tid;
$tid = intval(File::whereId($tid)->value('pid'));
}
if (in_array($id, $arr)) {
throw new ApiException('移动位置错误');
}
}
//
$file->pid = $pid;
$file->save();
$files[] = $file;
}
});
foreach ($files as $file) {
$file->pushMsg('update', $file);
}
return Base::retSuccess('操作成功', $files);
$file->pid = $pid;
$file->save();
$file->pushMsg('update', $file);
return Base::retSuccess('操作成功', $file);
}
/**
@ -387,7 +354,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName remove
*
* @apiParam {Numbers} ids 文件ID(格式:[id1, id2]
* @apiParam {Number} id 文件ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -397,25 +364,12 @@ class FileController extends AbstractController
{
User::auth();
//
$ids = Request::input('ids');
$id = intval(Request::input('id'));
//
if (!is_array($ids) || empty($ids)) {
return Base::retError('请选择删除的文件或文件夹');
}
if (count($ids) > 100) {
return Base::retError('一次最多只能删除100个文件或文件夹');
}
$file = File::permissionFind($id, 1000);
//
$files = [];
AbstractModel::transaction(function() use ($ids, &$files) {
foreach ($ids as $id) {
$file = File::permissionFind($id, 1000);
$file->deleteFile();
$files[] = $file;
}
});
//
return Base::retSuccess('删除成功', $files);
$file->deleteFile();
return Base::retSuccess('删除成功', $file);
}
/**
@ -429,10 +383,7 @@ class FileController extends AbstractController
* @apiParam {Number|String} id
* - Number: 文件ID需要登录
* - String: 链接码(不需要登录,用于预览)
* @apiParam {String} only_update_at 仅获取update_at字段
* - no (默认)
* - yes
* @apiParam {String} down 直接下载
* @apiParam {String} down 直接下载
* - no: 浏览(默认)
* - yes: 下载office文件直接下载
*
@ -444,7 +395,6 @@ class FileController extends AbstractController
{
$id = Request::input('id');
$down = Request::input('down', 'no');
$only_update_at = Request::input('only_update_at', 'no');
//
if (Base::isNumber($id)) {
User::auth();
@ -459,13 +409,6 @@ class FileController extends AbstractController
return Base::retError('参数错误');
}
//
if ($only_update_at == 'yes') {
return Base::retSuccess('success', [
'id' => $file->id,
'update_at' => Carbon::parse($file->updated_at)->toDateTimeString()
]);
}
//
$content = FileContent::whereFid($file->id)->orderByDesc('id')->first();
return FileContent::formatContent($file, $content?->content, $down == 'yes');
}
@ -478,7 +421,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName content__save
*
* @apiParam {Number} id 文件ID
* @apiParam {Number} id 文件ID
* @apiParam {Object} [D] Request Payload 提交
* - content: 内容
*
@ -488,7 +431,6 @@ class FileController extends AbstractController
*/
public function content__save()
{
Base::checkClientVersion('0.9.13');
$user = User::auth();
//
$id = Base::getPostInt('id');
@ -502,11 +444,12 @@ class FileController extends AbstractController
$isRep = false;
preg_match_all("/<img\s*src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $data['content'], $matchs);
foreach ($matchs[2] as $key => $text) {
$tmpPath = "uploads/file/document/" . date("Ym") . "/" . $id . "/attached/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (file_put_contents(public_path($tmpPath), base64_decode($text))) {
$data['content'] = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '"', $data['content']);
$p = "uploads/files/document/" . $id . "/";
Base::makeDir(public_path($p));
$p.= md5($text) . "." . $matchs[1][$key];
$r = file_put_contents(public_path($p), base64_decode($text));
if ($r) {
$data['content'] = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($p) . '"', $data['content']);
$isRep = true;
}
}
@ -516,41 +459,11 @@ class FileController extends AbstractController
}
}
//
switch ($file->type) {
case 'document':
$contentArray = Base::json2array($content);
$contentString = $contentArray['content'];
$file->ext = $contentArray['type'] == 'md' ? 'md' : 'text';
break;
case 'drawio':
$contentArray = Base::json2array($content);
$contentString = $contentArray['xml'];
$file->ext = 'drawio';
break;
case 'mind':
$contentString = $content;
$file->ext = 'mind';
break;
case 'code':
case 'txt':
$contentString = $content;
break;
default:
return Base::retError('参数错误');
}
$path = "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $id . "/" . md5($contentString);
$save = public_path($path);
Base::makeDir(dirname($save));
file_put_contents($save, $contentString);
//
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => [
'type' => $file->ext,
'url' => $path
],
'content' => $content,
'text' => $text,
'size' => filesize($save),
'size' => strlen($content),
'userid' => $user->userid,
]);
$content->save();
@ -590,7 +503,7 @@ class FileController extends AbstractController
if ($status === 2) {
$parse = parse_url($url);
$from = 'http://' . env('APP_IPPR') . '.3' . $parse['path'] . '?' . $parse['query'];
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
$path = 'uploads/office/' . date("Ym") . '/' . $file->id . '/' . $user->userid . '-' . $key;
$save = public_path($path);
Base::makeDir(dirname($save));
$res = Ihttp::download($from, $save);
@ -608,7 +521,6 @@ class FileController extends AbstractController
$content->save();
//
$file->size = $content->size;
$file->updated_at = Carbon::now();
$file->save();
$file->pushMsg('update', $file);
}
@ -652,11 +564,10 @@ class FileController extends AbstractController
}
//
$dirs = explode("/", $webkitRelativePath);
while (count($dirs) > 1) {
$dirName = array_shift($dirs);
if ($dirName) {
$pushMsg = [];
AbstractModel::transaction(function () use ($dirName, $user, $userid, &$pid, &$pushMsg) {
AbstractModel::transaction(function() use ($user, $userid, $dirs, &$pid) {
while (count($dirs) > 1) {
$dirName = array_shift($dirs);
if ($dirName) {
$dirRow = File::wherePid($pid)->whereType('folder')->whereName($dirName)->lockForUpdate()->first();
if (empty($dirRow)) {
$dirRow = File::createInstance([
@ -667,21 +578,19 @@ class FileController extends AbstractController
'created_id' => $user->userid,
]);
if ($dirRow->save()) {
$pushMsg[] = File::find($dirRow->id);
$tmpRow = File::find($dirRow->id);
$tmpRow->pushMsg('add', $tmpRow);
}
}
if (empty($dirRow)) {
throw new ApiException('创建文件夹失败');
}
$pid = $dirRow->id;
});
foreach ($pushMsg as $tmpRow) {
$tmpRow->pushMsg('add', $tmpRow);
}
}
}
});
//
$path = 'uploads/tmp/' . date("Ym") . '/';
$path = 'uploads/file/' . date("Ym") . '/u' . $user->userid . '/';
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'more',
@ -694,9 +603,6 @@ class FileController extends AbstractController
$data = $data['data'];
//
$type = match ($data['ext']) {
'text', 'md', 'markdown' => 'document',
'drawio' => 'drawio',
'mind' => 'mind',
'doc', 'docx' => "word",
'xls', 'xlsx' => "excel",
'ppt', 'pptx' => "ppt",
@ -710,7 +616,7 @@ class FileController extends AbstractController
'txt' => "txt",
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
'dockerfile', 'go', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'ocamlmakefile', 'make', 'md', 'markdown', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
@ -721,9 +627,6 @@ class FileController extends AbstractController
'rp' => "axure",
default => "",
};
if ($data['ext'] == 'markdown') {
$data['ext'] = 'md';
}
$file = File::createInstance([
'pid' => $pid,
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
@ -737,7 +640,6 @@ class FileController extends AbstractController
$file->size = $data['size'] * 1024;
$file->save();
//
$data = Base::uploadMove($data, "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $file->id . "/");
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => [

View File

@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api;
use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\File;
use App\Models\Project;
use App\Models\ProjectColumn;
use App\Models\ProjectFlow;
@ -17,13 +16,10 @@ use App\Models\ProjectUser;
use App\Models\User;
use App\Models\WebSocketDialog;
use App\Module\Base;
use App\Module\BillExport;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Madzipper;
use Request;
use Response;
use Session;
/**
* @apiDefine project
@ -208,9 +204,6 @@ class ProjectController extends AbstractController
* @apiParam {String} name 项目名称
* @apiParam {String} [desc] 项目介绍
* @apiParam {String} [columns] 列表格式列表名称1,列表名称2
* @apiParam {String} [flow] 开启流程
* - open: 开启
* - close: 关闭(默认)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -222,7 +215,6 @@ class ProjectController extends AbstractController
// 项目名称
$name = trim(Request::input('name', ''));
$desc = trim(Request::input('desc', ''));
$flow = trim(Request::input('flow', 'close'));
if (mb_strlen($name) < 2) {
return Base::retError('项目名称不可以少于2个字');
} elseif (mb_strlen($name) > 32) {
@ -259,7 +251,7 @@ class ProjectController extends AbstractController
'desc' => $desc,
'userid' => $user->userid,
]);
AbstractModel::transaction(function() use ($flow, $insertColumns, $project) {
AbstractModel::transaction(function() use ($insertColumns, $project) {
$project->save();
ProjectUser::createInstance([
'project_id' => $project->id,
@ -276,10 +268,6 @@ class ProjectController extends AbstractController
}
$project->dialog_id = $dialog->id;
$project->save();
//
if ($flow == 'open') {
$project->addFlow(Base::json2array('[{"id":"-10","name":"\u5f85\u5904\u7406","status":"start","turns":["-10","-11","-12","-13"],"usertype":"add","userlimit":"0","sort":"0"},{"id":"-11","name":"\u8fdb\u884c\u4e2d","status":"progress","turns":["-10","-11","-12","-13"],"usertype":"add","userlimit":"0","sort":"1"},{"id":"-12","name":"\u5df2\u5b8c\u6210","status":"end","turns":["-10","-11","-12","-13"],"usertype":"add","userlimit":"0","sort":"2"},{"id":"-13","name":"\u5df2\u53d6\u6d88","status":"end","turns":["-10","-11","-12","-13"],"usertype":"add","userlimit":"0","sort":"3"}]'));
}
});
//
$data = Project::find($project->id);
@ -607,14 +595,13 @@ class ProjectController extends AbstractController
if (!is_array($item['task'])) continue;
$index = 0;
foreach ($item['task'] as $task_id) {
if (ProjectTask::whereId($task_id)->whereProjectId($project->id)->whereCompleteAt(null)->update([
ProjectTask::whereId($task_id)->whereProjectId($project->id)->update([
'column_id' => $item['id'],
'sort' => $index
])) {
ProjectTask::whereParentId($task_id)->whereProjectId($project->id)->update([
'column_id' => $item['id'],
]);
}
]);
ProjectTask::whereParentId($task_id)->whereProjectId($project->id)->update([
'column_id' => $item['id'],
]);
$index++;
}
}
@ -980,154 +967,6 @@ class ProjectController extends AbstractController
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/project/task/export 18. 导出任务(限管理员)
*
* @apiDescription 导出指定范围任务已完成、未完成、已归档返回下载地址需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__export
*
* @apiParam {Array} [userid] 指定会员,如:[1, 2]
* @apiParam {Array} [time] 指定时间范围,如:['2020-12-12', '2020-12-30']
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__export()
{
$user = User::auth('admin');
//
$userid = Base::arrayRetainInt(Request::input('userid'), true);
$time = Request::input('time');
if (empty($userid) || empty($time)) {
return Base::retError('参数错误');
}
if (count($userid) > 20) {
return Base::retError('导出会员限制最多20个');
}
if (!(is_array($time) && Base::isDateOrTime($time[0]) && Base::isDateOrTime($time[1]))) {
return Base::retError('时间选择错误');
}
if (Carbon::parse($time[1])->timestamp - Carbon::parse($time[0])->timestamp > 90 * 86400) {
return Base::retError('时间范围限制最大90天');
}
//
$headings = [];
$headings[] = '任务ID';
$headings[] = '任务标题';
$headings[] = '负责人';
$headings[] = '创建人';
$headings[] = '是否完成';
$headings[] = '完成时间';
$headings[] = '是否归档';
$headings[] = '归档时间';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '结束剩余';
$headings[] = '所属项目';
$headings[] = '父级任务ID';
$datas = [];
//
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
->where('project_task_users.owner', 1)
->whereIn('project_task_users.userid', $userid)
->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay());
$builder->orderByDesc('project_tasks.id')->chunk(100, function($tasks) use (&$datas) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
if ($task->complete_at) {
$a = Carbon::parse($task->complete_at)->timestamp;
$b = Carbon::parse($task->end_at)->timestamp;
if ($b > $a) {
$endSurplus = Base::timeDiff($a, $b);
} else {
$endSurplus = "-" . Base::timeDiff($b, $a);
}
} else {
$endSurplus = '-';
}
$datas[] = [
$task->id,
Base::filterEmoji($task->name),
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
$task->complete_at ? '已完成' : '-',
$task->complete_at ?: '-',
$task->archived_at ? '已归档' : '-',
$task->archived_at ?: '-',
$task->start_at ?: '-',
$task->end_at ?: '-',
$endSurplus,
Base::filterEmoji($task->project?->name) ?: '-',
$task->parent_id ?: '-',
];
}
});
//
$fileName = User::userid2nickname($userid[0]) ?: $userid[0];
if (count($userid) > 1) {
$fileName .= "" . count($userid) . "位成员";
}
$fileName .= '任务统计_' . Base::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Base::time());
$res = BillExport::create()->setHeadings($headings)->setData($datas)->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls'). ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Exception) { }
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('task::export:userid', $user->userid);
return Base::retSuccess('success', [
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
'url' => Base::fillUrl('api/project/task/down?key=' . urlencode($base64)),
]);
} else {
return Base::retError('打包失败,请稍后再试...');
}
}
/**
* @api {get} api/project/task/down 18. 导出任务(限管理员)
*
* @apiDescription 导出指定范围任务已完成、未完成、已归档返回下载地址需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__down
*
* @apiParam {String} key 通过export接口得到的下载钥匙
*
* @apiSuccess {File} 文件下载
*/
public function task__down()
{
$userid = Session::get('task::export:userid');
if (empty($userid)) {
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
}
//
$array = Base::string2array(base64_decode(urldecode(Request::input('key'))));
$file = $array['file'];
if (empty($file) || !file_exists(storage_path($file))) {
return Base::ajaxError("文件不存在!", [], 0, 502);
}
return response()->download(storage_path($file));
}
/**
* @api {get} api/project/task/one 19. 获取单个任务信息
*
@ -1247,15 +1086,12 @@ class ProjectController extends AbstractController
/**
* @api {get} api/project/task/filedetail 23. 获取任务文件详情
*
* @apiDescription 需要token身份
* @apiDescription 需要token身份(限:项目、任务负责人)
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__filedetail
*
* @apiParam {Number} file_id 文件ID
* @apiParam {String} only_update_at 仅获取update_at字段
* - no (默认)
* - yes
* @apiParam {Number} file_id 文件ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -1266,32 +1102,45 @@ class ProjectController extends AbstractController
User::auth();
//
$file_id = intval(Request::input('file_id'));
$only_update_at = Request::input('only_update_at', 'no');
//
$file = ProjectTaskFile::find($file_id);
if (empty($file)) {
return Base::retError("文件不存在");
}
//
if ($only_update_at == 'yes') {
return Base::retSuccess('success', [
'id' => $file->id,
'update_at' => Carbon::parse($file->updated_at)->toDateTimeString()
]);
}
//
$data = $file->toArray();
$data['path'] = $file->getRawOriginal('path');
//
ProjectTask::userTask($file->task_id, null);
ProjectTask::userTask($file->task_id, true, true);
//
return Base::retSuccess('success', File::formatFileData($data));
$codeExt = ['txt'];
$officeExt = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
$localExt = ['jpg', 'jpeg', 'png', 'gif'];
$filePath = public_path($data['path']);
if (in_array($data['ext'], $codeExt) && $data['size'] < 2 * 1024 * 1024) {
// 文本预览限制2M内的文件
$data['content'] = file_get_contents($filePath);
$data['file_mode'] = 1;
} elseif (in_array($data['ext'], $officeExt)) {
// office预览
$data['file_mode'] = 2;
} else {
// 其他预览
if (in_array($data['ext'], $localExt)) {
$url = Base::fillUrl($data['path']);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $data['path'];
}
$data['url'] = base64_encode($url);
$data['file_mode'] = 3;
}
//
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/project/task/filedown 24. 下载任务文件
*
* @apiDescription 需要token身份
* @apiDescription 需要token身份(限:项目、任务负责人)
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__filedown
@ -1314,7 +1163,7 @@ class ProjectController extends AbstractController
}
//
try {
ProjectTask::userTask($file->task_id, null);
ProjectTask::userTask($file->task_id, true, true);
} catch (\Exception $e) {
abort(403, $e->getMessage() ?: "This file not support download.");
}
@ -1792,7 +1641,95 @@ class ProjectController extends AbstractController
//
$project = Project::userProject($project_id, true, true);
//
return Base::retSuccess('保存成功', $project->addFlow($flows));
return AbstractModel::transaction(function() use ($project, $flows) {
$projectFlow = ProjectFlow::whereProjectId($project->id)->first();
if (empty($projectFlow)) {
$projectFlow = ProjectFlow::createInstance([
'project_id' => $project->id,
'name' => 'Default'
]);
if (!$projectFlow->save()) {
throw new ApiException('工作流创建失败');
}
}
//
$ids = [];
$idc = [];
$hasStart = false;
$hasEnd = false;
foreach ($flows as $item) {
$id = intval($item['id']);
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
$usertype = trim($item['usertype']);
$userlimit = intval($item['userlimit']);
if ($usertype == 'replace' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置流转模式时必须填写状态负责人");
}
if ($usertype == 'merge' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置剔除模式时必须填写状态负责人");
}
if ($userlimit && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
}
$flow = ProjectFlowItem::updateInsert([
'id' => $id,
'project_id' => $project->id,
'flow_id' => $projectFlow->id,
], [
'name' => trim($item['name']),
'status' => trim($item['status']),
'sort' => intval($item['sort']),
'turns' => $turns,
'userids' => $userids,
'usertype' => trim($item['usertype']),
'userlimit' => $userlimit,
]);
if ($flow) {
$ids[] = $flow->id;
if ($flow->id != $id) {
$idc[$id] = $flow->id;
}
if ($flow->status == 'start') {
$hasStart = true;
}
if ($flow->status == 'end') {
$hasEnd = true;
}
}
}
if (!$hasStart) {
throw new ApiException('至少需要1个开始状态');
}
if (!$hasEnd) {
throw new ApiException('至少需要1个结束状态');
}
ProjectFlowItem::whereFlowId($projectFlow->id)->whereNotIn('id', $ids)->chunk(100, function($list) {
foreach ($list as $item) {
$item->deleteFlowItem();
}
});
//
$projectFlow = ProjectFlow::with(['projectFlowItem'])->whereProjectId($project->id)->find($projectFlow->id);
$itemIds = $projectFlow->projectFlowItem->pluck('id')->toArray();
foreach ($projectFlow->projectFlowItem as $item) {
$turns = $item->turns;
foreach ($idc as $oid => $nid) {
if (in_array($oid, $turns)) {
$turns = array_diff($turns, [$oid]);
$turns[] = $nid;
}
}
if (!in_array($item->id, $turns)) {
$turns[] = $item->id;
}
$turns = array_values(array_filter(array_unique(array_intersect($turns, $itemIds))));
sort($turns);
$item->turns = $turns;
ProjectFlowItem::whereId($item->id)->update([ 'turns' => Base::array2json($turns) ]);
}
return Base::retSuccess('保存成功', $projectFlow);
});
}
/**
@ -1878,31 +1815,4 @@ class ProjectController extends AbstractController
//
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/project/top 37. 项目置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName top
*
* @apiParam {Number} project_id 项目ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function top()
{
$user = User::auth();
$projectId = intval(Request::input('project_id'));
$projectUser = ProjectUser::whereUserid($user->userid)->whereProjectId($projectId)->first();
if (!$projectUser) {
return Base::retError("项目不存在");
}
$projectUser->top_at = $projectUser->top_at ? null : Carbon::now();
$projectUser->save();
return Base::retSuccess("success", $projectId);
}
}

View File

@ -101,16 +101,12 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/priority 03. 任务优先级
* @api {post} api/system/priority 03. 获取优先级、保存优先级
*
* @apiDescription 获取任务优先级、保存任务优先级
* @apiVersion 1.0.0
* @apiGroup system
* @apiName priority
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存(限管理员)
* @apiParam {Array} list 优先级数据,格式:[{name,color,days,priority}]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@ -150,54 +146,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/column/template 04. 创建项目模板
*
* @apiDescription 获取创建项目模板、保存创建项目模板
* @apiVersion 1.0.0
* @apiGroup system
* @apiName column__template
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存(限管理员)
* @apiParam {Array} list 优先级数据,格式:[{name,columns}]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function column__template()
{
$type = trim(Request::input('type'));
if ($type == 'save') {
User::auth('admin');
$list = Base::getPostValue('list');
$array = [];
if (empty($list) || !is_array($list)) {
return Base::retError('参数错误');
}
foreach ($list AS $item) {
if (empty($item['name']) || empty($item['columns'])) {
continue;
}
$array[] = [
'name' => $item['name'],
'columns' => array_values(array_filter(array_unique(explode(",", $item['columns']))))
];
}
if (empty($array)) {
return Base::retError('参数为空');
}
$setting = Base::setting('columnTemplate', $array);
} else {
$setting = Base::setting('columnTemplate');
}
//
return Base::retSuccess('success', $setting);
}
/**
* @api {get} api/system/get/info 05. 获取终端详细信息
* @api {get} api/system/get/info 04. 获取终端详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@ -226,7 +175,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ip 06. 获取IP地址
* @api {get} api/system/get/ip 05. 获取IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@ -241,7 +190,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/cnip 07. 是否中国IP地址
* @api {get} api/system/get/cnip 06. 是否中国IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@ -258,7 +207,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipgcj02 08. 获取IP地址经纬度
* @api {get} api/system/get/ipgcj02 07. 获取IP地址经纬度
*
* @apiVersion 1.0.0
* @apiGroup system
@ -275,7 +224,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipinfo 09. 获取IP地址详细信息
* @api {get} api/system/get/ipinfo 08. 获取IP地址详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@ -292,7 +241,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/imgupload 10. 上传图片
* @api {post} api/system/imgupload 09. 上传图片
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -315,7 +264,7 @@ class SystemController extends AbstractController
if (!$scale[0] && !$scale[1]) {
$scale = [2160, 4160, -1];
}
$path = "uploads/user/picture/" . User::userid() . "/" . date("Ym") . "/";
$path = "uploads/picture/" . User::userid() . "/" . date("Ym") . "/";
$image64 = trim(Base::getPostValue('image64'));
$fileName = trim(Base::getPostValue('filename'));
if ($image64) {
@ -342,7 +291,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/imgview 11. 浏览图片空间
* @api {get} api/system/get/imgview 10. 浏览图片空间
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -360,7 +309,7 @@ class SystemController extends AbstractController
if (User::userid() === 0) {
return Base::retError('身份失效,等重新登录');
}
$publicPath = "uploads/user/picture/" . User::userid() . "/";
$publicPath = "uploads/picture/" . User::userid() . "/";
$dirPath = public_path($publicPath);
$dirs = $files = [];
//
@ -438,7 +387,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/fileupload 12. 上传文件
* @api {post} api/system/fileupload 11. 上传文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -458,7 +407,7 @@ class SystemController extends AbstractController
if (User::userid() === 0) {
return Base::retError('身份失效,等重新登录');
}
$path = "uploads/user/file/" . User::userid() . "/" . date("Ym") . "/";
$path = "uploads/files/" . User::userid() . "/" . date("Ym") . "/";
$image64 = trim(Base::getPostValue('image64'));
$fileName = trim(Base::getPostValue('filename'));
if ($image64) {

View File

@ -21,9 +21,6 @@ class VerifyCsrfToken extends Middleware
// 保存任务优先级
'api/system/priority/',
// 保存创建项目列表模板
'api/system/column/template/',
// 添加任务
'api/project/task/add/',
@ -33,9 +30,6 @@ class VerifyCsrfToken extends Middleware
// 修改任务
'api/project/task/update/',
// 聊天发文本
'api/dialog/msg/sendtext/',
// 聊天发文件
'api/dialog/msg/sendfile/',

View File

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

View File

@ -50,39 +50,6 @@ class File extends AbstractModel
{
use SoftDeletes;
/**
* 文件文件
*/
const codeExt = [
'txt',
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
'dockerfile', 'go', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx'
];
/**
* office文件
*/
const officeExt = [
'doc', 'docx',
'xls', 'xlsx',
'ppt', 'pptx',
];
/**
* 本地媒体文件
*/
const localExt = [
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw',
'tif', 'tiff',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
];
/**
* 是否有访问权限
* @param $userid
@ -272,77 +239,4 @@ class File extends AbstractModel
}
return $file;
}
/**
* 格式化内容数据
* @param array $data [path, size, ext, name]
* @return array
*/
public static function formatFileData(array $data)
{
$filePath = $data['path'];
$fileSize = $data['size'];
$fileExt = $data['ext'];
$fileDotExt = '.' . $fileExt;
$fileName = Base::rightDelete($data['name'], $fileDotExt) . $fileDotExt;
$publicPath = public_path($filePath);
//
switch ($fileExt) {
case 'md':
case 'text':
// 文本
$data['content'] = [
'type' => $fileExt,
'content' => file_get_contents($publicPath),
];
$data['file_mode'] = $fileExt;
break;
case 'drawio':
// 图表
$data['content'] = [
'xml' => file_get_contents($publicPath)
];
$data['file_mode'] = $fileExt;
break;
case 'mind':
// 思维导图
$data['content'] = Base::json2array(file_get_contents($publicPath));
$data['file_mode'] = $fileExt;
break;
default:
if (in_array($fileExt, self::codeExt) && $fileSize < 2 * 1024 * 1024)
{
// 文本预览限制2M内的文件
$data['content'] = file_get_contents($publicPath);
$data['file_mode'] = 'code';
}
elseif (in_array($fileExt, File::officeExt))
{
// office预览
$data['content'] = '';
$data['file_mode'] = 'office';
}
else
{
// 其他预览
if (in_array($fileExt, File::localExt)) {
$url = Base::fillUrl($filePath);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $filePath;
}
$data['content'] = [
'preview' => true,
'url' => base64_encode(Base::urlAddparameter($url, [
'fullfilename' => $fileName
])),
];
$data['file_mode'] = 'preview';
}
break;
}
return $data;
}
}

View File

@ -60,7 +60,7 @@ class FileContent extends AbstractModel
if (empty($content)) {
$content = match ($file->type) {
'document' => [
"type" => $file->ext,
"type" => "md",
"content" => "",
],
default => json_decode('{}'),
@ -69,20 +69,24 @@ class FileContent extends AbstractModel
abort(403, "This file is empty.");
}
} else {
$path = $content['url'];
$content['preview'] = false;
if ($file->ext) {
$res = File::formatFileData([
'path' => $path,
'ext' => $file->ext,
'size' => $file->size,
'name' => $file->name,
]);
$content = $res['content'];
} else {
$content['preview'] = false;
$filePath = public_path($content['url']);
if (in_array($file->type, ['txt', 'code']) && $file->size < 2 * 1024 * 1024) {
// 支持编辑限制2M内的文件
$content['content'] = file_get_contents($filePath);
} else {
// 支持预览
if (in_array($file->type, ['picture', 'image', 'tif', 'media'])) {
$url = Base::fillUrl($content['url']);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $content['url'];
}
$content['url'] = base64_encode($url);
$content['preview'] = true;
}
}
if ($download) {
$filePath = public_path($path);
if (isset($filePath)) {
return Response::download($filePath, $name);
} else {

View File

@ -3,7 +3,6 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Tasks\PushTask;
use Carbon\Carbon;
use DB;
@ -113,7 +112,6 @@ class Project extends AbstractModel
->select([
'projects.*',
'project_users.owner',
'project_users.top_at',
])
->leftJoin('project_users', function ($leftJoin) use ($userid) {
$leftJoin
@ -137,7 +135,6 @@ class Project extends AbstractModel
->select([
'projects.*',
'project_users.owner',
'project_users.top_at',
])
->join('project_users', 'projects.id', '=', 'project_users.project_id')
->where('project_users.userid', $userid);
@ -214,7 +211,7 @@ class Project extends AbstractModel
*/
public function relationUserids()
{
return ProjectUser::whereProjectId($this->id)->orderBy('id')->pluck('userid')->toArray();
return $this->projectUser->pluck('userid')->toArray();
}
/**
@ -353,114 +350,6 @@ class Project extends AbstractModel
}
}
/**
* 添加工作流
* @param $flows
* @return mixed
*/
public function addFlow($flows)
{
return AbstractModel::transaction(function() use ($flows) {
$projectFlow = ProjectFlow::whereProjectId($this->id)->first();
if (empty($projectFlow)) {
$projectFlow = ProjectFlow::createInstance([
'project_id' => $this->id,
'name' => 'Default'
]);
if (!$projectFlow->save()) {
throw new ApiException('工作流创建失败');
}
}
//
$ids = [];
$idc = [];
$hasStart = false;
$hasEnd = false;
$upTaskList = [];
foreach ($flows as $item) {
$id = intval($item['id']);
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
$usertype = trim($item['usertype']);
$userlimit = intval($item['userlimit']);
if ($usertype == 'replace' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置流转模式时必须填写状态负责人");
}
if ($usertype == 'merge' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置剔除模式时必须填写状态负责人");
}
if ($userlimit && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
}
$flow = ProjectFlowItem::updateInsert([
'id' => $id,
'project_id' => $this->id,
'flow_id' => $projectFlow->id,
], [
'name' => trim($item['name']),
'status' => trim($item['status']),
'sort' => intval($item['sort']),
'turns' => $turns,
'userids' => $userids,
'usertype' => trim($item['usertype']),
'userlimit' => $userlimit,
], [], $isInsert);
if ($flow) {
$ids[] = $flow->id;
if ($flow->id != $id) {
$idc[$id] = $flow->id;
}
if ($flow->status == 'start') {
$hasStart = true;
}
if ($flow->status == 'end') {
$hasEnd = true;
}
if (!$isInsert) {
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name;
}
}
}
if (!$hasStart) {
throw new ApiException('至少需要1个开始状态');
}
if (!$hasEnd) {
throw new ApiException('至少需要1个结束状态');
}
ProjectFlowItem::whereFlowId($projectFlow->id)->whereNotIn('id', $ids)->chunk(100, function($list) {
foreach ($list as $item) {
$item->deleteFlowItem();
}
});
//
foreach ($upTaskList as $id => $value) {
ProjectTask::whereFlowItemId($id)->update([
'flow_item_name' => $value
]);
}
//
$projectFlow = ProjectFlow::with(['projectFlowItem'])->whereProjectId($this->id)->find($projectFlow->id);
$itemIds = $projectFlow->projectFlowItem->pluck('id')->toArray();
foreach ($projectFlow->projectFlowItem as $item) {
$turns = $item->turns;
foreach ($idc as $oid => $nid) {
if (in_array($oid, $turns)) {
$turns = array_diff($turns, [$oid]);
$turns[] = $nid;
}
}
if (!in_array($item->id, $turns)) {
$turns[] = $item->id;
}
$turns = array_values(array_filter(array_unique(array_intersect($turns, $itemIds))));
sort($turns);
$item->turns = $turns;
ProjectFlowItem::whereId($item->id)->update([ 'turns' => Base::array2json($turns) ]);
}
return $projectFlow;
});
}
/**
* 获取项目信息(用于判断会员是否存在项目内)
* @param int $project_id

View File

@ -508,15 +508,13 @@ class ProjectTask extends AbstractModel
{
AbstractModel::transaction(function () use ($data, &$updateMarking) {
// 判断版本
Base::checkClientVersion('0.6.0');
if (version_compare(Base::getClientVersion(), '0.6.0', '<')) {
throw new ApiException('当前版本过低');
}
// 主任务
$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('任务状态未发生改变');
}
@ -537,7 +535,8 @@ class ProjectTask extends AbstractModel
throw new ApiException("当前状态[{$currentFlowItem->name}]不可流转到[{$newFlowItem->name}]");
}
if ($currentFlowItem->userlimit) {
if (!$isProjectOwner && !in_array(User::userid(), $currentFlowItem->userids)) {
if (!in_array(User::userid(), $currentFlowItem->userids)
&& !ProjectUser::whereProjectId($this->project_id)->whereOwner(1)->exists()) {
throw new ApiException("当前状态[{$currentFlowItem->name}]仅限状态负责人或项目负责人修改");
}
}
@ -585,14 +584,6 @@ class ProjectTask extends AbstractModel
'flow' => $flowData,
'change' => [$currentFlowItem?->name, $newFlowItem->name]
]);
ProjectTaskFlowChange::createInstance([
'task_id' => $this->id,
'userid' => User::userid(),
'before_flow_item_id' => $flowData['flow_item_id'],
'before_flow_item_name' => $flowData['flow_item_name'],
'after_flow_item_id' => $this->flow_item_id,
'after_flow_item_name' => $this->flow_item_name,
])->save();
}
// 状态
if (Arr::exists($data, 'complete_at')) {
@ -614,6 +605,7 @@ class ProjectTask extends AbstractModel
$this->completeTask(null);
}
$updateMarking['is_update_project'] = true;
return;
}
// 标题
if (Arr::exists($data, 'name') && $this->name != $data['name']) {
@ -672,7 +664,6 @@ class ProjectTask extends AbstractModel
// 计划时间(原则:子任务时间在主任务时间内)
if (Arr::exists($data, 'times')) {
$oldAt = [Carbon::parse($this->start_at), Carbon::parse($this->end_at)];
$oldStringAt = $this->start_at ? ($oldAt[0]->toDateTimeString() . '~' . $oldAt[1]->toDateTimeString()) : '';
$this->start_at = null;
$this->end_at = null;
$times = $data['times'];
@ -736,10 +727,7 @@ class ProjectTask extends AbstractModel
}
});
}
$newStringAt = $this->start_at ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
$this->addLog("修改{任务}时间", [
'change' => [$oldStringAt, $newStringAt]
]);
$this->addLog("修改{任务}时间");
}
// 以下紧顶级任务可修改
if ($this->parent_id === 0) {
@ -809,7 +797,6 @@ class ProjectTask extends AbstractModel
}
// 优先级
$p = false;
$oldPName = $this->p_name;
if (Arr::exists($data, 'p_level') && $this->p_level != $data['p_level']) {
$this->p_level = intval($data['p_level']);
$p = true;
@ -823,9 +810,7 @@ class ProjectTask extends AbstractModel
$p = true;
}
if ($p) {
$this->addLog("修改{任务}优先级", [
'change' => [$oldPName, $this->p_name]
]);
$this->addLog("修改{任务}优先级");
}
}
$this->save();
@ -866,7 +851,7 @@ class ProjectTask extends AbstractModel
*/
public function relationUserids()
{
$userids = ProjectTaskUser::whereTaskId($this->id)->orderByDesc('owner')->orderByDesc('id')->pluck('userid')->toArray();
$userids = $this->taskUser->pluck('userid')->toArray();
$items = ProjectTask::with(['taskUser'])->where('parent_id', $this->id)->whereNull('archived_at')->get();
foreach ($items as $item) {
$userids = array_merge($userids, $item->taskUser->pluck('userid')->toArray());
@ -902,44 +887,6 @@ class ProjectTask extends AbstractModel
return $user->owner ? 2 : 1;
}
/**
* 权限版本
* @param int $level 1-负责人2-协助人/负责人3-创建人/协助人/负责人
* @return bool
*/
public function permission($level = 1)
{
if ($level >= 3 && $this->isCreater()) {
return true;
}
if ($level >= 2 && $this->isAssister()) {
return true;
}
return $this->isOwner();
}
/**
* 判断是否创建者
* @return bool
*/
public function isCreater()
{
return $this->userid == User::userid();
}
/**
* 判断是否协助人员
* @return bool
*/
public function isAssister()
{
$row = $this;
while ($row->parent_id > 0) {
$row = self::find($row->parent_id);
}
return ProjectTaskUser::whereTaskId($row->id)->whereUserid(User::userid())->whereOwner(0)->exists();
}
/**
* 判断是否负责人(或者是主任务的负责人)
* @return bool
@ -1007,16 +954,6 @@ class ProjectTask extends AbstractModel
*/
public function archivedTask($archived_at, $isAuto = false)
{
if (!$this->complete_at) {
$flowItems = ProjectFlowItem::whereProjectId($this->project_id)->whereStatus('end')->pluck('name');
if ($flowItems) {
$flowItems = implode(",", array_values(array_unique($flowItems->toArray())));
}
if (empty($flowItems)) {
$flowItems = "已完成";
}
throw new ApiException('仅限【' . $flowItems . '】状态的任务归档');
}
AbstractModel::transaction(function () use ($isAuto, $archived_at) {
if ($archived_at === null) {
// 取消归档
@ -1169,11 +1106,11 @@ class ProjectTask extends AbstractModel
* 获取任务(会员有任务权限 会员存在项目内)
* @param int $task_id
* @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param int|bool $permission 0|false:不限制, 1|true:限制项目负责人、任务负责人、协助人员及任务创建者, 2:已有负责人才限制true (子任务时如果是主任务负责人也可以)
* @param int|bool $mustOwner 0|false:不限制, 1|true:限制任务或项目负责人, 2:已有负责人才限制任务或项目负责人(子任务时如果是主任务负责人也可以)
* @param array $with
* @return self
*/
public static function userTask($task_id, $archived = true, $permission = 0, $with = [])
public static function userTask($task_id, $archived = true, $mustOwner = 0, $with = [])
{
$task = self::with($with)->allData()->where("project_tasks.id", intval($task_id))->first();
//
@ -1199,11 +1136,11 @@ class ProjectTask extends AbstractModel
}
}
//
if ($permission === 2) {
$permission = $task->hasOwner() ? 1 : 0;
if ($mustOwner === 2) {
$mustOwner = $task->hasOwner() ? 1 : 0;
}
if (($permission === 1 || $permission === true) && !$project->owner && !$task->permission(3)) {
throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作');
if (($mustOwner === 1 || $mustOwner === true) && !$task->isOwner() && !$project->owner) {
throw new ApiException('仅限项目或任务负责人操作');
}
//
return $task;

View File

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

View File

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

View File

@ -181,7 +181,7 @@ class User extends AbstractModel
public static function reg($email, $password, $other = [])
{
//邮箱
if (!Base::isEmail($email)) {
if (!Base::isMail($email)) {
throw new ApiException('请输入正确的邮箱地址');
}
if (User::email2userid($email) > 0) {

View File

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

View File

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

View File

@ -2,7 +2,6 @@
namespace App\Module;
use App\Exceptions\ApiException;
use App\Models\Setting;
use App\Models\Tmp;
use Cache;
@ -88,18 +87,6 @@ class Base
return $_A["__static_client_version"];
}
/**
* 检查客户端版本
* @param string $min 最小版本
* @return void
*/
public static function checkClientVersion($min)
{
if (version_compare(Base::getClientVersion(), $min, '<')) {
throw new ApiException('当前版本 (v' . Base::getClientVersion() . ') 过低');
}
}
/**
* 判断是否域名格式
* @param $domain
@ -342,15 +329,19 @@ class Base
{
if (strtolower($charset) == 'utf-8') {
if (Base::getStrlen($string) <= $length) return $string;
$strcut = Base::utf8Substr($string, $length, $start);
$strcut = str_replace(array('&amp;', '&quot;', '&lt;', '&gt;'), array('&', '"', '<', '>'), $string);
$strcut = Base::utf8Substr($strcut, $length, $start);
$strcut = str_replace(array('&', '"', '<', '>'), array('&amp;', '&quot;', '&lt;', '&gt;'), $strcut);
return $strcut . $dot;
} else {
$length = $length * 2;
if (strlen($string) <= $length) return $string;
$string = str_replace(array('&amp;', '&quot;', '&lt;', '&gt;'), array('&', '"', '<', '>'), $string);
$strcut = '';
for ($i = 0; $i < $length; $i++) {
$strcut .= ord($string[$i]) > 127 ? $string[$i] . $string[++$i] : $string[$i];
}
$strcut = str_replace(array('&', '"', '<', '>'), array('&amp;', '&quot;', '&lt;', '&gt;'), $strcut);
}
return $strcut . $dot;
}
@ -808,31 +799,6 @@ class Base
return $scheme.($_SERVER['HTTP_HOST'] ?? '');
}
/**
* 地址后拼接参数
* @param $url
* @param $parames
* @return mixed|string
*/
public static function urlAddparameter($url, $parames)
{
if ($parames && is_array($parames)) {
$array = [];
foreach ($parames as $key => $val) {
$array[] = $key . "=" . $val;
}
if ($array) {
$query = implode("&", $array);
if (str_contains($url, "?")) {
$url .= "&" . $query;
} else {
$url .= "?" . $query;
}
}
}
return $url;
}
/**
* 格式化内容图片地址
* @param $content
@ -988,7 +954,7 @@ class Base
* @param string $str 需要检测的字符串
* @return int
*/
public static function isEmail($str)
public static function isMail($str)
{
$RegExp = '/^[a-z0-9][a-z\.0-9-_]+@[a-z0-9_-]+(?:\.[a-z]{0,3}\.[a-z]{0,2}|\.[a-z]{0,3}|\.[a-z]{0,2})$/i';
return preg_match($RegExp, $str);
@ -2270,7 +2236,7 @@ class Base
$type = ['zip'];
break;
case 'file':
$type = ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz', 'ai', 'avi', 'bmp', 'cdr', 'eps', 'mov', 'mp3', 'mp4', 'pr', 'psd', 'svg', 'tif'];
$type = ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz'];
break;
case 'firmware':
$type = ['img', 'tar', 'bin'];
@ -2280,9 +2246,6 @@ class Base
break;
case 'more':
$type = [
'text', 'md', 'markdown',
'drawio',
'mind',
'docx', 'wps', 'doc', 'xls', 'xlsx', 'ppt', 'pptx',
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw',
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z',
@ -2293,7 +2256,7 @@ class Base
'txt',
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
'dockerfile', 'go', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'ocamlmakefile', 'make', 'md', 'markdown', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
@ -2423,37 +2386,6 @@ class Base
}
}
/**
* 上传文件移动
* @param array $uploadResult
* @param string $newPath "/" 结尾
* @return array
*/
public static function uploadMove($uploadResult, $newPath)
{
if (str_ends_with($newPath, "/") && file_exists($uploadResult['file'])) {
Base::makeDir(public_path($newPath));
$oldPath = dirname($uploadResult['path']) . "/";
$newFile = str_replace($oldPath, $newPath, $uploadResult['file']);
if (rename($uploadResult['file'], $newFile)) {
$oldUrl = $uploadResult['url'];
$uploadResult['file'] = $newFile;
$uploadResult['path'] = str_replace($oldPath, $newPath, $uploadResult['path']);
$uploadResult['url'] = str_replace($oldPath, $newPath, $uploadResult['url']);
if ($uploadResult['thumb'] == $oldUrl) {
$uploadResult['thumb'] = $uploadResult['url'];
} elseif ($uploadResult['thumb']) {
$oldThumb = substr($uploadResult['thumb'], strpos($uploadResult['thumb'], $newPath));
$newThumb = str_replace($oldPath, $newPath, $oldThumb);
if (file_exists(public_path($oldThumb)) && rename(public_path($oldThumb), public_path($newThumb))) {
$uploadResult['thumb'] = str_replace($oldPath, $newPath, $uploadResult['thumb']);
}
}
}
}
return $uploadResult;
}
/**
* 生成缩略图
* @param string $src_img 源图绝对完整地址{带文件名及后缀名}
@ -2963,19 +2895,4 @@ class Base
$matrix = array_unique($matrix, SORT_REGULAR);
return array_merge($matrix);
}
/**
* 去除emoji表情
* @param $str
* @return string|string[]|null
*/
public static function filterEmoji($str)
{
return preg_replace_callback(
'/./u',
function (array $match) {
return strlen($match[0]) >= 4 ? '' : $match[0];
},
$str);
}
}

View File

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

View File

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

View File

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

47
cmd
View File

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

9554
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,40 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class InsertSettingColumnTemplate extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$array = \App\Module\Base::setting('columnTemplate');
if (empty($array)) {
\App\Module\Base::setting('columnTemplate', [
[
'name' => '软件开发',
'columns' => ['产品规划', '前端开发', '后端开发', '测试', '发布', '其他'],
],
[
'name' => '产品开发',
'columns' => ['产品计划', '正在设计', '正在研发', '测试', '准备发布', '发布成功'],
],
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@ -1,34 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class WebSocketDialogUsersAddTopAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_users', 'top_at')) {
$table->timestamp('top_at')->nullable()->after('userid')->comment('置顶时间');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
$table->dropColumn("top_at");
});
}
}

View File

@ -1,30 +0,0 @@
<?php
use App\Models\File;
use Illuminate\Database\Migrations\Migration;
class FilesUpdateType extends Migration
{
/**
* 更改流程图文件类型
* @return void
*/
public function up()
{
File::whereType('flow')->update([
'type' => 'drawio'
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
File::whereType('drawio')->update([
'type' => 'flow'
]);
}
}

View File

@ -1,75 +0,0 @@
<?php
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\File;
use App\Models\FileContent;
use App\Module\Base;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
class FilesUpdateExt extends Migration
{
/**
* 更新后缀
* @return void
*/
public function up()
{
File::whereIn('type', ['mind', 'drawio', 'document'])->where('ext', '')->orderBy('id')->chunk(100, function($files) {
/** @var File $file */
foreach ($files as $file) {
$fileContent = FileContent::whereFid($file->id)->orderByDesc('id')->first();
$contentArray = Base::json2array($fileContent?->content);
$contentString = '';
//
switch ($file->type) {
case 'document':
$file->ext = $contentArray['type'] ?: 'md';
$contentString = $contentArray['content'];
break;
case 'drawio':
$file->ext = 'drawio';
$contentString = $contentArray['xml'];
break;
case 'mind':
$file->ext = 'mind';
$contentString = $fileContent?->content;
break;
}
$file->save();
//
$path = 'uploads/file/' . $file->type . '/' . date("Ym", Carbon::parse($file->created_at)->timestamp) . '/' . $file->id . '/' . md5($contentString);
$save = public_path($path);
Base::makeDir(dirname($save));
file_put_contents($save, $contentString);
$content = [
'type' => $file->ext,
'url' => $path
];
//
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => $content,
'text' => $fileContent?->text,
'size' => $file->size,
'userid' => $file->userid,
]);
$content->save();
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
File::whereIn('ext', ['mind', 'drawio', 'md'])->update([
'ext' => ''
]);
// ... 退回去意义不大,文件内容不做回滚操作
}
}

View File

@ -1,34 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ProjectUsersAddTopAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_users', function (Blueprint $table) {
if (!Schema::hasColumn('project_users', 'top_at')) {
$table->timestamp('top_at')->nullable()->after('owner')->comment('置顶时间');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_users', function (Blueprint $table) {
$table->dropColumn("top_at");
});
}
}

View File

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

View File

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

View File

@ -599,5 +599,7 @@ curl -O https://task.hitosea.com/uploads/files/3/202105/ba786dfc2f4c2fe916880474
'deleted_at' => NULL,
),
));
}
}

View File

@ -2,10 +2,6 @@
namespace Database\Seeders;
use App\Models\File;
use App\Models\FileContent;
use App\Module\Base;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
class FilesTableSeeder extends Seeder
@ -207,7 +203,7 @@ class FilesTableSeeder extends Seeder
'pid' => 0,
'cid' => 0,
'name' => '流程图',
'type' => 'drawio',
'type' => 'flow',
'ext' => '',
'size' => 5418,
'userid' => 1,
@ -284,47 +280,5 @@ class FilesTableSeeder extends Seeder
));
File::whereIn('type', ['mind', 'drawio', 'document'])->where('ext', '')->orderBy('id')->chunk(100, function($files) {
/** @var File $file */
foreach ($files as $file) {
$fileContent = FileContent::whereFid($file->id)->orderByDesc('id')->first();
$contentArray = Base::json2array($fileContent?->content);
$contentString = '';
//
switch ($file->type) {
case 'document':
$file->ext = $contentArray['type'] ?: 'md';
$contentString = $contentArray['content'];
break;
case 'drawio':
$file->ext = 'drawio';
$contentString = $contentArray['xml'];
break;
case 'mind':
$file->ext = 'mind';
$contentString = $fileContent?->content;
break;
}
$file->save();
//
$path = 'uploads/file/' . $file->type . '/' . date("Ym", Carbon::parse($file->created_at)->timestamp) . '/' . $file->id . '/' . md5($contentString);
$save = public_path($path);
Base::makeDir(dirname($save));
file_put_contents($save, $contentString);
$content = [
'type' => $file->ext,
'url' => $path
];
//
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => $content,
'text' => $fileContent?->text,
'size' => $file->size,
'userid' => $file->userid,
]);
$content->save();
}
});
}
}

View File

@ -16,13 +16,14 @@ class SettingsTableSeeder extends Seeder
{
if (\DB::table('settings')->where('name', 'system')->count() > 0) {
if (\DB::table('settings')->count() > 0) {
return;
}
\DB::table('settings')->insert(array (
0 =>
array (
'id' => 1,
'name' => 'system',
'desc' => '',
'setting' => '{"reg":"open","project_invite":"open","login_code":"auto"}',
@ -31,6 +32,7 @@ class SettingsTableSeeder extends Seeder
),
1 =>
array (
'id' => 2,
'name' => 'priority',
'desc' => '',
'setting' => '[{"name":"\\u91cd\\u8981\\u4e14\\u7d27\\u6025","color":"#ED4014","days":1,"priority":1},{"name":"\\u91cd\\u8981\\u4e0d\\u7d27\\u6025","color":"#F16B62","days":3,"priority":2},{"name":"\\u7d27\\u6025\\u4e0d\\u91cd\\u8981","color":"#19C919","days":5,"priority":3},{"name":"\\u4e0d\\u91cd\\u8981\\u4e0d\\u7d27\\u6025","color":"#2D8CF0","days":0,"priority":4}]',

File diff suppressed because one or more lines are too long

View File

@ -45,8 +45,6 @@ services:
- php
- office
- fileview
- drawio-webapp
- drawio-export
restart: unless-stopped
redis:
@ -82,16 +80,13 @@ services:
office:
container_name: "dootask-office-${APP_ID}"
image: "onlyoffice/documentserver:7.0.0.132"
image: "onlyoffice/documentserver:6.4.2.6"
volumes:
- ./docker/office/data:/var/www/onlyoffice/Data
- ./docker/office/logs:/var/log/onlyoffice
- ./docker/office/resources/documenteditor/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/main/resources/css/app.css
- ./docker/office/resources/presentationeditor/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/main/resources/css/app.css
- ./docker/office/resources/spreadsheeteditor/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
- ./docker/office/resources/documenteditor/mobile/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/mobile/css/app.css
- ./docker/office/resources/presentationeditor/mobile/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/mobile/css/app.css
- ./docker/office/resources/spreadsheeteditor/mobile/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/mobile/css/app.css
environment:
TZ: "Asia/Shanghai"
networks:
@ -110,36 +105,6 @@ services:
ipv4_address: "${APP_IPPR}.7"
restart: unless-stopped
drawio-webapp:
container_name: "dootask-drawio-webapp-${APP_ID}"
image: "jgraph/drawio:16.6.1"
volumes:
- ./docker/drawio/webapp/index.html:/usr/local/tomcat/webapps/draw/index.html
- ./docker/drawio/webapp/stencils:/usr/local/tomcat/webapps/draw/stencils
- ./docker/drawio/webapp/js/app.min.js:/usr/local/tomcat/webapps/draw/js/app.min.js
- ./docker/drawio/webapp/js/croppie/croppie.min.css:/usr/local/tomcat/webapps/draw/js/croppie/croppie.min.css
- ./docker/drawio/webapp/js/diagramly/ElectronApp.js:/usr/local/tomcat/webapps/draw/js/diagramly/ElectronApp.js
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.8"
environment:
TZ: "Asia/Shanghai"
depends_on:
- drawio-export
restart: unless-stopped
drawio-export:
container_name: "dootask-drawio-export-${APP_ID}"
image: "jgraph/export-server"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.9"
environment:
TZ: "Asia/Shanghai"
volumes:
- ./docker/drawio/export/fonts:/usr/share/fonts/drawio
restart: unless-stopped
networks:
extnetwork:
name: "dootask-networks-${APP_ID}"

View File

@ -1 +0,0 @@

View File

@ -1,17 +0,0 @@
# Change
## js/diagramly/ElectronApp.js
- 隐藏文件中的无用菜单
## js/app.min.js
- 隐藏帮助菜单
- 取消未保存关闭窗口提示
- `EmbedFile.prototype.getTitle=...``EmbedFile.prototype.getTitle=function(){return this.desc.title||(urlParams.title?decodeURIComponent(urlParams.title):"")}`
- `c.insertTemplateEnabled&&!c.isOffline()&&this.addMenuItems(b,["insertTemplate"],d)``c.insertTemplateEnabled&&this.addMenuItems(b,["insertTemplate"],d)`
- `390:270``390:285`
## index.html
- 隐藏加载中的提示

View File

@ -1,477 +0,0 @@
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5" ><![endif]-->
<!DOCTYPE html>
<html>
<head>
<title>Flowchart Maker &amp; Online Diagram Software</title>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="Description" content="diagrams.net is free online diagram software for making flowcharts, process diagrams, org charts, UML, ER and network diagrams">
<meta name="Keywords" content="diagram, online, flow chart, flowchart maker, uml, erd">
<meta itemprop="name" content="diagrams.net - free flowchart maker and diagrams online">
<meta itemprop="description" content="diagrams.net is a free online diagramming application and flowchart maker . You can use it to create UML, entity relationship,
org charts, BPMN and BPM, database schema and networks. Also possible are telecommunication network, workflow, flowcharts, maps overlays and GIS, electronic
circuit and social network diagrams.">
<meta itemprop="image" content="https://lh4.googleusercontent.com/-cLKEldMbT_E/Tx8qXDuw6eI/AAAAAAAAAAs/Ke0pnlk8Gpg/w500-h344-k/BPMN%2Bdiagram%2Brc2f.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="msapplication-config" content="images/browserconfig.xml">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#d89000">
<script type="text/javascript">
window.EXPORT_URL = window.location.origin + "/drawio/export/";
window.DRAWIO_LIGHTBOX_URL = window.location.origin + "/drawio/webapp";
/**
* URL Parameters and protocol description are here:
*
* https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported
*
* Parameters for developers:
*
* - dev=1: For developers only
* - test=1: For developers only
* - export=URL for export: For developers only
* - ignoremime=1: For developers only (see DriveClient.js). Use Cmd-S to override mime.
* - createindex=1: For developers only (see etc/build/README)
* - filesupport=0: For developers only (see Editor.js in core)
* - savesidebar=1: For developers only (see Sidebar.js)
* - pages=1: For developers only (see Pages.js)
* - lic=email: For developers only (see LicenseServlet.java)
* --
* - networkshapes=1: For testing network shapes (temporary)
*/
var urlParams = (function()
{
var result = new Object();
var params = window.location.search.slice(1).split('&');
for (var i = 0; i < params.length; i++)
{
idx = params[i].indexOf('=');
if (idx > 0)
{
result[params[i].substring(0, idx)] = params[i].substring(idx + 1);
}
}
return result;
})();
// Forces CDN caches by passing URL parameters via URL hash
if (window.location.hash != null && window.location.hash.substring(0, 2) == '#P')
{
try
{
urlParams = JSON.parse(decodeURIComponent(window.location.hash.substring(2)));
if (urlParams.hash != null)
{
window.location.hash = urlParams.hash;
}
}
catch (e)
{
// ignore
}
}
// Global variable for desktop
var mxIsElectron = window && window.process && window.process.type;
// Redirects page if required
if (urlParams['dev'] != '1')
{
(function()
{
var proto = window.location.protocol;
if (!mxIsElectron)
{
var host = window.location.host;
// Redirects apex, drive and rt to www
if (host === 'draw.io' || host === 'rt.draw.io' || host === 'drive.draw.io')
{
host = 'www.draw.io';
}
var href = proto + '//' + host + window.location.href.substring(
window.location.protocol.length +
window.location.host.length + 2);
// Redirects if href changes
if (href != window.location.href)
{
window.location.href = href;
}
}
})();
}
/**
* Adds meta tag to the page.
*/
function mxmeta(name, content, httpEquiv)
{
try
{
var s = document.createElement('meta');
if (name != null)
{
s.setAttribute('name', name);
}
s.setAttribute('content', content);
if (httpEquiv != null)
{
s.setAttribute('http-equiv', httpEquiv);
}
var t = document.getElementsByTagName('meta')[0];
t.parentNode.insertBefore(s, t);
}
catch (e)
{
// ignore
}
};
/**
* Synchronously adds scripts to the page.
*/
function mxscript(src, onLoad, id, dataAppKey, noWrite)
{
var defer = onLoad == null && !noWrite;
if ((urlParams['dev'] != '1' && typeof document.createElement('canvas').getContext === "function") ||
onLoad != null || noWrite)
{
var s = document.createElement('script');
s.setAttribute('type', 'text/javascript');
s.setAttribute('defer', 'true');
s.setAttribute('src', src);
if (id != null)
{
s.setAttribute('id', id);
}
if (dataAppKey != null)
{
s.setAttribute('data-app-key', dataAppKey);
}
if (onLoad != null)
{
var r = false;
s.onload = s.onreadystatechange = function()
{
if (!r && (!this.readyState || this.readyState == 'complete'))
{
r = true;
onLoad();
}
};
}
var t = document.getElementsByTagName('script')[0];
if (t != null)
{
t.parentNode.insertBefore(s, t);
}
}
else
{
document.write('<script src="' + src + '"' + ((id != null) ? ' id="' + id +'" ' : '') +
((dataAppKey != null) ? ' data-app-key="' + dataAppKey +'" ' : '') + '></scr' + 'ipt>');
}
};
/**
* Asynchronously adds scripts to the page.
*/
function mxinclude(src)
{
var g = document.createElement('script');
g.type = 'text/javascript';
g.async = true;
g.src = src;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(g, s);
};
/**
* Adds meta tags with application name (depends on offline URL parameter)
*/
(function()
{
var name = 'diagrams.net';
mxmeta('apple-mobile-web-app-title', name);
mxmeta('application-name', name);
if (mxIsElectron)
{
mxmeta(null, 'default-src \'self\' \'unsafe-inline\'; connect-src \'self\' https://*.draw.io https://fonts.googleapis.com https://fonts.gstatic.com; img-src * data:; media-src *; font-src *; style-src-elem \'self\' \'unsafe-inline\' https://fonts.googleapis.com', 'Content-Security-Policy');
}
})();
// Checks for local storage
var isLocalStorage = false;
try
{
isLocalStorage = urlParams['local'] != '1' && typeof(localStorage) != 'undefined';
}
catch (e)
{
// ignored
}
var mxScriptsLoaded = false, mxWinLoaded = false;
function checkAllLoaded()
{
if (mxScriptsLoaded && mxWinLoaded)
{
App.main();
}
};
var t0 = new Date();
// Changes paths for local development environment
if (urlParams['dev'] == '1')
{
// Used to request grapheditor/mxgraph sources in dev mode
var mxDevUrl = document.location.protocol + '//devhost.jgraph.com/drawio/src/main';
// Used to request draw.io sources in dev mode
var drawDevUrl = document.location.protocol + '//devhost.jgraph.com/drawio/src/main/webapp/';
var geBasePath = drawDevUrl + '/js/grapheditor';
var mxBasePath = mxDevUrl + '/mxgraph';
if (document.location.protocol == 'file:')
{
geBasePath = './js/grapheditor';
mxBasePath = './mxgraph';
drawDevUrl = './';
// Forces includes for dev environment in node.js
mxForceIncludes = true;
}
mxscript(drawDevUrl + 'js/PreConfig.js');
mxscript(drawDevUrl + 'js/diagramly/Init.js');
mxscript(geBasePath + '/Init.js');
mxscript(mxBasePath + '/mxClient.js');
// Adds all JS code that depends on mxClient. This indirection via Devel.js is
// required in some browsers to make sure mxClient.js (and the files that it
// loads asynchronously) are available when the code loaded in Devel.js runs.
mxscript(drawDevUrl + 'js/diagramly/Devel.js');
// Electron
if (mxIsElectron)
{
mxscript('js/diagramly/DesktopLibrary.js');
mxscript('js/diagramly/ElectronApp.js');
}
mxscript(drawDevUrl + 'js/PostConfig.js');
}
else
{
(function()
{
var hostName = window.location.hostname;
// Supported domains are *.draw.io and the packaged version in Quip
var supportedDomain = (hostName.substring(hostName.length - 8, hostName.length) === '.draw.io') ||
(hostName.substring(hostName.length - 13, hostName.length) === '.diagrams.net');
function loadAppJS()
{
mxscript('js/app.min.js', function()
{
mxScriptsLoaded = true;
checkAllLoaded();
// Electron
if (mxIsElectron)
{
mxscript('js/diagramly/DesktopLibrary.js', function()
{
mxscript('js/diagramly/ElectronApp.js', function()
{
mxscript('js/extensions.min.js', function()
{
mxscript('js/stencils.min.js', function()
{
mxscript('js/shapes-14-6-5.min.js', function()
{
mxscript('js/PostConfig.js');
});
});
});
});
});
}
else if (!supportedDomain)
{
mxscript('js/PostConfig.js');
}
});
};
if (!supportedDomain || mxIsElectron)
{
mxscript('js/PreConfig.js', loadAppJS);
}
else
{
loadAppJS();
}
})();
}
// Adds basic error handling
window.onerror = function()
{
var status = document.getElementById('geStatus');
if (status != null)
{
status.innerHTML = 'Page could not be loaded. Please try refreshing.';
}
};
</script>
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/plgmlhohecdddhbmmkncjdmlhcmaachm">
<link rel="stylesheet" type="text/css" href="js/croppie/croppie.min.css">
<link rel="stylesheet" type="text/css" href="styles/grapheditor.css">
<link rel="preconnect" href="https://storage.googleapis.com">
<link rel="canonical" href="https://app.diagrams.net">
<link rel="manifest" href="images/manifest.json">
<style type="text/css">
body { overflow:hidden; }
div.picker { z-index: 10007; }
.geSidebarContainer .geTitle input {
font-size:8pt;
color:#606060;
}
.geBlock {
display: none;
z-index:-3;
margin:100px;
margin-top:40px;
margin-bottom:30px;
padding:20px;
text-align:center;
min-width:50%;
}
.geBlock h1, .geBlock h2 {
margin-top:0px;
padding-top:0px;
}
.geEditor *:not(.geScrollable)::-webkit-scrollbar {
width:14px;
height:14px;
}
.geEditor ::-webkit-scrollbar-track {
background-clip:padding-box;
border:solid transparent;
border-width:1px;
}
.geEditor ::-webkit-scrollbar-corner {
background-color:transparent;
}
.geEditor ::-webkit-scrollbar-thumb {
background-color:rgba(0,0,0,.1);
background-clip:padding-box;
border:solid transparent;
border-radius:10px;
}
.geEditor ::-webkit-scrollbar-thumb:hover {
background-color:rgba(0,0,0,.4);
}
.geTemplate {
border:1px solid transparent;
display:inline-block;
_display:inline;
vertical-align:top;
border-radius:3px;
overflow:hidden;
font-size:14pt;
cursor:pointer;
margin:5px;
}
.geDialog h2 {
line-height: 1.2;
}
</style>
<!-- Workaround for binary XHR in IE 9/10, see App.loadUrl -->
<!--[if (IE 9)|(IE 10)]><!-->
<script type="text/vbscript">
Function mxUtilsBinaryToArray(Binary)
Dim i
ReDim byteArray(LenB(Binary))
For i = 1 To LenB(Binary)
byteArray(i-1) = AscB(MidB(Binary, i, 1))
Next
mxUtilsBinaryToArray = byteArray
End Function
</script>
<!--<![endif]-->
</head>
<body class="geEditor">
<div id="geInfo">
<div class="geBlock">
<h1>Flowchart Maker and Online Diagram Software</h1>
<p>
diagrams.net (formerly draw.io) is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool,
to design database schema, to build BPMN online, as a circuit diagram maker, and more. draw.io can import .vsdx, Gliffy&trade; and Lucidchart&trade; files .
</p>
<h2 id="geStatus">Loading...</h2>
<p>
Please ensure JavaScript is enabled.
</p>
</div>
</div>
<script type="text/javascript">
/**
* Main
*/
if (navigator.userAgent != null && navigator.userAgent.toLowerCase().
indexOf(' electron/') >= 0 && typeof process !== 'undefined' && process.versions.electron < 5)
{
// Redirects old Electron app to latest version
var div = document.getElementById('geInfo');
if (div != null)
{
div.innerHTML = '<center><h2>You are using an out of date version of this app.<br>Please download the latest version ' +
'<a href="https://github.com/jgraph/drawio-desktop/releases/latest" target="_blank">here</a>.</h2></center>';
}
}
else
{
if (urlParams['dev'] != '1' && typeof document.createElement('canvas').getContext === "function")
{
window.addEventListener('load', function()
{
mxWinLoaded = true;
checkAllLoaded();
});
}
else
{
App.main();
}
}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.croppie-container{width:100%;height:100%}.croppie-container .cr-image{z-index:-1;position:absolute;top:0;left:0;transform-origin:0 0;max-height:none;max-width:none}.croppie-container .cr-boundary{position:relative;overflow:hidden;margin:0 auto;z-index:1;width:100%;height:100%}.croppie-container .cr-resizer,.croppie-container .cr-viewport{position:absolute;border:2px solid #fff;margin:auto;top:0;bottom:0;right:0;left:0;box-shadow:0 0 2000px 2000px rgba(0,0,0,.5);z-index:0}.croppie-container .cr-resizer{z-index:2;box-shadow:none;pointer-events:none}.croppie-container .cr-resizer-horisontal,.croppie-container .cr-resizer-vertical{position:absolute;pointer-events:all}.croppie-container .cr-resizer-horisontal::after,.croppie-container .cr-resizer-vertical::after{display:block;position:absolute;box-sizing:border-box;border:1px solid #000;background:#fff;width:10px;height:10px;content:''}.croppie-container .cr-resizer-vertical{bottom:-5px;cursor:row-resize;width:100%;height:10px}.croppie-container .cr-resizer-vertical::after{left:50%;margin-left:-5px}.croppie-container .cr-resizer-horisontal{right:-5px;cursor:col-resize;width:10px;height:100%}.croppie-container .cr-resizer-horisontal::after{top:50%;margin-top:-5px}.croppie-container .cr-original-image{display:none}.croppie-container .cr-vp-circle{border-radius:50%}.croppie-container .cr-overlay{z-index:1;position:absolute;cursor:move;touch-action:none}.croppie-container .cr-slider-wrap{width:75%;margin:15px auto;text-align:center}.croppie-result{position:relative;overflow:hidden}.croppie-result img{position:absolute}.croppie-container .cr-image,.croppie-container .cr-overlay,.croppie-container .cr-viewport{-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0)}.cr-slider{-webkit-appearance:none;width:300px;max-width:100%;padding-top:8px;padding-bottom:8px;background-color:transparent}.cr-slider::-webkit-slider-runnable-track{width:100%;height:3px;background:rgba(0,0,0,.5);border:0;border-radius:3px}.cr-slider::-webkit-slider-thumb{-webkit-appearance:none;border:none;height:16px;width:16px;border-radius:50%;background:#ddd;margin-top:-6px}.cr-slider:focus{outline:0}.cr-slider::-moz-range-track{width:100%;height:3px;background:rgba(0,0,0,.5);border:0;border-radius:3px}.cr-slider::-moz-range-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#ddd;margin-top:-6px}.cr-slider:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.cr-slider::-ms-track{width:100%;height:5px;background:0 0;border-color:transparent;border-width:6px 0;color:transparent}.cr-slider::-ms-fill-lower{background:rgba(0,0,0,.5);border-radius:10px}.cr-slider::-ms-fill-upper{background:rgba(0,0,0,.5);border-radius:10px}.cr-slider::-ms-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#ddd;margin-top:1px}.cr-slider:focus::-ms-fill-lower{background:rgba(0,0,0,.5)}.cr-slider:focus::-ms-fill-upper{background:rgba(0,0,0,.5)}.cr-rotate-controls{position:absolute;bottom:5px;left:5px;z-index:1}.cr-rotate-controls button{border:0;background:0 0}.cr-rotate-controls i:before{display:inline-block;font-style:normal;font-weight:900;font-size:22px}.cr-rotate-l i:before{content:'↺'}.cr-rotate-r i:before{content:'↻'}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -18,6 +18,14 @@ upstream service {
server php:20000 weight=5 max_fails=3 fail_timeout=30s;
keepalive 16;
}
upstream office {
server office weight=5 max_fails=3 fail_timeout=30s;
keepalive 16;
}
upstream fileview {
server fileview:8012 weight=5 max_fails=3 fail_timeout=30s;
keepalive 16;
}
server {
listen 80;
@ -77,23 +85,6 @@ server {
proxy_pass http://service;
}
location /fileview {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://fileview:8012;
}
location /office/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
@ -112,11 +103,12 @@ server {
proxy_pass http://office/;
}
location /drawio/webapp/ {
location /fileview {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host/drawio/webapp;
proxy_set_header X-Forwarded-Host $the_host;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
@ -125,27 +117,7 @@ server {
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://drawio-webapp:8080/;
}
location /drawio/export/ {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host/drawio/export;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://drawio-export:8000/;
proxy_pass http://fileview;
}
}

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

@ -10,7 +10,7 @@ command=php bin/laravels start -i
numprocs=1
autostart=true
autorestart=true
startretries=100
startretries=3
user=root
redirect_stderr=true
stdout_logfile=/var/log/supervisor/%(program_name)s.log

29
electron/build.js vendored
View File

@ -15,48 +15,25 @@ const packageFile = path.resolve(__dirname, "package.json");
const packageBakFile = path.resolve(__dirname, "package-bak.json");
const platform = ["build-mac", "build-mac-arm", "build-win"];
// 克隆 Drawio
function cloneDrawio(systemInfo) {
child_process.spawnSync("git", ["submodule", "update", "--quiet", "--init", "--depth=1"], {stdio: "inherit"});
const drawioSrcDir = path.resolve(__dirname, "../resources/drawio/src/main/webapp");
const drawioCoverDir = path.resolve(__dirname, "../docker/drawio/webapp");
const drawioDestDir = path.resolve(electronDir, "drawio/webapp");
fse.copySync(drawioSrcDir, drawioDestDir)
fse.copySync(drawioCoverDir, drawioDestDir)
//
const preConfigFile = path.resolve(drawioDestDir, "js/PreConfig.js");
if (!fse.existsSync(preConfigFile)) {
console.log("clone drawio error!");
process.exit()
}
let preConfigString = fs.readFileSync(preConfigFile, 'utf8');
preConfigString += "\nwindow.systemInfo = " + JSON.stringify(systemInfo) + ";\n";
preConfigString += fs.readFileSync(path.resolve(__dirname, "drawio.js"), 'utf8');
fs.writeFileSync(preConfigFile, preConfigString, 'utf8');
}
// 生成配置、编译应用
function startBuild(data, publish) {
// information
console.log("Name: " + data.name);
console.log("AppId: " + data.id);
console.log("Version: " + config.version);
// config.js
let systemInfo = {
title: data.name,
version: config.version,
origin: "./",
apiUrl: utils.formatUrl(data.url) + "api/",
}
// drawio
cloneDrawio(systemInfo)
// config.js
fs.writeFileSync(electronDir + "/config.js", "window.systemInfo = " + JSON.stringify(systemInfo), 'utf8');
fs.writeFileSync(electronDir + "/config.js", "window.systemInfo = " + JSON.stringify(systemInfo, null, 2), 'utf8');
fs.writeFileSync(nativeCachePath, utils.formatUrl(data.url));
fs.writeFileSync(devloadCachePath, "", 'utf8');
// index.html
let indexFile = path.resolve(electronDir, "index.html");
let indexString = fs.readFileSync(indexFile, 'utf8');
indexString = indexString.replace(/<title>(.*?)<\/title>/g, `<title>${data.name}</title>`);
indexString = indexString.replace(`<title></title>`, `<title>${data.name}</title>`);
fs.writeFileSync(indexFile, indexString, 'utf8');
// package.json Backup
fse.copySync(packageFile, packageBakFile)

11
electron/drawio.js vendored
View File

@ -1,11 +0,0 @@
window.utilsStorage={getStorageString(key,def=''){let value=this.storage(key);return typeof value==="string"||typeof value==="number"?value:def},storage(key,value){if(!key){return}let keyName='__state__';if(key.substring(0,5)==='cache'){keyName='__state:'+key+'__'}if(typeof value==='undefined'){return this.loadFromlLocal(key,'',keyName)}else{this.savaToLocal(key,value,keyName)}},savaToLocal(key,value,keyName){try{if(typeof keyName==='undefined')keyName='__seller__';let seller=window.localStorage[keyName];if(!seller){seller={}}else{seller=JSON.parse(seller)}seller[key]=value;window.localStorage[keyName]=JSON.stringify(seller)}catch(e){}},loadFromlLocal(key,def,keyName){try{if(typeof keyName==='undefined')keyName='__seller__';let seller=window.localStorage[keyName];if(!seller){return def}seller=JSON.parse(seller);if(!seller||typeof seller[key]==='undefined'){return def}return seller[key]}catch(e){return def}},}
window.cacheServerUrl = window.utilsStorage.getStorageString("cacheServerUrl")
if (window.cacheServerUrl) {
window.systemInfo.apiUrl = window.cacheServerUrl
}
window.EXPORT_URL = window.systemInfo.apiUrl + "../drawio/export/";
window.DRAWIO_LIGHTBOX_URL = window.systemInfo.apiUrl + "../drawio/webapp";
while (window.EXPORT_URL.indexOf("/../") !== -1) {window.EXPORT_URL = window.EXPORT_URL.replace(/\/(((?!\/).)*)\/\.\.\//, "/")}
while (window.DRAWIO_LIGHTBOX_URL.indexOf("/../") !== -1) {window.DRAWIO_LIGHTBOX_URL = window.DRAWIO_LIGHTBOX_URL.replace(/\/(((?!\/).)*)\/\.\.\//, "/")}

View File

@ -1,67 +0,0 @@
const {
contextBridge,
ipcRenderer
} = require("electron");
let reqId = 1;
let reqInfo = {};
let fileChangedListeners = {};
ipcRenderer.on('mainResp', (event, resp) => {
let callbacks = reqInfo[resp.reqId];
if (resp.error) {
callbacks.error(resp.msg, resp.e);
} else {
callbacks.callback(resp.data);
}
delete reqInfo[resp.reqId];
});
ipcRenderer.on('fileChanged', (event, resp) => {
let listener = fileChangedListeners[resp.path];
if (listener) {
listener(resp.curr, resp.prev);
}
});
contextBridge.exposeInMainWorld(
'electron', {
request: (msg, callback, error) => {
msg.reqId = reqId++;
reqInfo[msg.reqId] = {callback: callback, error: error};
if (msg.action == 'watchFile') {
fileChangedListeners[msg.path] = msg.listener;
delete msg.listener;
}
ipcRenderer.send('rendererReq', msg);
},
registerMsgListener: (action, callback) => {
ipcRenderer.on(action, (event, args) => {
callback(args);
});
},
listenOnce: (action, callback) => {
ipcRenderer.once(action, (event, args) => {
callback(args);
});
},
sendMessage: (action, args) => {
ipcRenderer.send(action, args);
},
sendSyncMessage: (action, args) => {
ipcRenderer.sendSync(action, args);
}
}
);
contextBridge.exposeInMainWorld(
'process', {
type: process.type,
versions: process.versions
}
);

1199
electron/electron.js vendored

File diff suppressed because it is too large Load Diff

423
electron/main.js vendored Normal file
View File

@ -0,0 +1,423 @@
const fs = require('fs')
const fse = require('fs-extra')
const os = require("os");
const path = require('path')
const XLSX = require('xlsx');
const {app, BrowserWindow, ipcMain, dialog} = require('electron')
const utils = require('./utils');
const config = require('./package.json');
const log = require("electron-log");
let mainWindow = null,
subWindow = [],
willQuitApp = false,
inheritClose = false,
devloadUrl = "",
devloadCachePath = path.resolve(__dirname, ".devload"),
downloadList = [],
downloadCacheFile = path.join(app.getPath('cache'), config.name, '.downloadCache');
if (fs.existsSync(devloadCachePath)) {
devloadUrl = fs.readFileSync(devloadCachePath, 'utf8')
}
if (fs.existsSync(downloadCacheFile)) {
downloadList = utils.jsonParse(fs.readFileSync(downloadCacheFile, 'utf8'), [])
} else {
fse.ensureDirSync(path.join(app.getPath('cache'), config.name))
}
function downloadUpdate(item) {
const chain = item.getURLChain()
if (chain.length == 0) {
return
}
let currentState = item.getState()
if (currentState == "progressing" && item.isPaused()) {
currentState = "paused"
}
//
const downloadItem = downloadList.find(item => item.url == chain[0])
if (downloadItem && downloadItem.state != currentState) {
downloadItem.state = currentState;
downloadItem.result = {
url: item.getURL(),
name: item.getFilename(),
savePath: item.getSavePath(),
mimeType: item.getMimeType(),
totalBytes: item.getTotalBytes(),
chain,
};
fs.writeFileSync(downloadCacheFile, utils.jsonStringify(downloadList), 'utf8');
//
if (currentState == 'completed') {
mainWindow.webContents.send("downloadDone", downloadItem)
log.info("下载完成", downloadItem)
} else {
mainWindow.webContents.send("downloadUpdate", downloadItem)
log.info("下载更新", downloadItem)
}
}
}
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
center: true,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: false
}
})
mainWindow.webContents.setUserAgent(mainWindow.webContents.getUserAgent() + " MainTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
if (devloadUrl) {
mainWindow.loadURL(devloadUrl).then(r => {
})
} else {
mainWindow.loadFile('./public/index.html').then(r => {
})
}
mainWindow.on('page-title-updated', (event, title) => {
if (title == "index.html") {
event.preventDefault()
}
})
mainWindow.on('close', (e) => {
if (!willQuitApp) {
e.preventDefault();
if (inheritClose) {
mainWindow.webContents.send("windowClose", {})
} else {
if (process.platform === 'darwin') {
app.hide();
} else {
app.quit();
}
}
}
})
mainWindow.webContents.session.on('will-download', (event, item) => {
item.setSavePath(path.join(app.getPath('cache'), config.name, item.getFilename()));
item.on('updated', () => {
downloadUpdate(item)
})
item.on('done', () => {
downloadUpdate(item)
})
})
}
function createSubWindow(args) {
if (!args) {
return;
}
if (typeof args !== "object") {
args = {
path: args,
config: {},
}
}
let name = args.name || "auto_" + utils.randomString(6);
let item = subWindow.find(item => item.name == name);
let browser = item ? item.browser : null;
if (browser) {
browser.focus();
if (args.force === false) {
return;
}
} else {
let config = args.config || {};
if (typeof args.title !== "undefined") {
config.title = args.title;
}
browser = new BrowserWindow(Object.assign({
width: 1280,
height: 800,
center: true,
parent: mainWindow,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
devTools: args.devTools !== false,
nodeIntegration: true,
contextIsolation: false
}
}, config))
browser.on('page-title-updated', (event, title) => {
if (title == "index.html" || args.titleFixed === true) {
event.preventDefault()
}
})
browser.on('close', () => {
let index = subWindow.findIndex(item => item.name == name);
if (index > -1) {
subWindow.splice(index, 1)
}
})
subWindow.push({ name, browser })
}
browser.webContents.setUserAgent(browser.webContents.getUserAgent() + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0" + (args.userAgent ? (" " + args.userAgent) : ""));
if (devloadUrl) {
browser.loadURL(devloadUrl + '#' + (args.hash || args.path)).then(r => {
})
} else {
browser.loadFile('./public/index.html', {
hash: args.hash || args.path
}).then(r => {
})
}
}
app.whenReady().then(() => {
createMainWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('before-quit', () => {
willQuitApp = true
})
/**
* 继承关闭窗口事件
*/
ipcMain.on('inheritClose', (event) => {
inheritClose = true
event.returnValue = "ok"
})
/**
* 下载文件
* @param args {url}
*/
ipcMain.on('downloadFile', (event, args) => {
event.returnValue = "ok"
//
let appendJson = {state: "progressing", startTime: utils.Time()}
let downloadItem = downloadList.find(({url}) => url == args.url)
if (downloadItem) {
switch (downloadItem.state) {
case "completed":
if (fs.existsSync(downloadItem.result.savePath)) { // 下载完成,文件存在
log.info("下载已完成", downloadItem)
mainWindow.webContents.send("downloadDone", downloadItem)
return
}
break;
case "progressing":
if (downloadItem.startTime + 480 > utils.Time()) { // 下载中未超时超时时间8分钟
log.info("下载已存在", downloadItem)
return;
}
break;
}
downloadItem = Object.assign(downloadItem, appendJson)
} else {
downloadList.push(downloadItem = Object.assign(args, appendJson))
}
fs.writeFileSync(downloadCacheFile, utils.jsonStringify(downloadList), 'utf8');
mainWindow.webContents.downloadURL(downloadItem.url);
log.info("下载开始", downloadItem)
})
/**
* 打开文件
* @param args {path}
*/
ipcMain.on('openFile', (event, args) => {
utils.openFile(args.path)
event.returnValue = "ok"
})
/**
* 退出客户端
*/
ipcMain.on('windowQuit', (event) => {
event.returnValue = "ok"
app.quit();
})
/**
* 创建路由窗口
* @param args {path, ?}
*/
ipcMain.on('windowRouter', (event, args) => {
createSubWindow(args)
event.returnValue = "ok"
})
/**
* 隐藏窗口mac隐藏其他关闭
*/
ipcMain.on('windowHidden', (event) => {
if (process.platform === 'darwin') {
app.hide();
} else {
app.quit();
}
event.returnValue = "ok"
})
/**
* 关闭窗口
*/
ipcMain.on('windowClose', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.close()
event.returnValue = "ok"
})
/**
* 设置窗口尺寸
* @param args {width, height, autoZoom, minWidth, minHeight, maxWidth, maxHeight}
*/
ipcMain.on('windowSize', (event, args) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
if (args.width || args.height) {
let [w, h] = win.getSize()
const width = args.width || w
const height = args.height || h
win.setSize(width, height, args.animate === true)
//
if (args.autoZoom === true) {
let move = false
let [x, y] = win.getPosition()
if (Math.abs(width - w) > 10) {
move = true
x -= (width - w) / 2
}
if (Math.abs(height - h) > 10) {
move = true
y -= (height - h) / 2
}
if (move) {
win.setPosition(Math.max(0, Math.floor(x)), Math.max(0, Math.floor(y)))
}
}
}
if (args.minWidth || args.minHeight) {
win.setMinimumSize(args.minWidth || win.getMinimumSize()[0], args.minHeight || win.getMinimumSize()[1])
}
if (args.maxWidth || args.maxHeight) {
win.setMaximumSize(args.maxWidth || win.getMaximumSize()[0], args.maxHeight || win.getMaximumSize()[1])
}
}
event.returnValue = "ok"
})
/**
* 设置窗口最小尺寸
* @param args {minWidth, minHeight}
*/
ipcMain.on('windowMinSize', (event, args) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.setMinimumSize(args.minWidth || win.getMinimumSize()[0], args.minHeight || win.getMinimumSize()[1])
}
event.returnValue = "ok"
})
/**
* 设置窗口最大尺寸
* @param args {maxWidth, maxHeight}
*/
ipcMain.on('windowMaxSize', (event, args) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.setMaximumSize(args.maxWidth || win.getMaximumSize()[0], args.maxHeight || win.getMaximumSize()[1])
}
event.returnValue = "ok"
})
/**
* 窗口居中
*/
ipcMain.on('windowCenter', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.center();
}
event.returnValue = "ok"
})
/**
* 窗口最大化或恢复
*/
ipcMain.on('windowMax', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win.isMaximized()) {
win.restore();
} else {
win.maximize();
}
event.returnValue = "ok"
})
/**
* 给主窗口发送信息
* @param args {channel, data}
*/
ipcMain.on('sendForwardMain', (event, args) => {
if (mainWindow) {
mainWindow.webContents.send(args.channel, args.data)
}
event.returnValue = "ok"
})
/**
* 设置Dock标记
* @param args
*/
ipcMain.on('setDockBadge', (event, args) => {
if(process.platform !== 'darwin'){
// Mac only
return;
}
if (utils.runNum(args) > 0) {
app.dock.setBadge(String(args))
} else {
app.dock.setBadge("")
}
event.returnValue = "ok"
})
/**
* 保存sheets
*/
ipcMain.on('saveSheet', (event, data, filename, opts) => {
const EXTENSIONS = "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html".split("|");
dialog.showSaveDialog({
title: 'Save file as',
defaultPath: filename,
filters: [{
name: "Spreadsheets",
extensions: EXTENSIONS
}]
}).then(o => {
XLSX.writeFile(data, o.filePath, opts);
});
event.returnValue = "ok"
})

View File

@ -1,8 +1,8 @@
{
"name": "DooTask",
"version": "0.10.5",
"version": "0.7.73",
"description": "DooTask is task management system.",
"main": "electron.js",
"main": "main.js",
"license": "MIT",
"scripts": {
"start": "electron-forge start",
@ -28,31 +28,30 @@
"url": "https://github.com/kuaifan/dootask.git"
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.63",
"@electron-forge/maker-deb": "^6.0.0-beta.63",
"@electron-forge/maker-rpm": "^6.0.0-beta.63",
"@electron-forge/maker-squirrel": "^6.0.0-beta.63",
"@electron-forge/maker-zip": "^6.0.0-beta.63",
"@electron-forge/cli": "^6.0.0-beta.61",
"@electron-forge/maker-deb": "^6.0.0-beta.61",
"@electron-forge/maker-rpm": "^6.0.0-beta.61",
"@electron-forge/maker-squirrel": "^6.0.0-beta.61",
"@electron-forge/maker-zip": "^6.0.0-beta.61",
"dmg-license": "^1.0.10",
"dotenv": "^16.0.0",
"electron": "^17.0.1",
"electron-builder": "^22.14.13"
"dotenv": "^10.0.0",
"electron": "^16.0.5",
"electron-builder": "^22.14.5"
},
"dependencies": {
"axios": "^0.26.0",
"crc": "^3.8.0",
"axios": "^0.24.0",
"electron-squirrel-startup": "^1.0.0",
"electron-log": "^4.4.6",
"electron-log": "^4.4.3",
"fs-extra": "^10.0.0",
"pdf-lib": "^1.16.0"
"xlsx": "^0.17.2"
},
"build": {
"appId": "com.dootask.task",
"artifactName": "${productName}-v${version}-${os}-${arch}.${ext}",
"files": [
"public/**/*",
"electron-preload.js",
"electron.js",
"main.js",
"preload.js",
"utils.js"
],
"mac": {

14
electron/preload.js vendored Normal file
View File

@ -0,0 +1,14 @@
// preload.js
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})

37
electron/utils.js vendored
View File

@ -1,5 +1,5 @@
const fs = require("fs");
const {shell, dialog} = require("electron");
const {shell} = require("electron");
module.exports = {
/**
@ -269,39 +269,4 @@ module.exports = {
}
return Math.round(time / 1000)
},
/**
* 窗口关闭事件
* @param event
* @param app
*/
onBeforeUnload(event, app) {
const sender = event.sender
const contents = sender.webContents
if (contents != null) {
const destroy = () => {
if (typeof app === "undefined") {
sender.destroy()
} else {
if (process.platform === 'darwin') {
app.hide()
} else {
app.quit()
}
}
}
contents.executeJavaScript('if(typeof window.__onBeforeUnload === \'function\'){window.__onBeforeUnload()}', true).then(options => {
if (this.isJson(options)) {
let choice = dialog.showMessageBoxSync(sender, options)
if (choice === 1) {
contents.executeJavaScript('if(typeof window.__removeBeforeUnload === \'function\'){window.__removeBeforeUnload()}', true).catch(() => {});
destroy()
}
} else if (options !== true) {
destroy()
}
})
event.preventDefault()
}
},
}

View File

@ -1,6 +1,6 @@
{
"name": "DooTask",
"version": "0.10.5",
"version": "0.7.73",
"description": "DooTask is task management system.",
"scripts": {
"start": "./cmd dev",
@ -41,6 +41,7 @@
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"echarts": "^5.2.2",
"electron": "^16.0.5",
"element-ui": "^2.15.6",
"file-loader": "^6.2.0",
"inquirer": "^8.2.0",
@ -53,7 +54,7 @@
"less-loader": "^10.2.0",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"node-sass": "^6.0.1",
"node-sass": "^4.11.0",
"notification-koro1": "^1.1.1",
"postcss": "^8.4.5",
"resolve-url-loader": "^4.0.0",
@ -61,9 +62,9 @@
"sass-loader": "^12.4.0",
"stylus": "^0.56.0",
"stylus-loader": "^6.2.0",
"tinymce": "^5.10.3",
"tinymce": "^5.10.2",
"tui-calendar-hi": "^1.15.1-5",
"view-design-hi": "^4.7.0-16",
"view-design-hi": "^4.7.0-12",
"vue": "^2.6.14",
"vue-clipboard2": "^0.3.3",
"vue-emoji-picker": "^1.0.3",
@ -74,7 +75,8 @@
"vue-template-compiler": "^2.6.14",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"webpack": "^5.69.1",
"webpack-cli": "^4.9.2"
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"xlsx": "^0.17.4"
}
}

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More