From 383664aef7b70628f8fefad899c61a0071c5a181 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Sat, 13 Jun 2026 01:21:22 +0000 Subject: [PATCH] =?UTF-8?q?chore(quality):=20=E5=BC=95=E5=85=A5=20phpstan/?= =?UTF-8?q?ESLint/CI=20=E9=97=A8=E7=A6=81=E3=80=81Claude=20hooks=20?= =?UTF-8?q?=E4=B8=8E=E4=BB=A3=E7=A0=81=E6=A3=80=E7=B4=A2=E5=9C=B0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - phpstan(larastan ^3.10, level 1 + baseline 封存 86 个存量错误),composer stan / stan-baseline - ESLint 9 flat config(vue2-essential,存量违规降 warn,error 基线为 0),npm run lint - CI:.github/workflows/tests.yml(static-checks + phpunit,phpunit 用 kuaifan/php 镜像跑,FFI doo.so 不在仓库) - Claude Code hooks:编辑 app/ 下 PHP 后自动单文件 phpstan,失败回灌 - 检索地图:routes/api-map.md(doc:api-map 生成,325 接口)、docs/events-map.md(events:map)、types/dootask-globals.d.ts($A 207 方法)、npm run check:lang(存量缺失 93 条,CI 暂非阻塞) - CLAUDE.md:版本号更正 Laravel 13/PHP 8.4,新增质量门禁、检索地图、架构增量规则章节 Co-Authored-By: Claude Fable 5 --- .claude/hooks/php-stan-check.sh | 53 +++ .claude/settings.json | 16 + .github/workflows/tests.yml | 81 ++++ CLAUDE.md | 22 +- app/Console/Commands/DocApiMap.php | 143 +++++++ composer.json | 5 +- composer.lock | 203 ++++++++- docs/events-map.md | 323 ++++++++++++++ eslint.config.mjs | 67 +++ jsconfig.json | 2 +- package.json | 11 +- phpstan-baseline.neon | 349 +++++++++++++++ phpstan.neon | 12 + routes/api-map.md | 398 +++++++++++++++++ scripts/check-language.mjs | 173 ++++++++ scripts/gen-events-map.mjs | 155 +++++++ types/dootask-globals.d.ts | 666 +++++++++++++++++++++++++++++ 17 files changed, 2669 insertions(+), 10 deletions(-) create mode 100755 .claude/hooks/php-stan-check.sh create mode 100644 .claude/settings.json create mode 100644 .github/workflows/tests.yml create mode 100644 app/Console/Commands/DocApiMap.php create mode 100644 docs/events-map.md create mode 100644 eslint.config.mjs create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon create mode 100644 routes/api-map.md create mode 100644 scripts/check-language.mjs create mode 100644 scripts/gen-events-map.mjs create mode 100644 types/dootask-globals.d.ts diff --git a/.claude/hooks/php-stan-check.sh b/.claude/hooks/php-stan-check.sh new file mode 100755 index 000000000..60249321a --- /dev/null +++ b/.claude/hooks/php-stan-check.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Claude Code PostToolUse hook:Edit/Write 改动 app/ 下的 PHP 文件后,自动在 PHP 容器内 +# 对该文件跑 phpstan 单文件分析。失败时 exit 2,把错误回灌给 Claude 修复。 +# 任何环境不满足(无 python3 / 容器未运行 / 未装 phpstan)都静默放行,绝不阻塞编辑。 +set -u + +INPUT=$(cat) +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}" + +command -v python3 >/dev/null 2>&1 || exit 0 +command -v docker >/dev/null 2>&1 || exit 0 + +FILE_PATH=$(printf '%s' "$INPUT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null) || exit 0 +[ -n "$FILE_PATH" ] || exit 0 + +case "$FILE_PATH" in + *.php) ;; + *) exit 0 ;; +esac + +REL_PATH="${FILE_PATH#"$PROJECT_DIR"/}" +case "$REL_PATH" in + app/*) ;; + *) exit 0 ;; +esac +[ -f "$PROJECT_DIR/$REL_PATH" ] || exit 0 + +# 定位挂载本项目的 PHP 容器: +# ① 环境变量 DOOTASK_PHP_CONTAINER;② .env 的 APP_ID;③ 扫描 /var/www 挂载源为本项目的容器 +CONTAINER="${DOOTASK_PHP_CONTAINER:-}" +if [ -z "$CONTAINER" ] && [ -f "$PROJECT_DIR/.env" ]; then + APP_ID=$(grep -E '^APP_ID=' "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2 | tr -d '"' | tr -d "'") + if [ -n "$APP_ID" ] && docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "dootask-php-$APP_ID"; then + CONTAINER="dootask-php-$APP_ID" + fi +fi +if [ -z "$CONTAINER" ]; then + RUNNING=$(docker ps -q 2>/dev/null) + [ -n "$RUNNING" ] && CONTAINER=$(docker inspect --format '{{.Name}}|{{range .Mounts}}{{if eq .Destination "/var/www"}}{{.Source}}{{end}}{{end}}' $RUNNING 2>/dev/null \ + | awk -F'|' -v dir="$PROJECT_DIR" '$2 == dir {gsub(/^\//, "", $1); print $1; exit}') +fi +[ -n "$CONTAINER" ] || exit 0 +docker exec "$CONTAINER" test -f /var/www/vendor/bin/phpstan 2>/dev/null || exit 0 + +OUTPUT=$(docker exec "$CONTAINER" sh -c "cd /var/www && php vendor/bin/phpstan analyse --no-progress --error-format=raw --memory-limit=-1 '$REL_PATH'" 2>&1) +if [ $? -ne 0 ]; then + { + echo "phpstan 检查未通过($REL_PATH),请修复以下问题:" + printf '%s\n' "$OUTPUT" | grep -v '^Note:' | tail -30 + } >&2 + exit 2 +fi +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..863b0171b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/php-stan-check.sh", + "timeout": 120 + } + ] + } + ] + } +} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..f618ce0c2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,81 @@ +name: Tests + +on: + push: + branches: [pro, master] + pull_request: + +jobs: + static-checks: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: mbstring, intl, gd, xml, zip, swoole, redis + tools: composer:v2 + + - name: Install Composer Dependencies + run: composer install --prefer-dist --no-interaction + + - name: PHPStan + run: composer stan + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install NPM Dependencies + run: npm install --no-audit --no-fund + + - name: ESLint + run: npm run lint + + # 存量缺失文案 93 条(见 scripts/check-language.mjs 输出),清零后移除 continue-on-error 改为强制 + - name: Language Check + run: npm run check:lang + continue-on-error: true + + phpunit: + runs-on: ubuntu-latest + services: + mariadb: + image: mariadb:10.7.3 + env: + MYSQL_ROOT_PASSWORD: test + MYSQL_DATABASE: dootask + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h localhost -ptest" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + redis: + image: redis:alpine + ports: + - 6379:6379 + steps: + - name: Checkout + uses: actions/checkout@v4 + + # 用项目自身的 PHP 镜像跑测试(内含 /usr/lib/doo/doo.so FFI 库,裸 runner 无法运行) + - name: Run PHPUnit in DooTask PHP image + run: | + docker run --rm --network host -v "$GITHUB_WORKSPACE":/var/www -w /var/www \ + kuaifan/php:swoole-8.4 sh -c ' + composer install --prefer-dist --no-interaction && + cp .env.example .env && + sed -i "s/^DB_HOST=.*/DB_HOST=127.0.0.1/" .env && + sed -i "s/^DB_DATABASE=.*/DB_DATABASE=dootask/" .env && + sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=test/" .env && + sed -i "s/^REDIS_HOST=.*/REDIS_HOST=127.0.0.1/" .env && + php artisan key:generate && + php artisan migrate --seed --force && + php vendor/bin/phpunit + ' diff --git a/CLAUDE.md b/CLAUDE.md index c7c1fbe87..419c22e0e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ ## 项目概述 -Laravel 8 (LaravelS/Swoole) + Vue 2 (Vite) + Electron。开源任务/项目管理系统。 +Laravel 13 (LaravelS/Swoole, PHP 8.4) + Vue 2 (Vite) + Electron。开源任务/项目管理系统。 ## 开发命令 @@ -17,11 +17,31 @@ Laravel 8 (LaravelS/Swoole) + Vue 2 (Vite) + Electron。开源任务/项目管 前端代码改动只做 Edit/Write,不要为了"验证"启动 dev server。用户明确说"跑一下 / 出包"时除外。 +### 质量门禁(改完代码必须自查,CI 同步在跑,见 .github/workflows/tests.yml) + +- `./cmd composer stan` — phpstan(level 1 + baseline,存量已封存,新增错误必须清零) +- `npm run lint` — ESLint(error 必须为 0;warn 是存量遗留,见 eslint.config.mjs 注释) +- `npm run check:lang` — 校验前端 `$L()` 字面量是否已登记到 `language/original-web.txt` +- 改动控制器 public 方法或路由后跑 `./cmd artisan doc:api-map` 重新生成对照表 + +## 代码检索地图(先查表,再 grep) + +- API URL ↔ 控制器方法对照:`routes/api-map.md`(生成式文件,勿手改) +- 前端事件总线(mitt)收发对照:`docs/events-map.md`(`npm run events:map` 重新生成) +- `$A` / `$L` 全局工具类型声明:`types/dootask-globals.d.ts`(新增 `$A` 方法须同步此文件) + +## 架构增量规则(只约束新增代码,存量"动到哪迁到哪") + +- **巨型文件冻结**:不再往 `ProjectController`、`UsersController`、`DialogController`、`app/Module/Base.php`、`resources/assets/js/store/actions.js` 新增方法/函数;新功能领域开新控制器或新模块文件(动态路由天然支持多控制器) +- **业务编排归层**:跨模型的业务流程写在 `app/Module/`(或 `app/Services/`),模型只保留数据访问与自身状态变更;Swoole Task 只做投递与调用,不直接编排业务 +- **配置读取**:业务代码禁止直接 `env()`,统一走 `config()`(项目自有配置集中在 `config/dootask.php`) + ## Gotchas ### LaravelS/Swoole - **避免在静态属性、单例、全局变量中存储请求级状态**——请求间共享进程,会导致数据串联和内存泄漏 + - 要存请求级状态,用 `RequestContext::save('key', $value)` / `RequestContext::get('key')`(参考 `User::authInfo()` 的用法,见 `app/Services/RequestContext.php`) - 构造函数、服务提供者、`boot()` 方法不会在每个请求重新执行 - 配置/路由变更需要 `./cmd php restart` 或容器重启才能生效 - 长生命周期逻辑(WebSocket、定时器)应复用现有模式,避免阻塞协程/事件循环 diff --git a/app/Console/Commands/DocApiMap.php b/app/Console/Commands/DocApiMap.php new file mode 100644 index 000000000..4a6aac647 --- /dev/null +++ b/app/Console/Commands/DocApiMap.php @@ -0,0 +1,143 @@ +collectControllers(); + if (empty($controllers)) { + $this->error('未从路由中解析到任何 api 控制器'); + return 1; + } + + $total = 0; + $sections = []; + foreach ($controllers as $prefix => $class) { + $rows = $this->collectMethods($prefix, $class); + $total += count($rows); + $sections[] = $this->renderSection($prefix, $class, $rows); + } + + $path = base_path('routes/api-map.md'); + file_put_contents($path, $this->renderHeader($total) . implode("\n", $sections)); + + $this->info("已生成: routes/api-map.md(控制器 " . count($controllers) . " 个,接口 {$total} 个)"); + return 0; + } + + /** + * 从已注册路由中收集 api 前缀与控制器的映射 + * 匹配 routes/web.php 中的动态路由:api/{prefix}/{method} + * @return array [prefix => 控制器类名] + */ + private function collectControllers(): array + { + $controllers = []; + foreach (Route::getRoutes() as $route) { + if (!preg_match('/^api\/(\w+)\/\{method}$/', $route->uri())) { + continue; + } + preg_match('/^api\/(\w+)\/\{method}$/', $route->uri(), $match); + $class = $route->getAction('controller'); + if ($class && class_exists($class)) { + $controllers[$match[1]] = $class; + } + } + return $controllers; + } + + /** + * 反射收集控制器的接口方法 + * @param string $prefix 路由前缀(如 project) + * @param string $class 控制器类名 + * @return array [['url' => ..., 'method' => ..., 'http' => ..., 'title' => ...], ...] + */ + private function collectMethods(string $prefix, string $class): array + { + $rows = []; + $reflection = new ReflectionClass($class); + foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + // 仅保留本类声明的实例方法,排除 __invoke/__before/__construct 等魔术/框架方法 + if ($method->getDeclaringClass()->getName() !== $class + || $method->isStatic() + || str_starts_with($method->getName(), '__')) { + continue; + } + [$http, $title] = $this->parseApiDoc($method); + $rows[] = [ + 'url' => "api/{$prefix}/" . str_replace('__', '/', $method->getName()), + 'method' => $method->getName() . '()', + 'http' => $http, + 'title' => $title, + ]; + } + return $rows; + } + + /** + * 解析方法 docblock 中的 @api 注释行 + * 格式如:@api {get} api/project/lists 获取项目列表 + * @return array [HTTP 方法, 标题],无 @api 注释时为 ['any', ''] + */ + private function parseApiDoc(ReflectionMethod $method): array + { + $doc = $method->getDocComment(); + if ($doc && preg_match('/@api\s+\{(\w+)}\s+(\S+)(?:[ \t]+(.+))?/', $doc, $match)) { + return [strtolower($match[1]), trim($match[3] ?? '')]; + } + return ['any', '']; + } + + /** + * 生成文件头说明 + */ + private function renderHeader(int $total): string + { + return << 此文件由 `php artisan doc:api-map` 生成,勿手改。 + + 接口总数:{$total} + + ## 路由规则 + + API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方法名: + + - `api/{controller}/{method}` → `{method}()`,如 `api/project/lists` → `ProjectController::lists()` + - `api/{controller}/{method}/{action}` → `{method}__{action}()`(双下划线连接),如 `api/project/invite/join` → `ProjectController::invite__join()` + - 路由最多两段,方法名最多一个双下划线 + + + MD; + } + + /** + * 生成单个控制器的表格段落 + */ + private function renderSection(string $prefix, string $class, array $rows): string + { + $short = class_basename($class); + $lines = [ + "## {$prefix}({$short})", + '', + '| URL | 方法名 | HTTP | 说明 |', + '| --- | --- | --- | --- |', + ]; + foreach ($rows as $row) { + $lines[] = "| {$row['url']} | {$row['method']} | {$row['http']} | {$row['title']} |"; + } + $lines[] = ''; + return implode("\n", $lines); + } +} diff --git a/composer.json b/composer.json index 5a6c8b5ed..117e18498 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "fakerphp/faker": "^1.24", "hhxsv5/laravel-s": "~3.8.0", "kitloong/laravel-migrations-generator": "^7.4", + "larastan/larastan": "^3.10", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", "phpunit/phpunit": "^11.5" @@ -75,7 +76,9 @@ ], "post-create-project-cmd": [ "@php artisan key:generate --ansi" - ] + ], + "stan": "phpstan analyse --no-progress --memory-limit=-1", + "stan-baseline": "phpstan analyse --generate-baseline --memory-limit=-1" }, "extra": { "laravel": { diff --git a/composer.lock b/composer.lock index a10691187..640faa57b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "27bf8f16ef5c8e3d75a68a7b666531b6", + "content-hash": "ed96e1ded2a0dd87cdc42fc951ea848c", "packages": [ { "name": "brick/math", @@ -8732,6 +8732,47 @@ ], "time": "2026-01-17T09:14:03+00:00" }, + { + "name": "iamcal/sql-parser", + "version": "v0.7", + "source": { + "type": "git", + "url": "https://github.com/iamcal/SQLParser.git", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8", + "shasum": "" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^1.0", + "phpunit/phpunit": "^5|^6|^7|^8|^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "iamcal\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cal Henderson", + "email": "cal@iamcal.com" + } + ], + "description": "MySQL schema parser", + "support": { + "issues": "https://github.com/iamcal/SQLParser/issues", + "source": "https://github.com/iamcal/SQLParser/tree/v0.7" + }, + "time": "2026-01-28T22:20:33+00:00" + }, { "name": "kitloong/laravel-migrations-generator", "version": "v7.4.0", @@ -8810,6 +8851,96 @@ ], "time": "2026-05-10T14:54:43+00:00" }, + { + "name": "larastan/larastan", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/larastan/larastan.git", + "reference": "2970f83398154178a739609c244577267c7ee8eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/larastan/larastan/zipball/2970f83398154178a739609c244577267c7ee8eb", + "reference": "2970f83398154178a739609c244577267c7ee8eb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "iamcal/sql-parser": "^0.7.0", + "illuminate/console": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/container": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/database": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/http": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/support": "^11.44.2 || ^12.4.1 || ^13", + "php": "^8.2", + "phpstan/phpstan": "^2.2.0" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "laravel/framework": "^11.44.2 || ^12.7.2 || ^13", + "mockery/mockery": "^1.6.12", + "nikic/php-parser": "^5.4", + "orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11", + "orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8 || ^13.1.8" + }, + "suggest": { + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench", + "phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Larastan\\Larastan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Can Vural", + "email": "can9119@gmail.com" + } + ], + "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "larastan", + "laravel", + "package", + "php", + "static analysis" + ], + "support": { + "issues": "https://github.com/larastan/larastan/issues", + "source": "https://github.com/larastan/larastan/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://github.com/canvural", + "type": "github" + } + ], + "time": "2026-05-28T08:00:58+00:00" + }, { "name": "mockery/mockery", "version": "1.6.12", @@ -9167,6 +9298,70 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpstan/phpstan", + "version": "2.2.2", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-06-05T09:00:01+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "11.0.12", @@ -10747,7 +10942,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -10764,6 +10959,6 @@ "ext-simplexml": "*", "ext-zip": "*" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/docs/events-map.md b/docs/events-map.md new file mode 100644 index 000000000..bc7864c1b --- /dev/null +++ b/docs/events-map.md @@ -0,0 +1,323 @@ +# 前端事件总线注册表 + +> **本文件由脚本自动生成,请勿手改。** +> +> - 生成命令: `node scripts/gen-events-map.mjs` +> - 扫描范围: `resources/assets/js` 下所有 `.js` / `.vue` 文件(共 268 个) +> - 事件总线: `resources/assets/js/store/events.js`(mitt 实例) +> - 仅匹配裸 `emitter.emit/on/off(` 调用;`xxx.emitter.emit(`(如 Quill 内部 emitter)不属于本总线,已排除 + +共 **29** 个静态可解析事件,**125** 处 `emitter.emit/on/off` 调用。 + +## 事件清单 + +### `addMeeting` + +- **emit(10)** + - `resources/assets/js/App.vue:420` + - `resources/assets/js/pages/manage.vue:1236` + - `resources/assets/js/pages/manage.vue:1243` + - `resources/assets/js/pages/manage/application.vue:1188` + - `resources/assets/js/pages/manage/application.vue:1194` + - `resources/assets/js/pages/manage/components/ChatInput/index.vue:1882` + - `resources/assets/js/pages/manage/components/DialogView/index.vue:621` + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:2017` + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:2025` + - `resources/assets/js/pages/manage/messenger.vue:1219` +- **on(1)** + - `resources/assets/js/pages/manage/components/MeetingManager/index.vue:187` +- **off(1)** + - `resources/assets/js/pages/manage/components/MeetingManager/index.vue:191` + +### `addTask` + +- **emit(3)** + - `resources/assets/js/pages/manage/calendar.vue:255` + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:3513` + - `resources/assets/js/pages/manage/components/ProjectPanel.vue:1357` +- **on(1)** + - `resources/assets/js/pages/manage.vue:621` +- **off(1)** + - `resources/assets/js/pages/manage.vue:641` + +### `aiAssistantClosed` + +- **emit(1)** + - `resources/assets/js/components/AIAssistant/index.vue:420` +- **on(1)** + - `resources/assets/js/components/AIAssistant/float-button.vue:154` +- **off(1)** + - `resources/assets/js/components/AIAssistant/float-button.vue:162` + +### `aiOperationRequest` + +- **emit(1)** + - `resources/assets/js/store/actions.js:4781` +- **on(1)** + - `resources/assets/js/components/AIAssistant/float-button.vue:155` +- **off(1)** + - `resources/assets/js/components/AIAssistant/float-button.vue:163` + +### `approveDetails` + +- **emit(2)** + - `resources/assets/js/pages/manage/approve/index.vue:497` + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:3826` +- **on(1)** + - `resources/assets/js/pages/manage.vue:624` +- **off(1)** + - `resources/assets/js/pages/manage.vue:644` + +### `clickAgainDialog` + +- **emit(1)** + - `resources/assets/js/components/Mobile/Tabbar.vue:182` +- **on(1)** + - `resources/assets/js/pages/manage/messenger.vue:344` +- **off(1)** + - `resources/assets/js/pages/manage/messenger.vue:348` + +### `createGroup` + +- **emit(3)** + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:2871` + - `resources/assets/js/pages/manage/components/UserDetail.vue:288` + - `resources/assets/js/pages/manage/messenger.vue:1224` +- **on(1)** + - `resources/assets/js/pages/manage.vue:622` +- **off(1)** + - `resources/assets/js/pages/manage.vue:642` + +### `dialogMsgPush` + +- **emit(1)** + - `resources/assets/js/store/actions.js:4852` +- **on(2)** + - `resources/assets/js/components/Mobile/Tabbar.vue:49` + - `resources/assets/js/pages/manage.vue:623` +- **off(2)** + - `resources/assets/js/components/Mobile/Tabbar.vue:53` + - `resources/assets/js/pages/manage.vue:643` + +### `handleMoveTop` + +- **emit(2)** + - `resources/assets/js/store/actions.js:2727` + - `resources/assets/js/store/actions.js:3720` +- **on(2)** + - `resources/assets/js/pages/manage/components/DialogModal.vue:41` + - `resources/assets/js/pages/manage/components/TaskModal.vue:49` +- **off(2)** + - `resources/assets/js/pages/manage/components/DialogModal.vue:45` + - `resources/assets/js/pages/manage/components/TaskModal.vue:53` + +### `observeMicroApp:open` + +- **emit(1)** + - `resources/assets/js/store/actions.js:5361` +- **on(1)** + - `resources/assets/js/components/MicroApps/index.vue:144` +- **off(1)** + - `resources/assets/js/components/MicroApps/index.vue:149` + +### `observeMicroApp:updatedOrUninstalled` + +- **emit(1)** + - `resources/assets/js/store/mutations.js:429` +- **on(1)** + - `resources/assets/js/components/MicroApps/index.vue:145` +- **off(1)** + - `resources/assets/js/components/MicroApps/index.vue:150` + +### `openAIAssistant` + +- **emit(7)** + - `resources/assets/js/components/AIAssistant/float-button.vue:476` + - `resources/assets/js/components/SearchBox.vue:582` + - `resources/assets/js/pages/manage.vue:1267` + - `resources/assets/js/pages/manage/components/ChatInput/index.vue:1925` + - `resources/assets/js/pages/manage/components/ReportDetail.vue:176` + - `resources/assets/js/pages/manage/components/ReportEdit.vue:267` + - `resources/assets/js/pages/manage/components/TaskAdd.vue:703` +- **on(1)** + - `resources/assets/js/components/AIAssistant/index.vue:361` +- **off(1)** + - `resources/assets/js/components/AIAssistant/index.vue:368` + +### `openAIAssistantGlobal` + +- **emit(1)** + - `resources/assets/js/pages/manage.vue:1255` +- **on(1)** + - `resources/assets/js/components/AIAssistant/float-button.vue:153` +- **off(1)** + - `resources/assets/js/components/AIAssistant/float-button.vue:161` + +### `openDownloadClient` + +- **emit(1)** + - `resources/assets/js/pages/manage.vue:1128` +- **on(1)** + - `resources/assets/js/components/RightBottom.vue:73` +- **off(1)** + - `resources/assets/js/components/RightBottom.vue:78` + +### `openFavorite` + +- **emit(1)** + - `resources/assets/js/pages/manage/application.vue:1061` +- **on(1)** + - `resources/assets/js/pages/manage.vue:626` +- **off(1)** + - `resources/assets/js/pages/manage.vue:646` + +### `openManageExport` + +- **emit(1)** + - `resources/assets/js/pages/manage/application.vue:1108` +- **on(1)** + - `resources/assets/js/pages/manage.vue:628` +- **off(1)** + - `resources/assets/js/pages/manage.vue:648` + +### `openMobileNotification` + +- **emit(1)** + - `resources/assets/js/pages/manage.vue:1641` +- **on(1)** + - `resources/assets/js/components/Mobile/Notification.vue:38` +- **off(1)** + - `resources/assets/js/components/Mobile/Notification.vue:42` + +### `openProjectInvite` + +- **emit(1)** + - `resources/assets/js/App.vue:432` +- **on(1)** + - `resources/assets/js/pages/manage/components/ProjectInvite.vue:83` +- **off(1)** + - `resources/assets/js/pages/manage/components/ProjectInvite.vue:87` + +### `openRecent` + +- **emit(1)** + - `resources/assets/js/pages/manage/application.vue:1064` +- **on(1)** + - `resources/assets/js/pages/manage.vue:627` +- **off(1)** + - `resources/assets/js/pages/manage.vue:647` + +### `openReport` + +- **emit(1)** + - `resources/assets/js/pages/manage/application.vue:1058` +- **on(1)** + - `resources/assets/js/pages/manage.vue:625` +- **off(1)** + - `resources/assets/js/pages/manage.vue:645` + +### `openSearch` + +- **emit(1)** + - `resources/assets/js/pages/manage/dashboard.vue:256` +- **on(1)** + - `resources/assets/js/components/SearchBox.vue:128` +- **off(1)** + - `resources/assets/js/components/SearchBox.vue:132` + +### `openUser` + +- **emit(5)** + - `resources/assets/js/components/UserAvatar/index.vue:184` + - `resources/assets/js/pages/manage/approve/details.vue:546` + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:2940` + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:4494` + - `resources/assets/js/pages/manage/messenger.vue:1229` +- **on(1)** + - `resources/assets/js/pages/manage/components/UserDetail.vue:166` +- **off(1)** + - `resources/assets/js/pages/manage/components/UserDetail.vue:170` + +### `receiveTask` + +- **emit(2)** + - `resources/assets/js/pages/manage/components/ProjectPanel.vue:1768` + - `resources/assets/js/pages/manage/components/TaskRow.vue:280` +- **on(1)** + - `resources/assets/js/pages/manage/components/TaskDetail.vue:738` +- **off(1)** + - `resources/assets/js/pages/manage/components/TaskDetail.vue:745` + +### `streamMsgData` + +- **emit(1)** + - `resources/assets/js/store/actions.js:4469` +- **on(1)** + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:946` +- **off(1)** + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:956` + +### `taskRelationUpdate` + +- **emit(1)** + - `resources/assets/js/store/actions.js:4992` +- **on(1)** + - `resources/assets/js/pages/manage/components/TaskDetail.vue:739` +- **off(1)** + - `resources/assets/js/pages/manage/components/TaskDetail.vue:746` + +### `updateNotification` + +- **emit(2)** + - `resources/assets/js/pages/manage.vue:1125` + - `resources/assets/js/pages/manage/setting/index.vue:191` +- **on(1)** + - `resources/assets/js/components/RightBottom.vue:65` +- **off(1)** + - `resources/assets/js/components/RightBottom.vue:77` + +### `useSSOLogin` + +- **emit(1)** + - `resources/assets/js/components/RightBottom.vue:231` +- **on(1)** + - `resources/assets/js/pages/login.vue:217` +- **off(1)** + - `resources/assets/js/pages/login.vue:222` + +### `userActive` + +- **emit(3)** + - `resources/assets/js/store/actions.js:870` + - `resources/assets/js/store/actions.js:952` + - `resources/assets/js/store/actions.js:4769` +- **on(1)** + - `resources/assets/js/components/UserAvatar/index.vue:43` +- **off(1)** + - `resources/assets/js/components/UserAvatar/index.vue:47` + +### `websocketMsg` + +- **emit(1)** + - `resources/assets/js/store/actions.js:4786` +- **on(3)** + - `resources/assets/js/pages/manage/approve/index.vue:380` + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:945` + - `resources/assets/js/pages/manage/components/FileContent.vue:202` +- **off(3)** + - `resources/assets/js/pages/manage/approve/index.vue:383` + - `resources/assets/js/pages/manage/components/DialogWrapper.vue:957` + - `resources/assets/js/pages/manage/components/FileContent.vue:225` + +## 动态事件名(无法静态解析) + +以下调用的第一参数不是字符串字面量,无法静态解析事件名: + +- `resources/assets/js/components/MicroApps/index.vue:365` — `emitter.emit(actionName...)` + +## 统计 + +- 事件总数(静态可解析): **29** +- 只 emit 无 on(疑似死事件): **0** +- 只 on 无 emit(无人发射): **0** +- 动态事件名调用: **1** diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..224f5211b --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,67 @@ +import pluginVue from 'eslint-plugin-vue'; +import globals from 'globals'; + +// 起步策略:只开能抓真实 bug 的规则,存量代码必须 0 error; +// 风格类/噪声规则后续按需逐步收紧。 +export default [ + { + ignores: [ + 'node_modules/**', + 'vendor/**', + 'public/**', + 'electron/**', + 'docker/**', + 'resources/assets/statics/**', + // 第三方移植的 directive,内含过期的 babel/* eslint 注释 + 'resources/assets/js/directives/v-click-outside-x.js', + ], + }, + ...pluginVue.configs['flat/vue2-essential'], + { + files: ['resources/assets/js/**/*.{js,mjs,vue}', 'scripts/**/*.mjs'], + linterOptions: { + reportUnusedDisableDirectives: 'off', + }, + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + $A: 'readonly', + $L: 'readonly', + $: 'readonly', + jQuery: 'readonly', + LANGUAGE_DATA: 'readonly', + SystemConfig: 'readonly', + }, + }, + rules: { + 'no-dupe-keys': 'error', + 'no-dupe-args': 'error', + 'no-const-assign': 'error', + 'no-class-assign': 'error', + 'no-compare-neg-zero': 'error', + 'no-self-assign': 'error', + 'use-isnan': 'error', + 'valid-typeof': 'error', + // 以下规则存量代码有违规,先降为 warn 保持可见;清零后逐条升回 error + 'no-unreachable': 'warn', + 'no-cond-assign': 'warn', + 'vue/multi-word-component-names': 'off', + 'vue/require-v-for-key': 'warn', + 'vue/no-use-v-if-with-v-for': 'warn', + 'vue/valid-template-root': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/no-mutating-props': 'warn', + 'vue/no-unused-vars': 'warn', + 'vue/no-textarea-mustache': 'warn', + 'vue/no-reserved-keys': 'warn', + 'vue/no-side-effects-in-computed-properties': 'warn', + 'vue/no-v-text-v-html-on-component': 'warn', + 'vue/require-valid-default-prop': 'warn', + 'vue/valid-v-show': 'warn', + }, + }, +]; diff --git a/jsconfig.json b/jsconfig.json index 688262282..f265a7f27 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -21,7 +21,7 @@ "resources/assets/js/**/*", "resources/assets/**/*.vue", "resources/assets/sass/**/*", - "types/**/*.d.ts" + "types/**/*" ], "exclude": [ "node_modules", diff --git a/package.json b/package.json index df57d589c..eff33ac89 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "description": "DooTask is task management system.", "scripts": { "start": "./cmd dev", - "build": "./cmd prod" + "build": "./cmd prod", + "lint": "eslint resources/assets/js", + "check:lang": "node scripts/check-language.mjs", + "events:map": "node scripts/gen-events-map.mjs" }, "author": { "name": "KuaiFan", @@ -32,7 +35,10 @@ "dexie": "^3.2.3", "echarts": "^5.2.2", "element-sea": "^2.15.10-9", + "eslint": "^9.39.4", + "eslint-plugin-vue": "^10.9.2", "file-loader": "^6.2.0", + "globals": "^17.6.0", "highlight.js": "^11.7.0", "html-to-md": "^0.8.8", "inquirer": "^8.2.0", @@ -91,6 +97,5 @@ "url": "https://www.dootask.com/api/download/update" } } - ], - "dependencies": {} + ] } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 000000000..38ca2d34e --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,349 @@ +parameters: + ignoreErrors: + - + message: '#^Variable \$rules in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Exceptions/ImagePathHandler.php + + - + message: '#^Anonymous function has an unused use \$user\.$#' + identifier: closure.unusedUse + count: 1 + path: app/Http/Controllers/Api/ApproveController.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 1 + path: app/Http/Controllers/Api/ApproveController.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 2 + path: app/Http/Controllers/Api/DialogController.php + + - + message: '#^Variable \$dialog in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Http/Controllers/Api/DialogController.php + + - + message: '#^Variable \$user might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: app/Http/Controllers/Api/DialogController.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 1 + path: app/Http/Controllers/Api/FileController.php + + - + message: '#^Anonymous function has an unused use \$user\.$#' + identifier: closure.unusedUse + count: 2 + path: app/Http/Controllers/Api/ProjectController.php + + - + message: '#^Variable \$column in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Http/Controllers/Api/ProjectController.php + + - + message: '#^Relation ''sendUser'' is not found in App\\Models\\Report model\.$#' + identifier: larastan.relationExistence + count: 3 + path: app/Http/Controllers/Api/ReportController.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 14 + path: app/Http/Controllers/Api/SystemController.php + + - + message: '#^Anonymous function has an unused use \$keys\.$#' + identifier: closure.unusedUse + count: 1 + path: app/Http/Controllers/Api/UsersController.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 1 + path: app/Http/Controllers/Api/UsersController.php + + - + message: '#^Variable \$basic in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Http/Controllers/Api/UsersController.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 1 + path: app/Http/Controllers/IndexController.php + + - + message: '#^Access to an undefined property App\\Ldap\\LdapUser\:\:\$displayName\.$#' + identifier: property.notFound + count: 1 + path: app/Ldap/LdapUser.php + + - + message: '#^Access to an undefined property App\\Ldap\\LdapUser\:\:\$jpegPhoto\.$#' + identifier: property.notFound + count: 1 + path: app/Ldap/LdapUser.php + + - + message: '#^Access to an undefined property App\\Ldap\\LdapUser\:\:\$uid\.$#' + identifier: property.notFound + count: 1 + path: app/Ldap/LdapUser.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 1 + path: app/Ldap/LdapUser.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 3 + path: app/Models/AbstractModel.php + + - + message: '#^Variable \$arr in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Models/File.php + + - + message: '#^Variable \$dirRow in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Models/File.php + + - + message: '#^Variable \$filePath might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: app/Models/File.php + + - + message: '#^Variable \$filePath in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: app/Models/FileContent.php + + - + message: '#^Deprecated in PHP 8\.4\: Parameter \#3 \$task \(App\\Models\\ProjectTask\) is implicitly nullable via default value null\.$#' + identifier: parameter.implicitlyNullable + count: 1 + path: app/Models/ProjectPermission.php + + - + message: '#^Access to an undefined property App\\Models\\ProjectTask\:\:\$owner\.$#' + identifier: property.notFound + count: 1 + path: app/Models/ProjectTask.php + + - + message: '#^Class App\\Models\\ProjectFlowItem referenced with incorrect case\: App\\Models\\projectFlowItem\.$#' + identifier: class.nameCase + count: 1 + path: app/Models/ProjectTask.php + + - + message: '#^Variable \$receivers in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Models/ProjectTask.php + + - + message: '#^Deprecated in PHP 8\.4\: Parameter \#3 \$time \(Carbon\\Carbon\) is implicitly nullable via default value null\.$#' + identifier: parameter.implicitlyNullable + count: 1 + path: app/Models/Report.php + + - + message: '#^Property ''receives'' does not exist in model\.$#' + identifier: rules.modelAppends + count: 1 + path: app/Models/Report.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 1 + path: app/Models/Setting.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 2 + path: app/Models/User.php + + - + message: '#^Anonymous function has an unused use \$originalUserid\.$#' + identifier: closure.unusedUse + count: 1 + path: app/Models/UserDepartment.php + + - + message: '#^Anonymous function has an unused use \$token\.$#' + identifier: closure.unusedUse + count: 1 + path: app/Models/UserDevice.php + + - + message: '#^Deprecated in PHP 8\.4\: Parameter \#1 \$token \(App\\Models\\UserDevice\|string\|int\) is implicitly nullable via default value null\.$#' + identifier: parameter.implicitlyNullable + count: 1 + path: app/Models/UserDevice.php + + - + message: '#^Deprecated in PHP 8\.4\: Parameter \#1 \$token \(string\) is implicitly nullable via default value null\.$#' + identifier: parameter.implicitlyNullable + count: 1 + path: app/Models/UserDevice.php + + - + message: '#^Variable \$emailVerify in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Models/UserEmailVerification.php + + - + message: '#^Relation ''task'' is not found in App\\Models\\UserTaskBrowse model\.$#' + identifier: larastan.relationExistence + count: 1 + path: app/Models/UserTaskBrowse.php + + - + message: '#^Anonymous function has an unused use \$reserves\.$#' + identifier: closure.unusedUse + count: 2 + path: app/Models/WebSocketDialogMsg.php + + - + message: '#^Variable \$embedding in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Module/AI.php + + - + message: '#^Variable \$headers on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.variable + count: 1 + path: app/Module/AI.php + + - + message: '#^Variable \$post on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.variable + count: 1 + path: app/Module/AI.php + + - + message: '#^Call to static method parseFile\(\) on an unknown class Symfony\\Component\\Yaml\\Yaml\.$#' + identifier: class.notFound + count: 1 + path: app/Module/Apps.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 1 + path: app/Module/Apps.php + + - + message: '#^Variable \$deptIds in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Module/Apps.php + + - + message: '#^Variable \$file on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.variable + count: 1 + path: app/Module/Base.php + + - + message: '#^Variable \$url in empty\(\) always exists and is always falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Module/Base.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 1 + path: app/Module/Doo.php + + - + message: '#^Static method Redis\:\:eval\(\) invoked with 4 parameters, 1\-3 required\.$#' + identifier: arguments.count + count: 1 + path: app/Module/Lock.php + + - + message: '#^Static method Redis\:\:set\(\) invoked with 5 parameters, 2\-3 required\.$#' + identifier: arguments.count + count: 2 + path: app/Module/Lock.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 2 + path: app/Module/Manticore/ManticoreBase.php + + - + message: '#^Variable \$idsToUpdate in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Module/Manticore/ManticoreBase.php + + - + message: '#^Variable \$h might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: app/Module/RandomColor.php + + - + message: '#^Variable \$s might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: app/Module/RandomColor.php + + - + message: '#^Variable \$v might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: app/Module/RandomColor.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 1 + path: app/Module/Table/AbstractData.php + + - + message: '#^Variable \$strArr in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: app/Module/Timer.php + + - + message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' + identifier: larastan.noEnvCallsOutsideOfConfig + count: 2 + path: app/Tasks/DeleteTmpTask.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..3fdc44c50 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,12 @@ +includes: + - vendor/larastan/larastan/extension.neon + - phpstan-baseline.neon + +parameters: + level: 1 + paths: + - app + excludePaths: + - app/Http/Controllers/Api/apidoc.php + # 允许单文件分析(.claude hooks 用),baseline 中未命中的忽略项不报错 + reportUnmatchedIgnoredErrors: false diff --git a/routes/api-map.md b/routes/api-map.md new file mode 100644 index 000000000..852d8b6df --- /dev/null +++ b/routes/api-map.md @@ -0,0 +1,398 @@ +# API 路由对照表 + +> 此文件由 `php artisan doc:api-map` 生成,勿手改。 + +接口总数:325 + +## 路由规则 + +API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方法名: + +- `api/{controller}/{method}` → `{method}()`,如 `api/project/lists` → `ProjectController::lists()` +- `api/{controller}/{method}/{action}` → `{method}__{action}()`(双下划线连接),如 `api/project/invite/join` → `ProjectController::invite__join()` +- 路由最多两段,方法名最多一个双下划线 + +## users(UsersController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/users/login | login() | get | 登录、注册 | +| api/users/login/qrcode | login__qrcode() | get | 二维码登录 | +| api/users/login/needcode | login__needcode() | get | 是否需要验证码 | +| api/users/login/codeimg | login__codeimg() | get | 验证码图片 | +| api/users/login/codejson | login__codejson() | get | 验证码json | +| api/users/logout | logout() | get | 退出登录 | +| api/users/token/expire | token__expire() | get | 查询 token 过期时间 | +| api/users/reg/needinvite | reg__needinvite() | get | 是否需要邀请码 | +| api/users/info | info() | get | 获取我的信息 | +| api/users/info/managed_departments | info__managed_departments() | get | 获取我可切换负责人视角的部门列表 | +| api/users/info/departments | info__departments() | get | 获取我的部门列表 | +| api/users/editdata | editdata() | get | 修改自己的资料 | +| api/users/editpass | editpass() | get | 修改自己的密码 | +| api/users/search | search() | get | 搜索会员列表 | +| api/users/search/ai | search__ai() | get | 获取AI机器人 | +| api/users/basic | basic() | get | 获取指定会员基础信息 | +| api/users/extra | extra() | get | 获取会员扩展信息 | +| api/users/lists | lists() | get | 会员列表(限管理员) | +| api/users/operation | operation() | get | 操作会员(限管理员) | +| api/users/createuser | createuser() | post | 创建用户(管理员) | +| api/users/import/preview | import__preview() | post | 批量导入预览(管理员) | +| api/users/import | import() | post | 批量导入用户(管理员) | +| api/users/import/template | import__template() | get | 下载批量导入模板(管理员) | +| api/users/email/verification | email__verification() | get | 邮箱验证 | +| api/users/umeng/alias | umeng__alias() | get | 设置友盟别名 | +| api/users/meeting/open | meeting__open() | get | 【会议】创建会议、加入会议 | +| api/users/tags/lists | tags__lists() | get | 获取个性标签列表 | +| api/users/tags/add | tags__add() | post | 新增个性标签 | +| api/users/tags/update | tags__update() | post | 修改个性标签 | +| api/users/tags/delete | tags__delete() | post | 删除个性标签 | +| api/users/tags/recognize | tags__recognize() | post | 认可个性标签 | +| api/users/meeting/link | meeting__link() | get | 【会议】获取分享链接 | +| api/users/meeting/tourist | meeting__tourist() | get | 【会议】游客信息 | +| api/users/meeting/invitation | meeting__invitation() | get | 【会议】发送邀请 | +| api/users/email/send | email__send() | get | 发送邮箱验证码 | +| api/users/email/edit | email__edit() | get | 修改邮箱 | +| api/users/delete/account | delete__account() | get | 删除帐号 | +| api/users/department/list | department__list() | get | 部门列表(限管理员) | +| api/users/department/add | department__add() | get | 新建、修改部门(限管理员) | +| api/users/department/adddeputy | department__adddeputy() | post | 任命部门管理员(限管理员) | +| api/users/department/deldeputy | department__deldeputy() | post | 罢免部门管理员(限管理员) | +| api/users/department/del | department__del() | get | 删除部门(限管理员) | +| api/users/department/sync | department__sync() | get | 同步部门成员(限管理员) | +| api/users/checkin/get | checkin__get() | get | 获取签到设置 | +| api/users/checkin/save | checkin__save() | post | 保存签到设置 | +| api/users/checkin/list | checkin__list() | get | 获取签到数据 | +| api/users/socket/status | socket__status() | get | 获取socket状态 | +| api/users/key/client | key__client() | get | 客户端KEY | +| api/users/bot/list | bot__list() | get | 机器人列表 | +| api/users/bot/info | bot__info() | get | 机器人信息 | +| api/users/bot/edit | bot__edit() | post | 添加、编辑机器人 | +| api/users/bot/delete | bot__delete() | get | 删除机器人 | +| api/users/share/list | share__list() | get | 获取分享列表 | +| api/users/annual/report | annual__report() | get | 年度报告 | +| api/users/device/list | device__list() | get | 获取设备列表 | +| api/users/device/logout | device__logout() | get | 登出设备(删除设备) | +| api/users/device/edit | device__edit() | get | 编辑设备 | +| api/users/task/browse | task__browse() | get | 获取任务浏览历史 | +| api/users/task/browse_save | task__browse_save() | get | 记录任务浏览历史 | +| api/users/task/browse_clean | task__browse_clean() | post | 清理任务浏览历史 | +| api/users/recent/browse | recent__browse() | get | 获取最近访问记录 | +| api/users/recent/delete | recent__delete() | post | 删除最近访问记录 | +| api/users/appsort | appsort() | get | 获取个人应用排序 | +| api/users/appsort/save | appsort__save() | post | 保存个人应用排序 | +| api/users/favorites | favorites() | get | 获取用户收藏列表 | +| api/users/favorite/toggle | favorite__toggle() | post | 切换收藏状态 | +| api/users/favorite/remark | favorite__remark() | post | 修改收藏备注 | +| api/users/favorites/clean | favorites__clean() | post | 清理用户收藏 | +| api/users/favorite/check | favorite__check() | get | 检查收藏状态 | + +## project(ProjectController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/project/lists | lists() | get | 获取项目列表 | +| api/project/one | one() | get | 获取一个项目信息 | +| api/project/add | add() | get | 添加项目 | +| api/project/update | update() | get | 修改项目 | +| api/project/user | user() | post | 修改项目成员 | +| api/project/invite | invite() | get | 获取邀请链接 | +| api/project/invite/info | invite__info() | get | 通过邀请链接code获取项目信息 | +| api/project/invite/join | invite__join() | get | 通过邀请链接code加入项目 | +| api/project/transfer | transfer() | get | 移交项目 | +| api/project/adddeputy | adddeputy() | post | 任命项目管理员(仅负责人可操作) | +| api/project/deldeputy | deldeputy() | post | 罢免项目管理员(仅负责人可操作) | +| api/project/sort | sort() | post | 排序任务 | +| api/project/user/sort | user__sort() | post | 项目列表排序 | +| api/project/exit | exit() | get | 退出项目 | +| api/project/archived | archived() | get | 归档项目 | +| api/project/remove | remove() | get | 删除项目 | +| api/project/column/lists | column__lists() | get | 获取任务列表 | +| api/project/column/add | column__add() | get | 添加任务列表 | +| api/project/column/update | column__update() | get | 修改任务列表 | +| api/project/column/remove | column__remove() | get | 删除任务列表 | +| api/project/column/one | column__one() | get | 获取任务列详细 | +| api/project/task/lists | task__lists() | get | 任务列表 | +| api/project/user/projects | user__projects() | get | 会员参与的项目列表 | +| api/project/user/tasks | user__tasks() | get | 会员参与的任务列表 | +| api/project/user/counts | user__counts() | get | 会员参与的项目/任务数量 | +| api/project/task/easylists | task__easylists() | get | 任务列表-简单的 | +| api/project/task/export | task__export() | get | 导出任务(限管理员) | +| api/project/task/exportoverdue | task__exportoverdue() | get | 导出超期任务(限管理员) | +| api/project/task/down | task__down() | get | 下载导出的任务 | +| api/project/task/one | task__one() | get | 获取单个任务信息 | +| api/project/task/subdata | task__subdata() | get | 获取子任务数据 | +| api/project/task/related | task__related() | get | 获取任务关联任务列表 | +| api/project/task/related/delete | task__related__delete() | post | 删除任务关联 | +| api/project/task/content | task__content() | get | 获取任务详细描述 | +| api/project/task/content_history | task__content_history() | get | 获取任务详细历史描述 | +| api/project/task/files | task__files() | get | 获取任务文件列表 | +| api/project/task/filedelete | task__filedelete() | get | 删除任务文件 | +| api/project/task/filedetail | task__filedetail() | get | 获取任务文件详情 | +| api/project/task/filedown | task__filedown() | get | 下载任务文件 | +| api/project/task/add | task__add() | post | 添加任务 | +| api/project/task/addsub | task__addsub() | get | 添加子任务 | +| api/project/task/upgrade | task__upgrade() | get | 子任务升级为主任务 | +| api/project/task/update | task__update() | post | 修改任务、子任务 | +| api/project/task/dialog | task__dialog() | get | 创建/获取聊天室 | +| api/project/task/archived | task__archived() | get | 归档任务 | +| api/project/task/remove | task__remove() | get | 删除任务 | +| api/project/task/resetfromlog | task__resetfromlog() | get | 根据日志重置任务 | +| api/project/task/flow | task__flow() | get | 任务工作流信息 | +| api/project/task/move | task__move() | get | 任务移动 | +| api/project/task/copy | task__copy() | post | 复制任务 | +| api/project/task/ai_generate | task__ai_generate() | any | | +| api/project/ai/generate | ai__generate() | any | | +| api/project/flow/list | flow__list() | get | 工作流列表 | +| api/project/flow/save | flow__save() | post | 保存工作流 | +| api/project/flow/delete | flow__delete() | get | 删除工作流 | +| api/project/log/lists | log__lists() | get | 获取项目、任务日志 | +| api/project/top | top() | get | 项目置顶 | +| api/project/permission | permission() | get | 获取项目权限设置 | +| api/project/permission/update | permission__update() | get | 项目权限设置 | +| api/project/task/template_list | task__template_list() | get | 任务模板列表 | +| api/project/task/template_visible | task__template_visible() | get | 当前用户跨项目可见的全部任务模板 | +| api/project/task/template_search | task__template_search() | get | 跨项目模板搜索分页 | +| api/project/task/template_save | task__template_save() | post | 保存任务模板 | +| api/project/task/template_sort | task__template_sort() | post | 排序任务模板 | +| api/project/task/template_delete | task__template_delete() | get | 删除任务模板 | +| api/project/task/template_default | task__template_default() | get | 设置(取消)任务模板为默认 | +| api/project/tag/save | tag__save() | post | 保存标签 | +| api/project/tag/sort | tag__sort() | post | 标签排序 | +| api/project/tag/delete | tag__delete() | get | 删除标签 | +| api/project/tag/list | tag__list() | get | 标签列表 | +| api/project/task/ai_apply | task__ai_apply() | post | 采纳AI建议 | +| api/project/task/ai_dismiss | task__ai_dismiss() | post | 忽略AI建议 | + +## system(SystemController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/system/setting | setting() | get | 获取设置、保存设置 | +| api/system/setting/email | setting__email() | get | 获取邮箱设置、保存邮箱设置(限管理员) | +| api/system/setting/meeting | setting__meeting() | get | 获取会议设置、保存会议设置(限管理员) | +| api/system/setting/ai | setting__ai() | any | | +| api/system/setting/aibot | setting__aibot() | get | 获取AI设置、保存AI机器人设置(限管理员) | +| api/system/setting/aibot_models | setting__aibot_models() | any | | +| api/system/setting/aibot_defmodels | setting__aibot_defmodels() | any | | +| api/system/setting/checkin | setting__checkin() | get | 获取签到设置、保存签到设置(限管理员) | +| api/system/setting/apppush | setting__apppush() | get | 获取APP推送设置、保存APP推送设置(限管理员) | +| api/system/setting/thirdaccess | setting__thirdaccess() | get | 第三方帐号(限管理员) | +| api/system/setting/file | setting__file() | get | 文件设置(限管理员) | +| api/system/demo | demo() | get | 获取演示帐号 | +| api/system/priority | priority() | post | 任务优先级 | +| api/system/microapp_menu | microapp_menu() | post | 自定义应用菜单 | +| api/system/column/template | column__template() | post | 创建项目模板 | +| api/system/license | license() | post | License | +| api/system/get/info | get__info() | get | 获取终端详细信息 | +| api/system/get/ip | get__ip() | get | 获取IP地址 | +| api/system/get/cnip | get__cnip() | get | 是否中国IP地址 | +| api/system/imgupload | imgupload() | post | 上传图片 | +| api/system/imgview | imgview() | get | 浏览图片空间 | +| api/system/fileupload | fileupload() | post | 上传文件 | +| api/system/get/updatelog | get__updatelog() | get | 获取更新日志 | +| api/system/email/check | email__check() | get | 邮件发送测试(限管理员) | +| api/system/checkin/export | checkin__export() | get | 导出签到数据(限管理员) | +| api/system/checkin/down | checkin__down() | get | 下载导出的签到数据 | +| api/system/version | version() | get | 获取版本号 | +| api/system/prefetch | prefetch() | get | 预加载的资源 | + +## dialog(DialogController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/dialog/lists | lists() | get | 对话列表 | +| api/dialog/beyond | beyond() | get | 列表外对话 | +| api/dialog/search | search() | get | 搜索会话 | +| api/dialog/search/tag | search__tag() | get | 搜索标注会话 | +| api/dialog/one | one() | get | 获取单个会话信息 | +| api/dialog/user | user() | get | 获取会话成员 | +| api/dialog/todo | todo() | get | 获取会话待办 | +| api/dialog/top | top() | get | 会话置顶 | +| api/dialog/hide | hide() | get | 会话隐藏 | +| api/dialog/tel | tel() | get | 获取对方联系电话 | +| api/dialog/open/user | open__user() | get | 打开会话 | +| api/dialog/open/event | open__event() | get | 打开会话事件 | +| api/dialog/msg/list | msg__list() | get | 获取消息列表 | +| api/dialog/msg/latest | msg__latest() | get | 获取最新消息列表 | +| api/dialog/msg/one | msg__one() | get | 获取单条消息 | +| api/dialog/msg/dot | msg__dot() | get | 聊天消息去除点 | +| api/dialog/msg/read | msg__read() | get | 已读聊天消息 | +| api/dialog/msg/unread | msg__unread() | get | 获取未读消息数据 | +| api/dialog/msg/checked | msg__checked() | get | 设置消息checked | +| api/dialog/msg/stream | msg__stream() | post | 通知成员监听消息 | +| api/dialog/msg/ai_generate | msg__ai_generate() | any | | +| api/dialog/msg/sendtext | msg__sendtext() | post | 发送消息 | +| api/dialog/msg/sendnotice | msg__sendnotice() | post | 发送通知 | +| api/dialog/msg/sendtemplate | msg__sendtemplate() | post | 发送模板消息 | +| api/dialog/msg/sendrecord | msg__sendrecord() | post | 发送语音 | +| api/dialog/msg/convertrecord | msg__convertrecord() | post | 录音转文字 | +| api/dialog/msg/sendfile | msg__sendfile() | post | 文件上传 | +| api/dialog/msg/sendfiles | msg__sendfiles() | post | 群发文件上传 | +| api/dialog/msg/sendfileid | msg__sendfileid() | get | 通过文件ID发送文件 | +| api/dialog/msg/sendtaskid | msg__sendtaskid() | get | 通过任务ID发送任务 | +| api/dialog/msg/sendanon | msg__sendanon() | post | 发送匿名消息 | +| api/dialog/msg/sendbot | msg__sendbot() | post | 发送机器人消息 | +| api/dialog/msg/send_ai_assistant | msg__send_ai_assistant() | post | 以AI助手身份发送消息到对话 | +| api/dialog/msg/sendlocation | msg__sendlocation() | post | 发送位置消息 | +| api/dialog/msg/readlist | msg__readlist() | get | 获取消息阅读情况 | +| api/dialog/msg/detail | msg__detail() | get | 消息详情 | +| api/dialog/msg/download | msg__download() | get | 文件下载 | +| api/dialog/msg/withdraw | msg__withdraw() | get | 聊天消息撤回 | +| api/dialog/msg/voice2text | msg__voice2text() | get | 语音消息转文字 | +| api/dialog/msg/translation | msg__translation() | get | 翻译消息 | +| api/dialog/msg/mark | msg__mark() | get | 消息标记操作 | +| api/dialog/msg/silence | msg__silence() | get | 消息免打扰 | +| api/dialog/msg/forward | msg__forward() | get | 转发消息给 | +| api/dialog/msg/mergeforward | msg__mergeforward() | get | 合并转发消息 | +| api/dialog/msg/mergedetail | msg__mergedetail() | get | 合并转发消息详情 | +| api/dialog/msg/emoji | msg__emoji() | get | emoji回复 | +| api/dialog/msg/tag | msg__tag() | get | 标注/取消标注 | +| api/dialog/msg/todo | msg__todo() | get | 设待办/取消待办 | +| api/dialog/msg/todolist | msg__todolist() | get | 获取消息待办情况 | +| api/dialog/msg/todoremind | msg__todoremind() | post | 设置/修改/取消待办提醒时间 | +| api/dialog/msg/done | msg__done() | get | 完成待办 | +| api/dialog/msg/color | msg__color() | get | 设置颜色 | +| api/dialog/msg/webhookmsg2ai | msg__webhookmsg2ai() | any | | +| api/dialog/group/add | group__add() | get | 新增群组 | +| api/dialog/group/edit | group__edit() | get | 修改群组 | +| api/dialog/group/adduser | group__adduser() | get | 添加群成员 | +| api/dialog/group/deluser | group__deluser() | get | 移出(退出)群成员 | +| api/dialog/group/transfer | group__transfer() | get | 转让群组 | +| api/dialog/group/adddeputy | group__adddeputy() | any | | +| api/dialog/group/deldeputy | group__deldeputy() | any | | +| api/dialog/group/disband | group__disband() | get | 解散群组 | +| api/dialog/group/searchuser | group__searchuser() | get | 搜索个人群(仅限管理员) | +| api/dialog/common/list | common__list() | get | 共同群组群聊 | +| api/dialog/okr/add | okr__add() | post | 创建OKR评论会话 | +| api/dialog/okr/push | okr__push() | post | 推送OKR相关信息 | +| api/dialog/msg/wordchain | msg__wordchain() | post | 发送接龙消息 | +| api/dialog/msg/vote | msg__vote() | post | 发起投票 | +| api/dialog/msg/top | msg__top() | get | 置顶/取消置顶 | +| api/dialog/msg/topinfo | msg__topinfo() | get | 获取置顶消息 | +| api/dialog/msg/applied | msg__applied() | any | | +| api/dialog/sticker/search | sticker__search() | get | 搜索在线表情 | +| api/dialog/config | config() | get | 获取会话配置 | +| api/dialog/config/save | config__save() | post | 保存会话配置 | +| api/dialog/session/create | session__create() | get | AI-开启新会话 | +| api/dialog/session/list | session__list() | get | AI-获取会话列表 | +| api/dialog/session/open | session__open() | get | AI-打开会话 | +| api/dialog/session/rename | session__rename() | post | AI-重命名会话 | + +## file(FileController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/file/lists | lists() | get | 获取文件列表 | +| api/file/one | one() | get | 获取单条数据 | +| api/file/fetch | fetch() | get | 通过路径获取文件文本内容 | +| api/file/search | search() | get | 搜索文件列表 | +| api/file/add | add() | get | 添加、修改文件(夹) | +| api/file/copy | copy() | get | 复制文件(夹) | +| api/file/move | move() | get | 移动文件(夹) | +| api/file/remove | remove() | get | 删除文件(夹) | +| api/file/content | content() | get | 获取文件内容 | +| api/file/content/save | content__save() | get | 保存文件内容 | +| api/file/office/token | office__token() | get | 获取token | +| api/file/content/office | content__office() | get | 保存文件内容(office) | +| api/file/content/upload | content__upload() | get | 保存文件内容(上传文件) | +| api/file/content/history | content__history() | get | 获取内容历史 | +| api/file/content/restore | content__restore() | get | 恢复文件历史 | +| api/file/share | share() | get | 获取共享信息 | +| api/file/share/update | share__update() | get | 设置共享 | +| api/file/share/out | share__out() | get | 退出共享 | +| api/file/link | link() | get | 获取链接 | +| api/file/download/pack | download__pack() | get | 打包文件 | + +## report(ReportController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/report/my | my() | get | 我发送的汇报 | +| api/report/receive | receive() | get | 我接收的汇报 | +| api/report/store | store() | get | 保存并发送工作汇报 | +| api/report/template | template() | get | 生成汇报模板 | +| api/report/detail | detail() | get | 报告详情 | +| api/report/analysave | analysave() | post | 保存工作汇报 AI 分析 | +| api/report/mark | mark() | get | 标记已读/未读 | +| api/report/share | share() | get | 分享报告到消息 | +| api/report/last_submitter | last_submitter() | get | 获取最后一次提交的接收人 | +| api/report/unread | unread() | get | 获取未读 | +| api/report/read | read() | get | 标记汇报已读,可批量 | + +## public(PublicController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/public/checkin/install | checkin__install() | any | | +| api/public/checkin/report | checkin__report() | any | | + +## approve(ApproveController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/approve/verifyToken | verifyToken() | get | 验证APi登录 | +| api/approve/procdef/all | procdef__all() | post | 查询流程定义 | +| api/approve/procdef/del | procdef__del() | get | 删除流程定义 | +| api/approve/process/start | process__start() | post | 启动流程(审批中) | +| api/approve/process/addGlobalComment | process__addGlobalComment() | post | 添加全局评论 | +| api/approve/task/complete | task__complete() | post | 审批 | +| api/approve/task/withdraw | task__withdraw() | post | 撤回 | +| api/approve/process/delById | process__delById() | post | 删除审批(流程实例) | +| api/approve/process/findTask | process__findTask() | post | 查询需要我审批的流程(审批中) | +| api/approve/process/startByMyselfAll | process__startByMyselfAll() | post | 查询我启动的流程(全部) | +| api/approve/process/startByMyself | process__startByMyself() | post | 查询我启动的流程(审批中) | +| api/approve/process/findProcNotify | process__findProcNotify() | post | 查询抄送我的流程(审批中) | +| api/approve/identitylink/findParticipant | identitylink__findParticipant() | get | 查询流程实例的参与者(审批中) | +| api/approve/procHistory/findTask | procHistory__findTask() | post | 查询需要我审批的流程(已结束) | +| api/approve/procHistory/startByMyself | procHistory__startByMyself() | post | 查询我启动的流程(已结束) | +| api/approve/procHistory/findProcNotify | procHistory__findProcNotify() | post | 查询抄送我的流程(已结束) | +| api/approve/identitylinkHistory/findParticipant | identitylinkHistory__findParticipant() | get | 查询流程实例的参与者(已结束) | +| api/approve/process/detail | process__detail() | get | 根据流程ID查询流程详情 | +| api/approve/export | export() | post | 导出数据 | +| api/approve/getStateDescription | getStateDescription() | any | | +| api/approve/down | down() | get | 下载导出的审批数据 | +| api/approve/handleParticipant | handleParticipant() | any | | +| api/approve/approveMsg | approveMsg() | any | | +| api/approve/getProcessById | getProcessById() | any | | +| api/approve/handleProcessNode | handleProcessNode() | any | | +| api/approve/getUserProcessParticipantById | getUserProcessParticipantById() | any | | +| api/approve/user/status | user__status() | get | 获取用户审批状态 | +| api/approve/process/doto | process__doto() | get | 查询需要我审批的流程数量 | + +## assistant(AssistantController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/assistant/auth | auth() | post | 生成授权码 | +| api/assistant/models | models() | get | 获取AI模型 | +| api/assistant/match_elements | match_elements() | post | 元素向量匹配 | +| api/assistant/log/search | log__search() | post | 记录帮助知识库检索日志 | +| api/assistant/feedback/save | feedback__save() | post | 保存回复反馈 | +| api/assistant/operation/dispatch | operation__dispatch() | post | 派发页面操作 | +| api/assistant/operation/result | operation__result() | get | 取页面操作结果 | +| api/assistant/session/list | session__list() | any | | +| api/assistant/session/save | session__save() | any | | +| api/assistant/session/delete | session__delete() | any | | + +## complaint(ComplaintController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/complaint/lists | lists() | get | 获取举报投诉列表 | +| api/complaint/submit | submit() | post | 举报投诉 | +| api/complaint/action | action() | post | 举报投诉 - 操作 | + +## search(SearchController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | +| api/search/contact | contact() | get | 搜索联系人 | +| api/search/project | project() | get | 搜索项目 | +| api/search/task | task() | get | 搜索任务 | +| api/search/file | file() | get | 搜索文件 | +| api/search/message | message() | get | 搜索消息 | + +## test(TestController) + +| URL | 方法名 | HTTP | 说明 | +| --- | --- | --- | --- | diff --git a/scripts/check-language.mjs b/scripts/check-language.mjs new file mode 100644 index 000000000..8abd4df9d --- /dev/null +++ b/scripts/check-language.mjs @@ -0,0 +1,173 @@ +#!/usr/bin/env node +/** + * 前端翻译文案校验脚本 + * + * 递归扫描 resources/assets/js 下的 .js/.vue 文件,提取 + * $L('...') / $L("...") / this.$L(...) / $A.L(...) 的第一个字符串字面量参数, + * 与 language/original-web.txt 的行集合(trim 后)比对: + * - 代码中有、txt 中没有 → 「缺失文案」,列出文案及所有出现位置,exit 1 + * - txt 中有、代码中没有 → 「疑似未使用」,仅输出数量与前 20 条样例,不影响退出码 + * + * 用法:node scripts/check-language.mjs + * 零第三方依赖,要求 Node >= 20。 + */ + +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join, relative, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const SCAN_DIR = join(ROOT, "resources", "assets", "js"); +const TXT_FILE = join(ROOT, "language", "original-web.txt"); + +// ---------- 工具函数 ---------- + +/** 递归收集 .js/.vue 文件 */ +function collectFiles(dir, out = []) { + for (const name of readdirSync(dir)) { + const full = join(dir, name); + const st = statSync(full); + if (st.isDirectory()) { + collectFiles(full, out); + } else if (st.isFile() && /\.(js|vue)$/.test(name)) { + out.push(full); + } + } + return out; +} + +/** 反转义字符串字面量中的常见转义序列 */ +function unescapeLiteral(raw) { + return raw.replace(/\\(.)/g, (_, ch) => { + switch (ch) { + case "n": return "\n"; + case "t": return "\t"; + case "r": return "\r"; + default: return ch; // \' \" \\ 以及其他都还原为字符本身 + } + }); +} + +/** + * 从 content 的 pos(指向开引号 ' 或 ")开始解析字符串字面量。 + * 返回 { raw, end }:raw 为引号内原始文本(未反转义),end 指向闭引号的下一位; + * 未闭合返回 null。 + */ +function parseStringLiteral(content, pos) { + const quote = content[pos]; + let i = pos + 1; + let raw = ""; + while (i < content.length) { + const ch = content[i]; + if (ch === "\\") { + if (i + 1 >= content.length) return null; + raw += ch + content[i + 1]; + i += 2; + continue; + } + if (ch === quote) { + return { raw, end: i + 1 }; + } + if (ch === "\n") return null; // 普通字符串字面量不允许裸换行 + raw += ch; + i++; + } + return null; +} + +/** + * 提取单个文件中所有翻译调用的第一个字符串字面量参数。 + * 返回 [{ text, line }]。 + * 第一个参数不是普通字符串字面量(模板字符串、变量、函数调用等), + * 或字面量后紧跟 + (拼接表达式)时跳过,不报错。 + */ +function extractCalls(content) { + const results = []; + // $L( / this.$L( —— 都含 "$L(",要求 $ 前不是标识符字符或 $;$A.L( 单独匹配 + const callRe = /(? l.trim()) + .filter(Boolean) +); + +// 2. 扫描源码并提取 +const files = collectFiles(SCAN_DIR); +/** Map> */ +const usages = new Map(); +for (const file of files) { + const rel = relative(ROOT, file); + const content = readFileSync(file, "utf8"); + for (const { text, line } of extractCalls(content)) { + if (!usages.has(text)) usages.set(text, []); + usages.get(text).push(`${rel}:${line}`); + } +} + +// 3. 比对 +const missing = []; // 代码有、txt 无 +for (const [text, locations] of usages) { + if (!txtLines.has(text)) { + missing.push({ text, locations }); + } +} +const unused = [...txtLines].filter(l => !usages.has(l)); // txt 有、代码无 + +// 4. 输出 +if (missing.length > 0) { + console.log("== 缺失文案(代码中使用但 language/original-web.txt 中没有)==\n"); + for (const { text, locations } of missing) { + console.log(` 「${text}」`); + for (const loc of locations) { + console.log(` ${loc}`); + } + } + console.log(""); +} + +console.log("== 汇总 =="); +console.log(` 扫描文件数: ${files.length}`); +console.log(` 提取字面量数(去重): ${usages.size}`); +console.log(` 缺失数: ${missing.length}`); +console.log(` 疑似未使用数: ${unused.length}`); + +if (unused.length > 0) { + console.log("\n== 疑似未使用(txt 中有、代码中未发现,仅提示,前 20 条样例)=="); + for (const text of unused.slice(0, 20)) { + console.log(` ${text}`); + } + if (unused.length > 20) { + console.log(` ...(共 ${unused.length} 条)`); + } +} + +if (missing.length > 0) { + console.log(`\n校验失败:存在 ${missing.length} 条缺失文案,请将原文追加到 language/original-web.txt`); + process.exit(1); +} +console.log("\n校验通过:未发现缺失文案。"); diff --git a/scripts/gen-events-map.mjs b/scripts/gen-events-map.mjs new file mode 100644 index 000000000..ca2b9528d --- /dev/null +++ b/scripts/gen-events-map.mjs @@ -0,0 +1,155 @@ +#!/usr/bin/env node +/** + * 事件总线注册表生成脚本 + * + * 扫描前端代码中 mitt 事件总线(resources/assets/js/store/events.js 导出的 emitter) + * 的 emit/on/off 调用,按事件名聚合生成 docs/events-map.md。 + * + * 用法: node scripts/gen-events-map.mjs + * 零第三方依赖(仅 node:fs / node:path)。 + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// ===== 常量配置 ===== +const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +// 扫描范围(相对仓库根目录) +const SCAN_DIR = 'resources/assets/js'; +// 输出文件(相对仓库根目录) +const OUTPUT_FILE = 'docs/events-map.md'; +// 扫描的文件扩展名 +const EXTENSIONS = new Set(['.js', '.vue']); +// ==================== + +// 匹配 emitter.emit( / emitter.on( / emitter.off( +// 负向断言排除 xxx.emitter.emit(如 Quill 的 this.quill.emitter,不是 mitt 总线) +const CALL_RE = /(? +const events = new Map(); +// 动态事件名(第一参数非字符串字面量) +const dynamics = []; +let totalCalls = 0; + +for (const file of files) { + const content = fs.readFileSync(file, 'utf8'); + const rel = path.relative(ROOT_DIR, file).split(path.sep).join('/'); + let m; + CALL_RE.lastIndex = 0; + while ((m = CALL_RE.exec(content)) !== null) { + totalCalls++; + const method = m[1]; + const argStart = m.index + m[0].length; + const line = lineOf(content, m.index); + const loc = `${rel}:${line}`; + const rest = content.slice(argStart, argStart + 200); + const lit = rest.match(LITERAL_RE); + if (lit) { + const name = lit[2]; + if (!events.has(name)) { + events.set(name, { emit: [], on: [], off: [] }); + } + events.get(name)[method].push(loc); + } else { + // 截取第一参数片段用于展示 + const snippet = rest.split(/[\n,)]/)[0].trim(); + dynamics.push({ method, loc, snippet }); + } + } +} + +const names = [...events.keys()].sort(); + +// 统计 +const deadEmit = names.filter(n => events.get(n).emit.length > 0 && events.get(n).on.length === 0); +const deadOn = names.filter(n => events.get(n).on.length > 0 && events.get(n).emit.length === 0); + +// ===== 生成 Markdown ===== +const out = []; +out.push('# 前端事件总线注册表'); +out.push(''); +out.push('> **本文件由脚本自动生成,请勿手改。**'); +out.push('>'); +out.push('> - 生成命令: `node scripts/gen-events-map.mjs`'); +out.push(`> - 扫描范围: \`${SCAN_DIR}\` 下所有 \`.js\` / \`.vue\` 文件(共 ${files.length} 个)`); +out.push('> - 事件总线: `resources/assets/js/store/events.js`(mitt 实例)'); +out.push('> - 仅匹配裸 `emitter.emit/on/off(` 调用;`xxx.emitter.emit(`(如 Quill 内部 emitter)不属于本总线,已排除'); +out.push(''); +out.push(`共 **${names.length}** 个静态可解析事件,**${totalCalls}** 处 \`emitter.emit/on/off\` 调用。`); +out.push(''); +out.push('## 事件清单'); +out.push(''); + +for (const name of names) { + const ev = events.get(name); + out.push(`### \`${name}\``); + out.push(''); + out.push(`- **emit(${ev.emit.length})**${ev.emit.length ? '' : ':无(疑似死事件)'}`); + for (const loc of ev.emit) out.push(` - \`${loc}\``); + out.push(`- **on(${ev.on.length})**${ev.on.length ? '' : ':无(无人监听)'}`); + for (const loc of ev.on) out.push(` - \`${loc}\``); + if (ev.off.length) { + out.push(`- **off(${ev.off.length})**`); + for (const loc of ev.off) out.push(` - \`${loc}\``); + } + out.push(''); +} + +out.push('## 动态事件名(无法静态解析)'); +out.push(''); +if (dynamics.length === 0) { + out.push('无。'); +} else { + out.push('以下调用的第一参数不是字符串字面量,无法静态解析事件名:'); + out.push(''); + for (const d of dynamics) { + out.push(`- \`${d.loc}\` — \`emitter.${d.method}(${d.snippet}...)\``); + } +} +out.push(''); +out.push('## 统计'); +out.push(''); +out.push(`- 事件总数(静态可解析): **${names.length}**`); +out.push(`- 只 emit 无 on(疑似死事件): **${deadEmit.length}**${deadEmit.length ? ` — ${deadEmit.map(n => `\`${n}\``).join('、')}` : ''}`); +out.push(`- 只 on 无 emit(无人发射): **${deadOn.length}**${deadOn.length ? ` — ${deadOn.map(n => `\`${n}\``).join('、')}` : ''}`); +out.push(`- 动态事件名调用: **${dynamics.length}**`); +out.push(''); + +const outputPath = path.join(ROOT_DIR, OUTPUT_FILE); +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +fs.writeFileSync(outputPath, out.join('\n'), 'utf8'); + +console.log(`[gen-events-map] 扫描 ${files.length} 个文件,${totalCalls} 处调用,${names.length} 个事件,${dynamics.length} 处动态事件名`); +console.log(`[gen-events-map] 已生成 ${OUTPUT_FILE}`); +console.log(`[gen-events-map] 只 emit 无 on: ${deadEmit.length ? deadEmit.join(', ') : '无'}`); +console.log(`[gen-events-map] 只 on 无 emit: ${deadOn.length ? deadOn.join(', ') : '无'}`); diff --git a/types/dootask-globals.d.ts b/types/dootask-globals.d.ts new file mode 100644 index 000000000..085114fec --- /dev/null +++ b/types/dootask-globals.d.ts @@ -0,0 +1,666 @@ +/** + * DooTask 前端全局工具 $A / 翻译函数 $L 的 TypeScript 类型声明(纯声明,无运行时代码)。 + * + * 来源文件($A 是 jQuery 实例,由以下三个 IIFE 文件通过 $.extend() 扩展而成, + * 挂载于 window.$A 与 Vue.prototype.$A): + * - resources/assets/js/functions/common.js (基础函数 / localForage / Storage / ihttp / ajaxc / time / sort) + * - resources/assets/js/functions/web.js (页面专用 / iviewui 弹窗提示 / dark 暗黑模式) + * - resources/assets/js/functions/eeui.js (EEUI App 专用) + * $L 来源:resources/assets/js/language/index.js 的 switchLanguage, + * 挂载于 window.$L、Vue.prototype.$L 以及 $A.L(见 resources/assets/js/app.js)。 + * + * 维护提示:在上述源文件中新增/修改 $.extend 挂载的 $A 方法时,须同步更新本声明文件。 + */ + +/** modal 系列配置(字符串等价于 { content: 字符串 }) */ +interface DooTaskModalConfig { + /** 标题,默认「温馨提示」 */ + title?: string; + /** 内容 */ + content?: string | false; + /** 确定按钮文字,默认「确定」 */ + okText?: string; + /** 取消按钮文字,默认「取消」 */ + cancelText?: string; + /** 传 false 时由调用方自行处理翻译,否则内部自动 $L 翻译 */ + language?: boolean; + /** onOk 返回 Promise 时启用 loading 等待 */ + loading?: boolean; + onOk?: (...args: any[]) => any; + onCancel?: (...args: any[]) => any; + render?: (...args: any[]) => any; + [key: string]: any; +} + +/** notice 系列配置(字符串等价于 { desc: 字符串 }) */ +interface DooTaskNoticeConfig { + /** 标题,默认「温馨提示」 */ + title?: string; + /** 描述内容 */ + desc?: string; + /** 显示时长(秒) */ + duration?: number; + render?: (...args: any[]) => any; + [key: string]: any; +} + +/** ajaxc 请求参数 */ +interface DooTaskAjaxcParams { + url: string; + data?: any; + cache?: boolean; + method?: string; + timeout?: number; + dataType?: string; + header?: Record; + requestId?: string | number | null; + before?: () => void; + complete?: () => void; + after?: (success: boolean) => void; + success?: (data: any, status: number | string, xhr: XMLHttpRequest) => void; + error?: (xhr: XMLHttpRequest, status: number | string) => void; + [key: string]: any; +} + +/** extractImageParameter 返回的图片参数 */ +interface DooTaskImageParameter { + src: string | null; + width: number; + height: number; + original: string; +} + +/** imageRatioHandle 参数/返回值 */ +interface DooTaskImageRatioParams { + /** 原图地址 */ + src: string; + /** 原图宽度 */ + width: number; + /** 原图高度 */ + height: number; + /** 裁剪参数,如:{ratio:3, percentage:"80x0"} */ + crops?: { ratio?: number; size?: string; percentage?: string; cover?: string; contain?: string }; + /** 返回尺寸缩放最高尺寸 */ + scaleSize?: number; + [key: string]: any; +} + +/** dark 暗黑模式工具 */ +interface DooTaskDark { + utils: { + /** 判断浏览器支持的暗黑实现方式 chrome|webkit|null */ + supportMode(): "chrome" | "webkit" | null; + /** 默认反色滤镜样式 */ + defaultFilter(): string; + /** 反向反色滤镜样式 */ + reverseFilter(): string; + /** 取消滤镜样式 */ + noneFilter(): string; + /** 附加额外样式 */ + addExtraStyle(): string; + /** 添加样式标签 */ + addStyle(id: string, tag: string, css: string): void; + /** 获取元素 classList */ + getClassList(node: Element): DOMTokenList | any[]; + /** 给元素添加 class */ + addClass(node: Element, name: string): DooTaskDark["utils"]; + /** 移除元素 class */ + removeClass(node: Element, name: string): DooTaskDark["utils"]; + /** 判断元素是否含有 class */ + hasClass(node: Element, name: string): boolean; + /** 根据 id 获取元素 */ + hasElementById(eleId: string): HTMLElement | null; + /** 根据 id 删除元素 */ + removeElementById(eleId: string): void; + }; + /** 创建暗黑模式样式 */ + createDarkStyle(): void; + /** 开启暗黑模式 */ + enableDarkMode(): void; + /** 关闭暗黑模式 */ + disableDarkMode(): void; + /** 跟随系统自动切换暗黑模式 */ + autoDarkMode(): void; + /** 是否已开启暗黑模式 */ + isDarkEnabled(): boolean; +} + +/** + * DooTask 全局工具对象。 + * 说明:$A 本体是 jQuery 实例(window.$ / window.jQuery),为避免引入 @types/jquery 依赖, + * 这里不 extends JQueryStatic,而是用「可调用签名 + 字符串索引签名」兜底 jQuery 本体的 + * 选择器调用(如 $A(el))与 each/extend 等静态方法。 + */ +interface DooTaskGlobal { + /** jQuery 选择器调用兜底($A(selector),返回 jQuery 对象) */ + (selector?: any, context?: any): any; + /** jQuery 本体其余静态属性/方法兜底(each/extend/fn 等) */ + [key: string]: any; + + /* ========================================================================= + * app.js 挂载的全局属性 + * ========================================================================= */ + + /** 翻译函数(同 window.$L,见 language/index.js switchLanguage) */ + L(text: string, ...args: Array): string; + /** 应用是否初始化完成 */ + Ready: boolean; + /** Electron 桥接对象(非 Electron 环境为 null) */ + Electron: any; + /** 运行平台:web|mac|win|ios|android */ + Platform: string; + /** 是否 Electron 主窗口 */ + isMainElectron: boolean; + /** 是否 Electron 子窗口 */ + isSubElectron: boolean; + /** 是否 EEUI App 环境 */ + isEEUIApp: boolean; + /** 是否 Electron 环境 */ + isElectron: boolean; + /** 是否客户端软件环境(Electron 或 EEUI) */ + isSoftware: boolean; + /** 是否开启调试日志(VConsole) */ + openLog: boolean; + /** iView Modal 实例(app 初始化后可用) */ + Modal: any; + /** iView Message 实例(app 初始化后可用) */ + Message: any; + /** iView Notice 实例(app 初始化后可用) */ + Notice: any; + + /* ========================================================================= + * common.js —— 基础函数类 + * ========================================================================= */ + + /** 是否数组 */ + isArray(obj: any): boolean; + /** 规范化为整型数组(去重、过滤非正整数) */ + normalizeIntArray(data: any): number[]; + /** 是否数组对象(普通 JSON 对象) */ + isJson(obj: any): boolean; + /** 是否在数组里(regular 为 true 时支持 * 通配) */ + inArray(key: any, array: any[], regular?: boolean): boolean; + /** 随机获取范围内的整数 */ + randNum(Min: number, Max: number): number; + /** 获取数组最后一个值(无则返回 false) */ + last(array: any[]): any; + /** 字符串是否包含(lower 为 true 时区分大小写) */ + strExists(string: any, find: any, lower?: boolean): boolean; + /** 字符串是否左边包含 */ + leftExists(string: any, find: any, lower?: boolean): boolean; + /** 删除左边字符串 */ + leftDelete(string: any, find: any, lower?: boolean): string; + /** 字符串是否右边包含 */ + rightExists(string: any, find: any, lower?: boolean): boolean; + /** 删除右边字符串 */ + rightDelete(string: any, find: any, lower?: boolean): string; + /** 取字符串中间 */ + getMiddle(string: any, start?: string | null, end?: string | null): string; + /** 截取字符串 */ + subString(string: any, start: number, end?: number): string; + /** 随机字符(默认 32 位) */ + randomString(len?: number): string; + /** 判断是否有值(enhanced 为 true 时空数组/空对象视为无) */ + isHave(val: any, enhanced?: boolean): boolean; + /** 判断是否为真(true/1/"true"/"1") */ + isTrue(value: any): boolean; + /** 相当于 intval(fixed 指定小数位时返回字符串) */ + runNum(str: any, fixed?: number | string | null): number | string; + /** 补零 */ + zeroFill(str: any, length: number, after?: boolean): string; + /** 检测手机号码格式 */ + isMobile(str: any): boolean; + /** 检测邮箱地址格式 */ + isEmail(email: any): boolean; + /** 根据两点间的经纬度计算距离(米,字符串) */ + getDistance(lng1: number, lat1: number, lng2: number, lat2: number): string; + /** 设置网页标题 */ + setTile(title: string): void; + /** 克隆对象 */ + cloneJSON(value: T, useParse?: boolean): T; + /** 将一个 JSON 字符串转换为对象(已try) */ + jsonParse(str: any, defaultVal?: any): any; + /** 将 JavaScript 值转换为 JSON 字符串(已try) */ + jsonStringify(json: any, defaultVal?: string): string; + /** 监听对象尺寸发生改变 */ + resize(obj: any, callback?: () => void): void; + /** 获取屏幕方向 */ + screenOrientation(): "landscape" | "portrait"; + /** 是否IOS */ + isIos(): boolean; + /** 是否iPad */ + isIpad(): boolean; + /** 是否安卓 */ + isAndroid(): boolean; + /** 是否微信 */ + isWeixin(): boolean; + /** 是否Chrome */ + isChrome(): boolean; + /** 是否桌面端 */ + isDesktop(): boolean; + /** 获取对象(支持 a.b.c 路径取值) */ + getObject(obj: any, keys: string | Array, defaultValue?: any): any; + /** 统计数组或对象长度 */ + count(obj: any): number; + /** 获取文本长度 */ + stringLength(string: any): number; + /** 获取数组长度(处理数组不存在) */ + arrayLength(array: any): number; + /** 将数组或对象内容部分拼成字符串 */ + objImplode(obj: any): string; + /** 指定键获取url参数(不传 key 返回全部) */ + urlParameter(key?: string): any; + /** 获取所有url参数 */ + urlParameterAll(): Record; + /** 删除地址中的参数 */ + removeURLParameter(url: string, keys: string | string[]): string; + /** 连接加上参数 */ + urlAddParams(url: string, params: Record): string; + /** 替换url中的hash(只传一个参数时视为 path,url 默认当前页面) */ + urlReplaceHash(url: string, path?: string): string; + /** 刷新当前地址 */ + reloadUrl(): void; + /** 链接字符串(第一个参数为连接符) */ + stringConnect(...value: any[]): string; + /** 判断两个对象是否相等 */ + objEquals(x: any, y: any): boolean; + /** 输入框内插入文本 */ + insert2Input(object: any, content: string): void; + /** 输入框数字限制 */ + inputNumberLimit(object: any, min?: number | null, max?: number | null): void; + /** iOS上虚拟键盘引起的触控错位修正 */ + iOSKeyboardFixer(): void; + /** 动态加载js文件 */ + loadScript(url: string): Promise; + /** 按顺序动态加载多个js文件 */ + loadScriptS(urls: string[]): Promise; + /** 动态加载css文件 */ + loadCss(url: string): Promise; + /** 按顺序动态加载多个css文件 */ + loadCssS(urls: string[]): Promise; + /** 动态加载iframe */ + loadIframe(url: string, loadedRemove?: number): Promise; + /** 按顺序动态加载多个iframe */ + loadIframes(urls: string[]): Promise; + /** 字节转换(如 1024 -> "1 KB") */ + bytesToSize(bytes: number): string; + /** html代码转义 */ + html2Escape(sHtml: string): string; + /** 正则提取域名 */ + getDomain(weburl: any): string; + /** 提取 URL 协议 */ + getProtocol(weburl: any): string; + /** 滚动到View */ + scrollToView(element: Element | null, options?: boolean | Record): void; + /** 按需滚动到View */ + scrollIntoViewIfNeeded(element?: Element | null, smooth?: boolean): void; + /** 给元素添加一个class,过指定时间之后再去除这个class */ + addClassWithTimeout(element: Element | null, className: string, duration: number): void; + /** 滚动到元素并抖动 */ + scrollIntoAndShake(element: Element | Element[] | null, viewIfNeeded?: boolean): void; + /** 等比缩放尺寸 */ + scaleToScale(width: number, height: number, maxW: number, maxH?: number): { width: number; height: number }; + /** 阻止滑动穿透 */ + scrollPreventThrough(el: HTMLElement | null): void; + /** 获取元素属性 */ + getAttr(el: Element | null, attrName: string, def?: string): string | null; + /** 排序JSON对象 */ + sortObject(obj: Record, ignore?: string[]): Record; + /** 从HTML中提取图片参数 */ + extractImageParameter(imgTag: string): DooTaskImageParameter; + /** 从HTML中提取所有图片参数 */ + extractImageParameterAll(html: string): DooTaskImageParameter[]; + /** 增强版的字符串截取(超长自动加后缀) */ + cutString(str: string, length: number, start?: number, suffix?: string): string; + /** 获取两个数组后面的交集 */ + getLastSameElements(arr1: any[], arr2: any[]): any[]; + /** 查找元素并在失败时重试 */ + findElementWithRetry(findElementFn: () => any, maxAttempts?: number, delayMs?: number): Promise; + /** 轮询等待条件满足 */ + waitForCondition(conditionFn: () => boolean, intervalMs?: number, timeoutMs?: number): Promise; + /** 执行指定次数的定时任务,返回取消函数 */ + repeatWithCount(fn: (count: number) => boolean | void, delay: number, interval?: number, times?: number): () => void; + /** 通过URL生成base64图片 */ + generateBase64Image(url: string, quality?: number, maxWidth?: number, maxHeight?: number): Promise; + /** 是否全屏(根据尺寸对比) */ + isFullScreen(): boolean; + + /* ========================================================================= + * common.js —— localForage(IndexedDB) + * ========================================================================= */ + + /** 测试 IndexedDB 是否可用 */ + IDBTest(): Promise; + /** 延迟保存(防抖,默认 100ms) */ + IDBSave(key: string, value: any, delay?: number): void; + /** 删除缓存 */ + IDBDel(key: string): Promise; + /** 设置缓存 */ + IDBSet(key: string, value: any): Promise; + /** 删除缓存(同 IDBDel) */ + IDBRemove(key: string): Promise; + /** 清除缓存(可指定保留的 key) */ + IDBClear(keysToKeep?: string[]): Promise; + /** 获取缓存值 */ + IDBValue(key: string): Promise; + /** 获取缓存值(字符串) */ + IDBString(key: string, def?: string): Promise; + /** 获取缓存值(整数) */ + IDBInt(key: string, def?: number): Promise; + /** 获取缓存值(布尔) */ + IDBBoolean(key: string, def?: boolean): Promise; + /** 获取缓存值(数组) */ + IDBArray(key: string, def?: any[]): Promise; + /** 获取缓存值(对象) */ + IDBJson(key: string, def?: Record): Promise>; + + /* ========================================================================= + * common.js —— localStorage + * ========================================================================= */ + + /** 设置本地存储 */ + setStorage(key: string, value: any): void; + /** 获取本地存储值 */ + getStorageValue(key: string): any; + /** 获取本地存储值(字符串) */ + getStorageString(key: string, def?: string): string | number; + /** 获取本地存储值(整数) */ + getStorageInt(key: string, def?: number): number; + /** 获取本地存储值(布尔) */ + getStorageBoolean(key: string, def?: boolean): boolean; + /** 获取本地存储值(数组) */ + getStorageArray(key: string, def?: any[]): any[]; + /** 获取本地存储值(对象) */ + getStorageJson(key: string, def?: Record): Record; + /** 本地存储是否存在 */ + existsStorage(key: string): boolean; + + /* ========================================================================= + * common.js —— sessionStorage + * ========================================================================= */ + + /** 设置会话存储 */ + setSessionStorage(key: string, value: any): void; + /** 获取会话存储值 */ + getSessionStorageValue(key: string): any; + /** 获取会话存储值(字符串) */ + getSessionStorageString(key: string, def?: string): string | number; + /** 获取会话存储值(整数) */ + getSessionStorageInt(key: string, def?: number): number; + + /* ========================================================================= + * common.js —— ihttp / ajaxc + * ========================================================================= */ + + /** 序列化对象为查询字符串 */ + serializeObject(obj: any, parents?: string[]): string; + /** 全局 Ajax 配置 */ + globalAjaxOptions: Record; + /** 设置全局 Ajax 配置 */ + ajaxSetup(options: Record): void; + /** XHR 请求(jQuery.ajax 风格,JSONP 时无返回值) */ + ihttp(options: Record): XMLHttpRequest | void; + /** Ajax 请求封装(带请求列表管理,可用 ajaxcCancel 取消) */ + ajaxc(params: DooTaskAjaxcParams): void | false; + /** 取消 ajaxc 请求,返回取消的数量 */ + ajaxcCancel(requestId: string | number): number; + + /* ========================================================================= + * common.js —— time / sort + * ========================================================================= */ + + /** 时间对象(dayjs 实例,自动识别 10/13 位时间戳;返回 any 以避免引入 dayjs 类型依赖) */ + dayjs(v?: any): any; + /** 时间对象(减去时区差,dayjs 实例) */ + daytz(v?: any): any; + /** 更新时区,返回时区差(小时) */ + updateTimezone(tz?: string): number; + /** 当前时区名称 */ + timezoneName: string | null; + /** 时区差(小时) */ + timezoneDifference: number; + /** 对象中有Date格式的转成指定格式(支持 dayjs、Date、string,递归处理对象/数组) */ + newDateString(value: any, format?: string, key?: string | null): any; + /** 对象中有Date格式的转成时间戳(递归处理对象/数组) */ + newTimestamp(value: any): any; + /** 判断是否是日期格式(YYYY-MM-DD[ HH[:mm[:ss]]]) */ + isDateString(value: any): boolean; + /** 秒数倒计时,格式:00:00:00, 00:00, 0s */ + secondsToTime(s: number): string; + /** 格式化时间(本地时间自动减去时区差) */ + timeFormat(date: any): string; + /** 倒计时(开始时间自动减去时区差) */ + countDownFormat(s: any, e: any): string; + /** 计算排序值(日期格式) */ + sortDay(v1: any, v2: any): number; + /** 计算排序值(数字格式) */ + sortFloat(v1: any, v2: any): number; + + /* ========================================================================= + * web.js —— 页面专用 + * ========================================================================= */ + + /** 接口地址(补全为完整 API URL) */ + apiUrl(str: string): string; + /** 主页地址 */ + mainUrl(str?: string | null): string; + /** 获取 mainUrl 的域名 */ + mainDomain(): string; + /** 移除 mainUrl 前缀(忽略 http/https 协议差异,只匹配域名) */ + removeMainUrlPrefix(url: any): string; + /** 服务地址 */ + originUrl(str: string): string; + /** 预览文件地址 */ + onlinePreviewUrl(name: string, key: string): string; + /** 项目配置模板 */ + projectParameterTemplate(project_id: number | string): { + project_id: number | string; + menuInit: boolean; + menuType: string; + chat: boolean; + showMy: boolean; + showHelp: boolean; + showUndone: boolean; + showCompleted: boolean; + completedTask: boolean; + }; + /** 获取日期选择器的 shortcuts 模板参数 */ + timeOptionShortcuts(): Array<{ text: string; value(): [Date, Date] }>; + /** 对话标签(已完成/已删除/已归档) */ + dialogTags(dialog: any): Array<{ color: string; text: string }>; + /** 对话是否完成(返回 success 标签) */ + dialogCompleted(dialog: any): { color: string; text: string } | undefined; + /** 返回对话未读数量(不含免打扰,但如果免打扰中有@则返回@数量) */ + getDialogNum(dialog: any): number; + /** 返回对话未读数量(containSilence 是否包含免打扰消息) */ + getDialogUnread(dialog: any, containSilence?: boolean): number; + /** 返回对话@提及未读数量 */ + getDialogMention(dialog: any): number; + /** 返回文本信息预览格式 */ + getMsgTextPreview(msgData: { type?: string; text?: string }, imgClassName?: string | null): string; + /** 消息格式化处理(将消息内的RemoteURL换成真实地址) */ + formatMsgBasic(data: T): T; + /** 消息格式化处理(@提及、链接、图片尺寸) */ + formatTextMsg(text: string, userid: number | string): string; + /** 获取文本消息图片 */ + getTextImagesInfo(text: string): Array<{ src: string; width: number | string; height: number | string }>; + /** 合并转发消息标题 */ + getMergeForwardTitle(msg: any): string; + /** 消息简单描述 */ + getMsgSimpleDesc(data: any, imgClassName?: string | null): string; + /** 文件消息简单描述 */ + fileMsgSimpleDesc(msg: any, imgClassName?: string | null): string; + /** 模板消息简单描述 */ + templateMsgSimpleDesc(msg: any): string; + /** 获取文件标题(name.ext) */ + getFileName(file: { name?: string; ext?: string }): string; + /** 是否是doo服务器 */ + isDooServer(): boolean; + /** 缩略图还原 */ + thumbRestore(url: any): string; + /** 拖拽或粘贴的数据是否包含文件夹 */ + dataHasFolder(data: { items?: any }): boolean; + /** 图片尺寸比例超出处理(裁剪 + 等比缩放) */ + imageRatioHandle(params: DooTaskImageRatioParams): DooTaskImageRatioParams; + /** 判断图片地址是否满足比例缩放 */ + imageRatioJudge(url: string): boolean; + /** 图片尺寸比例超出(返回超出时的 ratio,否则 0) */ + imageRatioExceed(width: number, height: number, ratio: number, float?: number): number; + /** 去除html内容中无效的部分 */ + filterInvalidLine(content: any): string; + /** 加载 VConsole 日志组件(key: 'log.o' 开 / 'log.c' 关) */ + loadVConsole(key?: string): boolean | void; + /** 提取工作报告中的时间 */ + reportExtractTime(text: string): string; + /** 根据十六进制颜色生成通用 CSS 变量样式 */ + generateColorVarStyle(hexColor: string, levels?: number[], prefix?: string, styles?: Record | null): Record | null; + /** 转换工作流状态 */ + convertWorkflow(item: string | { flow_item_name?: string; complete_at?: any }): { status: string | null; name: string; color: string | null }; + + /* ========================================================================= + * web.js —— iviewui assist(弹窗/提示/通知) + * 注意:modal/message/notice 系列内部自动 $L 翻译,调用方勿再包 $L + * (仅当 config.language === false 时由调用方自行处理翻译)。 + * ========================================================================= */ + + /** 弹窗配置规范化(内部自动 $L 翻译 title/content/okText/cancelText,调用方勿再包 $L) */ + modalConfig(config?: string | DooTaskModalConfig): DooTaskModalConfig; + /** 弹窗文案翻译辅助(language === false 时翻译,否则原样返回交给 modalConfig 统一翻译) */ + modalTranslation(title: string, language?: boolean): string; + /** 输入弹窗(内部自动 $L 翻译,调用方勿再包 $L;millisecond 为延迟弹出毫秒数) */ + modalInput(config: string | DooTaskModalConfig, millisecond?: number): void; + /** 确认弹窗(内部自动 $L 翻译,调用方勿再包 $L) */ + modalConfirm(config: string | DooTaskModalConfig | false, millisecond?: number): void; + /** 成功弹窗(内部自动 $L 翻译,调用方勿再包 $L) */ + modalSuccess(config: string | DooTaskModalConfig | false, millisecond?: number): void; + /** 信息弹窗(内部自动 $L 翻译,调用方勿再包 $L) */ + modalInfo(config: string | DooTaskModalConfig | false, millisecond?: number): void; + /** 警告弹窗(内部自动 $L 翻译,调用方勿再包 $L) */ + modalWarning(config: string | DooTaskModalConfig | false, millisecond?: number): void; + /** 错误弹窗(内部自动 $L 翻译,调用方勿再包 $L) */ + modalError(config: string | DooTaskModalConfig | false, millisecond?: number): void; + /** alert 弹窗(内部自动 $L 翻译,调用方勿再包 $L) */ + modalAlert(msg: string | false): void; + /** 成功提示(内部自动 $L 翻译,调用方勿再包 $L) */ + messageSuccess(msg: string): void; + /** 信息提示(内部自动 $L 翻译,调用方勿再包 $L) */ + messageInfo(msg: string): void; + /** 警告提示(内部自动 $L 翻译,调用方勿再包 $L) */ + messageWarning(msg: string | false): void; + /** 错误提示(内部自动 $L 翻译,调用方勿再包 $L) */ + messageError(msg: string | false): void; + /** 通知配置规范化(内部自动 $L 翻译 title/desc,调用方勿再包 $L) */ + noticeConfig(config?: string | DooTaskNoticeConfig): DooTaskNoticeConfig; + /** 成功通知(内部自动 $L 翻译,调用方勿再包 $L) */ + noticeSuccess(config: string | DooTaskNoticeConfig | false): void; + /** 警告通知(内部自动 $L 翻译,调用方勿再包 $L) */ + noticeWarning(config: string | DooTaskNoticeConfig | false): void; + /** 错误通知(内部自动 $L 翻译,调用方勿再包 $L;字符串默认 duration 6 秒) */ + noticeError(config: string | DooTaskNoticeConfig | false): void; + + /* ========================================================================= + * web.js —— dark 暗黑模式 + * ========================================================================= */ + + /** 暗黑模式工具对象 */ + dark: DooTaskDark; + + /* ========================================================================= + * eeui.js —— EEUI App 专用 + * ========================================================================= */ + + /** 获取eeui模块 */ + eeuiModule(name?: string): any; + /** 获取eeui模块(Promise) */ + eeuiModulePromise(name?: string): Promise; + /** 获取eeui版本号 */ + eeuiAppVersion(): string | undefined; + /** 获取本地软件版本号 */ + eeuiAppLocalVersion(): string | undefined; + /** Alert 弹窗 */ + eeuiAppAlert(object: any, callback?: (result: any) => void): void; + /** Toast 提示 */ + eeuiAppToast(object: any): void; + /** 相对地址基于当前地址补全 */ + eeuiAppRewriteUrl(val: string): string | undefined; + /** 获取页面信息 */ + eeuiAppGetPageInfo(pageName?: string): any; + /** 打开app新页面 */ + eeuiAppOpenPage(object: Record, callback?: (result: any) => void): void; + /** 使用系统浏览器打开网页 */ + eeuiAppOpenWeb(url: string): void; + /** 拦截返回按键事件(仅支持android、iOS无效) */ + eeuiAppSetPageBackPressed(object: any, callback?: (result: any) => void): void; + /** 返回手机桌面 */ + eeuiAppGoDesktop(): void; + /** 打开屏幕常亮 */ + eeuiAppKeepScreenOn(): void; + /** 关闭屏幕常亮 */ + eeuiAppKeepScreenOff(): void; + /** 隐藏软键盘 */ + eeuiAppKeyboardHide(): void; + /** 给app发送消息 */ + eeuiAppSendMessage(object: any): void; + /** 设置浏览器地址 */ + eeuiAppSetUrl(url: string): void; + /** 生成webview快照 */ + eeuiAppGetWebviewSnapshot(callback: (result: any) => void): void; + /** 显示webview快照 */ + eeuiAppShowWebviewSnapshot(): void; + /** 隐藏webview快照 */ + eeuiAppHideWebviewSnapshot(): void; + /** 扫码(成功时回调扫码文本) */ + eeuiAppScan(callback: (text: string) => void): void; + /** 检查更新 */ + eeuiAppCheckUpdate(): void; + /** 获取主题名称 light|dark */ + eeuiAppGetThemeName(): string | undefined; + /** 判断软键盘是否可见 */ + eeuiAppKeyboardStatus(): boolean | undefined; + /** 设置全局变量 */ + eeuiAppSetVariate(key: string, value: any): void; + /** 获取全局变量 */ + eeuiAppGetVariate(key: string, defaultVal?: any): any; + /** 设置缓存数据 */ + eeuiAppSetCachesString(key: string, value: string, expired?: number): void; + /** 获取缓存数据 */ + eeuiAppGetCachesString(key: string, defaultVal?: string): string | undefined; + /** 是否长按内容震动(仅支持android、iOS无效) */ + eeuiAppSetHapticBackEnabled(val: boolean): void; + /** 禁止长按选择(仅支持android、iOS无效;传毫秒数则临时禁止) */ + eeuiAppSetDisabledUserLongClickSelect(val: boolean | number | string): void; + /** 复制文本 */ + eeuiAppCopyText(text: string): void; + /** 设置是否禁止滚动 */ + eeuiAppSetScrollDisabled(disabled: boolean): void; + /** 设置应用程序级别的摇动撤销(仅支持iOS、android无效) */ + eeuiAppShakeToEditEnabled(enabled: boolean): void; + /** 获取最新一张照片 */ + eeuiAppGetLatestPhoto(expiration?: number, timeout?: number): Promise; + /** 上传照片(params 参数:{url,data,headers,path,fieldName,onReady?}) */ + eeuiAppUploadPhoto(params: Record, timeout?: number): Promise; + /** 取消上传照片 */ + eeuiAppCancelUploadPhoto(id: any): Promise; + /** 获取导航栏和状态栏高度 */ + eeuiAppGetSafeAreaInsets(): Promise; + /** 获取当前语言(zh -> zh-Hans 等映射) */ + eeuiAppConvertLanguage(): string; + /** 获取设备信息 */ + eeuiAppGetDeviceInfo(): Promise; + /** 判断是否窗口化 */ + eeuiAppIsWindowed(): Promise; +} + +/** DooTask 全局工具对象(jQuery 实例 + $.extend 扩展方法) */ +declare const $A: DooTaskGlobal; + +/** + * 翻译函数(language/index.js switchLanguage)。 + * 动态值用 (*) 占位:$L('共(*)条', n);禁止拼接翻译。 + */ +declare function $L(text: string, ...args: Array): string; + +interface Window { + $A: DooTaskGlobal; + $L: typeof $L; +}