mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 02:12:53 +00:00
Compare commits
10 Commits
f65da118d7
...
ff53e1fac3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff53e1fac3 | ||
|
|
cf4894b7c3 | ||
|
|
678dfd2d5c | ||
|
|
bf4a62ae04 | ||
|
|
7e6f3f92cf | ||
|
|
df382dafb4 | ||
|
|
10925d3a47 | ||
|
|
66252072c7 | ||
|
|
29918882bd | ||
|
|
4983fe8feb |
4
.gitignore
vendored
4
.gitignore
vendored
@ -23,6 +23,9 @@
|
||||
vars.yaml
|
||||
|
||||
# IDE and editor files
|
||||
.cursor/*
|
||||
!.cursor/rules/
|
||||
!.cursor/rules/**
|
||||
.idea
|
||||
.vscode
|
||||
.windsurfrules
|
||||
@ -57,5 +60,4 @@ laravels.pid
|
||||
.DS_Store
|
||||
|
||||
# Documentation
|
||||
AGENTS.md
|
||||
README_LOCAL.md
|
||||
|
||||
127
AGENTS.md
Normal file
127
AGENTS.md
Normal file
@ -0,0 +1,127 @@
|
||||
# DooTask 项目说明
|
||||
|
||||
## 一、项目总览
|
||||
|
||||
- **项目定位**:DooTask 是一套开源的任务 / 项目管理系统,支持看板、任务、子任务、评论、对话、文件、报表等协作能力。
|
||||
- **后端技术栈**
|
||||
- 基于 Laravel(运行在 LaravelS / Swoole 常驻进程上),代码集中在 `app/`、`routes/`、`config/` 等目录。
|
||||
- 数据库通过 Laravel Eloquent 模型访问,所有表结构变更必须通过 migration 完成,禁止直接手工改库。
|
||||
- **前端技术栈**
|
||||
- 主 Web 前端基于 Vue2 + Vite,代码集中在 `resources/assets/js`。
|
||||
- 打包与开发通过根目录的 `./cmd` 脚本间接调用 Vite。
|
||||
- **桌面端**
|
||||
- 使用 Electron 作为桌面壳,核心业务逻辑仍在 Web 前端与 Laravel 后端中。
|
||||
|
||||
更多安装、升级、迁移说明见根目录 `README.md`。
|
||||
|
||||
## 二、开发与运行(命令约定)
|
||||
|
||||
- 开发 / 构建命令统一通过根目录的 `./cmd` 脚本执行,以保证与 Docker / 容器环境一致:
|
||||
- 启动服务:`./cmd up`
|
||||
- 停止服务:`./cmd down`
|
||||
- 重启服务:`./cmd reup` 或 `./cmd restart`
|
||||
- Laravel 工具:`./cmd artisan ...`
|
||||
- 前端开发:`./cmd dev`
|
||||
- 前端构建:`./cmd prod` 或 `./cmd build`
|
||||
- 其他工具:`./cmd composer ...`、`./cmd php ...`、`./cmd doc` 等
|
||||
- 在示例、脚本与回答中,优先使用 `./cmd ...` 形式,而不是直接调用 `php`、`composer`、`npm` 等命令。
|
||||
|
||||
## 三、代码结构(后端 + 前端)
|
||||
|
||||
- **Controller(`app/Http/Controllers`)**
|
||||
- 负责路由入口、参数接收与基础校验,编排调用模型 / 模块,并组装 API 响应。
|
||||
- 原则:控制器尽量保持「薄」,复杂业务逻辑不要堆积在控制器中。
|
||||
- 业务异常优先使用 `App\Exceptions\ApiException` 抛出,由全局 Handler 统一转换为标准 JSON 响应。
|
||||
|
||||
- **Model(`app/Models`)**
|
||||
- 负责数据表结构映射、关系(relations)、访问器 / 修改器、自定义查询 Scope 等「数据层」逻辑。
|
||||
- 避免在模型方法中塞入大量跨业务的流程控制逻辑,复杂业务应下沉到模块中。
|
||||
|
||||
- **Module(`app/Module`)**
|
||||
- 承载跨控制器 / 跨模型的业务逻辑与独立功能子域,例如:
|
||||
- 外部服务集成:`AgoraIO/*`、`ZincSearch/*` 等;
|
||||
- 通用工具:`Lock.php`、`TextExtractor.php`、`Image.php` 等;
|
||||
- 项目 / 任务 / 对话等领域里的复杂协作逻辑。
|
||||
- 原则:
|
||||
- 新增较复杂的业务功能时,优先考虑创建 / 扩展 Module,而不是在 Controller 或 Model 中堆砌流程。
|
||||
- Module 尽量保持单一职责与可复用,命名能直接反映其业务或能力作用。
|
||||
|
||||
- **运行环境注意事项(LaravelS / Swoole)**
|
||||
- 避免在静态属性、单例、全局变量中存储请求级状态或可变数据,防止请求间数据串联和内存泄漏。
|
||||
- 不要假设构造函数、服务提供者或 `boot()` 方法会在每个请求重新执行;涉及配置、路由等改动时,通常需要通过 `./cmd php restart` 或容器重启后才能生效。
|
||||
- 编写长连接、定时任务、WebSocket 等长生命周期逻辑时,优先复用现有模式,并避免长时间阻塞协程 / 事件循环的操作。
|
||||
|
||||
- **前端(`resources/assets/js`,Vue2 + Vite)**
|
||||
- 结构大致包括:
|
||||
- `app.js`、`App.vue`:应用入口与根组件;
|
||||
- `components/`:通用与业务组件(任务看板、文件预览、聊天等);
|
||||
- `pages/`:页面级组件(登录、项目、任务视图、消息、报表等);
|
||||
- `store/`:Vuex 全局状态管理;
|
||||
- `routes.js`:前端路由配置。
|
||||
- 构建与开发:
|
||||
- 开发模式:使用 `./cmd dev` 或类似子命令,内部通过 Vite 启动开发服务器。
|
||||
- 生产构建:使用 `./cmd prod` 或 `./cmd build`,内部通过 Vite 产出前端静态资源。
|
||||
- 与后端接口协作:
|
||||
- 接口调用默认通过已有的 Vuex 封装发起请求,新增接口时优先扩展集中封装,而不是在组件中直接散落 `axios/fetch`。
|
||||
|
||||
- **Electron**
|
||||
- Electron 主要作为桌面入口壳,核心业务逻辑仍在 Web/Vue2 前端与 PHP/Laravel 后端。
|
||||
- 日常开发与调试优先使用 `./cmd electron ...`;需要构建 App 端资源时使用 `./cmd appbuild`。
|
||||
- 原则:优先保证 Web 端行为正确,再通过 Electron 壳复用 Web 逻辑;桌面专有能力(本地文件、托盘等)需在代码中明确边界。
|
||||
|
||||
## 四、在本项目中使用 Graphiti 作为长期记忆
|
||||
|
||||
- **角色与 group_id**
|
||||
- Graphiti 作为本项目的「长期记忆层」,用于持久化:
|
||||
- 用户偏好(Preferences)、工作流程 / 习惯(Procedures)、重要约束(Requirements)、关键事实 / 关系(Facts)。
|
||||
- 目标是:跨对话、跨任务保持一致的行为和决策,而不是简单堆积信息。
|
||||
- 本项目统一使用的 `group_id`:`dootask-main`。
|
||||
|
||||
- **任务开始前(读)**
|
||||
- 在进行实质性工作(写代码、设计方案、做大改动)前,应先通过 Graphiti 查询已有记忆:
|
||||
- 使用节点搜索(如 `search_nodes`)在 `group_id = "dootask-main"` 下查找与当前任务相关的 Preference / Procedure / Requirement;
|
||||
- 使用事实搜索(如 `search_facts`)查找相关事实与实体关系;
|
||||
- 查询语句中可包含:任务类型(Bug 修复 / 重构 / 新功能等)、涉及模块(任务、项目、对话、WebSocket、报表等)以及关键字 `dootask`。
|
||||
- 发现与当前任务高度相关的偏好 / 流程 / 约束时,应优先遵守;如存在冲突,应在回答中说明并做合理选择。
|
||||
|
||||
- **什么时候写入 Graphiti(写)**
|
||||
- **偏好(Preferences)**:用户表达持续性偏好时(语言、输出格式、技术选型等),应尽快写入;
|
||||
- **流程 / 习惯(Procedures)**:形成「以后都按这个流程来」的稳定开发 / 发布 / 调试流程时,应记录为可复用步骤;
|
||||
- **约束 / 决策(Requirements)**:项目长期有效的决策,如不再支持某版本、某模块的架构约定等;
|
||||
- **事实 / 关系(Facts)**:模块边界约定、服务之间的调用关系、与外部系统(如 AgoraIO、ZincSearch)集成方式等。
|
||||
- 写入建议:
|
||||
- 默认使用 `source: "text"`,在 `episode_body` 中用简洁结构化自然语言描述背景、类型、范围、具体内容;
|
||||
- 需要结构化数据时可用 `source: "json"`,保证 `episode_body` 是合法 JSON 字符串;
|
||||
- 所有写入默认使用 `group_id: "dootask-main"`。
|
||||
|
||||
- **更新与更正**
|
||||
- 偏好 / 流程发生变化时,新增一条 episode 说明新约定,并标明这是对旧习惯的更新,后续以最新、最明确的为准;
|
||||
- 用户要求「忘记」某些记忆时,可通过删除或更正相关 episode / 关系的方式处理;
|
||||
- 尽量通过新增 episode 记录「更正 / 废弃说明」,而不是直接改写历史事实。
|
||||
|
||||
- **在工作中的使用方式**
|
||||
- 尊重已存偏好:编码风格、回答结构、工具选择等应对齐已知偏好;
|
||||
- 遵循已有流程:若图谱中已有与当前任务匹配的 Procedure,应尽量按步骤执行;
|
||||
- 利用事实:理解系统行为、模块边界、历史决策时优先查已存 Facts,减少重新摸索;
|
||||
- 如 Graphiti 与当前代码实际冲突,应以代码实际为准,并视情况新增 episode 更新事实。
|
||||
|
||||
- **不要写入 Graphiti 的内容**
|
||||
- 含敏感信息(密钥、密码、隐私数据等);
|
||||
- 只与当前一次任务相关、未来不会复用的临时信息(调试日志、一次性命令输出等);
|
||||
- 体量巨大的原始数据(完整日志、长脚本全文等),应只存摘要和关键结论。
|
||||
|
||||
- **最佳实践小结**
|
||||
- 先查再做:在提出方案或改动架构前,优先查阅 Graphiti 中已有的设计、偏好和约束;
|
||||
- 能复用就沉淀:只要发现某个偏好 / 流程 / 约束未来会反复用到,就尽快写入 Graphiti,而不是只放在当前对话里;
|
||||
- 保持项目内外一致:确保 Graphiti 中的记忆与实际代码长期保持一致,避免「记忆漂移」。
|
||||
|
||||
## 五、前端弹窗文案
|
||||
|
||||
- 在前端 Vue 代码中调用 `$A.modalXXX`、`$A.messageXXX`、`$A.noticeXXX` 时,这些方法内部会统一处理 `$L` 翻译,调用方默认不要再额外包一层 `$L`。
|
||||
- 仅当 `modalXXX` 特殊场景显式传入 `language: false`(关闭内部自动翻译)时,才由调用方在传入前自行决定是否使用 `$L` 处理文案。
|
||||
|
||||
## 六、AI 回复风格与语言偏好
|
||||
|
||||
- 总体说明与重要总结(尤其是最终回答的 recap 部分),在不影响技术表达准确性的前提下,应优先使用简体中文进行回复。
|
||||
- 如用户在对话中明确要求使用其他语言(例如英文),则以用户的显式指令为最高优先级。
|
||||
- 当本次协作的改动已经较为完整且自然形成一个提交单元时,应在最终回答中附带一条或数条推荐的 Git 提交 message,方便用户直接复制使用。
|
||||
@ -722,6 +722,47 @@ class SystemController extends AbstractController
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/microapp_menu 自定义应用菜单
|
||||
*
|
||||
* @apiDescription 获取或保存自定义微应用菜单,仅管理员可配置
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName microapp_menu
|
||||
*
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - save: 保存(限管理员)
|
||||
* @apiParam {Array} list 菜单列表,格式:[{id,name,version,menu_items}]
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function microapp_menu()
|
||||
{
|
||||
$type = trim(Request::input('type'));
|
||||
$user = User::auth();
|
||||
if ($type == 'save') {
|
||||
User::auth('admin');
|
||||
$list = Request::input('list');
|
||||
if (empty($list) || !is_array($list)) {
|
||||
$list = [];
|
||||
}
|
||||
$apps = Setting::normalizeCustomMicroApps($list);
|
||||
$setting = Base::setting('microapp_menu', $apps);
|
||||
$setting = Setting::formatCustomMicroAppsForResponse($setting);
|
||||
} else {
|
||||
$setting = Base::setting('microapp_menu');
|
||||
if (!is_array($setting)) {
|
||||
$setting = [];
|
||||
}
|
||||
$setting = Setting::filterCustomMicroAppsForUser($setting, $user);
|
||||
$setting = Setting::formatCustomMicroAppsForResponse($setting);
|
||||
}
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/column/template 创建项目模板
|
||||
*
|
||||
|
||||
@ -164,6 +164,213 @@ class Setting extends AbstractModel
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范自定义微应用配置
|
||||
* @param array $list
|
||||
* @return array
|
||||
*/
|
||||
public static function normalizeCustomMicroApps($list)
|
||||
{
|
||||
if (!is_array($list)) {
|
||||
return [];
|
||||
}
|
||||
$apps = [];
|
||||
foreach ($list as $item) {
|
||||
$app = self::normalizeCustomMicroAppItem($item);
|
||||
if ($app) {
|
||||
$apps[] = $app;
|
||||
}
|
||||
}
|
||||
return $apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户身份过滤可见的自定义微应用
|
||||
* @param array $apps
|
||||
* @param \App\Models\User|null $user
|
||||
* @return array
|
||||
*/
|
||||
public static function filterCustomMicroAppsForUser(array $apps, $user)
|
||||
{
|
||||
if (empty($apps)) {
|
||||
return [];
|
||||
}
|
||||
$isAdmin = $user ? $user->isAdmin() : false;
|
||||
$userId = $user ? intval($user->userid) : 0;
|
||||
$filtered = [];
|
||||
foreach ($apps as $app) {
|
||||
$visible = self::normalizeCustomMicroVisible($app['visible_to'] ?? ['admin']);
|
||||
if (!self::isCustomMicroVisibleTo($visible, $isAdmin, $userId)) {
|
||||
continue;
|
||||
}
|
||||
if (empty($app['menu_items']) || !is_array($app['menu_items'])) {
|
||||
continue;
|
||||
}
|
||||
$menus = array_values(array_filter($app['menu_items'], function ($menu) use ($isAdmin, $userId) {
|
||||
if (!isset($menu['visible_to'])) {
|
||||
return true;
|
||||
}
|
||||
$visible = self::normalizeCustomMicroVisible($menu['visible_to']);
|
||||
return self::isCustomMicroVisibleTo($visible, $isAdmin, $userId);
|
||||
}));
|
||||
if (empty($menus)) {
|
||||
continue;
|
||||
}
|
||||
$app['menu_items'] = $menus;
|
||||
$filtered[] = $app;
|
||||
}
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将存储结构转换成 appstore 接口同款格式
|
||||
* @param array $apps
|
||||
* @return array
|
||||
*/
|
||||
public static function formatCustomMicroAppsForResponse(array $apps)
|
||||
{
|
||||
return array_values(array_map(function ($app) {
|
||||
unset($app['visible_to']);
|
||||
if (!empty($app['menu_items']) && is_array($app['menu_items'])) {
|
||||
$app['menu_items'] = array_values(array_map(function ($menu) {
|
||||
$menu['keep_alive'] = isset($menu['keep_alive']) ? (bool)$menu['keep_alive'] : true;
|
||||
$menu['disable_scope_css'] = (bool)($menu['disable_scope_css'] ?? false);
|
||||
$menu['auto_dark_theme'] = isset($menu['auto_dark_theme']) ? (bool)$menu['auto_dark_theme'] : true;
|
||||
$menu['transparent'] = (bool)($menu['transparent'] ?? false);
|
||||
if (isset($menu['visible_to'])) {
|
||||
unset($menu['visible_to']);
|
||||
}
|
||||
return $menu;
|
||||
}, $app['menu_items']));
|
||||
}
|
||||
return $app;
|
||||
}, $apps));
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范自定义微应用
|
||||
* @param array $item
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function normalizeCustomMicroAppItem($item)
|
||||
{
|
||||
if (!is_array($item)) {
|
||||
return null;
|
||||
}
|
||||
$id = trim($item['id'] ?? '');
|
||||
if ($id === '') {
|
||||
return null;
|
||||
}
|
||||
$name = Base::newTrim($item['name'] ?? '');
|
||||
$version = Base::newTrim($item['version'] ?? '') ?: 'custom';
|
||||
$menuItems = [];
|
||||
if (isset($item['menu_items']) && is_array($item['menu_items'])) {
|
||||
$menuItems = $item['menu_items'];
|
||||
} elseif (isset($item['menu']) && is_array($item['menu'])) {
|
||||
$menuItems = [$item['menu']];
|
||||
}
|
||||
if (empty($menuItems)) {
|
||||
return null;
|
||||
}
|
||||
$normalizedMenus = [];
|
||||
foreach ($menuItems as $menu) {
|
||||
$formattedMenu = self::normalizeCustomMicroMenuItem($menu, $name ?: $id);
|
||||
if ($formattedMenu) {
|
||||
$normalizedMenus[] = $formattedMenu;
|
||||
}
|
||||
}
|
||||
if (empty($normalizedMenus)) {
|
||||
return null;
|
||||
}
|
||||
return Base::newTrim([
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
'version' => $version,
|
||||
'menu_items' => $normalizedMenus,
|
||||
'visible_to' => self::normalizeCustomMicroVisible($item['visible_to'] ?? 'admin'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范自定义微应用菜单项
|
||||
* @param array $menu
|
||||
* @param string $fallbackLabel
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function normalizeCustomMicroMenuItem($menu, $fallbackLabel = '')
|
||||
{
|
||||
if (!is_array($menu)) {
|
||||
return null;
|
||||
}
|
||||
$url = trim($menu['url'] ?? '');
|
||||
if ($url === '') {
|
||||
return null;
|
||||
}
|
||||
$location = trim($menu['location'] ?? 'application');
|
||||
$label = trim($menu['label'] ?? $fallbackLabel);
|
||||
$urlType = strtolower(trim($menu['url_type'] ?? 'iframe'));
|
||||
$payload = [
|
||||
'location' => $location,
|
||||
'label' => $label,
|
||||
'icon' => Base::newTrim($menu['icon'] ?? ''),
|
||||
'url' => $url,
|
||||
'url_type' => $urlType,
|
||||
'keep_alive' => isset($menu['keep_alive']) ? (bool)$menu['keep_alive'] : true,
|
||||
'disable_scope_css' => (bool)($menu['disable_scope_css'] ?? false),
|
||||
'auto_dark_theme' => isset($menu['auto_dark_theme']) ? (bool)$menu['auto_dark_theme'] : true,
|
||||
'transparent' => (bool)($menu['transparent'] ?? false),
|
||||
];
|
||||
if (!empty($menu['background'])) {
|
||||
$payload['background'] = Base::newTrim($menu['background']);
|
||||
}
|
||||
if (!empty($menu['capsule']) && is_array($menu['capsule'])) {
|
||||
$payload['capsule'] = Base::newTrim($menu['capsule']);
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范自定义微应用可见范围
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
protected static function normalizeCustomMicroVisible($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$list = array_filter(array_map('trim', $value));
|
||||
} else {
|
||||
$list = array_filter(array_map('trim', explode(',', (string)$value)));
|
||||
}
|
||||
if (empty($list)) {
|
||||
return ['admin'];
|
||||
}
|
||||
if (in_array('all', $list)) {
|
||||
return ['all'];
|
||||
}
|
||||
return array_values($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断自定义微应用是否可见
|
||||
* @param array $visible
|
||||
* @param bool $isAdmin
|
||||
* @param int $userId
|
||||
* @return bool
|
||||
*/
|
||||
protected static function isCustomMicroVisibleTo(array $visible, bool $isAdmin, int $userId)
|
||||
{
|
||||
if (in_array('all', $visible)) {
|
||||
return true;
|
||||
}
|
||||
if ($isAdmin && in_array('admin', $visible)) {
|
||||
return true;
|
||||
}
|
||||
if ($userId > 0 && in_array((string)$userId, $visible, true)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱地址(过滤忽略地址)
|
||||
* @param $array
|
||||
|
||||
@ -1301,7 +1301,7 @@ class Base
|
||||
/**
|
||||
* 获取或设置
|
||||
* @param $setname // 配置名称
|
||||
* @param bool $array // 保存内容
|
||||
* @param bool|array $array // 保存内容
|
||||
* @param bool $isUpdate // 保存内容为更新模式,默认否
|
||||
* @return array
|
||||
*/
|
||||
|
||||
@ -96,10 +96,10 @@ services:
|
||||
appstore:
|
||||
container_name: "dootask-appstore-${APP_ID}"
|
||||
privileged: true
|
||||
image: "dootask/appstore:0.3.2"
|
||||
image: "dootask/appstore:0.3.3"
|
||||
volumes:
|
||||
- shared_data:/usr/share/dootask
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${HOST_DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
- ./:/var/www
|
||||
environment:
|
||||
HOST_PWD: "${PWD}"
|
||||
|
||||
14
electron/lib/utils.js
vendored
14
electron/lib/utils.js
vendored
@ -109,14 +109,22 @@ const utils = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 兜底处理尺寸类数值,确保传入的是有限数字
|
||||
* 兜底处理尺寸类数值,返回四舍五入后的正整数
|
||||
* @param value
|
||||
* @param fallback
|
||||
* @returns {number}
|
||||
*/
|
||||
normalizeSize(value, fallback) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
const toPositiveNumber = (candidate) => {
|
||||
const num = Number(candidate);
|
||||
return Number.isFinite(num) && num > 0 ? num : null;
|
||||
};
|
||||
|
||||
const primary = toPositiveNumber(value);
|
||||
const secondary = toPositiveNumber(fallback);
|
||||
const safeValue = primary ?? secondary ?? 1;
|
||||
|
||||
return Math.max(1, Math.round(safeValue));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -2267,4 +2267,8 @@ AI 分析已更新
|
||||
归档已完成任务
|
||||
你确定将列表【(*)】中所有已完成的任务归档吗?
|
||||
已归档列表中所有已完成任务
|
||||
归档失败,请稍后再试
|
||||
归档失败,请稍后再试
|
||||
|
||||
请输入 URL
|
||||
URL不能为空
|
||||
仅管理员可使用此功能
|
||||
5
public/images/application/appstore-default.svg
Normal file
5
public/images/application/appstore-default.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M0 0m349.206261 0l325.587478 0q349.206261 0 349.206261 349.206261l0 325.587478q0 349.206261-349.206261 349.206261l-325.587478 0q-349.206261 0-349.206261-349.206261l0-325.587478q0-349.206261 349.206261-349.206261Z" fill="#84c56a" class="selected"></path>
|
||||
<path d="M442.189913 298.284522c9.728 0 18.053565 3.372522 24.976696 10.128695 6.912 6.745043 10.373565 14.981565 10.373565 24.731826v140.566261c0 9.750261-3.450435 18.086957-10.373565 25.021218a34.003478 34.003478 0 0 1-24.976696 10.395826H301.924174a33.090783 33.090783 0 0 1-24.698435-10.395826A34.626783 34.626783 0 0 1 267.130435 473.711304V333.145043c0-9.750261 3.361391-17.986783 10.095304-24.742956 6.733913-6.745043 14.970435-10.117565 24.698435-10.117565h140.265739zM442.189913 579.417043c9.728 0 18.053565 3.372522 24.976696 10.128696 6.912 6.733913 10.373565 14.981565 10.373565 24.731826v141.133913c0 9.73913-3.450435 17.986783-10.373565 24.731826-6.92313 6.745043-15.248696 10.128696-24.976696 10.128696H301.924174c-9.728 0-17.964522-3.383652-24.698435-10.128696C270.491826 773.398261 267.130435 765.150609 267.130435 755.400348V614.266435c0-9.73913 3.361391-17.986783 10.095304-24.731826 6.733913-6.745043 14.970435-10.128696 24.698435-10.128696h140.265739zM723.311304 579.417043c9.71687 0 17.953391 3.372522 24.687305 10.128696 6.733913 6.733913 10.095304 14.981565 10.095304 24.731826v141.133913c0 9.73913-3.361391 17.986783-10.095304 24.731826-6.733913 6.745043-14.970435 10.128696-24.687305 10.128696H583.034435a34.482087 34.482087 0 0 1-24.976696-10.128696 33.224348 33.224348 0 0 1-10.373565-24.742956V614.266435c0-9.73913 3.450435-17.986783 10.373565-24.731826a34.482087 34.482087 0 0 1 24.976696-10.128696h140.276869z" fill="#FFFFFF"></path>
|
||||
<path d="M667.826087 243.287534m23.611218 23.611218l110.185682 110.185682q23.611218 23.611218 0 47.222436l-110.185682 110.185683q-23.611218 23.611218-47.222436 0l-110.185683-110.185683q-23.611218-23.611218 0-47.222436l110.185683-110.185682q23.611218-23.611218 47.222436 0Z" fill="#FFFFFF" fill-opacity=".7"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@ -15,6 +15,7 @@
|
||||
<DropdownMenu slot="list">
|
||||
<DropdownItem v-if="!sortingMode" name="sort">{{ $L('调整排序') }}</DropdownItem>
|
||||
<DropdownItem v-else name="cancelSort">{{ $L('退出排序') }}</DropdownItem>
|
||||
<DropdownItem v-if="userIsAdmin" divided name="customMicro">{{ $L('自定义应用菜单') }}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
@ -111,6 +112,114 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--自定义应用菜单-->
|
||||
<Modal
|
||||
v-if="userIsAdmin"
|
||||
v-model="customMicroModalVisible"
|
||||
:title="$L('自定义应用菜单')"
|
||||
:mask-closable="false"
|
||||
width="760">
|
||||
<Alert type="info" show-icon class="custom-micro-alert">
|
||||
{{ $L('仅管理员可配置,保存后会在应用列表中生成对应菜单。') }}
|
||||
</Alert>
|
||||
<div v-if="customMicroLoading" class="custom-micro-loading">
|
||||
<Loading/>
|
||||
</div>
|
||||
<div v-else class="custom-micro-body">
|
||||
<div v-if="!customMicroMenus.length" class="custom-micro-empty">
|
||||
{{ $L('暂无自定义菜单,请点击下方按钮新增。') }}
|
||||
</div>
|
||||
<Collapse v-else v-model="customMicroCollapsed" accordion simple>
|
||||
<Panel v-for="(item, index) in customMicroMenus" :key="item.uid" :name="item.uid">
|
||||
<div class="custom-micro-card__header">
|
||||
<div class="custom-micro-card__title">
|
||||
{{ item.id || $L('未命名应用') }}
|
||||
</div>
|
||||
<div class="custom-micro-card__actions">
|
||||
<Button @click.stop="duplicateCustomMenu(index)">{{ $L('复制') }}</Button>
|
||||
<Button type="error" @click.stop="removeCustomMenu(index)">{{ $L('删除') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="content">
|
||||
<Form label-position="top">
|
||||
<Row :gutter="16">
|
||||
<Col :sm="12" :xs="24">
|
||||
<FormItem :label="$L('应用 ID')" required>
|
||||
<Input v-model.trim="item.id" placeholder="custom-okr"/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :sm="12" :xs="24">
|
||||
<FormItem :label="$L('应用名称')">
|
||||
<Input v-model.trim="item.name" placeholder="OKR 开发"/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
<FormItem :label="$L('菜单标题')" required>
|
||||
<Input v-model.trim="item.menu.label" placeholder="OKR 开发入口"/>
|
||||
</FormItem>
|
||||
<Row :gutter="16">
|
||||
<Col :sm="12" :xs="24">
|
||||
<FormItem :label="$L('菜单位置')">
|
||||
<Select v-model="item.menu.location" transfer>
|
||||
<Option value="application">{{ $L('应用中心 - 常用') }}</Option>
|
||||
<Option value="application/admin">{{ $L('应用中心 - 管理') }}</Option>
|
||||
<Option value="main/menu">{{ $L('主导航') }}</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :sm="12" :xs="24">
|
||||
<FormItem :label="$L('可见范围')">
|
||||
<Select v-model="item.menu.visible_to" transfer>
|
||||
<Option value="admin">{{ $L('仅管理员') }}</Option>
|
||||
<Option value="all">{{ $L('所有成员') }}</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
<FormItem :label="$L('图标地址')">
|
||||
<Input v-model.trim="item.menu.icon" placeholder="https://example.com/icon.png"/>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('菜单 URL')" required>
|
||||
<Input v-model.trim="item.menu.url" placeholder="https://example.com/app?token={user_token}"/>
|
||||
</FormItem>
|
||||
<Row :gutter="16">
|
||||
<Col :sm="12" :xs="24">
|
||||
<FormItem :label="$L('URL 类型')">
|
||||
<Select v-model="item.menu.url_type" transfer>
|
||||
<Option value="iframe">iframe</Option>
|
||||
<Option value="iframe_blank">iframe_blank</Option>
|
||||
<Option value="inline">inline</Option>
|
||||
<Option value="inline_blank">inline_blank</Option>
|
||||
<Option value="external">external</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :sm="12" :xs="24">
|
||||
<FormItem :label="$L('背景颜色')">
|
||||
<Input v-model.trim="item.menu.background" placeholder="#FFFFFF 或 #FFFFFF|#000000"/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
<div class="custom-micro-checkbox-group">
|
||||
<Checkbox v-model="item.menu.keep_alive">{{ $L('保持激活状态 (keep_alive)') }}</Checkbox>
|
||||
<Checkbox v-model="item.menu.disable_scope_css">{{ $L('禁用作用域样式') }}</Checkbox>
|
||||
<Checkbox v-model="item.menu.transparent">{{ $L('透明背景') }}</Checkbox>
|
||||
<Checkbox v-model="item.menu.auto_dark_theme">{{ $L('自动暗黑模式') }}</Checkbox>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
<Button class="custom-micro-add-btn" type="dashed" long icon="md-add" @click="addCustomMenu">
|
||||
{{ $L('新增菜单') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div slot="footer" class="adaption">
|
||||
<Button @click="customMicroModalVisible=false">{{ $L('关闭') }}</Button>
|
||||
<Button type="primary" :loading="customMicroSaving" @click="saveCustomMenus">{{ $L('保存') }}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!--MY BOT-->
|
||||
<DrawerOverlay v-model="mybotShow" placement="right" :size="720">
|
||||
<template v-if="mybotShow" #title>
|
||||
@ -323,6 +432,20 @@ import ImgUpload from "../../components/ImgUpload.vue";
|
||||
import {webhookEventOptions} from "../../utils/webhook";
|
||||
import Draggable from "vuedraggable";
|
||||
|
||||
const createCustomMicroMenu = () => ({
|
||||
uid: `custom_${Math.random().toString(36).slice(2, 10)}`,
|
||||
id: '',
|
||||
name: '',
|
||||
version: 'custom',
|
||||
menu: {
|
||||
location: 'application',
|
||||
url_type: 'iframe',
|
||||
visible_to: 'admin',
|
||||
keep_alive: true,
|
||||
auto_dark_theme: true,
|
||||
}
|
||||
});
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Draggable,
|
||||
@ -383,6 +506,12 @@ export default {
|
||||
//
|
||||
sendData: [],
|
||||
sendType: '',
|
||||
//
|
||||
customMicroModalVisible: false,
|
||||
customMicroMenus: [],
|
||||
customMicroLoading: false,
|
||||
customMicroSaving: false,
|
||||
customMicroCollapsed: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -494,8 +623,122 @@ export default {
|
||||
this.enterSortMode();
|
||||
} else if (action === 'cancelSort') {
|
||||
this.exitSortMode();
|
||||
} else if (action === 'customMicro') {
|
||||
this.openCustomMicroModal();
|
||||
}
|
||||
},
|
||||
openCustomMicroModal() {
|
||||
if (!this.userIsAdmin) {
|
||||
return;
|
||||
}
|
||||
this.customMicroModalVisible = true;
|
||||
this.loadCustomMicroMenus();
|
||||
},
|
||||
loadCustomMicroMenus() {
|
||||
this.customMicroLoading = true;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'system/microapp_menu?type=get',
|
||||
method: 'post',
|
||||
}).then(({data}) => {
|
||||
this.customMicroMenus = this.normalizeCustomMenus(data);
|
||||
this.customMicroCollapsed = this.customMicroMenus.length > 0 ? this.customMicroMenus[0].uid : '';
|
||||
}).catch(({msg}) => {
|
||||
if (msg) {
|
||||
$A.modalError(msg);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.customMicroLoading = false;
|
||||
});
|
||||
},
|
||||
normalizeCustomMenus(list = []) {
|
||||
if (!$A.isArray(list)) {
|
||||
return [];
|
||||
}
|
||||
return list.map(app => {
|
||||
const draft = createCustomMicroMenu();
|
||||
return Object.assign({}, draft, app, {
|
||||
menu: Object.assign({}, draft.menu, $A.isArray(app.menu_items) && app.menu_items.length > 0 ? app.menu_items[0] : {}),
|
||||
});
|
||||
});
|
||||
},
|
||||
pickCustomMenuLabel(label, fallback = '') {
|
||||
if (typeof label === 'string') {
|
||||
return label || fallback;
|
||||
}
|
||||
if ($A.isJson(label)) {
|
||||
return label.zh || label.en || fallback;
|
||||
}
|
||||
return fallback;
|
||||
},
|
||||
addCustomMenu() {
|
||||
const draft = createCustomMicroMenu();
|
||||
this.customMicroMenus.push(draft);
|
||||
this.customMicroCollapsed = draft.uid;
|
||||
},
|
||||
duplicateCustomMenu(index) {
|
||||
const target = this.customMicroMenus[index];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const copy = $A.cloneJSON(target);
|
||||
copy.uid = createCustomMicroMenu().uid;
|
||||
copy.id = copy.id ? `${copy.id}_copy` : '';
|
||||
copy.name = copy.name ? `${copy.name} copy` : '';
|
||||
copy.menu.label = copy.menu.label ? `${copy.menu.label} copy` : '';
|
||||
this.customMicroMenus.splice(index + 1, 0, copy);
|
||||
this.customMicroCollapsed = copy.uid;
|
||||
},
|
||||
removeCustomMenu(index) {
|
||||
this.customMicroMenus.splice(index, 1);
|
||||
},
|
||||
saveCustomMenus() {
|
||||
if (this.customMicroSaving) {
|
||||
return;
|
||||
}
|
||||
const payload = [];
|
||||
for (const item of this.customMicroMenus) {
|
||||
const formatted = this.formatCustomMenuForSave(item);
|
||||
if (!formatted) {
|
||||
$A.modalWarning({
|
||||
title: '提示',
|
||||
content: '请为每个菜单填写应用ID、菜单标题和有效的 URL。',
|
||||
});
|
||||
return;
|
||||
}
|
||||
payload.push(formatted);
|
||||
}
|
||||
this.customMicroSaving = true;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'system/microapp_menu?type=save',
|
||||
method: 'post',
|
||||
data: {
|
||||
list: payload
|
||||
},
|
||||
}).then(_ => {
|
||||
$A.messageSuccess('保存成功');
|
||||
this.$store.dispatch("updateMicroAppsStatus");
|
||||
}).catch(({msg}) => {
|
||||
if (msg) {
|
||||
$A.modalError(msg);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.customMicroSaving = false;
|
||||
});
|
||||
},
|
||||
formatCustomMenuForSave(item) {
|
||||
const id = (item.id || '').trim();
|
||||
const url = (item.menu.url || '').trim();
|
||||
const label = (item.menu.label || item.name || item.id || '').trim();
|
||||
if (!id || !url || !label) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: (item.name || '').trim(),
|
||||
version: item.version || 'custom',
|
||||
menu_items: [Object.assign({}, item.menu, { url, label })],
|
||||
};
|
||||
},
|
||||
currentCards(type) {
|
||||
return this.sortingMode ? (this.sortLists[type] || []) : this.getDisplayItems(type);
|
||||
},
|
||||
@ -682,9 +925,9 @@ export default {
|
||||
}).then(({data, msg}) => {
|
||||
this.appSorts = this.normalizeSortPayload(data?.sorts || payload);
|
||||
this.exitSortMode();
|
||||
$A.messageSuccess(msg || this.$L('保存成功'));
|
||||
$A.messageSuccess(msg || '保存成功');
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg || this.$L('保存失败'));
|
||||
$A.modalError(msg || '保存失败');
|
||||
}).finally(() => {
|
||||
this.appSortSaving = false;
|
||||
});
|
||||
@ -876,7 +1119,7 @@ export default {
|
||||
// 与我的机器人聊天
|
||||
chatMybot(userid) {
|
||||
this.$store.dispatch("openDialogUserid", userid).catch(({msg}) => {
|
||||
$A.modalError(msg || this.$L('打开会话失败'))
|
||||
$A.modalError(msg || '打开会话失败')
|
||||
});
|
||||
},
|
||||
// 添加修改我的机器人
|
||||
@ -985,7 +1228,7 @@ export default {
|
||||
} else {
|
||||
// 其他文本
|
||||
$A.modalInfo({
|
||||
title: this.$L('扫描结果'),
|
||||
title: '扫描结果',
|
||||
content: text,
|
||||
width: 400,
|
||||
});
|
||||
|
||||
@ -3,10 +3,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from "vuex";
|
||||
import MicroApps from "../../components/MicroApps";
|
||||
|
||||
export default {
|
||||
components: { MicroApps },
|
||||
computed: {
|
||||
...mapState(['userIsAdmin']),
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
const {name} = this.$route.params;
|
||||
@ -15,6 +19,37 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
// iframe 测试
|
||||
if (name === 'iframe-test') {
|
||||
if (!this.userIsAdmin) {
|
||||
$A.modalError("仅管理员可使用此功能")
|
||||
return
|
||||
}
|
||||
let {url} = this.$route.query;
|
||||
if (!url) {
|
||||
url = await this.promptIframeUrl();
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
this.$router.replace({
|
||||
path: this.$route.path,
|
||||
query: {
|
||||
...this.$route.query,
|
||||
url
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
await this.$refs.app.onOpen({
|
||||
id: 'iframe-test',
|
||||
name: 'iframe-test',
|
||||
url: url,
|
||||
url_type: 'iframe',
|
||||
transparent: true,
|
||||
keep_alive: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const app = (await $A.IDBArray("cacheMicroApps")).reverse().find(item => item.name === name);
|
||||
if (!app) {
|
||||
$A.modalError("应用不存在");
|
||||
@ -22,6 +57,24 @@ export default {
|
||||
}
|
||||
|
||||
await this.$refs.app.onOpen(app)
|
||||
},
|
||||
methods: {
|
||||
promptIframeUrl() {
|
||||
return new Promise((resolve, reject) => {
|
||||
$A.modalInput({
|
||||
title: this.$L("请输入 URL"),
|
||||
placeholder: "https://example.com",
|
||||
onOk: (val) => {
|
||||
const input = (val || "").trim();
|
||||
if (!input) {
|
||||
return this.$L("URL不能为空");
|
||||
}
|
||||
resolve(input);
|
||||
},
|
||||
onCancel: () => reject()
|
||||
});
|
||||
}).catch(() => null);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
20
resources/assets/js/store/actions.js
vendored
20
resources/assets/js/store/actions.js
vendored
@ -5144,7 +5144,7 @@ export default {
|
||||
* @param commit
|
||||
* @param dispatch
|
||||
*/
|
||||
async updateMicroAppsStatus({commit, state}) {
|
||||
async updateMicroAppsStatus({commit, state, dispatch}) {
|
||||
const {data: {code, data}} = await axios.get($A.mainUrl('appstore/api/v1/internal/installed'), {
|
||||
headers: {
|
||||
Token: state.userToken,
|
||||
@ -5152,7 +5152,23 @@ export default {
|
||||
}
|
||||
})
|
||||
if (code === 200) {
|
||||
commit("microApps/data", data|| [])
|
||||
let apps = Array.isArray(data) ? data : [];
|
||||
try {
|
||||
const {data: customData} = await dispatch('call', {
|
||||
url: 'system/microapp_menu?type=get',
|
||||
});
|
||||
if ($A.isArray(customData) && customData.length > 0) {
|
||||
customData.forEach(item => {
|
||||
item.menu_items.forEach(menu => {
|
||||
menu.icon = menu.icon || $A.mainUrl("images/application/appstore-default.svg");
|
||||
});
|
||||
});
|
||||
apps = apps.concat(customData);
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略自定义菜单加载失败
|
||||
}
|
||||
commit("microApps/data", apps || [])
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
4
resources/assets/js/store/mutations.js
vendored
4
resources/assets/js/store/mutations.js
vendored
@ -420,8 +420,8 @@ export default {
|
||||
// 更新菜单
|
||||
const menus = [];
|
||||
data.forEach((item) => {
|
||||
if (item.menu_items) {
|
||||
menus.push(...item.menu_items.map(m => Object.assign(m, {id: item.id})));
|
||||
if (Array.isArray(item.menu_items) && item.menu_items.length > 0) {
|
||||
menus.push(...item.menu_items.map(menu => Object.assign({}, menu, {id: item.id})));
|
||||
}
|
||||
})
|
||||
menus.forEach(item => {
|
||||
|
||||
80
resources/assets/sass/pages/page-apply.scss
vendored
80
resources/assets/sass/pages/page-apply.scss
vendored
@ -582,6 +582,86 @@
|
||||
}
|
||||
}
|
||||
|
||||
.custom-micro-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.custom-micro-loading {
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-micro-body {
|
||||
margin: 0 -24px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.ivu-collapse {
|
||||
> .ivu-collapse-item {
|
||||
> .ivu-collapse-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding-left: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-micro-empty {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.custom-micro-card {
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.custom-micro-card__header {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.custom-micro-card__title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.custom-micro-card__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.ivu-btn {
|
||||
font-size: 13px;
|
||||
padding: 0 10px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-micro-checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.custom-micro-add-btn {
|
||||
flex-shrink: 0;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
body.window-portrait {
|
||||
.page-apply {
|
||||
.apply-wrapper {
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M0 0m349.206261 0l325.587478 0q349.206261 0 349.206261 349.206261l0 325.587478q0 349.206261-349.206261 349.206261l-325.587478 0q-349.206261 0-349.206261-349.206261l0-325.587478q0-349.206261 349.206261-349.206261Z" fill="#84c56a" class="selected"></path>
|
||||
<path d="M442.189913 298.284522c9.728 0 18.053565 3.372522 24.976696 10.128695 6.912 6.745043 10.373565 14.981565 10.373565 24.731826v140.566261c0 9.750261-3.450435 18.086957-10.373565 25.021218a34.003478 34.003478 0 0 1-24.976696 10.395826H301.924174a33.090783 33.090783 0 0 1-24.698435-10.395826A34.626783 34.626783 0 0 1 267.130435 473.711304V333.145043c0-9.750261 3.361391-17.986783 10.095304-24.742956 6.733913-6.745043 14.970435-10.117565 24.698435-10.117565h140.265739zM442.189913 579.417043c9.728 0 18.053565 3.372522 24.976696 10.128696 6.912 6.733913 10.373565 14.981565 10.373565 24.731826v141.133913c0 9.73913-3.450435 17.986783-10.373565 24.731826-6.92313 6.745043-15.248696 10.128696-24.976696 10.128696H301.924174c-9.728 0-17.964522-3.383652-24.698435-10.128696C270.491826 773.398261 267.130435 765.150609 267.130435 755.400348V614.266435c0-9.73913 3.361391-17.986783 10.095304-24.731826 6.733913-6.745043 14.970435-10.128696 24.698435-10.128696h140.265739zM723.311304 579.417043c9.71687 0 17.953391 3.372522 24.687305 10.128696 6.733913 6.733913 10.095304 14.981565 10.095304 24.731826v141.133913c0 9.73913-3.361391 17.986783-10.095304 24.731826-6.733913 6.745043-14.970435 10.128696-24.687305 10.128696H583.034435a34.482087 34.482087 0 0 1-24.976696-10.128696 33.224348 33.224348 0 0 1-10.373565-24.742956V614.266435c0-9.73913 3.450435-17.986783 10.373565-24.731826a34.482087 34.482087 0 0 1 24.976696-10.128696h140.276869z" fill="#FFFFFF"></path>
|
||||
<path d="M667.826087 243.287534m23.611218 23.611218l110.185682 110.185682q23.611218 23.611218 0 47.222436l-110.185682 110.185683q-23.611218 23.611218-47.222436 0l-110.185683-110.185683q-23.611218-23.611218 0-47.222436l110.185683-110.185682q23.611218-23.611218 47.222436 0Z" fill="#FFFFFF" fill-opacity=".7"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
Loading…
x
Reference in New Issue
Block a user