feat: 系统功能应用统一管理内置功能入口

- 新增 SystemFunctions 模块与 api/apps/functions 接口,读取 system-functions 应用的安装状态和开关字段
- 工作报告、日历、我的收藏、最近打开、签到打卡的入口(应用中心、头像菜单、左侧导航)统一受开关控制,卸载后全部隐藏
- 导出签到数据随签到打卡一起隐藏;日历停用后访问日历页跳回仪表盘
- 头像菜单按分组计算分割线,隐藏菜单项后不留多余分割线
- 新增迁移:无配置时自动安装并全部启用,已有配置不覆盖
- 同步 ai-kb 与 api-map
This commit is contained in:
kuaifan 2026-09-30 12:44:18 +00:00
parent 4c357ef36f
commit 1c90a11211
26 changed files with 528 additions and 70 deletions

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
use App\Models\User;
use App\Module\Badge;
use App\Module\Base;
use App\Module\SystemFunctions;
use Request;
/**
@ -14,6 +15,7 @@ use Request;
* api/apps/badge/set -> badge__set() 应用密钥鉴权,绝对设置/清除角标
* api/apps/badge/clear -> badge__clear() 当前用户 token 鉴权,清除自己的角标
* api/apps/badge/list -> badge__list() 当前用户 token 鉴权,拉取自己全部角标(初始同步)
* api/apps/functions -> functions() 当前用户 token 鉴权,获取「系统功能」应用的功能开关
*/
class AppsController extends AbstractController
{
@ -91,4 +93,25 @@ class AppsController extends AbstractController
$user = User::auth();
return Base::retSuccess('success', Badge::userBadges((int)$user->userid));
}
/**
* @api {get} api/apps/functions 获取系统功能开关
*
* @apiDescription 需要token身份。返回「系统功能」应用是否已安装,以及工作报告、日历、我的收藏、最近打开、签到打卡的启用状态。
* 应用未安装时全部为 false;前端据此统一显示或隐藏这些功能的入口。
* @apiVersion 1.0.0
* @apiGroup apps
* @apiName functions
*
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
* @apiSuccess {String} msg 返回信息
* @apiSuccess {Object} data 返回数据
* @apiSuccess {Boolean} data.installed 应用是否已安装
* @apiSuccess {Object} data.functions 功能开关(report/calendar/favorite/recent/checkin => Boolean)
*/
public function functions()
{
User::auth();
return Base::retSuccess('success', SystemFunctions::status());
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Module;
use App\Services\RequestContext;
use Symfony\Component\Yaml\Yaml;
/**
* 「系统功能」内置应用(appstore 应用 system-functions)。
*
* 该应用不启动容器,只通过应用字段保存各内置功能的开关;
* 主程序据此统一控制工作报告、日历、我的收藏、最近打开、签到打卡的入口。
* 仅控制入口显示,不拦截接口、不影响数据。
*/
class SystemFunctions
{
const APP_ID = 'system-functions';
/**
* 功能标识 => 应用字段名
*/
const FUNCTIONS = [
'report' => 'REPORT',
'calendar' => 'CALENDAR',
'favorite' => 'FAVORITE',
'recent' => 'RECENT',
'checkin' => 'CHECKIN',
];
/**
* 应用安装配置文件路径
*
* @return string
*/
public static function configFile(): string
{
return base_path('docker/appstore/config/' . self::APP_ID . '/config.yml');
}
/**
* 获取应用安装状态与各功能的启用状态。
* 应用未安装时全部为停用;已安装时缺失的字段视为启用(字段默认值即启用)。
*
* @return array ['installed' => bool, 'functions' => ['report' => bool, ...]]
*/
public static function status(): array
{
$key = 'system_functions_status';
if (RequestContext::has($key)) {
return RequestContext::get($key);
}
$params = Apps::isInstalled(self::APP_ID) ? self::readParams() : null;
$functions = [];
foreach (self::FUNCTIONS as $name => $field) {
$functions[$name] = $params !== null && self::isOn($params[$field] ?? true);
}
return RequestContext::save($key, [
'installed' => $params !== null,
'functions' => $functions,
]);
}
/**
* 指定功能是否启用
*
* @param string $name 功能标识(report/calendar/favorite/recent/checkin)
* @return bool
*/
public static function enabled(string $name): bool
{
return (bool)(self::status()['functions'][$name] ?? false);
}
/**
* 读取应用安装参数
*
* @return array
*/
private static function readParams(): array
{
try {
$config = Yaml::parseFile(self::configFile());
} catch (\Throwable $e) {
return [];
}
$params = is_array($config) ? ($config['params'] ?? []) : [];
return is_array($params) ? $params : [];
}
/**
* 开关值是否为开启(兼容布尔值与字符串)
*
* @param mixed $value
* @return bool
*/
private static function isOn($value): bool
{
if (is_string($value)) {
return !in_array(strtolower(trim($value)), ['', '0', 'false', 'off', 'no'], true);
}
return (bool)$value;
}
}

View File

@ -0,0 +1,68 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Symfony\Component\Yaml\Yaml;
/**
* 默认安装「系统功能」应用并启用全部功能。
*
* 应用的安装状态保存在 docker/appstore/config/system-functions/config.yml(与其它应用同源),
* 因此这里写的是配置文件而不是数据表。仅在配置文件不存在时写入:
* 已有配置(包括管理员卸载后留下的未安装状态、自行调整过的开关)一律不覆盖。
*/
return new class extends Migration {
public function up(): void
{
$appId = 'system-functions';
$configDir = base_path('docker/appstore/config/' . $appId);
$configFile = $configDir . '/config.yml';
if (file_exists($configFile)) {
return;
}
if (!is_dir($configDir) && !@mkdir($configDir, 0755, true) && !is_dir($configDir)) {
throw new RuntimeException("无法创建目录:{$configDir}");
}
$config = [
'install_at' => date('Y-m-d H:i:s'),
'install_num' => 1,
'install_version' => $this->latestVersion($appId),
'status' => 'installed',
'params' => [
'REPORT' => true,
'CALENDAR' => true,
'FAVORITE' => true,
'RECENT' => true,
'CHECKIN' => true,
],
'resources' => [
'cpu_limit' => '0',
'memory_limit' => '0',
],
];
if (@file_put_contents($configFile, Yaml::dump($config, 4, 4)) === false) {
throw new RuntimeException("无法写入配置文件:{$configFile}");
}
}
public function down(): void
{
// 不回滚:无法区分配置是本迁移写入的还是管理员后续调整的
}
/**
* 应用包内的最新版本号;应用包尚未同步到本地时使用首个版本。
*/
private function latestVersion(string $appId): string
{
$latest = '1.0.0';
foreach (glob(base_path("docker/appstore/apps/{$appId}/*"), GLOB_ONLYDIR) ?: [] as $dir) {
$version = basename($dir);
if (preg_match('/^v?\d+(\.\d+){1,2}$/', $version) && version_compare(ltrim($version, 'v'), ltrim($latest, 'v'), '>')) {
$latest = $version;
}
}
return $latest;
}
};

View File

@ -663,6 +663,19 @@ features:
- compliance.howto
- compliance.entry.menu-map
- id: system-functions
name: 系统功能(内置功能入口开关:工作报告/日历/我的收藏/最近打开/签到打卡)
scope: admin
batch: B4
priority: P0
chunk_count_est: 3
owner: ~
status: drafted
chunks:
- system-functions.overview.concept
- system-functions.toggle.howto
- system-functions.entry-missing.faq
- id: appstore
name: 应用市场(AppStore 管理)
scope: admin

View File

@ -17,7 +17,7 @@ prerequisites: []
negative:
- 应用中心不是「应用市场」(AppStore),后者是管理员安装/卸载插件的独立入口
- 普通成员看不到管理员应用区(如 LDAP、邮件通知、数据导出等)
last_verified: v1.7.90
last_verified: v1.9.36
---
# 应用中心是什么
@ -34,6 +34,9 @@ last_verified: v1.7.90
- **管理员应用**:仅系统管理员可见(LDAP、邮件通知、APP 推送、举报管理、数据导出、团队管理)
- **微应用**:由已安装插件提供(OKR、思维导图、流程图、在线文档、Memos、KPI 等),列表随安装情况变化
## 受「系统功能」应用控制的卡片
工作报告、我的收藏、最近打开、签到打卡(以及竖屏下的日历)这几张系统应用卡片,只有在「系统功能」应用已安装且对应开关开启时才显示。该应用自身不会在应用中心生成卡片,同一功能只会出现一张卡片。详见 [[system-functions.overview.concept]]。
## 个性化
- 拖拽:长按卡片可调整顺序(仅影响个人视图)
- 管理员可在「自定义应用菜单」里调整全员可见的应用项

View File

@ -0,0 +1,49 @@
---
id: system-functions.overview.concept
title: 「系统功能」应用是什么
type: concept
feature: system-functions
scope: end-user
locale: zh
aliases:
- 系统功能应用
- 系统功能是什么
- system-functions
- 内置功能开关
- 怎么隐藏日历
- 怎么关闭工作报告入口
related_tools: []
related_pages: [application]
prerequisites: []
negative:
- 「系统功能」只控制入口是否显示,不会删除报告、收藏、签到等已有数据
- 「系统功能」不启动任何容器,安装时没有 CPU、内存、镜像拉取等高级设置
- 「系统功能」自身不会在应用中心生成卡片,也不会新增菜单
- 不支持按成员或部门单独开关,开关对全员生效
last_verified: v1.9.36
---
# 「系统功能」应用是什么
## 定义
「系统功能」(应用 ID:system-functions)是 DooTask 的内置应用,用来统一管理五个内置功能的入口:工作报告、日历、我的收藏、最近打开、签到打卡。它在应用商店(AppStore)中安装、配置和卸载。
## 关键属性
- 每个功能对应一个开关字段,默认全部开启
- 新部署和升级上来的实例会自动安装「系统功能」并启用全部功能;已有配置不会被覆盖
- 开关对该功能的所有入口统一生效:应用中心卡片、头像下拉菜单、左侧导航
- 卸载「系统功能」后,五个功能的入口全部隐藏;重新安装即可恢复
## 各功能受控的入口
| 功能 | 受控入口 |
|---|---|
| 工作报告 | 应用中心「工作报告」卡片及未读角标、头像菜单「工作报告」 |
| 日历 | 左侧导航「日历」、竖屏应用中心「日历」卡片;停用后访问日历页会回到仪表盘 |
| 我的收藏 | 头像菜单「我的收藏」、应用中心「我的收藏」卡片 |
| 最近打开 | 头像菜单「最近打开的任务」、应用中心「最近打开」卡片 |
| 签到打卡 | 应用中心「签到打卡」卡片、「导出签到数据」入口 |
## 与其他概念的关系
- 修改开关的步骤见 [[system-functions.toggle.howto]]
- 入口找不到时的排查见 [[system-functions.entry-missing.faq]]
- 应用中心整体说明见 [[application.center.concept]]

View File

@ -0,0 +1,42 @@
---
id: system-functions.entry-missing.faq
title: 为什么找不到工作报告、日历、我的收藏、最近打开或签到打卡的入口
type: faq
feature: system-functions
scope: end-user
locale: zh
aliases:
- 日历不见了
- 工作报告入口没了
- 找不到我的收藏
- 最近打开不见了
- 签到打卡卡片消失
- 左侧栏没有日历
related_tools: []
related_pages: [application]
prerequisites: []
negative:
- 入口隐藏不代表数据丢失,重新启用后原有数据仍在
- 普通成员无法自行恢复入口,需要联系系统管理员
last_verified: v1.9.36
---
# 为什么找不到工作报告、日历、我的收藏、最近打开或签到打卡的入口
## 原因
这五个功能的入口由「系统功能」应用统一控制,出现以下任一情况时入口会隐藏:
1. 管理员在「系统功能」应用里关闭了对应功能的开关
2. 管理员卸载了「系统功能」应用(此时五个功能的入口全部隐藏)
## 怎么确认
- 某一个功能的入口在应用中心、头像菜单、左侧导航同时消失:多半是该功能的开关被关闭
- 五个功能的入口同时消失:多半是「系统功能」应用被卸载
## 解决办法
- 普通成员:联系系统管理员启用
- 管理员:在应用商店打开「系统功能」应用,安装或重新安装并打开对应开关,步骤见 [[system-functions.toggle.howto]]
## 其他可能
- 「工作报告」在系统管理员的头像菜单里本来就不显示,管理员请从应用中心进入
- 「日历」卡片只在竖屏(移动端)的应用中心显示,桌面端在左侧导航
- 「签到打卡」还可能被管理员在「签到设置」里整体关闭

View File

@ -14,8 +14,9 @@ aliases:
related_tools: []
related_pages: []
prerequisites:
- 「系统功能」应用已安装且其中的「签到打卡」开关为开启(默认开启)
- 需要系统管理员身份
last_verified: v1.7.90
last_verified: v1.9.36
---
# 怎么导出签到数据
@ -50,3 +51,6 @@ last_verified: v1.7.90
- 不支持单次导出超过 100 个成员,更多需要分批
- 不支持按部门一键全选,需手动勾选
- 不支持原始多次打卡明细,只导首签和末签两个时间
## 入口不显示时
「导出签到数据」入口受「系统功能」应用控制:管理员在应用商店停用了「签到打卡」开关,或卸载了「系统功能」应用时,入口会统一隐藏(数据不会删除)。恢复方法见 [[system-functions.toggle.howto]]。

View File

@ -14,6 +14,7 @@ aliases:
related_tools: []
related_pages: [application]
prerequisites:
- 「系统功能」应用已安装且其中的「签到打卡」开关为开启(默认开启)
- 需要系统管理员权限
- 系统设置中签到功能已开启(`checkinSetting.open = open`)
negative:
@ -21,7 +22,7 @@ negative:
- 单次最多 100 个成员
- 日期范围最多 35 天
- 必填成员 / 日期 / 时间三个参数,缺一不可
last_verified: v1.7.90
last_verified: v1.9.36
---
# 导出签到数据
@ -52,3 +53,6 @@ last_verified: v1.7.90
- 不支持导出无班次成员
- 不支持跨月超 35 天一次导
- 不支持自定义结果判定规则(用系统全局设置)
## 入口不显示时
「导出签到数据」入口受「系统功能」应用控制:管理员在应用商店停用了「签到打卡」开关,或卸载了「系统功能」应用时,入口会统一隐藏(数据不会删除)。恢复方法见 [[system-functions.toggle.howto]]。

View File

@ -0,0 +1,53 @@
---
id: system-functions.toggle.howto
title: 怎么启用或停用系统功能(工作报告、日历、收藏、最近打开、签到)
type: howto
feature: system-functions
scope: admin
locale: zh
aliases:
- 怎么隐藏日历
- 关闭签到打卡入口
- 停用工作报告
- 恢复我的收藏入口
- 系统功能怎么设置
- 卸载系统功能
related_tools: []
related_pages: [application]
prerequisites:
- 需要系统管理员权限
- 应用商店(AppStore)可用
negative:
- 普通成员不能修改这些开关
- 停用或卸载不会删除任何数据,只隐藏入口
- 安装页没有「高级设置」,因为该应用不启动容器
last_verified: v1.9.36
---
# 怎么启用或停用系统功能(工作报告、日历、收藏、最近打开、签到)
## 入口
- 桌面端:左侧栏「应用」→ 管理员区「应用商店」→ 找到「系统功能」应用
## 操作步骤
1. 在应用商店打开「系统功能」应用
2. 进入该应用的安装页(已安装时按钮为「重新安装」,未安装时为「安装」)
3. 在「应用配置」里按需打开或关闭五个开关:工作报告、日历、我的收藏、最近打开、签到打卡
4. 点击「重新安装」提交,配置立即保存,无需等待容器启动
5. 回到主界面,入口随即按新配置显示或隐藏;其他成员刷新页面或重新进入「应用」后生效
## 字段默认值
| 开关 | 默认 |
|---|---|
| 工作报告 | 开启 |
| 日历 | 开启 |
| 我的收藏 | 开启 |
| 最近打开 | 开启 |
| 签到打卡 | 开启 |
## 一次隐藏全部入口
在应用商店卸载「系统功能」应用,五个功能的入口全部隐藏。重新安装后恢复。
## 相关
- 应用说明见 [[system-functions.overview.concept]]
- 入口找不到的排查见 [[system-functions.entry-missing.faq]]

View File

@ -13,12 +13,13 @@ aliases:
- calendar 在哪
related_tools: [list_tasks]
related_pages: [calendar]
prerequisites: []
prerequisites:
- 「系统功能」应用已安装且其中的「日历」开关为开启(默认开启)
negative:
- 日历只显示有 end_at 的任务,不显示会议、签到等其他事件
- 移动端日历只读,不能拖动改时间
- 日历不支持其他人的视图
last_verified: v1.7.90
last_verified: v1.9.36
---
# 日历入口在哪
@ -50,3 +51,6 @@ last_verified: v1.7.90
- 不显示会议(B2 模块)、签到、报告
- 不支持订阅他人日历
- 详见 [[calendar.meeting-not-shown.faq]]
## 入口不显示时
「日历」的所有入口受「系统功能」应用控制:管理员在应用商店停用了「日历」开关,或卸载了「系统功能」应用时,入口会统一隐藏(数据不会删除)。恢复方法见 [[system-functions.toggle.howto]]。

View File

@ -13,11 +13,12 @@ aliases:
- 签到打卡入口
related_tools: []
related_pages: [application]
prerequisites: []
prerequisites:
- 「系统功能」应用已安装且其中的「签到打卡」开关为开启(默认开启)
negative:
- 左侧栏没有「签到」一级入口,必须从「应用」展开
- 应用中心看不到「签到打卡」卡片时,多半是被管理员在「签到设置」里整体关闭了
last_verified: v1.7.90
last_verified: v1.9.36
---
# 签到打卡入口在哪
@ -43,3 +44,6 @@ last_verified: v1.7.90
- 任何登录成员都能看到「签到打卡」卡片
- 全局「签到设置」按钮仅管理员可见
- 个别能力(如人脸上传 / 修改 MAC)受「允许修改」开关控制,默认允许
## 入口不显示时
「签到打卡」的所有入口受「系统功能」应用控制:管理员在应用商店停用了「签到打卡」开关,或卸载了「系统功能」应用时,入口会统一隐藏(数据不会删除)。恢复方法见 [[system-functions.toggle.howto]]。

View File

@ -13,12 +13,13 @@ aliases:
- 最近浏览入口
related_tools: []
related_pages: [application]
prerequisites: []
prerequisites:
- 「系统功能」应用已安装且其中的「我的收藏」开关为开启(默认开启)
negative:
- 没有顶部导航的独立入口,必须通过「应用」中心进入
- 不在左侧栏一级菜单上,不会默认置顶
- 桌面端和移动端入口结构一致,没有平台差异化路径
last_verified: v1.7.90
last_verified: v1.9.36
---
# 我的收藏 / 最近打开入口
@ -55,3 +56,6 @@ last_verified: v1.7.90
- 添加收藏:[[favorite.add.howto]]
- 取消收藏:[[favorite.remove.howto]]
- 收藏与最近的概念区别:[[favorite.concept]]
## 入口不显示时
「我的收藏」的所有入口受「系统功能」应用控制:管理员在应用商店停用了「我的收藏」开关,或卸载了「系统功能」应用时,入口会统一隐藏(数据不会删除)。恢复方法见 [[system-functions.toggle.howto]]。

View File

@ -13,12 +13,13 @@ aliases:
- 打卡机器人在哪
related_tools: []
related_pages: [application, messenger]
prerequisites: []
prerequisites:
- 「系统功能」应用已安装且其中的「签到打卡」开关为开启(默认开启)
negative:
- 没有左侧一级菜单,必须从「应用」展开
- 不支持快捷键
- 「签到打卡」卡片被管理员关闭时不会显示
last_verified: v1.7.90
last_verified: v1.9.36
---
# 签到打卡入口在哪
@ -39,3 +40,6 @@ last_verified: v1.7.90
- 签到入口总览:[[checkin.entry.menu-map]]
- 怎么打卡:[[checkin.regular.howto]]
- 管理员配置规则:[[checkin.rule.concept]]
## 入口不显示时
「签到打卡」的所有入口受「系统功能」应用控制:管理员在应用商店停用了「签到打卡」开关,或卸载了「系统功能」应用时,入口会统一隐藏(数据不会删除)。恢复方法见 [[system-functions.toggle.howto]]。

View File

@ -12,11 +12,12 @@ aliases:
- 我收藏过的怎么看
related_tools: []
related_pages: [application]
prerequisites: []
prerequisites:
- 「系统功能」应用已安装且其中的「我的收藏」开关为开启(默认开启)
negative:
- 没有左侧栏一级入口,藏在「应用」中心和头像下拉里
- 没有快捷键
last_verified: v1.7.90
last_verified: v1.9.36
---
# 我的收藏入口在哪
@ -39,3 +40,6 @@ last_verified: v1.7.90
- 收藏入口与最近打开:[[favorite.entry.menu-map]]
- 添加收藏:[[favorite.add.howto]]
- 与最近打开的区别:[[favorite.concept]]
## 入口不显示时
「我的收藏」的所有入口受「系统功能」应用控制:管理员在应用商店停用了「我的收藏」开关,或卸载了「系统功能」应用时,入口会统一隐藏(数据不会删除)。恢复方法见 [[system-functions.toggle.howto]]。

View File

@ -12,11 +12,12 @@ aliases:
- 历史浏览记录
related_tools: []
related_pages: [application]
prerequisites: []
prerequisites:
- 「系统功能」应用已安装且其中的「最近打开」开关为开启(默认开启)
negative:
- 「最近打开」只记录任务 / 文件 / 任务附件 / 消息附件,不包含项目和会话
- 不支持手动清空全部记录
last_verified: v1.7.90
last_verified: v1.9.36
---
# 最近打开入口在哪
@ -39,3 +40,6 @@ last_verified: v1.7.90
## 相关
- 收藏与最近的总入口:[[favorite.entry.menu-map]]
## 入口不显示时
「最近打开」的所有入口受「系统功能」应用控制:管理员在应用商店停用了「最近打开」开关,或卸载了「系统功能」应用时,入口会统一隐藏(数据不会删除)。恢复方法见 [[system-functions.toggle.howto]]。

View File

@ -14,11 +14,12 @@ aliases:
- 打开汇报
related_tools: []
related_pages: []
prerequisites: []
prerequisites:
- 「系统功能」应用已安装且其中的「工作报告」开关为开启(默认开启)
negative:
- 「工作报告」不在左侧主导航栏,是右上角头像菜单 + 应用中心系统应用
- 普通成员菜单显示「工作报告」;系统管理员菜单中不直接显示该项,可改走应用中心
last_verified: v1.7.90
last_verified: v1.9.36
---
# 工作报告入口在哪
@ -41,3 +42,6 @@ last_verified: v1.7.90
## 权限要求
- end-user 可见,所有登录用户都能写和收报告
- 不需要插件,是主程序内置功能
## 入口不显示时
「工作报告」的所有入口受「系统功能」应用控制:管理员在应用商店停用了「工作报告」开关,或卸载了「系统功能」应用时,入口会统一隐藏(数据不会删除)。恢复方法见 [[system-functions.toggle.howto]]。

View File

@ -206,6 +206,9 @@ class ActionExecutor {
* 导航到日历页面
*/
async navigateToCalendar() {
if (!this.store?.getters?.systemFunctionEnabled?.('calendar')) {
throw new Error('日历功能未启用');
}
window.$A.goForward({ name: 'manage-calendar' });
return { navigated: true, page: 'calendar' };
}

View File

@ -82,7 +82,7 @@
<DropdownItem name="allUser">{{$L('团队管理')}}</DropdownItem>
<DropdownItem name="exportTask">{{$L('导出任务统计')}}</DropdownItem>
<DropdownItem name="exportOverdueTask">{{$L('导出超期任务')}}</DropdownItem>
<DropdownItem name="exportCheckin">{{$L('导出签到数据')}}</DropdownItem>
<DropdownItem v-if="systemFunctionEnabled('checkin')" name="exportCheckin">{{$L('导出签到数据')}}</DropdownItem>
</DropdownMenu>
</Dropdown>
<!-- 部门负责人视角 -->
@ -141,7 +141,7 @@
<Badge v-else-if="dashboardTask.today_count > 0" class="menu-badge" type="info" :overflow-count="999" :count="dashboardTask.today_count"/>
<Badge v-else-if="dashboardTask.todo_count > 0" class="menu-badge" type="primary" :overflow-count="999" :count="dashboardTask.todo_count"/>
</li>
<li @click="toggleRoute('calendar')" :class="classNameRoute('calendar')">
<li v-if="systemFunctionEnabled('calendar')" @click="toggleRoute('calendar')" :class="classNameRoute('calendar')">
<i class="taskfont">&#xe6f5;</i>
<div class="menu-title">{{$L('日历')}}</div>
</li>
@ -655,7 +655,7 @@ export default {
'mcpServerStatus'
]),
...mapGetters(['aiVisible', 'dashboardTask', "filterMicroAppsMenusMain", 'visibleDialogs']),
...mapGetters(['aiVisible', 'dashboardTask', "filterMicroAppsMenusMain", 'visibleDialogs', 'systemFunctionEnabled']),
// 父『应用』入口聚合角标(规则收敛在 appBadges 模块 getter)
...mapGetters('appBadges', {
@ -663,6 +663,14 @@ export default {
applicationBadgeDot: 'applicationDot',
}),
/**
* 当前处于日历页且日历已明确停用(开关尚未加载时不判定为停用)
* @returns {boolean}
*/
calendarDisabled() {
return this.routeName === 'manage-calendar' && this.$store.state.systemFunctions.calendar === false;
},
departmentOwnerViewAvailable() {
return this.systemConfig.department_owner_project_view === 'open' && (this.userInfo.managed_departments || []).length > 0;
},
@ -783,51 +791,64 @@ export default {
* @returns {Array}
*/
menu() {
const {userIsAdmin} = this;
const array = [
{path: 'taskBrowse', name: '最近打开的任务'},
{path: 'favorite', name: '我的收藏'},
{path: 'download', name: '下载内容', visible: !!this.$Electron},
{path: 'mcpServer', name: '启用桌面 MCP 服务器', visible: !!this.$Electron, selected: this.mcpServerStatus.running === 'running'},
const {userIsAdmin, systemFunctionEnabled} = this;
// 按分组组织:每组(第一组除外)的首个可见项显示分割线。
// 先过滤不可见的菜单项再计算分割线,隐藏任意项都不会留下多余的分割线。
const groups = [
[
{path: 'taskBrowse', name: '最近打开的任务', visible: systemFunctionEnabled('recent')},
{path: 'favorite', name: '我的收藏', visible: systemFunctionEnabled('favorite')},
{path: 'download', name: '下载内容', visible: !!this.$Electron},
{path: 'mcpServer', name: '启用桌面 MCP 服务器', visible: !!this.$Electron, selected: this.mcpServerStatus.running === 'running'},
],
];
const departmentOwnerView = this.departmentOwnerViewAvailable ? [{
path: 'departmentOwnerView',
name: '负责人视角',
selected: this.departmentOwnerProjectViewEnabled,
selectedCount: this.departmentOwnerProjectViewEnabled ? this.cacheDepartmentOwnerIds.length : 0,
}] : [];
if (userIsAdmin) {
array.push(...[
{path: 'personal', name: '个人设置', divided: true},
{path: 'system', name: '系统设置'},
{path: 'license', name: 'License Key'},
{path: 'downloadClient', name: '客户端下载', divided: true, visible: this.showDownloadClient},
{path: 'version', name: '更新版本', divided: true, visible: !!this.clientNewVersion},
{path: 'allProject', name: '所有项目', divided: true},
{path: 'archivedProject', name: '已归档的项目'},
{path: 'team', name: '团队管理', divided: true},
])
groups.push(
[
{path: 'personal', name: '个人设置'},
{path: 'system', name: '系统设置'},
{path: 'license', name: 'License Key'},
],
[{path: 'downloadClient', name: '客户端下载', visible: this.showDownloadClient}],
[{path: 'version', name: '更新版本', visible: !!this.clientNewVersion}],
[
{path: 'allProject', name: '所有项目'},
{path: 'archivedProject', name: '已归档的项目'},
],
[
{path: 'team', name: '团队管理'},
...departmentOwnerView,
],
)
} else {
array.push(...[
{path: 'personal', name: '个人设置', divided: true},
{path: 'downloadClient', name: '客户端下载', divided: true, visible: this.showDownloadClient},
{path: 'version', name: '更新版本', divided: true, visible: !!this.clientNewVersion},
{path: 'workReport', name: '工作报告', divided: true},
{path: 'archivedProject', name: '已归档的项目'},
])
groups.push(
[{path: 'personal', name: '个人设置'}],
[{path: 'downloadClient', name: '客户端下载', visible: this.showDownloadClient}],
[{path: 'version', name: '更新版本', visible: !!this.clientNewVersion}],
[
{path: 'workReport', name: '工作报告', visible: systemFunctionEnabled('report')},
{path: 'archivedProject', name: '已归档的项目'},
],
departmentOwnerView,
)
}
if (this.departmentOwnerViewAvailable) {
array.push({
path: 'departmentOwnerView',
name: '负责人视角',
divided: !userIsAdmin,
visible: true,
selected: this.departmentOwnerProjectViewEnabled,
selectedCount: this.departmentOwnerProjectViewEnabled ? this.cacheDepartmentOwnerIds.length : 0,
});
}
array.push(...[
{path: 'clearCache', name: '清除缓存', divided: true},
groups.push([
{path: 'clearCache', name: '清除缓存'},
{path: 'logout', name: '退出登录', style: {color: '#f40'}}
])
const array = [];
groups.forEach(group => {
const items = group.filter(item => item.visible !== false);
items.forEach((item, index) => {
array.push(Object.assign({}, item, {divided: index === 0 && array.length > 0}));
});
});
return array
},
@ -913,6 +934,16 @@ export default {
this.chackPass();
},
// 日历被停用(或「系统功能」已卸载)时,不允许停留在日历页
calendarDisabled: {
handler(disabled) {
if (disabled) {
this.goForward({name: 'manage-dashboard'}, true);
}
},
immediate: true
},
userInfo() {
this.chackPass();
},

View File

@ -541,15 +541,16 @@ export default {
...mapGetters([
'filterMicroAppsMenus',
'filterMicroAppsMenusAdmin',
'systemFunctionEnabled',
]),
applyList() {
const list = [
// 常用应用
{value: "favorite", label: "我的收藏", sort: 45},
{value: "recent", label: "最近打开", sort: 47},
{value: "report", label: "工作报告", sort: 50},
{value: "favorite", label: "我的收藏", sort: 45, show: this.systemFunctionEnabled('favorite')},
{value: "recent", label: "最近打开", sort: 47, show: this.systemFunctionEnabled('recent')},
{value: "report", label: "工作报告", sort: 50, show: this.systemFunctionEnabled('report')},
{value: "mybot", label: "我的机器人", sort: 55},
{value: "signin", label: "签到打卡", sort: 70},
{value: "signin", label: "签到打卡", sort: 70, show: this.systemFunctionEnabled('checkin')},
{value: "meeting", label: "在线会议", sort: 80},
{value: "createGroup", label: "创建群组", sort: 85},
{value: "word-chain", label: "群接龙", sort: 90},
@ -569,7 +570,7 @@ export default {
// 竖屏模式
if (this.windowPortrait) {
list.push(...[
{value: "calendar", label: "日历", sort: 10},
{value: "calendar", label: "日历", sort: 10, show: this.systemFunctionEnabled('calendar')},
{value: "file", label: "文件", sort: 20},
{value: "setting", label: "设置", sort: 140},
])
@ -992,8 +993,10 @@ export default {
const list = [
{label: this.$L('导出任务统计'), value: 'task'},
{label: this.$L('导出超期任务'), value: 'overdue'},
{label: this.$L('导出签到数据'), value: 'checkin'},
];
if (this.systemFunctionEnabled('checkin')) {
list.push({label: this.$L('导出签到数据'), value: 'checkin'});
}
this.$store.commit('menu/operation', {
event: menuEvent,
list,

View File

@ -1206,7 +1206,8 @@ export default {
'taskRelatedCache',
'dialogCommonCountCache',
'collaborationFileCache',
'mcpServerStatus'
'mcpServerStatus',
'systemFunctions'
]
};
@ -5500,6 +5501,11 @@ export default {
* @param dispatch
*/
async updateMicroAppsStatus({commit, state, dispatch}) {
// 系统功能开关:不依赖应用商店接口,商店不可用时也能更新
dispatch('call', {url: 'apps/functions'}).then(({data}) => {
commit("systemFunctions/save", data?.functions)
}).catch(() => {})
//
const {data: {code, data}} = await axios.get($A.mainUrl('appstore/api/v1/internal/installed'), {
headers: {
Token: state.userToken,

View File

@ -308,6 +308,17 @@ export default {
return state.cacheDialogs.filter(dialog => !$A.isAiBotDialog(dialog))
},
/**
* 系统功能是否启用(「系统功能」应用未安装或该功能被停用时为 false)
* 工作报告、日历、我的收藏、最近打开、签到打卡的所有入口统一以此判断
*
* @param {Object} state
* @returns {function(string): boolean} 参数为功能标识:report/calendar/favorite/recent/checkin
*/
systemFunctionEnabled: (state) => (name) => {
return state.systemFunctions?.[name] === true
},
/**
* 获取应用菜单
* 过滤出location为application的菜单项

View File

@ -122,10 +122,11 @@ export default {
},
/**
* 父『应用』入口聚合数字:工作报告未读 + 所有插件角标数字之和
* 父『应用』入口聚合数字:工作报告未读(功能启用时)+ 所有插件角标数字之和
*/
applicationCount: (state, getters, rootState) => {
return (Number(rootState.reportUnreadNumber) || 0) + getters.totalCount
applicationCount: (state, getters, rootState, rootGetters) => {
const reportCount = rootGetters.systemFunctionEnabled('report') ? (Number(rootState.reportUnreadNumber) || 0) : 0
return reportCount + getters.totalCount
},
/**

View File

@ -463,6 +463,10 @@ export default {
// 更新菜单
const menus = [];
data.forEach((item) => {
// 「系统功能」只控制内置入口的显示,自身不产生应用菜单,避免与内置入口重复
if (item.id === 'system-functions') {
return;
}
if (Array.isArray(item.menu_items) && item.menu_items.length > 0) {
menus.push(...item.menu_items.map(menu => Object.assign({}, menu, {id: item.id})));
}
@ -483,6 +487,11 @@ export default {
$A.IDBSave("microAppsMenus", state.microAppsMenus = menus);
},
// 系统功能开关
'systemFunctions/save': function(state, data) {
$A.IDBSave("systemFunctions", state.systemFunctions = $A.isJson(data) ? data : {});
},
// MCP 服务器状态
'mcp/server/status': function(state, data) {
$A.IDBSave("mcpServerStatus", state.mcpServerStatus = data);

View File

@ -301,6 +301,9 @@ export default {
microAppsIds: [],
microAppsMenus: [],
// 系统功能开关(「系统功能」应用,功能标识 => 是否启用;未安装时全部为 false)
systemFunctions: {},
// MCP 服务器状态
mcpServerStatus: {running: 'stopped'},
};

View File

@ -2,7 +2,7 @@
> 此文件由 `php artisan doc:api-map` 生成,勿手改。
接口总数:329
接口总数:330
## 路由规则
@ -420,6 +420,7 @@ API 使用动态路由(见 `routes/web.php`),URL 段映射为控制器方
| api/apps/badge/set | badge__set() | post | 设置角标(应用密钥鉴权) |
| api/apps/badge/clear | badge__clear() | post | 清除角标(当前用户 token 鉴权) |
| api/apps/badge/list | badge__list() | get | 拉取自己全部角标 |
| api/apps/functions | functions() | get | 获取系统功能开关 |
## test(TestController)