chore(quality): 引入 phpstan/ESLint/CI 门禁、Claude hooks 与代码检索地图

- 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 <noreply@anthropic.com>
This commit is contained in:
kuaifan 2026-06-13 01:21:22 +00:00
parent 8fb6d331f8
commit 383664aef7
17 changed files with 2669 additions and 10 deletions

53
.claude/hooks/php-stan-check.sh Executable file
View File

@ -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

16
.claude/settings.json Normal file
View File

@ -0,0 +1,16 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/php-stan-check.sh",
"timeout": 120
}
]
}
]
}
}

81
.github/workflows/tests.yml vendored Normal file
View File

@ -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
'

View File

@ -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` — phpstanlevel 1 + baseline存量已封存新增错误必须清零
- `npm run lint` — ESLinterror 必须为 0warn 是存量遗留,见 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、定时器应复用现有模式避免阻塞协程/事件循环

View File

@ -0,0 +1,143 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Route;
use ReflectionClass;
use ReflectionMethod;
class DocApiMap extends Command
{
protected $signature = 'doc:api-map';
protected $description = '生成 API 路由对照表routes/api-map.md';
public function handle(): int
{
$controllers = $this->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 <<<MD
# API 路由对照表
> 此文件由 `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);
}
}

View File

@ -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": {

203
composer.lock generated
View File

@ -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"
}

323
docs/events-map.md Normal file
View File

@ -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`
- **emit10**
- `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`
- **on1**
- `resources/assets/js/pages/manage/components/MeetingManager/index.vue:187`
- **off1**
- `resources/assets/js/pages/manage/components/MeetingManager/index.vue:191`
### `addTask`
- **emit3**
- `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`
- **on1**
- `resources/assets/js/pages/manage.vue:621`
- **off1**
- `resources/assets/js/pages/manage.vue:641`
### `aiAssistantClosed`
- **emit1**
- `resources/assets/js/components/AIAssistant/index.vue:420`
- **on1**
- `resources/assets/js/components/AIAssistant/float-button.vue:154`
- **off1**
- `resources/assets/js/components/AIAssistant/float-button.vue:162`
### `aiOperationRequest`
- **emit1**
- `resources/assets/js/store/actions.js:4781`
- **on1**
- `resources/assets/js/components/AIAssistant/float-button.vue:155`
- **off1**
- `resources/assets/js/components/AIAssistant/float-button.vue:163`
### `approveDetails`
- **emit2**
- `resources/assets/js/pages/manage/approve/index.vue:497`
- `resources/assets/js/pages/manage/components/DialogWrapper.vue:3826`
- **on1**
- `resources/assets/js/pages/manage.vue:624`
- **off1**
- `resources/assets/js/pages/manage.vue:644`
### `clickAgainDialog`
- **emit1**
- `resources/assets/js/components/Mobile/Tabbar.vue:182`
- **on1**
- `resources/assets/js/pages/manage/messenger.vue:344`
- **off1**
- `resources/assets/js/pages/manage/messenger.vue:348`
### `createGroup`
- **emit3**
- `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`
- **on1**
- `resources/assets/js/pages/manage.vue:622`
- **off1**
- `resources/assets/js/pages/manage.vue:642`
### `dialogMsgPush`
- **emit1**
- `resources/assets/js/store/actions.js:4852`
- **on2**
- `resources/assets/js/components/Mobile/Tabbar.vue:49`
- `resources/assets/js/pages/manage.vue:623`
- **off2**
- `resources/assets/js/components/Mobile/Tabbar.vue:53`
- `resources/assets/js/pages/manage.vue:643`
### `handleMoveTop`
- **emit2**
- `resources/assets/js/store/actions.js:2727`
- `resources/assets/js/store/actions.js:3720`
- **on2**
- `resources/assets/js/pages/manage/components/DialogModal.vue:41`
- `resources/assets/js/pages/manage/components/TaskModal.vue:49`
- **off2**
- `resources/assets/js/pages/manage/components/DialogModal.vue:45`
- `resources/assets/js/pages/manage/components/TaskModal.vue:53`
### `observeMicroApp:open`
- **emit1**
- `resources/assets/js/store/actions.js:5361`
- **on1**
- `resources/assets/js/components/MicroApps/index.vue:144`
- **off1**
- `resources/assets/js/components/MicroApps/index.vue:149`
### `observeMicroApp:updatedOrUninstalled`
- **emit1**
- `resources/assets/js/store/mutations.js:429`
- **on1**
- `resources/assets/js/components/MicroApps/index.vue:145`
- **off1**
- `resources/assets/js/components/MicroApps/index.vue:150`
### `openAIAssistant`
- **emit7**
- `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`
- **on1**
- `resources/assets/js/components/AIAssistant/index.vue:361`
- **off1**
- `resources/assets/js/components/AIAssistant/index.vue:368`
### `openAIAssistantGlobal`
- **emit1**
- `resources/assets/js/pages/manage.vue:1255`
- **on1**
- `resources/assets/js/components/AIAssistant/float-button.vue:153`
- **off1**
- `resources/assets/js/components/AIAssistant/float-button.vue:161`
### `openDownloadClient`
- **emit1**
- `resources/assets/js/pages/manage.vue:1128`
- **on1**
- `resources/assets/js/components/RightBottom.vue:73`
- **off1**
- `resources/assets/js/components/RightBottom.vue:78`
### `openFavorite`
- **emit1**
- `resources/assets/js/pages/manage/application.vue:1061`
- **on1**
- `resources/assets/js/pages/manage.vue:626`
- **off1**
- `resources/assets/js/pages/manage.vue:646`
### `openManageExport`
- **emit1**
- `resources/assets/js/pages/manage/application.vue:1108`
- **on1**
- `resources/assets/js/pages/manage.vue:628`
- **off1**
- `resources/assets/js/pages/manage.vue:648`
### `openMobileNotification`
- **emit1**
- `resources/assets/js/pages/manage.vue:1641`
- **on1**
- `resources/assets/js/components/Mobile/Notification.vue:38`
- **off1**
- `resources/assets/js/components/Mobile/Notification.vue:42`
### `openProjectInvite`
- **emit1**
- `resources/assets/js/App.vue:432`
- **on1**
- `resources/assets/js/pages/manage/components/ProjectInvite.vue:83`
- **off1**
- `resources/assets/js/pages/manage/components/ProjectInvite.vue:87`
### `openRecent`
- **emit1**
- `resources/assets/js/pages/manage/application.vue:1064`
- **on1**
- `resources/assets/js/pages/manage.vue:627`
- **off1**
- `resources/assets/js/pages/manage.vue:647`
### `openReport`
- **emit1**
- `resources/assets/js/pages/manage/application.vue:1058`
- **on1**
- `resources/assets/js/pages/manage.vue:625`
- **off1**
- `resources/assets/js/pages/manage.vue:645`
### `openSearch`
- **emit1**
- `resources/assets/js/pages/manage/dashboard.vue:256`
- **on1**
- `resources/assets/js/components/SearchBox.vue:128`
- **off1**
- `resources/assets/js/components/SearchBox.vue:132`
### `openUser`
- **emit5**
- `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`
- **on1**
- `resources/assets/js/pages/manage/components/UserDetail.vue:166`
- **off1**
- `resources/assets/js/pages/manage/components/UserDetail.vue:170`
### `receiveTask`
- **emit2**
- `resources/assets/js/pages/manage/components/ProjectPanel.vue:1768`
- `resources/assets/js/pages/manage/components/TaskRow.vue:280`
- **on1**
- `resources/assets/js/pages/manage/components/TaskDetail.vue:738`
- **off1**
- `resources/assets/js/pages/manage/components/TaskDetail.vue:745`
### `streamMsgData`
- **emit1**
- `resources/assets/js/store/actions.js:4469`
- **on1**
- `resources/assets/js/pages/manage/components/DialogWrapper.vue:946`
- **off1**
- `resources/assets/js/pages/manage/components/DialogWrapper.vue:956`
### `taskRelationUpdate`
- **emit1**
- `resources/assets/js/store/actions.js:4992`
- **on1**
- `resources/assets/js/pages/manage/components/TaskDetail.vue:739`
- **off1**
- `resources/assets/js/pages/manage/components/TaskDetail.vue:746`
### `updateNotification`
- **emit2**
- `resources/assets/js/pages/manage.vue:1125`
- `resources/assets/js/pages/manage/setting/index.vue:191`
- **on1**
- `resources/assets/js/components/RightBottom.vue:65`
- **off1**
- `resources/assets/js/components/RightBottom.vue:77`
### `useSSOLogin`
- **emit1**
- `resources/assets/js/components/RightBottom.vue:231`
- **on1**
- `resources/assets/js/pages/login.vue:217`
- **off1**
- `resources/assets/js/pages/login.vue:222`
### `userActive`
- **emit3**
- `resources/assets/js/store/actions.js:870`
- `resources/assets/js/store/actions.js:952`
- `resources/assets/js/store/actions.js:4769`
- **on1**
- `resources/assets/js/components/UserAvatar/index.vue:43`
- **off1**
- `resources/assets/js/components/UserAvatar/index.vue:47`
### `websocketMsg`
- **emit1**
- `resources/assets/js/store/actions.js:4786`
- **on3**
- `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`
- **off3**
- `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**

67
eslint.config.mjs Normal file
View File

@ -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',
},
},
];

View File

@ -21,7 +21,7 @@
"resources/assets/js/**/*",
"resources/assets/**/*.vue",
"resources/assets/sass/**/*",
"types/**/*.d.ts"
"types/**/*"
],
"exclude": [
"node_modules",

View File

@ -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": {}
]
}

349
phpstan-baseline.neon Normal file
View File

@ -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

12
phpstan.neon Normal file
View File

@ -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

398
routes/api-map.md Normal file
View File

@ -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()`
- 路由最多两段,方法名最多一个双下划线
## usersUsersController
| 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 | 检查收藏状态 |
## projectProjectController
| 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建议 |
## systemSystemController
| 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 | 预加载的资源 |
## dialogDialogController
| 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-重命名会话 |
## fileFileController
| 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 | 打包文件 |
## reportReportController
| 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 | 标记汇报已读,可批量 |
## publicPublicController
| URL | 方法名 | HTTP | 说明 |
| --- | --- | --- | --- |
| api/public/checkin/install | checkin__install() | any | |
| api/public/checkin/report | checkin__report() | any | |
## approveApproveController
| 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 | 查询需要我审批的流程数量 |
## assistantAssistantController
| 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 | |
## complaintComplaintController
| URL | 方法名 | HTTP | 说明 |
| --- | --- | --- | --- |
| api/complaint/lists | lists() | get | 获取举报投诉列表 |
| api/complaint/submit | submit() | post | 举报投诉 |
| api/complaint/action | action() | post | 举报投诉 - 操作 |
## searchSearchController
| 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 | 搜索消息 |
## testTestController
| URL | 方法名 | HTTP | 说明 |
| --- | --- | --- | --- |

173
scripts/check-language.mjs Normal file
View File

@ -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 = /(?<![\w$])\$L\s*\(|\$A\.L\s*\(/g;
let m;
while ((m = callRe.exec(content)) !== null) {
let i = m.index + m[0].length;
// 跳过空白(含换行)
while (i < content.length && /\s/.test(content[i])) i++;
const ch = content[i];
if (ch !== "'" && ch !== '"') continue; // 模板字符串、变量、其他表达式:跳过
const lit = parseStringLiteral(content, i);
if (!lit) continue;
// 看字面量后第一个非空白字符:是 + 则为拼接表达式,跳过
let j = lit.end;
while (j < content.length && /\s/.test(content[j])) j++;
if (content[j] === "+") continue;
const text = unescapeLiteral(lit.raw).trim();
if (!text) continue;
const line = content.slice(0, m.index).split("\n").length;
results.push({ text, line });
}
return results;
}
// ---------- 主流程 ----------
// 1. 读取 original-web.txt 行集合trim 后,忽略空行)
const txtLines = new Set(
readFileSync(TXT_FILE, "utf8")
.split("\n")
.map(l => l.trim())
.filter(Boolean)
);
// 2. 扫描源码并提取
const files = collectFiles(SCAN_DIR);
/** Map<text, Array<"相对路径:行号">> */
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校验通过未发现缺失文案。");

155
scripts/gen-events-map.mjs Normal file
View File

@ -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 = /(?<![.\w])emitter\.(emit|on|off)\s*\(/g;
// 第一参数为字符串字面量:'xxx' 或 "xxx"
const LITERAL_RE = /^\s*(['"])((?:\\.|(?!\1).)*?)\1/;
/** 递归收集待扫描文件 */
function collectFiles(dir) {
const result = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
result.push(...collectFiles(full));
} else if (entry.isFile() && EXTENSIONS.has(path.extname(entry.name))) {
result.push(full);
}
}
return result;
}
/** 由字符偏移计算行号1-based */
function lineOf(content, offset) {
let line = 1;
for (let i = 0; i < offset; i++) {
if (content.charCodeAt(i) === 10) line++;
}
return line;
}
const scanRoot = path.join(ROOT_DIR, SCAN_DIR);
const files = collectFiles(scanRoot).sort();
// events: Map<eventName, {emit: loc[], on: loc[], off: loc[]}>
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(', ') : '无'}`);

666
types/dootask-globals.d.ts vendored Normal file
View File

@ -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.$LVue.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<string, string>;
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 | number>): 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;
/** 相当于 intvalfixed 指定小数位时返回字符串) */
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<T>(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<string | number>, 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<string, string>;
/** 删除地址中的参数 */
removeURLParameter(url: string, keys: string | string[]): string;
/** 连接加上参数 */
urlAddParams(url: string, params: Record<string, any>): string;
/** 替换url中的hash只传一个参数时视为 pathurl 默认当前页面) */
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<boolean>;
/** 按顺序动态加载多个js文件 */
loadScriptS(urls: string[]): Promise<void>;
/** 动态加载css文件 */
loadCss(url: string): Promise<boolean>;
/** 按顺序动态加载多个css文件 */
loadCssS(urls: string[]): Promise<void>;
/** 动态加载iframe */
loadIframe(url: string, loadedRemove?: number): Promise<boolean>;
/** 按顺序动态加载多个iframe */
loadIframes(urls: string[]): Promise<void>;
/** 字节转换(如 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<string, any>): 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<string, any>, ignore?: string[]): Record<string, any>;
/** 从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<any>;
/** 轮询等待条件满足 */
waitForCondition(conditionFn: () => boolean, intervalMs?: number, timeoutMs?: number): Promise<boolean>;
/** 执行指定次数的定时任务,返回取消函数 */
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<string>;
/** 是否全屏(根据尺寸对比) */
isFullScreen(): boolean;
/* =========================================================================
* common.js localForageIndexedDB
* ========================================================================= */
/** 测试 IndexedDB 是否可用 */
IDBTest(): Promise<boolean>;
/** 延迟保存(防抖,默认 100ms */
IDBSave(key: string, value: any, delay?: number): void;
/** 删除缓存 */
IDBDel(key: string): Promise<void>;
/** 设置缓存 */
IDBSet(key: string, value: any): Promise<any>;
/** 删除缓存(同 IDBDel */
IDBRemove(key: string): Promise<void>;
/** 清除缓存(可指定保留的 key */
IDBClear(keysToKeep?: string[]): Promise<void>;
/** 获取缓存值 */
IDBValue(key: string): Promise<any>;
/** 获取缓存值(字符串) */
IDBString(key: string, def?: string): Promise<string | number>;
/** 获取缓存值(整数) */
IDBInt(key: string, def?: number): Promise<number>;
/** 获取缓存值(布尔) */
IDBBoolean(key: string, def?: boolean): Promise<boolean>;
/** 获取缓存值(数组) */
IDBArray(key: string, def?: any[]): Promise<any[]>;
/** 获取缓存值(对象) */
IDBJson(key: string, def?: Record<string, any>): Promise<Record<string, any>>;
/* =========================================================================
* 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<string, any>): Record<string, any>;
/** 本地存储是否存在 */
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<string, any>;
/** 设置全局 Ajax 配置 */
ajaxSetup(options: Record<string, any>): void;
/** XHR 请求jQuery.ajax 风格JSONP 时无返回值) */
ihttp(options: Record<string, any>): 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<T>(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<string, string> | null): Record<string, string> | 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 翻译,调用方勿再包 $Lmillisecond 为延迟弹出毫秒数) */
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<any>;
/** 获取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<string, any>, 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<any>;
/** 上传照片params 参数:{url,data,headers,path,fieldName,onReady?} */
eeuiAppUploadPhoto(params: Record<string, any>, timeout?: number): Promise<any>;
/** 取消上传照片 */
eeuiAppCancelUploadPhoto(id: any): Promise<any>;
/** 获取导航栏和状态栏高度 */
eeuiAppGetSafeAreaInsets(): Promise<any>;
/** 获取当前语言zh -> zh-Hans 等映射) */
eeuiAppConvertLanguage(): string;
/** 获取设备信息 */
eeuiAppGetDeviceInfo(): Promise<any>;
/** 判断是否窗口化 */
eeuiAppIsWindowed(): Promise<boolean>;
}
/** DooTask 全局工具对象jQuery 实例 + $.extend 扩展方法) */
declare const $A: DooTaskGlobal;
/**
* language/index.js switchLanguage
* (*) $L('共(*)条', n)
*/
declare function $L(text: string, ...args: Array<string | number>): string;
interface Window {
$A: DooTaskGlobal;
$L: typeof $L;
}