feat(ai-assistant): 添加欢迎界面快捷提示功能和交互优化

主要变更:
  - 新增场景化快捷提示,根据页面类型显示相关操作建议
  - 重新设计欢迎界面 UI,支持图标和可点击的提示卡片
  - 修复浮动按钮点击判断逻辑(移动距离<5px 且 按下时间<200ms)
  - 优化加载状态显示,移除冗余文案
  - 支持 base64 编码格式的文件链接
This commit is contained in:
kuaifan 2026-01-16 02:31:13 +08:00
parent 70be6619e9
commit 9234fe3ed1
5 changed files with 546 additions and 25 deletions

View File

@ -2237,7 +2237,6 @@ Webhook事件
用户
应用此内容
生成中...
等待 AI 回复...
请输入你的问题...
选择模型
@ -2286,4 +2285,4 @@ AI 项目助手
AI 汇报分析
AI 整理汇报
AI 任务助手
AI 消息助手
AI 消息助手

View File

@ -205,8 +205,8 @@ export default {
this.savePosition();
this.dragging = false;
// 5px 200ms
if (moveDistance < 5 || duration < 200) {
// 5px 200ms
if (moveDistance < 5 && duration < 200) {
this.onClick();
}
},

View File

@ -70,8 +70,8 @@
</Button>
</template>
<template v-else>
<Icon type="ios-sync" class="ai-assistant-output-icon icon-loading"/>
<span class="ai-assistant-output-status">{{ loadingText || $L('生成中...') }}</span>
<Icon type="ios-loading" class="ai-assistant-output-icon icon-loading"/>
<span v-if="loadingText" class="ai-assistant-output-status">{{ loadingText }}</span>
</template>
</div>
<div class="ai-assistant-output-meta">
@ -90,13 +90,22 @@
</div>
<div v-else-if="displayMode === 'chat'" class="ai-assistant-welcome" @click="onFocus">
<div class="ai-assistant-welcome-icon">
<i class="taskfont">&#xe8a1;</i>
<svg class="no-dark-content" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M385.80516777 713.87417358c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404756l-48.91927648-123.9413531c-18.40341303-46.75969229-55.77360888-84.0359932-102.53330118-102.53330117l-123.94135309-48.91927649c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.8257541s7.79328205-24.13100586 19.62404757-28.82575407l123.94135309-48.91927649c46.75969229-18.40341303 84.0359932-55.77360888 102.53330118-102.53330119l48.91927648-123.94135308c4.69474822-11.83076552 16.05603892-19.62404757 28.8257541-19.62404757s24.13100586 7.79328205 28.82575408 19.62404757l48.91927648 123.94135308c18.40341303 46.75969229 55.77360888 84.0359932 102.53330118 102.53330119l123.94135309 48.91927649c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575407 0 12.76971517-7.79328205 24.13100586-19.62404757 28.8257541l-123.94135309 48.91927649c-46.75969229 18.40341303-84.0359932 55.77360888-102.53330118 102.53330117l-48.91927648 123.9413531c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575408 19.62404756zM177.45224165 390.12433614l50.89107073 20.0935224c62.62794129 24.69437565 112.67395736 74.74039171 137.368333 137.36833299l20.09352239 50.89107073 20.0935224-50.89107073c24.69437565-62.62794129 74.74039171-112.67395736 137.368333-137.36833299l50.89107072-20.0935224-50.89107073-20.09352239c-62.62794129-24.69437565-112.67395736-74.74039171-137.36833299-137.36833301l-20.09352239-50.89107074-20.0935224 50.89107074c-24.69437565 62.62794129-74.74039171 112.67395736-137.368333 137.36833301l-50.89107073 20.09352239zM771.33789183 957.62550131c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404758l-26.6661699-67.6043744c-8.63833672-21.87752672-26.10280012-39.34199011-47.98032684-47.98032684l-67.60437441-26.6661699c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.82575409s7.79328205-24.13100586 19.62404757-28.82575409l67.60437441-26.6661699c21.87752672-8.63833672 39.34199011-26.10280012 47.98032684-47.98032685l26.6661699-67.6043744c4.69474822-11.83076552 16.05603892-19.62404757 28.82575409-19.62404757s24.13100586 7.79328205 28.82575409 19.62404757l26.66616991 67.6043744c8.63833672 21.87752672 26.10280012 39.34199011 47.98032684 47.98032685l67.6043744 26.6661699c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575409s-7.79328205 24.13100586-19.62404757 28.82575409l-67.6043744 26.6661699c-21.87752672 8.63833672-39.34199011 26.10280012-47.98032684 47.98032684l-26.66616991 67.6043744c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575409 19.62404758z m-75.58544639-190.70067281c33.61439727 14.83540438 60.75004201 41.87715415 75.49155143 75.49155143 14.83540438-33.61439727 41.87715415-60.75004201 75.49155142-75.49155143-33.61439727-14.83540438-60.75004201-41.87715415-75.49155142-75.49155143-14.74150942 33.61439727-41.87715415 60.75004201-75.49155143 75.49155143z"/>
</svg>
</div>
<div class="ai-assistant-welcome-title">
欢迎使用 AI 助手
{{ $L('欢迎使用 AI 助手') }}
</div>
<div class="ai-assistant-welcome-swiper">
<!-- Swiper 容器 -->
<div class="ai-assistant-welcome-prompts">
<div
v-for="(prompt, index) in welcomePrompts"
:key="index"
class="ai-assistant-prompt-card"
@click="onPromptClick(prompt)">
<span v-if="prompt.svg" class="ai-assistant-prompt-icon no-dark-content" v-html="prompt.svg"></span>
<span>{{ prompt.text }}</span>
</div>
</div>
</div>
<div class="ai-assistant-input">
@ -151,6 +160,7 @@ import {AIBotMap, AIModelNames} from "../../utils/ai";
import DialogMarkdown from "../../pages/manage/components/DialogMarkdown.vue";
import FloatButton from "./float-button.vue";
import AssistantModal from "./modal.vue";
import {getWelcomePrompts} from "./welcome-prompts";
export default {
name: 'AIAssistant',
@ -236,6 +246,9 @@ export default {
hasSessionHistory() {
return this.currentSessionList.length > 0;
},
welcomePrompts() {
return getWelcomePrompts(this.$store, this.$route?.params || {});
},
},
watch: {
inputModel(value) {
@ -250,6 +263,19 @@ export default {
this.$refs.inputRef?.focus();
},
/**
* 点击快捷提示填入输入框
*/
onPromptClick(prompt) {
if (!prompt || !prompt.text) {
return;
}
this.inputValue = prompt.text;
this.$nextTick(() => {
this.onFocus();
});
},
/**
* 挂载浮动按钮到 body
*/
@ -1220,7 +1246,6 @@ export default {
</script>
<style lang="scss">
.ai-assistant-header {
display: flex;
align-items: center;
@ -1568,6 +1593,8 @@ export default {
color: #999;
cursor: pointer;
transition: all 0.2s;
border-radius: 50%;
overflow: hidden;
&:hover {
color: #444;
transform: rotate(-90deg);
@ -1596,17 +1623,93 @@ export default {
flex-direction: column;
.ai-assistant-welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
@media (max-height: 650px) {
justify-content: normal;
}
.ai-assistant-welcome-icon {
margin-top: 12px;
i {
font-size: 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 50%;
background: #8bcf70;
margin-bottom: 24px;
svg {
width: 28px;
height: 28px;
fill: #fff;
}
}
.ai-assistant-welcome-title {
margin-top: 12px;
font-size: 16px;
margin-bottom: 24px;
font-weight: 500;
color: #303133;
}
.ai-assistant-welcome-swiper {
margin-top: 24px;
.ai-assistant-welcome-prompts {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
max-width: 100%;
padding: 0 8px;
}
.ai-assistant-prompt-card {
min-width: 0;
overflow: hidden;
display: flex;
align-items: center;
gap: 8px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
color: #303133;
cursor: pointer;
transition: all 0.2s;
padding: 8px 12px;
font-size: 13px;
&:hover {
border-color: #8bcf70;
box-shadow: 0 2px 8px rgba(139, 207, 112, 0.15);
}
&:active {
transform: scale(0.98);
}
.ai-assistant-prompt-icon {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
flex-shrink: 0;
svg {
width: 100%;
height: 100%;
stroke: #8bcf70;
}
}
> span:last-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
@ -1635,6 +1738,15 @@ body.dark-mode-reverse {
.ai-assistant-output {
background-color: #f5f5f5;
}
.ai-assistant-prompt-card {
background: #fff;
border-color: #d9d9d9;
&:hover {
background: rgba(102, 126, 234, 0.06);
}
}
}
.ai-assistant-chat {
background-color: #e9e9e9;

View File

@ -0,0 +1,402 @@
/**
* AI 助手欢迎界面快捷提示配置
*
* 根据不同页面场景显示相关的快捷提示帮助用户快速开始对话
* 提示内容基于 DooTask MCP 工具的实际能力设计
*/
import {languageName} from "../../language";
// SVG 图标定义
const SVG_ICONS = {
// 任务/待办
task: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
// 列表/概览
list: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>',
// 搜索
search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
// 日历/时间
calendar: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
// 文档/报告
document: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
// 添加/新建
plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
// 消息/对话
message: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
// 分析/图表
chart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>',
// 警告/逾期
alert: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>',
// 文件夹
folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
// 编辑/优化
edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
// 用户/团队
user: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
// 发送
send: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>',
// 时钟/截止
clock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
// 完成/勾选
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>',
};
/**
* 根据当前语言获取文本
*/
function getText(textObj) {
if (typeof textObj === 'string') {
return textObj;
}
const isZh = languageName && languageName.includes('zh');
return isZh ? textObj.zh : textObj.en;
}
/**
* 获取当前场景的快捷提示列表
* @param {Object} store - Vuex store 实例
* @param {Object} routeParams - 路由参数
* @returns {Array} 快捷提示列表 [{ text, svg }]
*/
export function getWelcomePrompts(store, routeParams = {}) {
const routeName = store.state.routeName;
const promptsMap = {
// 主要管理页面
'manage-dashboard': getDashboardPrompts,
'manage-project': getProjectPrompts,
'manage-messenger': getMessengerPrompts,
'manage-calendar': getCalendarPrompts,
'manage-file': getFilePrompts,
// 独立页面
'single-task': getSingleTaskPrompts,
'single-task-content': getSingleTaskPrompts,
'single-dialog': getSingleDialogPrompts,
'single-file': getSingleFilePrompts,
'single-file-task': getSingleFileTaskPrompts,
'single-report-edit': getSingleReportEditPrompts,
'single-report-detail': getSingleReportDetailPrompts,
};
const getPrompts = promptsMap[routeName];
const rawPrompts = getPrompts ? getPrompts(store, routeParams) : getDefaultPrompts();
// 转换文本为当前语言
return rawPrompts.map(item => ({
text: getText(item.text),
svg: item.svg,
}));
}
/**
* 仪表盘提示 - 聚焦任务管理和工作安排
*/
function getDashboardPrompts(store) {
const dashboardTask = store.getters.dashboardTask || {};
const prompts = [];
const overdueCount = dashboardTask.overdue_count || 0;
const todayCount = dashboardTask.today_count || 0;
// 根据实际数据动态调整提示
if (overdueCount > 0) {
prompts.push({
text: {
zh: `列出我的 ${overdueCount} 个逾期任务`,
en: `List my ${overdueCount} overdue tasks`,
},
svg: SVG_ICONS.alert,
});
}
if (todayCount > 0) {
prompts.push({
text: {
zh: `今天要完成哪些任务?`,
en: `What tasks are due today?`,
},
svg: SVG_ICONS.calendar,
});
}
// 补充通用提示
prompts.push(
{
text: { zh: '我本周有哪些任务?', en: 'What are my tasks this week?' },
svg: SVG_ICONS.list,
},
{
text: { zh: '哪些任务需要我协助?', en: 'Which tasks need my assistance?' },
svg: SVG_ICONS.user,
},
);
return prompts.slice(0, 4);
}
/**
* 项目页提示 - 聚焦项目任务管理
*/
function getProjectPrompts(store) {
const project = store.getters.projectData || {};
if (!project.id) {
// 项目列表页
return [
{
text: { zh: '我参与了哪些项目?', en: 'Which projects am I involved in?' },
svg: SVG_ICONS.folder,
},
{
text: { zh: '哪个项目有逾期任务?', en: 'Which project has overdue tasks?' },
svg: SVG_ICONS.alert,
},
];
}
// 项目详情页 - 提供具体操作
const projectName = project.name || '';
return [
{
text: {
zh: '这个项目还有多少未完成的任务?',
en: 'How many incomplete tasks in this project?',
},
svg: SVG_ICONS.list,
},
{
text: {
zh: '帮我在这个项目创建一个任务',
en: 'Help me create a task in this project',
},
svg: SVG_ICONS.plus,
},
{
text: {
zh: '这个项目有哪些逾期任务?',
en: 'What tasks are overdue in this project?',
},
svg: SVG_ICONS.alert,
},
{
text: {
zh: '查看项目成员的任务分配',
en: 'View task assignments by member',
},
svg: SVG_ICONS.user,
},
];
}
/**
* 消息页提示 - 聚焦沟通和消息查找
*/
function getMessengerPrompts(store) {
const dialogId = store.state.dialogId;
const dialogs = store.state.cacheDialogs || [];
const dialog = dialogs.find(d => d.id === dialogId);
if (!dialog) {
// 消息列表页
return [
{
text: { zh: '给某人发送一条消息', en: 'Send a message to someone' },
svg: SVG_ICONS.send,
},
{
text: { zh: '搜索包含关键词的聊天', en: 'Search chats containing keyword' },
svg: SVG_ICONS.search,
},
];
}
// 对话详情页
const dialogName = dialog.name || '';
return [
{
text: {
zh: '查看这个对话最近的消息',
en: 'View recent messages in this chat',
},
svg: SVG_ICONS.message,
},
{
text: {
zh: '搜索对话中的文件',
en: 'Search files in this chat',
},
svg: SVG_ICONS.search,
},
{
text: {
zh: '给对方发一条消息',
en: 'Send a message',
},
svg: SVG_ICONS.send,
},
];
}
/**
* 日历页提示 - 聚焦时间维度的任务查看
*/
function getCalendarPrompts() {
return [
{
text: { zh: '今天有哪些任务到期?', en: 'What tasks are due today?' },
svg: SVG_ICONS.calendar,
},
{
text: { zh: '本周有哪些任务要完成?', en: 'What tasks are due this week?' },
svg: SVG_ICONS.list,
},
{
text: { zh: '下周的任务安排', en: 'Tasks scheduled for next week' },
svg: SVG_ICONS.clock,
},
];
}
/**
* 文件页提示 - 聚焦文件查找
*/
function getFilePrompts() {
return [
{
text: { zh: '搜索文件名包含...', en: 'Search files named...' },
svg: SVG_ICONS.search,
},
{
text: { zh: '查看我最近的文件', en: 'View my recent files' },
svg: SVG_ICONS.folder,
},
];
}
/**
* 任务详情提示 - 聚焦当前任务的操作
*/
function getSingleTaskPrompts() {
return [
{
text: { zh: '帮我添加一个子任务', en: 'Help me add a subtask' },
svg: SVG_ICONS.plus,
},
{
text: { zh: '修改这个任务的截止时间', en: 'Change the due date of this task' },
svg: SVG_ICONS.clock,
},
{
text: { zh: '将这个任务标记为完成', en: 'Mark this task as complete' },
svg: SVG_ICONS.check,
},
{
text: { zh: '查看这个任务的附件', en: 'View attachments of this task' },
svg: SVG_ICONS.folder,
},
];
}
/**
* 对话页提示
*/
function getSingleDialogPrompts() {
return [
{
text: { zh: '查看最近的消息记录', en: 'View recent messages' },
svg: SVG_ICONS.message,
},
{
text: { zh: '发送一条消息', en: 'Send a message' },
svg: SVG_ICONS.send,
},
];
}
/**
* 文件预览提示
*/
function getSingleFilePrompts() {
return [
{
text: { zh: '查看这个文件的详情', en: 'View file details' },
svg: SVG_ICONS.document,
},
{
text: { zh: '搜索类似的文件', en: 'Search similar files' },
svg: SVG_ICONS.search,
},
];
}
/**
* 任务附件提示
*/
function getSingleFileTaskPrompts() {
return [
{
text: { zh: '查看这个任务的所有附件', en: 'View all attachments of this task' },
svg: SVG_ICONS.folder,
},
{
text: { zh: '查看任务详情', en: 'View task details' },
svg: SVG_ICONS.task,
},
];
}
/**
* 汇报编辑提示 - 聚焦汇报生成
*/
function getSingleReportEditPrompts() {
return [
{
text: { zh: '根据本周任务生成周报', en: 'Generate weekly report from tasks' },
svg: SVG_ICONS.document,
},
{
text: { zh: '根据今天任务生成日报', en: 'Generate daily report from tasks' },
svg: SVG_ICONS.calendar,
},
{
text: { zh: '查看我上周的汇报', en: 'View my last week\'s report' },
svg: SVG_ICONS.search,
},
];
}
/**
* 汇报详情提示
*/
function getSingleReportDetailPrompts() {
return [
{
text: { zh: '标记为已读', en: 'Mark as read' },
svg: SVG_ICONS.check,
},
{
text: { zh: '查看这个人的其他汇报', en: 'View other reports from this person' },
svg: SVG_ICONS.list,
},
];
}
/**
* 默认提示 - 通用场景
*/
function getDefaultPrompts() {
return [
{
text: { zh: '我有哪些未完成的任务?', en: 'What tasks do I have pending?' },
svg: SVG_ICONS.task,
},
{
text: { zh: '搜索任务或项目', en: 'Search tasks or projects' },
svg: SVG_ICONS.search,
},
{
text: { zh: '帮我写一份工作汇报', en: 'Help me write a work report' },
svg: SVG_ICONS.document,
},
];
}

View File

@ -93,15 +93,17 @@ export default {
/**
* 处理 dootask://
* 格式: dootask://type/id dootask://type/id1/id2
* 文件链接支持: dootask://file/123 (ID) dootask://file/OSwxLHY3ZlN2R245 (base64)
*/
handleDooTaskLink(href) {
const match = href.match(/^dootask:\/\/(\w+)\/(\d+)(?:\/(\d+))?$/);
const match = href.match(/^dootask:\/\/(\w+)\/([^/]+)(?:\/(\d+))?$/);
if (!match) {
return;
}
const [, type, id, id2] = match;
const numId = parseInt(id, 10);
const isNumericId = /^\d+$/.test(id);
const numId = isNumericId ? parseInt(id, 10) : null;
const numId2 = id2 ? parseInt(id2, 10) : null;
switch (type) {
@ -115,12 +117,18 @@ export default {
break;
case 'file':
this.beforeNavigate?.();
this.goForward({ name: 'manage-file', params: { folderId: 0, fileId: null, shakeId: numId } });
this.$store.state.fileShakeId = numId;
setTimeout(() => {
this.$store.state.fileShakeId = 0;
}, 600);
if (isNumericId) {
// ID
this.beforeNavigate?.();
this.goForward({ name: 'manage-file', params: { folderId: 0, fileId: null, shakeId: numId } });
this.$store.state.fileShakeId = numId;
setTimeout(() => {
this.$store.state.fileShakeId = 0;
}, 600);
} else {
// IDbase64
window.open($A.mainUrl('single/file/' + id));
}
break;
case 'contact':