Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfa749f4f3 | ||
|
|
eeaff08673 | ||
|
|
0475e88dc2 | ||
|
|
e1f73a4639 | ||
|
|
e2296a6f64 | ||
|
|
1a6abf4e1b | ||
|
|
315851eb5f | ||
|
|
0b99b4a9a0 | ||
|
|
e8235dd0a2 | ||
|
|
123c74de46 | ||
|
|
9419ddd174 | ||
|
|
0666a8f5c2 | ||
|
|
81c019105c |
1
.github/workflows/publish.yml
vendored
1
.github/workflows/publish.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "pro"
|
||||
- "dev"
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -32,6 +32,9 @@ vars.yaml
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
|
||||
# Development file
|
||||
/index.html
|
||||
|
||||
# Testing
|
||||
.phpunit.result.cache
|
||||
test.*
|
||||
|
||||
@@ -2196,7 +2196,10 @@ class DialogController extends AbstractController
|
||||
switch ($type) {
|
||||
case 'read':
|
||||
// 标记已读
|
||||
$builder = WebSocketDialogMsgRead::whereDialogId($dialog_id)->whereUserid($user->userid)->whereReadAt(null);
|
||||
$builder = WebSocketDialogMsgRead::whereDialogId($dialog_id)
|
||||
->whereUserid($user->userid)
|
||||
->whereReadAt(null)
|
||||
->select(['id', 'msg_id']);
|
||||
if ($after_msg_id > 0) {
|
||||
$builder->where('msg_id', '>=', $after_msg_id);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ use App\Models\ProjectTaskVisibilityUser;
|
||||
use App\Models\ProjectTaskTemplate;
|
||||
use App\Models\ProjectTag;
|
||||
use App\Models\ProjectTaskRelation;
|
||||
use App\Observers\ProjectTaskObserver;
|
||||
|
||||
/**
|
||||
* @apiDefine project
|
||||
@@ -633,6 +634,10 @@ class ProjectController extends AbstractController
|
||||
if (!is_array($item['task'])) continue;
|
||||
$index = 0;
|
||||
foreach ($item['task'] as $task_id) {
|
||||
$task = ProjectTask::find($task_id);
|
||||
if ($task && intval($task->column_id) !== intval($item['id'])) {
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_MOVE, $task);
|
||||
}
|
||||
if (ProjectTask::whereId($task_id)->whereProjectId($project->id)->whereCompleteAt(null)->change([
|
||||
'column_id' => $item['id'],
|
||||
'sort' => $index
|
||||
@@ -991,7 +996,12 @@ class ProjectController extends AbstractController
|
||||
* - 大于0:指定主任务下的子任务
|
||||
* - 等于-1:表示仅主任务
|
||||
*
|
||||
* @apiParam {Array} [time] 指定时间范围,如:['2020-12-12', '2020-12-30']
|
||||
* @apiParam {String} [time] 指定时间范围,如:today, week, month, year, 2020-12-12,2020-12-30
|
||||
* - today: 今天
|
||||
* - week: 本周
|
||||
* - month: 本月
|
||||
* - year: 今年
|
||||
* - 自定义时间范围,如 (字符串):2020-12-12,2020-12-30 或 (数组):['2020-12-12', '2020-12-30']
|
||||
* @apiParam {String} [timerange] 时间范围(如:1678248944,1678248944)
|
||||
* - 第一个时间: 读取在这个时间之后更新的数据
|
||||
* - 第二个时间: 读取在这个时间之后删除的数据ID(第1页附加返回数据: deleted_id)
|
||||
@@ -1091,7 +1101,29 @@ class ProjectController extends AbstractController
|
||||
});
|
||||
}
|
||||
//
|
||||
if (is_array($time)) {
|
||||
if (is_string($time) && $time) {
|
||||
switch ($time) {
|
||||
case 'today':
|
||||
$builder->betweenTime(Carbon::now()->startOfDay(), Carbon::now()->endOfDay());
|
||||
break;
|
||||
case 'week':
|
||||
$builder->betweenTime(Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek());
|
||||
break;
|
||||
case 'month':
|
||||
$builder->betweenTime(Carbon::now()->startOfMonth(), Carbon::now()->endOfMonth());
|
||||
break;
|
||||
case 'year':
|
||||
$builder->betweenTime(Carbon::now()->startOfYear(), Carbon::now()->endOfYear());
|
||||
break;
|
||||
default:
|
||||
if (str_contains($time, ',')) {
|
||||
$times = explode(',', $time);
|
||||
if (Timer::isDateOrTime($times[0]) && Timer::isDateOrTime($times[1])) {
|
||||
$builder->betweenTime(Carbon::parse($times[0])->startOfDay(), Carbon::parse($times[1])->endOfDay());
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (is_array($time)) {
|
||||
if (Timer::isDateOrTime($time[0]) && Timer::isDateOrTime($time[1])) {
|
||||
$builder->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay());
|
||||
}
|
||||
@@ -1991,7 +2023,9 @@ class ProjectController extends AbstractController
|
||||
'path' => $file->getRawOriginal('path'),
|
||||
'thumb' => $file->getRawOriginal('thumb'),
|
||||
]);
|
||||
$task->pushMsg('filedelete', $file);
|
||||
$task->pushMsg('filedelete', [
|
||||
'id' => $file->id,
|
||||
]);
|
||||
$file->delete();
|
||||
//
|
||||
return Base::retSuccess('success', $file);
|
||||
@@ -2228,6 +2262,131 @@ class ProjectController extends AbstractController
|
||||
return Base::retSuccess('添加成功', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/task/upgrade 36. 子任务升级为主任务
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目、任务负责人)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName task__upgrade
|
||||
*
|
||||
* @apiParam {Number} task_id 子任务ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function task__upgrade()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id, true, true, ['taskUser']);
|
||||
if ($task->parent_id == 0) {
|
||||
return Base::retError('当前任务已是主任务');
|
||||
}
|
||||
//
|
||||
$project = Project::userProject($task->project_id);
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_MOVE, $task);
|
||||
//
|
||||
$parentTask = ProjectTask::withTrashed()->find($task->parent_id);
|
||||
$visibilityUserids = [];
|
||||
if ($task->visibility == 3) {
|
||||
$visibilityUserids = ProjectTaskVisibilityUser::whereTaskId($task->id)->pluck('userid')->toArray();
|
||||
if (empty($visibilityUserids) && $parentTask) {
|
||||
$visibilityUserids = ProjectTaskVisibilityUser::whereTaskId($parentTask->id)->pluck('userid')->toArray();
|
||||
}
|
||||
}
|
||||
//
|
||||
DB::transaction(function () use ($task, $parentTask, $visibilityUserids) {
|
||||
$task->lockForUpdate();
|
||||
$task->parent_id = 0;
|
||||
if ($parentTask) {
|
||||
$task->p_level = $parentTask->p_level;
|
||||
$task->p_name = $parentTask->p_name;
|
||||
$task->p_color = $parentTask->p_color;
|
||||
}
|
||||
$task->save();
|
||||
ProjectTaskUser::whereTaskId($task->id)->update(['task_pid' => $task->id]);
|
||||
if ($task->visibility == 3 && !empty($visibilityUserids)) {
|
||||
ProjectTaskVisibilityUser::whereTaskId($task->id)->delete();
|
||||
foreach (array_unique($visibilityUserids) as $userid) {
|
||||
if (!$userid) {
|
||||
continue;
|
||||
}
|
||||
ProjectTaskVisibilityUser::createInstance([
|
||||
'project_id' => $task->project_id,
|
||||
'task_id' => $task->id,
|
||||
'userid' => $userid,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
if ($parentTask) {
|
||||
$parentTask->addLog("子任务升级为主任务", [
|
||||
'subtask' => [
|
||||
'id' => $task->id,
|
||||
'name' => $task->name,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$task->addLog("升级为主任务");
|
||||
});
|
||||
//
|
||||
$task->refresh()->loadMissing(['project', 'taskUser']);
|
||||
if ($task->visibility != 1) {
|
||||
ProjectTaskObserver::visibilityUpdate($task);
|
||||
}
|
||||
$taskData = ProjectTask::oneTask($task->id);
|
||||
$parentData = null;
|
||||
if ($parentTask && !$parentTask->trashed()) {
|
||||
$parentTask->refresh()->loadMissing(['project', 'taskUser']);
|
||||
$parentData = ProjectTask::oneTask($parentTask->id);
|
||||
}
|
||||
//
|
||||
$taskArray = $taskData ? $taskData->toArray() : [];
|
||||
$parentArray = $parentData ? $parentData->toArray() : null;
|
||||
if ($taskArray) {
|
||||
$task->pushMsg('update', $taskArray);
|
||||
}
|
||||
if ($parentArray && $parentTask) {
|
||||
$parentTask->pushMsg('update', $parentArray);
|
||||
}
|
||||
if ($parentTask && !$parentTask->trashed()) {
|
||||
$mentionRelation = ProjectTaskRelation::updateOrCreate(
|
||||
[
|
||||
'task_id' => $task->id,
|
||||
'related_task_id' => $parentTask->id,
|
||||
'direction' => ProjectTaskRelation::DIRECTION_MENTIONED_BY,
|
||||
],
|
||||
[
|
||||
'userid' => $user->userid ?? null,
|
||||
]
|
||||
);
|
||||
$mentionedByRelation = ProjectTaskRelation::updateOrCreate(
|
||||
[
|
||||
'task_id' => $parentTask->id,
|
||||
'related_task_id' => $task->id,
|
||||
'direction' => ProjectTaskRelation::DIRECTION_MENTION,
|
||||
],
|
||||
[
|
||||
'userid' => $user->userid ?? null,
|
||||
]
|
||||
);
|
||||
if ($mentionRelation->wasRecentlyCreated || $mentionRelation->wasChanged()) {
|
||||
$task->pushMsg('relation', null, null, false);
|
||||
}
|
||||
if ($mentionedByRelation->wasRecentlyCreated || $mentionedByRelation->wasChanged()) {
|
||||
$parentTask->pushMsg('relation', null, null, false);
|
||||
}
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('操作成功', [
|
||||
'task' => $taskArray,
|
||||
'parent' => $parentArray,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/project/task/update 35. 修改任务、子任务
|
||||
*
|
||||
@@ -2840,7 +2999,7 @@ class ProjectController extends AbstractController
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName task__ai_generate
|
||||
*
|
||||
*
|
||||
* @apiParam {String} content 用户输入的任务描述(必填)
|
||||
* @apiParam {String} [current_title] 当前已有的任务标题(用于优化改进)
|
||||
* @apiParam {String} [current_content] 当前已有的任务内容(HTML格式,用于优化改进)
|
||||
@@ -2860,13 +3019,13 @@ class ProjectController extends AbstractController
|
||||
public function task__ai_generate()
|
||||
{
|
||||
User::auth();
|
||||
|
||||
|
||||
// 获取用户输入的任务描述
|
||||
$content = Request::input('content');
|
||||
if (empty($content)) {
|
||||
return Base::retError('任务描述不能为空');
|
||||
}
|
||||
|
||||
|
||||
// 获取上下文信息
|
||||
$context = [
|
||||
'current_title' => Request::input('current_title', ''),
|
||||
@@ -2877,7 +3036,7 @@ class ProjectController extends AbstractController
|
||||
'has_time_plan' => boolval(Request::input('has_time_plan', false)),
|
||||
'priority_level' => Request::input('priority_level', ''),
|
||||
];
|
||||
|
||||
|
||||
// 如果当前内容是HTML格式,转换为markdown
|
||||
if (!empty($context['current_content'])) {
|
||||
$context['current_content'] = Base::html2markdown($context['current_content']);
|
||||
@@ -2885,7 +3044,7 @@ class ProjectController extends AbstractController
|
||||
if (!empty($context['template_content'])) {
|
||||
$context['template_content'] = Base::html2markdown($context['template_content']);
|
||||
}
|
||||
|
||||
|
||||
$result = AI::generateTask($content, $context);
|
||||
if (Base::isError($result)) {
|
||||
return Base::retError('生成任务失败', $result);
|
||||
|
||||
@@ -61,6 +61,10 @@ class IndexController extends InvokeController
|
||||
$array = Base::json2array(file_get_contents($hotFile));
|
||||
$style = null;
|
||||
$script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js"));
|
||||
$proxyUri = Base::liveEnv('VSCODE_PROXY_URI');
|
||||
if (is_string($proxyUri) && preg_match('/^https?:\/\//i', $proxyUri)) {
|
||||
$script = preg_replace('/^(https?:\/\/|\/\/)[^\/]+/', rtrim($proxyUri, '/'), $script, 1);
|
||||
}
|
||||
} else {
|
||||
$array = Base::json2array(file_get_contents($manifestFile));
|
||||
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
|
||||
|
||||
@@ -1579,7 +1579,8 @@ class ProjectTask extends AbstractModel
|
||||
} elseif ($data instanceof self) {
|
||||
$data = $data->toArray();
|
||||
}
|
||||
//
|
||||
|
||||
// 获取接收会员
|
||||
if ($userid === null) {
|
||||
$userids = $this->project->relationUserids();
|
||||
} else {
|
||||
@@ -1590,11 +1591,7 @@ class ProjectTask extends AbstractModel
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Arr::exists($data, 'visibility')) {
|
||||
$data['visibility'] = $this->visibility;
|
||||
}
|
||||
|
||||
$visibility = intval($data['visibility']);
|
||||
// 按可见性分组推送
|
||||
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
|
||||
$ownerList = $taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
$assistList = $taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
@@ -1603,16 +1600,19 @@ class ProjectTask extends AbstractModel
|
||||
$assistUsers = array_values(array_diff(array_intersect($userids, $assistList), $ownerUsers));
|
||||
|
||||
$array = [];
|
||||
|
||||
// 负责人
|
||||
if ($ownerUsers) {
|
||||
$array[] = [
|
||||
'userid' => $ownerUsers,
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 1,
|
||||
'assist' => 1,
|
||||
'assist' => 0,
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
// 协助人
|
||||
if ($assistUsers) {
|
||||
$array[] = [
|
||||
'userid' => $assistUsers,
|
||||
@@ -1623,27 +1623,31 @@ class ProjectTask extends AbstractModel
|
||||
];
|
||||
}
|
||||
|
||||
// 其他人
|
||||
$otherUsers = [];
|
||||
switch ($visibility) {
|
||||
switch (intval($data['visibility'])) {
|
||||
case 1:
|
||||
// 项目人员:除了负责人、协助人项目其他人
|
||||
$otherUsers = array_diff($userids, $ownerUsers, $assistUsers);
|
||||
break;
|
||||
case 2:
|
||||
$otherUsers = [];
|
||||
// 任务人员:除了负责人、协助人
|
||||
// $otherUsers = [];
|
||||
break;
|
||||
case 3:
|
||||
// 指定成员
|
||||
$specifys = ProjectTaskVisibilityUser::select(['userid'])->whereTaskId($data['id'])->pluck('userid')->toArray();
|
||||
$otherUsers = array_diff(array_intersect($userids, $specifys), $ownerUsers, $assistUsers);
|
||||
break;
|
||||
default:
|
||||
$otherUsers = array_diff($userids, $ownerUsers, $assistUsers);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($otherUsers) {
|
||||
$array[] = [
|
||||
'userid' => array_values($otherUsers),
|
||||
'data' => $data
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 0,
|
||||
'assist' => 0,
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1651,6 +1655,7 @@ class ProjectTask extends AbstractModel
|
||||
return;
|
||||
}
|
||||
|
||||
// 推送
|
||||
foreach ($array as $item) {
|
||||
$params = [
|
||||
'ignoreFd' => $ignoreSelf ? Request::header('fd') : null,
|
||||
|
||||
@@ -213,6 +213,7 @@ class UmengAlias extends AbstractModel
|
||||
'policy' => [
|
||||
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
|
||||
],
|
||||
'category' => 1,
|
||||
'channel_properties' => [
|
||||
'oppo_channel_id' => 'dootask',
|
||||
'vivo_category' => 'IM',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogMsgRead
|
||||
@@ -76,24 +77,48 @@ class WebSocketDialogMsgRead extends AbstractModel
|
||||
*/
|
||||
public static function onlyMarkRead($list)
|
||||
{
|
||||
$dialogMsg = [];
|
||||
if (empty($list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$collection = collect($list);
|
||||
if ($collection->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
$ids = [];
|
||||
$msgCounts = [];
|
||||
|
||||
/** @var WebSocketDialogMsgRead $item */
|
||||
foreach ($list as $item) {
|
||||
$item->read_at = Carbon::now();
|
||||
$item->save();
|
||||
if (isset($dialogMsg[$item->msg_id])) {
|
||||
$dialogMsg[$item->msg_id]['readNum']++;
|
||||
} else {
|
||||
$dialogMsg[$item->msg_id] = [
|
||||
'dialogMsg' => $item->webSocketDialogMsg,
|
||||
'readNum' => 1
|
||||
];
|
||||
foreach ($collection as $item) {
|
||||
$ids[] = $item->id;
|
||||
if ($item->msg_id) {
|
||||
$msgCounts[$item->msg_id] = ($msgCounts[$item->msg_id] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
foreach ($dialogMsg as $item) {
|
||||
if ($item['dialogMsg']) {
|
||||
$item['dialogMsg']->increment('read', $item['readNum']);
|
||||
|
||||
if (!empty($ids)) {
|
||||
DB::table((new self())->getTable())
|
||||
->whereIn('id', $ids)
|
||||
->whereNull('read_at')
|
||||
->update(['read_at' => $now]);
|
||||
}
|
||||
|
||||
if (!empty($msgCounts)) {
|
||||
$cases = [];
|
||||
$bindings = [];
|
||||
foreach ($msgCounts as $msgId => $num) {
|
||||
$cases[] = 'WHEN ? THEN ?';
|
||||
$bindings[] = $msgId;
|
||||
$bindings[] = $num;
|
||||
}
|
||||
$msgIds = array_keys($msgCounts);
|
||||
$bindings = array_merge($bindings, $msgIds);
|
||||
$placeholders = implode(',', array_fill(0, count($msgIds), '?'));
|
||||
$table = DB::getTablePrefix() . (new WebSocketDialogMsg())->getTable();
|
||||
$sql = "UPDATE {$table} SET `read` = `read` + CASE `id` " . implode(' ', $cases) . " END WHERE `deleted_at` IS NULL AND `id` IN ({$placeholders})";
|
||||
DB::update($sql, $bindings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3073,4 +3073,61 @@ class Base
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时读取 .env 配置(不受配置缓存影响)
|
||||
* @param string $key 配置键名
|
||||
* @param mixed $default 默认值
|
||||
* @return mixed
|
||||
*/
|
||||
public static function liveEnv($key, $default = null)
|
||||
{
|
||||
$envFile = base_path('.env');
|
||||
if (!file_exists($envFile)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$envContent = file_get_contents($envFile);
|
||||
$lines = explode("\n", $envContent);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// 跳过注释和空行
|
||||
if (empty($line) || str_starts_with($line, '#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析 KEY=VALUE
|
||||
if (str_contains($line, '=')) {
|
||||
[$envKey, $envValue] = explode('=', $line, 2);
|
||||
$envKey = trim($envKey);
|
||||
|
||||
if ($envKey === $key) {
|
||||
$envValue = trim($envValue);
|
||||
|
||||
// 移除引号
|
||||
if (preg_match('/^(["\'])(.*)\1$/', $envValue, $matches)) {
|
||||
$envValue = $matches[2];
|
||||
}
|
||||
|
||||
// 处理布尔值
|
||||
$lowerValue = strtolower($envValue);
|
||||
if ($lowerValue === 'true') {
|
||||
return true;
|
||||
}
|
||||
if ($lowerValue === 'false') {
|
||||
return false;
|
||||
}
|
||||
if ($lowerValue === 'null' || $lowerValue === '(null)') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $envValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
58
cmd
58
cmd
@@ -119,6 +119,14 @@ switch_debug() {
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查是否有sudo
|
||||
check_sudo() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
error "请使用 sudo 运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查docker、docker-compose
|
||||
check_docker() {
|
||||
docker --version &> /dev/null
|
||||
@@ -175,7 +183,15 @@ web_build() {
|
||||
fi
|
||||
if [ "$type" = "dev" ]; then
|
||||
echo "<script>window.location.href=window.location.href.replace(/:\d+/, ':' + $(env_get APP_PORT))</script>" > ./index.html
|
||||
env_set APP_DEV_PORT $(rand 20001 30000)
|
||||
if [ -z "$(env_get APP_DEV_PORT)" ]; then
|
||||
env_set APP_DEV_PORT $(rand 20001 30000)
|
||||
fi
|
||||
if [ -n "${VSCODE_PROXY_URI:-}" ]; then
|
||||
APP_REAL_URI=$(TARGET_PORT="$(env_get APP_PORT)" node -p "process.env.VSCODE_PROXY_URI.replace(/\{\{port\}\}/g, process.env.TARGET_PORT || '')")
|
||||
VSCODE_PROXY_URI=$(APP_DEV_PORT="$(env_get APP_DEV_PORT)" node -p "process.env.VSCODE_PROXY_URI.replace(/\{\{port\}\}/g, process.env.APP_DEV_PORT || '')")
|
||||
echo "<script>window.location.href='${APP_REAL_URI}'</script>" > ./index.html
|
||||
fi
|
||||
env_set VSCODE_PROXY_URI "${VSCODE_PROXY_URI:-}"
|
||||
fi
|
||||
switch_debug "$type"
|
||||
#
|
||||
@@ -246,25 +262,33 @@ mysql_snapshot() {
|
||||
password=$(env_get DB_PASSWORD)
|
||||
# 还原数据库
|
||||
mkdir -p ${WORK_DIR}/docker/mysql/backup
|
||||
list=`ls -1 "${WORK_DIR}/docker/mysql/backup" | grep ".sql.gz"`
|
||||
if [ -z "$list" ]; then
|
||||
shopt -s nullglob
|
||||
backup_files=("${WORK_DIR}/docker/mysql/backup/"*.sql.gz)
|
||||
shopt -u nullglob
|
||||
if [ ${#backup_files[@]} -eq 0 ]; then
|
||||
error "没有备份文件!"
|
||||
exit 1
|
||||
fi
|
||||
echo "$list"
|
||||
read -rp "请输入备份文件名称还原:" inputname
|
||||
filename="${WORK_DIR}/docker/mysql/backup/${inputname}"
|
||||
if [ ! -f "$filename" ]; then
|
||||
error "备份文件:${inputname} 不存在!"
|
||||
exit 1
|
||||
fi
|
||||
echo "可用备份列表:"
|
||||
for idx in "${!backup_files[@]}"; do
|
||||
printf "%2d) %s\n" "$((idx + 1))" "$(basename "${backup_files[$idx]}")"
|
||||
done
|
||||
while true; do
|
||||
read -rp "请输入备份文件编号还原:" selection
|
||||
if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#backup_files[@]} ]; then
|
||||
break
|
||||
fi
|
||||
warning "编号无效,请重新输入。"
|
||||
done
|
||||
filename="${backup_files[$((selection - 1))]}"
|
||||
inputname="$(basename "$filename")"
|
||||
container_name=`docker_name mariadb`
|
||||
if [ -z "$container_name" ]; then
|
||||
error "没有找到 mariadb 容器!"
|
||||
exit 1
|
||||
fi
|
||||
docker cp $filename ${container_name}:/
|
||||
container_exec mariadb "gunzip < /${inputname} | mysql -u${username} -p${password} $database"
|
||||
docker cp "$filename" "${container_name}:/"
|
||||
container_exec mariadb "gunzip < '/${inputname}' | mysql -u${username} -p${password} $database"
|
||||
container_exec php "php artisan migrate"
|
||||
judge "还原数据库"
|
||||
fi
|
||||
@@ -459,6 +483,8 @@ EOF
|
||||
|
||||
# 安装函数
|
||||
handle_install() {
|
||||
check_sudo
|
||||
|
||||
local relock=$(arg_get relock)
|
||||
local port=$(arg_get port)
|
||||
|
||||
@@ -479,7 +505,8 @@ handle_install() {
|
||||
for vol in "${volumes[@]}"; do
|
||||
tmp_path="${WORK_DIR}/${vol}"
|
||||
mkdir -p "${tmp_path}"
|
||||
chmod -R 775 "${tmp_path}"
|
||||
find "${tmp_path}" -type d -exec chmod 775 {} \;
|
||||
|
||||
rm -f "${tmp_path}/dootask.lock"
|
||||
cmda="${cmda} -v ${tmp_path}:/usr/share/${vol}"
|
||||
cmdb="${cmdb} touch /usr/share/${vol}/dootask.lock &&"
|
||||
@@ -547,6 +574,8 @@ handle_install() {
|
||||
|
||||
# 更新函数
|
||||
handle_update() {
|
||||
check_sudo
|
||||
|
||||
local target_branch=$(arg_get branch)
|
||||
local is_local=$(arg_get local)
|
||||
local force_update=$(arg_get force)
|
||||
@@ -617,7 +646,7 @@ handle_update() {
|
||||
fi
|
||||
|
||||
# 更新依赖
|
||||
exec_judge "container_exec php 'composer update --optimize-autoloader'" "更新PHP依赖失败"
|
||||
exec_judge "container_exec php 'composer install --optimize-autoloader'" "更新PHP依赖失败"
|
||||
else
|
||||
# 本地更新模式
|
||||
echo "执行数据库备份..."
|
||||
@@ -644,6 +673,7 @@ handle_update() {
|
||||
|
||||
# 卸载函数
|
||||
handle_uninstall() {
|
||||
check_sudo
|
||||
# 确认卸载
|
||||
echo -e "${RedBG}警告:此操作将永久删除以下内容:${Font}"
|
||||
echo "- 数据库"
|
||||
|
||||
805
composer.lock
generated
805
composer.lock
generated
File diff suppressed because it is too large
Load Diff
10
electron/electron.js
vendored
10
electron/electron.js
vendored
@@ -46,6 +46,7 @@ const utils = require('./lib/utils');
|
||||
const config = require('./package.json');
|
||||
const electronDown = require("./electron-down");
|
||||
const electronMenu = require("./electron-menu");
|
||||
const { startMCPServer } = require("./lib/mcp");
|
||||
|
||||
// 实例初始化
|
||||
const userConf = new electronConf()
|
||||
@@ -73,6 +74,7 @@ let enableStoreBkp = true,
|
||||
|
||||
// 服务器配置
|
||||
let serverPort = 22223,
|
||||
mcpPort = 22224,
|
||||
serverPublicDir = path.join(__dirname, 'public'),
|
||||
serverUrl = "",
|
||||
serverTimer = null;
|
||||
@@ -1141,11 +1143,11 @@ if (!getTheLock) {
|
||||
app.on('ready', async () => {
|
||||
isReady = true
|
||||
isWin && app.setAppUserModelId(config.appId)
|
||||
// 启动web服务
|
||||
// 启动 Web 服务器
|
||||
try {
|
||||
await startWebServer()
|
||||
} catch (error) {
|
||||
dialog.showErrorBox('启动失败', `服务器启动失败:${error.message}`);
|
||||
dialog.showErrorBox('启动失败', `Web 服务器启动失败:${error.message}`);
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
@@ -1157,6 +1159,8 @@ if (!getTheLock) {
|
||||
preCreateChildWindow()
|
||||
// 监听主题变化
|
||||
monitorThemeChanges()
|
||||
// 启动 MCP 服务器
|
||||
startMCPServer(mainWindow, mcpPort)
|
||||
// 创建托盘
|
||||
if (['darwin', 'win32'].includes(process.platform) && utils.isJson(config.trayIcon)) {
|
||||
mainTray = new Tray(path.join(__dirname, config.trayIcon[isDevelopMode ? 'dev' : 'prod'][process.platform === 'darwin' ? 'mac' : 'win']));
|
||||
@@ -1217,7 +1221,7 @@ app.on('before-quit', () => {
|
||||
willQuitApp = true
|
||||
})
|
||||
|
||||
app.on("will-quit",function(){
|
||||
app.on("will-quit", () => {
|
||||
globalShortcut.unregisterAll();
|
||||
})
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div id="app" data-preload="false">
|
||||
<div id="app" data-preload="init">
|
||||
<div class="app-view-loading no-dark-content">
|
||||
<div>
|
||||
<div>PAGE LOADING</div>
|
||||
|
||||
417
electron/lib/mcp.js
vendored
Normal file
417
electron/lib/mcp.js
vendored
Normal file
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* DooTask MCP Server
|
||||
*
|
||||
* DooTask 的 Electron 客户端集成了 Model Context Protocol (MCP) 服务,
|
||||
* 允许 AI 助手(如 Claude)直接与 DooTask 任务进行交互。
|
||||
*
|
||||
* 提供的工具:
|
||||
* 1. list_tasks - 获取任务列表,支持按状态/项目/主任务筛选、搜索、分页
|
||||
* 2. get_task - 获取任务详情,包含负责人、协助人员、标签等完整信息
|
||||
* 3. complete_task - 标记任务完成,自动记录完成时间
|
||||
* 4. uncomplete_task - 取消完成任务,将已完成任务改为未完成
|
||||
* 5. get_task_content - 获取任务的富文本描述内容
|
||||
*
|
||||
* 配置方法:
|
||||
* 添加 DooTask MCP 服务器配置:
|
||||
* {
|
||||
* "mcpServers": {
|
||||
* "DooTask": {
|
||||
* "url": "http://localhost:22224/sse"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 使用示例:
|
||||
* - "请帮我查看目前有哪些未完成的任务"
|
||||
* - "任务 123 的详细信息是什么?"
|
||||
* - "帮我把任务 789 标记为已完成"
|
||||
*/
|
||||
|
||||
const { FastMCP } = require('fastmcp');
|
||||
const { z } = require('zod');
|
||||
const loger = require("electron-log");
|
||||
|
||||
let mcpServer = null;
|
||||
|
||||
class DooTaskMCP {
|
||||
constructor(mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.mcp = new FastMCP({
|
||||
name: 'DooTask MCP Server',
|
||||
version: '1.0.0',
|
||||
description: 'DooTask 任务管理 MCP 接口',
|
||||
});
|
||||
|
||||
this.setupTools();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用接口
|
||||
*/
|
||||
async request(method, path, data = {}) {
|
||||
try {
|
||||
// 通过主窗口执行前端代码来调用API
|
||||
if (!this.mainWindow || !this.mainWindow.webContents) {
|
||||
throw new Error('Main window not available, please open DooTask application first');
|
||||
}
|
||||
|
||||
// 检查 webContents 是否 ready
|
||||
if (this.mainWindow.webContents.isLoading()) {
|
||||
loger.warn(`[MCP] WebContents is loading, waiting...`);
|
||||
await new Promise(resolve => {
|
||||
this.mainWindow.webContents.once('did-finish-load', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
// 通过前端已有的API调用机制,添加超时处理
|
||||
const executePromise = this.mainWindow.webContents.executeJavaScript(`
|
||||
(async () => {
|
||||
try {
|
||||
// 检查API是否可用
|
||||
if (typeof $A === 'undefined') {
|
||||
return { error: 'API not available - $A is undefined' };
|
||||
}
|
||||
|
||||
if (typeof $A.apiCall !== 'function') {
|
||||
return { error: 'API not available - $A.apiCall is not a function' };
|
||||
}
|
||||
|
||||
// 调用 API
|
||||
const result = await $A.apiCall({
|
||||
url: '${path}',
|
||||
data: ${JSON.stringify(data)},
|
||||
method: '${method}'
|
||||
});
|
||||
|
||||
try {
|
||||
return { data: JSON.parse(JSON.stringify(result.data)) };
|
||||
} catch (serError) {
|
||||
return { error: 'Result contains non-serializable data' };
|
||||
}
|
||||
} catch (error) {
|
||||
return { error: error.msg || error.message || String(error) || 'API request failed' };
|
||||
}
|
||||
})()
|
||||
`);
|
||||
|
||||
// 添加超时处理(30秒)
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Request timeout after 30 seconds')), 30000);
|
||||
});
|
||||
|
||||
// 返回结果
|
||||
const result = await Promise.race([executePromise, timeoutPromise]);
|
||||
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 MCP 工具
|
||||
setupTools() {
|
||||
// 1. 获取任务列表
|
||||
this.mcp.addTool({
|
||||
name: 'list_tasks',
|
||||
description: '获取任务列表。可以按状态筛选(已完成/未完成)、搜索任务名称、按时间范围筛选等。',
|
||||
parameters: z.object({
|
||||
status: z.enum(['all', 'completed', 'uncompleted'])
|
||||
.optional()
|
||||
.describe('任务状态: all(所有), completed(已完成), uncompleted(未完成)'),
|
||||
search: z.string()
|
||||
.optional()
|
||||
.describe('搜索关键词(可搜索任务ID、名称、描述)'),
|
||||
time: z.string()
|
||||
.optional()
|
||||
.describe('时间范围: today(今天), week(本周), month(本月), year(今年), 自定义时间范围,如:2025-12-12,2025-12-30'),
|
||||
project_id: z.number()
|
||||
.optional()
|
||||
.describe('项目ID,只获取指定项目的任务'),
|
||||
parent_id: z.number()
|
||||
.optional()
|
||||
.describe('主任务ID。大于0:获取该主任务的子任务; -1:仅获取主任务; 不传:所有任务'),
|
||||
page: z.number()
|
||||
.optional()
|
||||
.describe('页码,默认1'),
|
||||
pagesize: z.number()
|
||||
.optional()
|
||||
.describe('每页数量,默认20,最大100'),
|
||||
}),
|
||||
execute: async (params) => {
|
||||
const requestData = {
|
||||
page: params.page || 1,
|
||||
pagesize: params.pagesize || 20,
|
||||
};
|
||||
|
||||
// 构建 keys 对象用于筛选
|
||||
const keys = {};
|
||||
if (params.search) {
|
||||
keys.name = params.search;
|
||||
}
|
||||
if (params.status && params.status !== 'all') {
|
||||
keys.status = params.status;
|
||||
}
|
||||
if (Object.keys(keys).length > 0) {
|
||||
requestData.keys = keys;
|
||||
}
|
||||
|
||||
// 其他筛选参数
|
||||
if (params.time !== undefined) {
|
||||
requestData.time = params.time;
|
||||
}
|
||||
if (params.project_id !== undefined) {
|
||||
requestData.project_id = params.project_id;
|
||||
}
|
||||
if (params.parent_id !== undefined) {
|
||||
requestData.parent_id = params.parent_id;
|
||||
}
|
||||
|
||||
const result = await this.request('GET', 'project/task/lists', requestData);
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
// 格式化返回数据,使其更易读
|
||||
const tasks = result.data.data.map(task => ({
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
status: task.complete_at ? '已完成' : '未完成',
|
||||
complete_at: task.complete_at || '未完成',
|
||||
project_id: task.project_id,
|
||||
parent_id: task.parent_id,
|
||||
sub_num: task.sub_num || 0,
|
||||
sub_complete: task.sub_complete || 0,
|
||||
percent: task.percent || 0,
|
||||
created_at: task.created_at,
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
total: result.data.total,
|
||||
page: result.data.current_page,
|
||||
pagesize: result.data.per_page,
|
||||
tasks: tasks,
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 获取任务详情
|
||||
this.mcp.addTool({
|
||||
name: 'get_task',
|
||||
description: '获取指定任务的详细信息,包括任务描述、负责人、协助人员、标签、时间等完整信息。',
|
||||
parameters: z.object({
|
||||
task_id: z.number()
|
||||
.describe('任务ID'),
|
||||
}),
|
||||
execute: async (params) => {
|
||||
const result = await this.request('GET', 'project/task/one', {
|
||||
task_id: params.task_id,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const task = result.data;
|
||||
|
||||
// 格式化任务详情
|
||||
const taskDetail = {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
desc: task.desc || '无描述',
|
||||
status: task.complete_at ? '已完成' : '未完成',
|
||||
complete_at: task.complete_at || '未完成',
|
||||
project_id: task.project_id,
|
||||
project_name: task.project_name,
|
||||
column_id: task.column_id,
|
||||
column_name: task.column_name,
|
||||
parent_id: task.parent_id,
|
||||
start_at: task.start_at,
|
||||
end_at: task.end_at,
|
||||
flow_item_id: task.flow_item_id,
|
||||
flow_item_name: task.flow_item_name,
|
||||
visibility: task.visibility === 1 ? '公开' : '指定人员',
|
||||
owners: task.taskUser?.filter(u => u.owner === 1).map(u => u.userid) || [],
|
||||
assistants: task.taskUser?.filter(u => u.owner === 0).map(u => u.userid) || [],
|
||||
tags: task.taskTag?.map(t => t.name) || [],
|
||||
created_at: task.created_at,
|
||||
updated_at: task.updated_at,
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(taskDetail, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 标记任务完成
|
||||
this.mcp.addTool({
|
||||
name: 'complete_task',
|
||||
description: '将指定任务标记为已完成。注意:主任务必须在所有子任务完成后才能标记完成。',
|
||||
parameters: z.object({
|
||||
task_id: z.number()
|
||||
.describe('要标记完成的任务ID'),
|
||||
}),
|
||||
execute: async (params) => {
|
||||
// 使用当前时间标记完成
|
||||
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
|
||||
const result = await this.request('POST', 'project/task/update', {
|
||||
task_id: params.task_id,
|
||||
complete_at: now,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: '任务已标记为完成',
|
||||
task_id: params.task_id,
|
||||
complete_at: result.data.complete_at,
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 取消完成任务
|
||||
this.mcp.addTool({
|
||||
name: 'uncomplete_task',
|
||||
description: '将已完成的任务标记为未完成。',
|
||||
parameters: z.object({
|
||||
task_id: z.number()
|
||||
.describe('要标记为未完成的任务ID'),
|
||||
}),
|
||||
execute: async (params) => {
|
||||
const result = await this.request('POST', 'project/task/update', {
|
||||
task_id: params.task_id,
|
||||
complete_at: false,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: '任务已标记为未完成',
|
||||
task_id: params.task_id,
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 获取任务内容详情
|
||||
this.mcp.addTool({
|
||||
name: 'get_task_content',
|
||||
description: '获取任务的详细内容描述(富文本内容)',
|
||||
parameters: z.object({
|
||||
task_id: z.number()
|
||||
.describe('任务ID'),
|
||||
}),
|
||||
execute: async (params) => {
|
||||
const result = await this.request('GET', 'project/task/content', {
|
||||
task_id: params.task_id,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
task_id: params.task_id,
|
||||
content: result.data.content || '无内容',
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 启动 MCP 服务器
|
||||
async start(port = 22224) {
|
||||
try {
|
||||
await this.mcp.start({
|
||||
transportType: 'httpStream',
|
||||
httpStream: {
|
||||
port: port
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止服务器
|
||||
async stop() {
|
||||
if (this.mcp && typeof this.mcp.stop === 'function') {
|
||||
await this.mcp.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 MCP 服务器
|
||||
*/
|
||||
function startMCPServer(mainWindow, mcpPort) {
|
||||
if (mcpServer) {
|
||||
loger.info('MCP server already running');
|
||||
return;
|
||||
}
|
||||
|
||||
mcpServer = new DooTaskMCP(mainWindow);
|
||||
mcpServer.start(mcpPort).then(() => {
|
||||
loger.info(`DooTask MCP Server started on http://localhost:${mcpPort}/sse`);
|
||||
}).catch((error) => {
|
||||
loger.error('Failed to start MCP server:', error);
|
||||
mcpServer = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 MCP 服务器
|
||||
*/
|
||||
function stopMCPServer() {
|
||||
if (!mcpServer) {
|
||||
loger.info('MCP server is not running');
|
||||
return;
|
||||
}
|
||||
|
||||
mcpServer.stop().then(() => {
|
||||
loger.info('MCP server stopped');
|
||||
}).catch((error) => {
|
||||
loger.error('Failed to stop MCP server:', error);
|
||||
}).finally(() => {
|
||||
mcpServer = null;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DooTaskMCP,
|
||||
startMCPServer,
|
||||
stopMCPServer,
|
||||
};
|
||||
@@ -53,10 +53,12 @@
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"express": "^5.1.0",
|
||||
"fastmcp": "^3.21.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"request": "^2.88.2",
|
||||
"tar": "^7.4.3",
|
||||
"zod": "^3.23.8",
|
||||
"yauzl": "^3.2.0"
|
||||
},
|
||||
"trayIcon": {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<script>window.location.href=window.location.href.replace(/:\d+/, ':' + 2222)</script>
|
||||
@@ -920,4 +920,8 @@ URL格式不正确
|
||||
收藏记录不存在
|
||||
修改备注成功
|
||||
请输入修改备注
|
||||
备注最多支持(*)个字符
|
||||
备注最多支持(*)个字符
|
||||
|
||||
当前任务已是主任务
|
||||
子任务升级为主任务
|
||||
升级为主任务
|
||||
@@ -2214,3 +2214,8 @@ AI 未生成内容
|
||||
暂无共同群组
|
||||
(*)个
|
||||
查看更多...
|
||||
|
||||
子任务升级为主任务
|
||||
升级为主任务
|
||||
升主任务
|
||||
你确定要将子任务【(*)】升级为主任务吗?
|
||||
2
resources/assets/js/app.js
vendored
2
resources/assets/js/app.js
vendored
@@ -2,6 +2,8 @@ const isElectron = !!(window && window.process && window.process.type && window.
|
||||
const isEEUIApp = window && window.navigator && /eeui/i.test(window.navigator.userAgent);
|
||||
const isSoftware = isElectron || isEEUIApp;
|
||||
|
||||
document.getElementById("app")?.setAttribute("data-preload", "false");
|
||||
|
||||
import {languageName, switchLanguage as $L} from "./language";
|
||||
import {isLocalHost} from "./components/Replace/utils";
|
||||
|
||||
|
||||
@@ -901,11 +901,17 @@ export default {
|
||||
},
|
||||
|
||||
onKeydown(event) {
|
||||
if (event.isComposing || event.key === 'Process') {
|
||||
return;
|
||||
}
|
||||
// 按下删除键时,判断是否符合删除条件
|
||||
this.backspaceDelete = event.key === 'Backspace' && !this.searchKey && this.selects.length > 0;
|
||||
},
|
||||
|
||||
onKeyup(event) {
|
||||
if (event.isComposing || event.key === 'Process') {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Backspace' && this.backspaceDelete) {
|
||||
// 从最后一个元素开始向前遍历,找到第一个不是不可取消的元素
|
||||
for (let i = this.selects.length - 1; i >= 0; i--) {
|
||||
|
||||
@@ -86,11 +86,18 @@
|
||||
</EDropdownItem>
|
||||
</template>
|
||||
</template>
|
||||
<EDropdownItem v-else-if="operationShow" command="remove" :divided="turns.length > 0">
|
||||
<div class="item">
|
||||
<Icon type="md-trash" />{{$L('删除')}}
|
||||
</div>
|
||||
</EDropdownItem>
|
||||
<template v-else-if="operationShow">
|
||||
<EDropdownItem command="upgrade" :divided="turns.length > 0">
|
||||
<div class="item">
|
||||
<Icon type="md-arrow-round-up" />{{$L('升主任务')}}
|
||||
</div>
|
||||
</EDropdownItem>
|
||||
<EDropdownItem command="remove">
|
||||
<div class="item hover-del">
|
||||
<Icon type="md-trash" />{{$L('删除')}}
|
||||
</div>
|
||||
</EDropdownItem>
|
||||
</template>
|
||||
</ul>
|
||||
</li>
|
||||
</EDropdownMenu>
|
||||
@@ -342,6 +349,10 @@ export default {
|
||||
this.$refs.forwarder.onSelection()
|
||||
break;
|
||||
|
||||
case 'upgrade':
|
||||
this.upgradeSubtask();
|
||||
break;
|
||||
|
||||
case 'archived':
|
||||
case 'remove':
|
||||
this.archivedOrRemoveTask(command);
|
||||
@@ -391,6 +402,33 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
upgradeSubtask() {
|
||||
if (this.loadIng) {
|
||||
return;
|
||||
}
|
||||
$A.modalConfirm({
|
||||
title: '升级为主任务',
|
||||
content: `你确定要将子任务【${this.task.name}】升级为主任务吗?`,
|
||||
loading: true,
|
||||
onOk: () => {
|
||||
if (this.loadIng) {
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.$store.dispatch("taskConvertToMain", this.task.id).then(({data, msg}) => {
|
||||
$A.messageSuccess(msg);
|
||||
this.hide();
|
||||
this.$store.dispatch("openTask", data?.task?.id || this.task.id);
|
||||
resolve();
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg);
|
||||
reject();
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
archivedOrRemoveTask(type) {
|
||||
let typeDispatch = 'removeTask';
|
||||
let typeName = '删除';
|
||||
|
||||
49
resources/assets/js/store/actions.js
vendored
49
resources/assets/js/store/actions.js
vendored
@@ -2276,6 +2276,47 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 子任务升级为主任务
|
||||
* @param dispatch
|
||||
* @param data Number|JSONObject{task_id}
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
taskConvertToMain({dispatch}, data) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (/^\d+$/.test(data)) {
|
||||
data = {task_id: data}
|
||||
}
|
||||
if ($A.runNum(data.task_id) === 0) {
|
||||
reject({msg: 'Parameter error'});
|
||||
return;
|
||||
}
|
||||
dispatch("setLoad", {
|
||||
key: `task-${data.task_id}`,
|
||||
delay: 300
|
||||
})
|
||||
dispatch("call", {
|
||||
url: 'project/task/upgrade',
|
||||
data,
|
||||
}).then(result => {
|
||||
const {task, parent} = result.data || {};
|
||||
if (task) {
|
||||
dispatch("saveTask", task);
|
||||
}
|
||||
if (parent) {
|
||||
dispatch("saveTask", parent);
|
||||
}
|
||||
resolve(result)
|
||||
}).catch(e => {
|
||||
console.warn(e);
|
||||
dispatch("getTaskOne", data.task_id).catch(() => {})
|
||||
reject(e)
|
||||
}).finally(_ => {
|
||||
dispatch("cancelLoad", `task-${data.task_id}`)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取任务详细描述
|
||||
* @param state
|
||||
@@ -4609,11 +4650,6 @@ export default {
|
||||
case 'recovery': // 恢复(归档)
|
||||
dispatch("saveTask", data)
|
||||
break;
|
||||
case 'relation':
|
||||
if (data?.id) {
|
||||
emitter.emit('taskRelationUpdate', data.id)
|
||||
}
|
||||
break;
|
||||
case 'dialog':
|
||||
dispatch("saveTask", data)
|
||||
dispatch("getDialogOne", data.dialog_id).catch(() => {})
|
||||
@@ -4627,6 +4663,9 @@ export default {
|
||||
case 'delete':
|
||||
dispatch("forgetTask", data)
|
||||
break;
|
||||
case 'relation':
|
||||
emitter.emit('taskRelationUpdate', data.id)
|
||||
break;
|
||||
}
|
||||
})(msgDetail);
|
||||
break;
|
||||
|
||||
@@ -853,6 +853,7 @@
|
||||
|
||||
ol {
|
||||
li {
|
||||
min-height: 20px;
|
||||
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
|
||||
|
||||
&::before {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<body>
|
||||
|
||||
@extends('ie')
|
||||
<div id="app" data-preload="false">
|
||||
<div id="app" data-preload="init">
|
||||
<div class="app-view-loading no-dark-content">
|
||||
<div>
|
||||
<div>PAGE LOADING</div>
|
||||
|
||||
15
vite.config.js
vendored
15
vite.config.js
vendored
@@ -22,6 +22,7 @@ export default defineConfig(({command, mode}) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const host = "0.0.0.0"
|
||||
const port = parseInt(env['APP_DEV_PORT'])
|
||||
const proxy_uri = env['VSCODE_PROXY_URI']
|
||||
|
||||
if (command === 'serve') {
|
||||
const hotFile = path.resolve(__dirname, 'public/hot')
|
||||
@@ -80,6 +81,17 @@ export default defineConfig(({command, mode}) => {
|
||||
})
|
||||
}
|
||||
|
||||
const serverHmr = {}
|
||||
if (/^https?:\/\//i.test(proxy_uri)) {
|
||||
const proxyUri = new URL(proxy_uri)
|
||||
if (proxyUri) {
|
||||
Object.assign(serverHmr, {
|
||||
host: proxyUri.host,
|
||||
clientPort: proxyUri.port || (/^https/.test(proxy_uri) ? 443 : 80)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
base: basePath,
|
||||
publicDir: publicPath,
|
||||
@@ -98,7 +110,8 @@ export default defineConfig(({command, mode}) => {
|
||||
'**/language/**',
|
||||
'**/electron/**',
|
||||
]
|
||||
}
|
||||
},
|
||||
hmr: serverHmr
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user