feat(app): support hiding AI interfaces

This commit is contained in:
kuaifan 2026-08-28 12:20:25 +00:00
parent 9dfcdc7b07
commit b05b0874f8
24 changed files with 177 additions and 79 deletions

View File

@ -156,6 +156,7 @@ class SystemController extends AbstractController
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: ''; $setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
$setting['task_ai_auto_analyze'] = $setting['task_ai_auto_analyze'] ?: 'open'; $setting['task_ai_auto_analyze'] = $setting['task_ai_auto_analyze'] ?: 'open';
$setting['department_owner_project_view'] = $setting['department_owner_project_view'] ?: 'open'; $setting['department_owner_project_view'] = $setting['department_owner_project_view'] ?: 'open';
$setting['app_ai_hidden'] = config('dootask.app_ai_hidden');
$setting['server_timezone'] = config('app.timezone'); $setting['server_timezone'] = config('app.timezone');
$setting['server_version'] = Base::getVersion(); $setting['server_version'] = Base::getVersion();
// 指定人员名单仅管理员可见 // 指定人员名单仅管理员可见

View File

@ -8,6 +8,9 @@ return [
// 许可证显示开关:设为 'hidden' 时隐藏系统许可证信息Doo::license // 许可证显示开关:设为 'hidden' 时隐藏系统许可证信息Doo::license
'system_license' => env('SYSTEM_LICENSE'), 'system_license' => env('SYSTEM_LICENSE'),
// 移动 APP AI 界面开关:设为 true 时仅在移动 APP 隐藏 AI 相关界面
'app_ai_hidden' => filter_var(env('APP_AI_HIDDEN', false), FILTER_VALIDATE_BOOLEAN),
// 演示账号登录页展示的演示账号SystemController::demo // 演示账号登录页展示的演示账号SystemController::demo
'demo_account' => env('DEMO_ACCOUNT'), 'demo_account' => env('DEMO_ACCOUNT'),

View File

@ -19,7 +19,8 @@ negative:
- 移动端缺失的复杂功能不会硬塞,大屏复杂操作仍建议在桌面端完成 - 移动端缺失的复杂功能不会硬塞,大屏复杂操作仍建议在桌面端完成
- 桌面端独有的能力(截图、全局快捷键、托盘)由原生 OS 集成,移动端无对应概念 - 桌面端独有的能力(截图、全局快捷键、托盘)由原生 OS 集成,移动端无对应概念
- 微应用 / 插件部分能在移动端打开(走 WebView),但触屏不一定可用 - 微应用 / 插件部分能在移动端打开(走 WebView),但触屏不一定可用
last_verified: v1.7.90 - 部署环境可通过 APP_AI_HIDDEN=true 隐藏移动 APP 的 AI 界面,但不会关闭 Web、Electron 或服务端 AI 接口
last_verified: v1.9.18
--- ---
# 移动端与桌面端功能差异 # 移动端与桌面端功能差异
@ -31,7 +32,7 @@ last_verified: v1.7.90
- 项目(成员、视图基本浏览) - 项目(成员、视图基本浏览)
- 文件(上传、下载、预览) - 文件(上传、下载、预览)
- 通知与日程 - 通知与日程
- AI 助手(基础对话) - AI 助手(基础对话;部署环境未隐藏移动 APP AI 界面时)
## 移动端独有 ## 移动端独有
- 底部 Tabbar 原生导航 - 底部 Tabbar 原生导航
@ -61,6 +62,9 @@ last_verified: v1.7.90
| 报表 / 仪表盘 | 完整图表 + 编辑 | 仅查看 | | 报表 / 仪表盘 | 完整图表 + 编辑 | 仅查看 |
| 会议 | 创建 / 主持 / 屏幕共享 | 主要参会,部分高级功能受限 | | 会议 | 创建 / 主持 / 屏幕共享 | 主要参会,部分高级功能受限 |
## AI 界面部署开关
部署管理员可设置 `APP_AI_HIDDEN=true`,仅在 iOS / Android APP 隐藏 AI 助手、AI 微应用、AI 生成与分析入口,以及邮箱符合 `ai-*@bot.system` 的 AI 机器人单聊入口。任务、项目或普通群聊中已有的 AI 消息仍会显示并正常通知Web 和 Electron 客户端不受影响,该配置也不会禁用服务端 AI 接口或删除会话、消息。
## 选哪端做什么 ## 选哪端做什么
- **创建复杂任务 / 编辑长文档** → 桌面端 - **创建复杂任务 / 编辑长文档** → 桌面端
- **快速回消息 / 收推送 / 移动审批** → 移动端 - **快速回消息 / 收推送 / 移动审批** → 移动端

View File

@ -42,7 +42,7 @@
<UserDetail/> <UserDetail/>
<!--AI 助理--> <!--AI 助理-->
<AIAssistant/> <AIAssistant v-if="aiVisible"/>
<!--返回效果--> <!--返回效果-->
<MobileBack v-if="isFirstPage"/> <MobileBack v-if="isFirstPage"/>
@ -100,7 +100,7 @@ import MobileNotification from "./components/Mobile/Notification.vue";
import MobileBack from "./components/Mobile/Back.vue"; import MobileBack from "./components/Mobile/Back.vue";
import DropdownMenu from "./components/DropdownMenu"; import DropdownMenu from "./components/DropdownMenu";
import {ctrlPressed} from "./mixins/ctrlPressed"; import {ctrlPressed} from "./mixins/ctrlPressed";
import {mapState} from "vuex"; import {mapGetters, mapState} from "vuex";
import emitter from "./store/events"; import emitter from "./store/events";
import AIAssistant from "./components/AIAssistant"; import AIAssistant from "./components/AIAssistant";
import UserDetail from "./pages/manage/components/UserDetail.vue"; import UserDetail from "./pages/manage/components/UserDetail.vue";
@ -152,6 +152,8 @@ export default {
}, },
computed: { computed: {
...mapGetters(['aiVisible']),
...mapState(['ws', 'themeConf', 'windowOrientation', 'safeAreaSize', 'isFirstPage', 'mobileTabbar', 'themeName']), ...mapState(['ws', 'themeConf', 'windowOrientation', 'safeAreaSize', 'isFirstPage', 'mobileTabbar', 'themeName']),
statusColor({routeName, windowLandscape}) { statusColor({routeName, windowLandscape}) {

View File

@ -24,7 +24,7 @@
</template> </template>
<script> <script>
import {mapState} from "vuex"; import {mapGetters} from "vuex";
import emitter from "../../store/events"; import emitter from "../../store/events";
import {createOperationModule} from "./operation-module"; import {createOperationModule} from "./operation-module";
import {loadFloatButtonVisible} from "./float-button-preference"; import {loadFloatButtonVisible} from "./float-button-preference";
@ -61,14 +61,10 @@ export default {
}, },
computed: { computed: {
...mapState(['microAppsIds']), ...mapGetters(['aiVisible']),
aiInstalled() {
return this.microAppsIds?.includes('ai');
},
visible() { visible() {
return this.aiInstalled && return this.aiVisible &&
this.userId > 0 && this.userId > 0 &&
this.positionLoaded && this.positionLoaded &&
this.visibilityLoaded && this.visibilityLoaded &&

View File

@ -27,7 +27,7 @@
</template> </template>
<script> <script>
import {mapGetters, mapState} from "vuex"; import {mapGetters} from "vuex";
import NetworkException from "../NetworkException"; import NetworkException from "../NetworkException";
import emitter from "../../store/events"; import emitter from "../../store/events";
@ -55,8 +55,7 @@ export default {
}, },
computed: { computed: {
...mapState(['cacheDialogs']), ...mapGetters(['dashboardTask', 'visibleDialogs']),
...mapGetters(['dashboardTask']),
// appBadges getter // appBadges getter
...mapGetters('appBadges', { ...mapGetters('appBadges', {
@ -71,7 +70,7 @@ export default {
msgUnreadMention() { msgUnreadMention() {
let num = 0; // let num = 0; //
let mention = 0; // let mention = 0; //
this.cacheDialogs.some(dialog => { this.visibleDialogs.some(dialog => {
num += $A.getDialogUnread(dialog, false); num += $A.getDialogUnread(dialog, false);
mention += $A.getDialogMention(dialog); mention += $A.getDialogMention(dialog);
}) })
@ -109,7 +108,7 @@ export default {
*/ */
msgAllUnread() { msgAllUnread() {
let num = 0; let num = 0;
this.cacheDialogs.some(dialog => { this.visibleDialogs.some(dialog => {
num += $A.getDialogNum(dialog); num += $A.getDialogNum(dialog);
}) })
return num; return num;
@ -120,7 +119,7 @@ export default {
* @returns {string|null} * @returns {string|null}
*/ */
msgTodoTotal() { msgTodoTotal() {
let todoNum = this.cacheDialogs.reduce((total, current) => total + (current.todo_num || 0), 0) let todoNum = this.visibleDialogs.reduce((total, current) => total + (current.todo_num || 0), 0)
if (todoNum > 0) { if (todoNum > 0) {
if (todoNum > 999) { if (todoNum > 999) {
todoNum = "999+" todoNum = "999+"

View File

@ -91,7 +91,7 @@
</template> </template>
<script> <script>
import {mapState} from "vuex"; import {mapGetters, mapState} from "vuex";
import emitter from "../store/events"; import emitter from "../store/events";
import transformEmojiToHtml from "../utils/emoji"; import transformEmojiToHtml from "../utils/emoji";
import {SEARCH_AI_SYSTEM_PROMPT, withLanguagePreferencePrompt} from "../utils/ai"; import {SEARCH_AI_SYSTEM_PROMPT, withLanguagePreferencePrompt} from "../utils/ai";
@ -147,6 +147,8 @@ export default {
}, },
computed: { computed: {
...mapGetters(['aiVisible', 'appAiHidden']),
...mapState([ ...mapState([
'themeName', 'themeName',
'keyboardShow', 'keyboardShow',
@ -156,7 +158,7 @@ export default {
aiSearchAvailable() { aiSearchAvailable() {
return this.microAppsIds return this.microAppsIds
&& this.microAppsIds.includes('search') && this.microAppsIds.includes('search')
&& this.microAppsIds.includes('ai') && this.aiVisible
}, },
isFullscreen({windowWidth}) { isFullscreen({windowWidth}) {
@ -164,7 +166,18 @@ export default {
}, },
items({searchKey, searchResults, action}) { items({searchKey, searchResults, action}) {
return searchResults.filter(item => item.key === searchKey && (!action || item.type === action)) return searchResults.filter(item => {
if (item.key !== searchKey || (action && item.type !== action)) {
return false
}
if (!this.appAiHidden) {
return true
}
if (item.type === 'message') {
return !$A.isAiBotDialog(item.rawData)
}
return true
})
}, },
total() { total() {

View File

@ -234,6 +234,11 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
// AI
hideAiBot: {
type: Boolean,
default: false
},
// //
showDisable: { showDisable: {
type: Boolean, type: Boolean,
@ -403,9 +408,7 @@ export default {
} }
}, },
computed: { computed: {
...mapState([ ...mapState(['cacheDialogs']),
'cacheDialogs',
]),
isFullscreen({windowWidth}) { isFullscreen({windowWidth}) {
return windowWidth < 576 return windowWidth < 576
@ -424,7 +427,7 @@ export default {
return recents return recents
case 'contact': case 'contact':
return contacts return this.hideAiBot ? contacts.filter(item => !$A.isAiBotUser(item)) : contacts
case 'project': case 'project':
return projects return projects
@ -595,6 +598,9 @@ export default {
searchRecent() { searchRecent() {
this.recents = this.cacheDialogs.filter(dialog => { this.recents = this.cacheDialogs.filter(dialog => {
if (this.hideAiBot && $A.isAiBotDialog(dialog)) {
return false
}
if (this.onlyGroup && dialog.type != 'group') { if (this.onlyGroup && dialog.type != 'group') {
return false return false
} }

View File

@ -213,6 +213,24 @@ import {convertLocalResourcePath} from "../components/Replace/utils";
return tags; return tags;
}, },
/**
* 是否为 AI 机器人账号
* @param user
* @returns {boolean}
*/
isAiBotUser(user) {
return !!user?.bot && /^ai-[^@]+@bot\.system$/.test(user.email || '')
},
/**
* 是否为 AI 机器人单聊
* @param dialog
* @returns {boolean}
*/
isAiBotDialog(dialog) {
return dialog?.type === 'user' && this.isAiBotUser(dialog)
},
/** /**
* 对话完成 * 对话完成
* @param dialog * @param dialog

View File

@ -272,7 +272,7 @@
<Icon type="ios-arrow-down"></Icon> <Icon type="ios-arrow-down"></Icon>
</Button> </Button>
<DropdownMenu slot="list"> <DropdownMenu slot="list">
<DropdownItem v-if="aiInstalled" name="aiAssistant">{{$L('AI 助手')}} ({{mateName}}+I)</DropdownItem> <DropdownItem v-if="aiVisible" name="aiAssistant">{{$L('AI 助手')}} ({{mateName}}+I)</DropdownItem>
<DropdownItem name="task">{{$L('新建任务')}} ({{mateName}}+K)</DropdownItem> <DropdownItem name="task">{{$L('新建任务')}} ({{mateName}}+K)</DropdownItem>
<DropdownItem name="project">{{$L('新建项目')}} ({{mateName}}+B)</DropdownItem> <DropdownItem name="project">{{$L('新建项目')}} ({{mateName}}+B)</DropdownItem>
<DropdownItem name="group">{{$L('创建群组')}} ({{mateName}}+U)</DropdownItem> <DropdownItem name="group">{{$L('创建群组')}} ({{mateName}}+U)</DropdownItem>
@ -313,6 +313,7 @@
<div class="page-manage-project-ai-wrapper"> <div class="page-manage-project-ai-wrapper">
<Input ref="projectName" type="text" v-model="addData.name"></Input> <Input ref="projectName" type="text" v-model="addData.name"></Input>
<div <div
v-if="aiVisible"
class="project-ai-button" class="project-ai-button"
type="text" type="text"
@click="onProjectAI"> @click="onProjectAI">
@ -651,11 +652,10 @@ export default {
'departmentOwnerProjectsRefreshing', 'departmentOwnerProjectsRefreshing',
'departmentOwnerProjectViewEnabled', 'departmentOwnerProjectViewEnabled',
'mcpServerStatus', 'mcpServerStatus'
'microAppsIds'
]), ]),
...mapGetters(['dashboardTask', "filterMicroAppsMenusMain"]), ...mapGetters(['aiVisible', 'dashboardTask', "filterMicroAppsMenusMain", 'visibleDialogs']),
// appBadges getter // appBadges getter
...mapGetters('appBadges', { ...mapGetters('appBadges', {
@ -663,10 +663,6 @@ export default {
applicationBadgeDot: 'applicationDot', applicationBadgeDot: 'applicationDot',
}), }),
aiInstalled() {
return this.microAppsIds?.includes('ai');
},
departmentOwnerViewAvailable() { departmentOwnerViewAvailable() {
return this.systemConfig.department_owner_project_view === 'open' && (this.userInfo.managed_departments || []).length > 0; return this.systemConfig.department_owner_project_view === 'open' && (this.userInfo.managed_departments || []).length > 0;
}, },
@ -701,7 +697,7 @@ export default {
msgUnreadMention() { msgUnreadMention() {
let num = 0; // let num = 0; //
let mention = 0; // let mention = 0; //
this.cacheDialogs.some(dialog => { this.visibleDialogs.some(dialog => {
num += $A.getDialogUnread(dialog, false); num += $A.getDialogUnread(dialog, false);
mention += $A.getDialogMention(dialog); mention += $A.getDialogMention(dialog);
}) })
@ -739,7 +735,7 @@ export default {
*/ */
msgAllUnread() { msgAllUnread() {
let num = 0; let num = 0;
this.cacheDialogs.some(dialog => { this.visibleDialogs.some(dialog => {
num += $A.getDialogNum(dialog); num += $A.getDialogNum(dialog);
}) })
return num; return num;
@ -750,7 +746,7 @@ export default {
* @returns {string|null} * @returns {string|null}
*/ */
msgTodoTotal() { msgTodoTotal() {
let todoNum = this.cacheDialogs.reduce((total, current) => total + (current.todo_num || 0), 0) let todoNum = this.visibleDialogs.reduce((total, current) => total + (current.todo_num || 0), 0)
if (todoNum > 0) { if (todoNum > 0) {
if (todoNum > 99) { if (todoNum > 99) {
todoNum = "99+" todoNum = "99+"
@ -1486,7 +1482,7 @@ export default {
break; break;
case 73: // I - AI case 73: // I - AI
if (this.aiInstalled) { if (this.aiVisible) {
e.preventDefault(); e.preventDefault();
this.onOpenAIAssistant(); this.onOpenAIAssistant();
} }

View File

@ -152,7 +152,7 @@
<em>{{$L('上传文件')}}</em> <em>{{$L('上传文件')}}</em>
</div> </div>
</template> </template>
<div v-if="dialogId > 0" class="chat-input-popover-item" @click="onToolbar('ai')"> <div v-if="dialogId > 0 && aiVisible" class="chat-input-popover-item" @click="onToolbar('ai')">
<i class="taskfont">&#xe8a1;</i> <i class="taskfont">&#xe8a1;</i>
<em>{{$L('AI 生成')}}</em> <em>{{$L('AI 生成')}}</em>
</div> </div>
@ -606,7 +606,7 @@ export default {
'viewportHeight', 'viewportHeight',
]), ]),
...mapGetters(['getDialogDraft', 'getDialogQuote']), ...mapGetters(['aiVisible', 'getDialogDraft', 'getDialogQuote']),
isEnterSend({cacheKeyboard}) { isEnterSend({cacheKeyboard}) {
if (this.$isEEUIApp) { if (this.$isEEUIApp) {

View File

@ -5,6 +5,7 @@
ref="forwardSelect" ref="forwardSelect"
:title="title" :title="title"
:show-bot="showBot" :show-bot="showBot"
:hide-ai-bot="appAiHidden"
:multiple-max="userMaxSelect" :multiple-max="userMaxSelect"
:before-submit="onSelectBefore" :before-submit="onSelectBefore"
:show-select-all="false" :show-select-all="false"
@ -31,6 +32,7 @@
<script> <script>
import UserSelect from "../../../../components/UserSelect.vue"; import UserSelect from "../../../../components/UserSelect.vue";
import ForwardConfirm from "./confirm.vue"; import ForwardConfirm from "./confirm.vue";
import {mapGetters} from "vuex";
export default { export default {
name: "Forwarder", name: "Forwarder",
@ -94,6 +96,10 @@ export default {
} }
}, },
computed: {
...mapGetters(['appAiHidden']),
},
methods: { methods: {
onSelection() { onSelection() {
this.$refs.forwardSelect.onSelection() this.$refs.forwardSelect.onSelection()

View File

@ -435,7 +435,7 @@
</template> </template>
</FormItem> </FormItem>
<FormItem :label="$L('AI任务分析')" prop="ai_auto_analyze"> <FormItem v-if="aiVisible" :label="$L('AI任务分析')" prop="ai_auto_analyze">
<RadioGroup v-model="settingData.ai_auto_analyze"> <RadioGroup v-model="settingData.ai_auto_analyze">
<Radio label="open" :disabled="systemConfig.task_ai_auto_analyze === 'close'">{{$L('开启')}}</Radio> <Radio label="open" :disabled="systemConfig.task_ai_auto_analyze === 'close'">{{$L('开启')}}</Radio>
<Radio label="close">{{$L('关闭')}}</Radio> <Radio label="close">{{$L('关闭')}}</Radio>
@ -765,7 +765,7 @@ export default {
'systemConfig', 'systemConfig',
]), ]),
...mapGetters(['projectData', 'transforTasks']), ...mapGetters(['aiVisible', 'projectData', 'transforTasks']),
tabTypeActive() { tabTypeActive() {
return this.projectData.cacheParameter.menuType return this.projectData.cacheParameter.menuType

View File

@ -43,7 +43,7 @@
</li> </li>
</ul> </ul>
<div ref="reportContent" @click="onClick" class="report-content user-select-auto" v-html="currentDetail.content"></div> <div ref="reportContent" @click="onClick" class="report-content user-select-auto" v-html="currentDetail.content"></div>
<div v-if="currentDetail.id" class="report-ai-analysis"> <div v-if="currentDetail.id && aiVisible" class="report-ai-analysis">
<div class="analysis-header"> <div class="analysis-header">
<div class="analysis-title">{{ $L("AI 分析") }}</div> <div class="analysis-title">{{ $L("AI 分析") }}</div>
<Button <Button
@ -72,7 +72,7 @@
<script> <script>
const VMPreview = () => import('../../../components/VMEditor/preview'); const VMPreview = () => import('../../../components/VMEditor/preview');
import {mapState} from "vuex"; import {mapGetters, mapState} from "vuex";
import emitter from "../../../store/events"; import emitter from "../../../store/events";
import {extractPlainText} from "../../../utils/text"; import {extractPlainText} from "../../../utils/text";
import {REPORT_ANALYSIS_SYSTEM_PROMPT, withLanguagePreferencePrompt} from "../../../utils/ai"; import {REPORT_ANALYSIS_SYSTEM_PROMPT, withLanguagePreferencePrompt} from "../../../utils/ai";
@ -97,6 +97,8 @@ export default {
} }
}, },
computed: { computed: {
...mapGetters(['aiVisible']),
...mapState(['formOptions', 'userInfo']), ...mapState(['formOptions', 'userInfo']),
currentDetail() { currentDetail() {
return this.detail || this.data || {}; return this.detail || this.data || {};

View File

@ -46,6 +46,7 @@
<div class="report-bottoms"> <div class="report-bottoms">
<Button type="primary" @click="handleSubmit" :loading="loadIng > 0" class="report-bottom">{{$L(id > 0 ? '修改' : '提交')}}</Button> <Button type="primary" @click="handleSubmit" :loading="loadIng > 0" class="report-bottom">{{$L(id > 0 ? '修改' : '提交')}}</Button>
<Button <Button
v-if="aiVisible"
type="default" type="default"
class="report-bottom" class="report-bottom"
@click="onOrganize"> @click="onOrganize">
@ -60,7 +61,7 @@
<script> <script>
import UserSelect from "../../../components/UserSelect.vue"; import UserSelect from "../../../components/UserSelect.vue";
import {mapState} from "vuex"; import {mapGetters, mapState} from "vuex";
import emitter from "../../../store/events"; import emitter from "../../../store/events";
import {MarkdownConver} from "../../../utils/markdown"; import {MarkdownConver} from "../../../utils/markdown";
import {extractPlainText} from "../../../utils/text"; import {extractPlainText} from "../../../utils/text";
@ -112,6 +113,8 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters(['aiVisible']),
...mapState(['formOptions']), ...mapState(['formOptions']),
}, },
methods: { methods: {

View File

@ -39,7 +39,7 @@
:placeholder="$L('任务描述')" :placeholder="$L('任务描述')"
enterkeyhint="done" enterkeyhint="done"
@on-keydown="onKeydown"/> @on-keydown="onKeydown"/>
<div class="ai-btn" @click="onAI"> <div v-if="aiVisible" class="ai-btn" @click="onAI">
<i class="taskfont">&#xe8a1;</i> <i class="taskfont">&#xe8a1;</i>
</div> </div>
</div> </div>
@ -201,7 +201,7 @@
</template> </template>
<script> <script>
import {mapState} from "vuex"; import {mapGetters, mapState} from "vuex";
import emitter from "../../../store/events"; import emitter from "../../../store/events";
import UserSelect from "../../../components/UserSelect.vue"; import UserSelect from "../../../components/UserSelect.vue";
import TaskExistTips from "./TaskExistTips.vue"; import TaskExistTips from "./TaskExistTips.vue";
@ -292,6 +292,8 @@ export default {
}, },
computed: { computed: {
...mapGetters(['aiVisible']),
...mapState(['cacheProjects', 'projectId', 'cacheColumns', 'taskPriority', 'taskTemplates', 'formOptions']), ...mapState(['cacheProjects', 'projectId', 'cacheColumns', 'taskPriority', 'taskTemplates', 'formOptions']),
taskTemplateShareEnabled() { taskTemplateShareEnabled() {

View File

@ -35,7 +35,7 @@
</div> </div>
</div> </div>
<div class="profile-actions"> <div v-if="!aiConversationHidden" class="profile-actions">
<Button @click="onOpenDialog"><i class="taskfont">&#xe6eb;</i>{{ $L("开始聊天") }}</Button> <Button @click="onOpenDialog"><i class="taskfont">&#xe6eb;</i>{{ $L("开始聊天") }}</Button>
<Button @click="onCreateGroup"><i class="taskfont">&#xe63f;</i>{{ $L("创建群组") }}</Button> <Button @click="onCreateGroup"><i class="taskfont">&#xe63f;</i>{{ $L("创建群组") }}</Button>
</div> </div>
@ -131,7 +131,7 @@
<script> <script>
import emitter from "../../../store/events"; import emitter from "../../../store/events";
import { mapState } from "vuex"; import { mapGetters, mapState } from "vuex";
import transformEmojiToHtml from "../../../utils/emoji"; import transformEmojiToHtml from "../../../utils/emoji";
import UserTagsModal from "./UserTagsModal.vue"; import UserTagsModal from "./UserTagsModal.vue";
import CommonDialogModal from "./CommonDialogModal.vue"; import CommonDialogModal from "./CommonDialogModal.vue";
@ -182,6 +182,12 @@ export default {
}, },
computed: { computed: {
...mapGetters(['appAiHidden']),
aiConversationHidden() {
return this.appAiHidden && $A.isAiBotUser(this.userData)
},
isFullscreen({ windowWidth }) { isFullscreen({ windowWidth }) {
return windowWidth < 576; return windowWidth < 576;
}, },
@ -464,4 +470,4 @@ export default {
}, },
}, },
}; };
</script> </script>

View File

@ -380,7 +380,12 @@ export default {
'longpressData' 'longpressData'
]), ]),
...mapGetters(['getDialogDraft', 'tagDialogDraft']), ...mapGetters([
'appAiHidden',
'getDialogDraft',
'tagDialogDraft',
'visibleDialogs',
]),
contactAvatarSize() { contactAvatarSize() {
return this.windowPortrait ? 36 : 30 return this.windowPortrait ? 36 : 30
@ -389,19 +394,19 @@ export default {
dialogList() { dialogList() {
const {dialogActive, dialogSearchKey, dialogSearchList} = this const {dialogActive, dialogSearchKey, dialogSearchList} = this
if (dialogSearchList.length > 0) { if (dialogSearchList.length > 0) {
return dialogSearchList.sort((a, b) => { return dialogSearchList.filter(dialog => !this.appAiHidden || !$A.isAiBotDialog(dialog)).sort((a, b) => {
// //
return (a.is_search === true ? 1 : 0) - (b.is_search === true ? 1 : 0) return (a.is_search === true ? 1 : 0) - (b.is_search === true ? 1 : 0)
}) })
} }
if (dialogActive == '' && dialogSearchKey == '') { if (dialogActive == '' && dialogSearchKey == '') {
return this.cacheDialogs.filter(dialog => this.filterDialog(dialog)).sort(this.dialogSort); return this.visibleDialogs.filter(dialog => this.filterDialog(dialog)).sort(this.dialogSort);
} }
if (dialogActive == 'mark' && !dialogSearchKey) { if (dialogActive == 'mark' && !dialogSearchKey) {
const lists = []; const lists = [];
this.dialogMsgs.filter(h => h.tag).forEach(h => { this.dialogMsgs.filter(h => h.tag).forEach(h => {
let dialog = $A.cloneJSON(this.cacheDialogs).find(p => p.id == h.dialog_id) let dialog = $A.cloneJSON(this.cacheDialogs).find(p => p.id == h.dialog_id)
if (dialog) { if (dialog && (!this.appAiHidden || !$A.isAiBotDialog(dialog))) {
dialog.last_msg = h; dialog.last_msg = h;
dialog.search_msg_id = h.id; dialog.search_msg_id = h.id;
lists.push(dialog); lists.push(dialog);
@ -409,7 +414,7 @@ export default {
}); });
return lists; return lists;
} }
const list = this.cacheDialogs.filter(dialog => { const list = this.visibleDialogs.filter(dialog => {
if (!this.filterDialog(dialog)) { if (!this.filterDialog(dialog)) {
return false; return false;
} }
@ -508,7 +513,7 @@ export default {
msgUnread() { msgUnread() {
return function (type) { return function (type) {
let num = 0 let num = 0
this.cacheDialogs.some((dialog) => { this.visibleDialogs.some((dialog) => {
switch (type) { switch (type) {
case 'project': case 'project':
case 'task': case 'task':
@ -839,6 +844,9 @@ export default {
}, },
filterDialog(dialog) { filterDialog(dialog) {
if (this.appAiHidden && $A.isAiBotDialog(dialog)) {
return false
}
if ((dialog.id > 0 && dialog.id == this.dialogId) || dialog.top_at || dialog.todo_num > 0 || $A.getDialogNum(dialog) > 0) { if ((dialog.id > 0 && dialog.id == this.dialogId) || dialog.top_at || dialog.todo_num > 0 || $A.getDialogNum(dialog) > 0) {
return true return true
} }

View File

@ -158,7 +158,7 @@
:placeholder="$L('请选择提醒时间')" :placeholder="$L('请选择提醒时间')"
transfer/> transfer/>
</FormItem> </FormItem>
<FormItem :label="$L('AI任务分析')" prop="taskAiAutoAnalyze"> <FormItem v-if="aiVisible" :label="$L('AI任务分析')" prop="taskAiAutoAnalyze">
<RadioGroup v-model="formDatum.task_ai_auto_analyze"> <RadioGroup v-model="formDatum.task_ai_auto_analyze">
<Radio label="open">{{$L('开启')}}</Radio> <Radio label="open">{{$L('开启')}}</Radio>
<Radio label="close">{{$L('关闭')}}</Radio> <Radio label="close">{{$L('关闭')}}</Radio>
@ -354,7 +354,7 @@
</template> </template>
<script> <script>
import {mapState} from "vuex"; import {mapGetters, mapState} from "vuex";
import UserSelect from "../../../../components/UserSelect.vue"; import UserSelect from "../../../../components/UserSelect.vue";
import SystemColumnTemplate from "./SystemColumnTemplate.vue"; import SystemColumnTemplate from "./SystemColumnTemplate.vue";
import SystemFileSetting from "./SystemFileSetting.vue"; import SystemFileSetting from "./SystemFileSetting.vue";
@ -394,6 +394,8 @@ export default {
}, },
computed: { computed: {
...mapGetters(['aiVisible']),
...mapState(['formOptions']), ...mapState(['formOptions']),
projectAddAll() { projectAddAll() {

View File

@ -46,7 +46,7 @@
</template> </template>
<script> <script>
import {mapState} from "vuex"; import {mapGetters, mapState} from "vuex";
import axios from "axios"; import axios from "axios";
import MobileNavTitle from "../../../components/Mobile/NavTitle.vue"; import MobileNavTitle from "../../../components/Mobile/NavTitle.vue";
import emitter from "../../../store/events"; import emitter from "../../../store/events";
@ -72,11 +72,9 @@ export default {
}, },
computed: { computed: {
...mapState(['userInfo', 'userIsAdmin', 'clientNewVersion', 'systemConfig', 'microAppsIds']), ...mapGetters(['aiVisible']),
aiInstalled() { ...mapState(['userInfo', 'userIsAdmin', 'clientNewVersion', 'systemConfig']),
return this.microAppsIds?.includes('ai');
},
showContent() { showContent() {
return this.$route.path.match(/^\/manage\/setting\/\w+$/) return this.$route.path.match(/^\/manage\/setting\/\w+$/)
@ -95,7 +93,7 @@ export default {
menu.push({path: 'keyboard', name: '键盘设置'}) menu.push({path: 'keyboard', name: '键盘设置'})
} }
if (this.aiInstalled) { if (this.aiVisible) {
menu.push({path: 'assistant', name: 'AI 助手设置'}) menu.push({path: 'assistant', name: 'AI 助手设置'})
} }
@ -147,14 +145,14 @@ export default {
handler(name) { handler(name) {
if (name === 'manage-setting' && this.windowLandscape) { if (name === 'manage-setting' && this.windowLandscape) {
this.goForward({name: 'manage-setting-personal'}, true); this.goForward({name: 'manage-setting-personal'}, true);
} else if (name === 'manage-setting-assistant' && !this.aiInstalled) { } else if (name === 'manage-setting-assistant' && !this.aiVisible) {
this.goForward({name: 'manage-setting-personal'}, true); this.goForward({name: 'manage-setting-personal'}, true);
} }
}, },
immediate: true immediate: true
}, },
microAppsIds() { aiVisible(visible) {
if (this.routeName === 'manage-setting-assistant' && !this.aiInstalled) { if (this.routeName === 'manage-setting-assistant' && !visible) {
this.goForward({name: 'manage-setting-personal'}, true); this.goForward({name: 'manage-setting-personal'}, true);
} }
}, },

View File

@ -12,7 +12,7 @@
{{mateName}}<div class="input-box-push">+</div>{{altName}}<div class="input-box-push">+</div>L {{mateName}}<div class="input-box-push">+</div>{{altName}}<div class="input-box-push">+</div>L
</div> </div>
</FormItem> </FormItem>
<FormItem v-if="aiInstalled" :label="$L('AI 助手')"> <FormItem v-if="aiVisible" :label="$L('AI 助手')">
<div class="input-box"> <div class="input-box">
{{mateName}}<div class="input-box-push">+</div>I {{mateName}}<div class="input-box-push">+</div>I
</div> </div>
@ -75,15 +75,11 @@
} }
</style> </style>
<script> <script>
import {mapState} from "vuex"; import {mapGetters} from "vuex";
export default { export default {
computed: { computed: {
...mapState(['microAppsIds']), ...mapGetters(['aiVisible']),
aiInstalled() {
return this.microAppsIds?.includes('ai');
},
}, },
data() { data() {

View File

@ -5401,7 +5401,7 @@ export default {
* - props 传递参数 * - props 传递参数
* 更多说明详见 https://appstore.dootask.com/development/manual * 更多说明详见 https://appstore.dootask.com/development/manual
*/ */
async openMicroApp({state}, data) { async openMicroApp({state, getters}, data) {
// 参数基础校验:必须是对象且包含 id/name/url // 参数基础校验:必须是对象且包含 id/name/url
if (!data || !$A.isJson(data)) { if (!data || !$A.isJson(data)) {
return return
@ -5419,6 +5419,9 @@ export default {
$A.modalWarning(`应用「${data.id}」未安装`) $A.modalWarning(`应用「${data.id}」未安装`)
return return
} }
if (microAppId === 'ai' && !getters.aiVisible) {
return
}
// 以「匹配到的应用菜单第一个 menumicroAppsMenus 中该应用的第一条记录)」作为基础数据,合并出本次打开的 data // 以「匹配到的应用菜单第一个 menumicroAppsMenus 中该应用的第一条记录)」作为基础数据,合并出本次打开的 data
if (data.skip_base_menu !== true) { if (data.skip_base_menu !== true) {

View File

@ -278,6 +278,36 @@ export default {
return quote || null return quote || null
}, },
/**
* AI 界面是否可见
* 移动 APP 等待系统配置加载完成后再展示避免启动时闪现
*/
aiVisible(state, getters) {
const installed = state.microAppsIds?.includes('ai')
if (!$A.isEEUIApp) {
return installed
}
return installed && state.systemConfig.__state === 'success' && !getters.appAiHidden
},
/**
* 移动 APP 是否隐藏 AI 功能
*/
appAiHidden(state) {
return $A.isEEUIApp
&& (state.systemConfig.__state !== 'success' || state.systemConfig.app_ai_hidden === true)
},
/**
* 当前客户端可见的会话
*/
visibleDialogs(state, getters) {
if (!getters.appAiHidden) {
return state.cacheDialogs
}
return state.cacheDialogs.filter(dialog => !$A.isAiBotDialog(dialog))
},
/** /**
* 获取应用菜单 * 获取应用菜单
* 过滤出location为application的菜单项 * 过滤出location为application的菜单项
@ -285,9 +315,9 @@ export default {
* @param {Object} state * @param {Object} state
* @returns {Array} * @returns {Array}
*/ */
filterMicroAppsMenus: (state) => { filterMicroAppsMenus: (state, getters) => {
return state.microAppsMenus.filter(item => { return state.microAppsMenus.filter(item => {
return item.location === 'application' return item.location === 'application' && (item.id !== 'ai' || getters.aiVisible)
}) })
}, },
@ -298,9 +328,9 @@ export default {
* @param {Object} state * @param {Object} state
* @returns {Array} * @returns {Array}
*/ */
filterMicroAppsMenusAdmin: (state) => { filterMicroAppsMenusAdmin: (state, getters) => {
return state.microAppsMenus.filter(item => { return state.microAppsMenus.filter(item => {
return item.location === 'application/admin' return item.location === 'application/admin' && (item.id !== 'ai' || getters.aiVisible)
}) })
}, },
@ -311,9 +341,9 @@ export default {
* @param {Object} state * @param {Object} state
* @returns {Array} * @returns {Array}
*/ */
filterMicroAppsMenusMain: (state) => { filterMicroAppsMenusMain: (state, getters) => {
return state.microAppsMenus.filter(item => { return state.microAppsMenus.filter(item => {
return item.location === 'main/menu' return item.location === 'main/menu' && (item.id !== 'ai' || getters.aiVisible)
}) })
} }
} }

View File

@ -468,6 +468,10 @@ interface DooTaskGlobal {
timeOptionShortcuts(): Array<{ text: string; value(): [Date, Date] }>; timeOptionShortcuts(): Array<{ text: string; value(): [Date, Date] }>;
/** 对话标签(已完成/已删除/已归档) */ /** 对话标签(已完成/已删除/已归档) */
dialogTags(dialog: any): Array<{ color: string; text: string }>; dialogTags(dialog: any): Array<{ color: string; text: string }>;
/** 是否为 AI 机器人账号 */
isAiBotUser(user: any): boolean;
/** 是否为 AI 机器人单聊 */
isAiBotDialog(dialog: any): boolean;
/** 对话是否完成(返回 success 标签) */ /** 对话是否完成(返回 success 标签) */
dialogCompleted(dialog: any): { color: string; text: string } | undefined; dialogCompleted(dialog: any): { color: string; text: string } | undefined;
/** 返回对话未读数量(不含免打扰,但如果免打扰中有@则返回@数量) */ /** 返回对话未读数量(不含免打扰,但如果免打扰中有@则返回@数量) */