mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-17 14:42:51 +00:00
perf: 支持AI在项目群里创建任务
This commit is contained in:
parent
61ebbac333
commit
08153cd99b
@ -2885,6 +2885,58 @@ class DialogController extends AbstractController
|
|||||||
return Base::retSuccess('success', $topMsg);
|
return Base::retSuccess('success', $topMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/dialog/msg/applied 55. 标记消息已应用
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup dialog
|
||||||
|
* @apiName msg__applied
|
||||||
|
*
|
||||||
|
* @apiParam {String} type 类型
|
||||||
|
* - CreateTask: 创建任务
|
||||||
|
* @apiParam {Number} index 索引
|
||||||
|
* @apiParam {Number} msg_id 消息ID
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function msg__applied()
|
||||||
|
{
|
||||||
|
User::auth();
|
||||||
|
//
|
||||||
|
$msg_id = intval(Request::input('msg_id'));
|
||||||
|
$type = trim(Request::input('type'));
|
||||||
|
$index = intval(Request::input('index'));
|
||||||
|
//
|
||||||
|
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
|
||||||
|
if (empty($msg)) {
|
||||||
|
return Base::retError("消息不存在或已被删除");
|
||||||
|
}
|
||||||
|
WebSocketDialog::checkDialog($msg->dialog_id);
|
||||||
|
//
|
||||||
|
$originalMsg = $msg->getRawOriginal('msg');
|
||||||
|
$pattern = '/```\s*' . preg_quote($type, '/') . '\s*(applying|applied)?\s*(\n|\\\\n)/';
|
||||||
|
$count = -1;
|
||||||
|
$updatedMsg = preg_replace_callback($pattern, function($matches) use (&$count, $index, $type) {
|
||||||
|
$count++;
|
||||||
|
if ($count === $index || ($index === 0 && $count === 1)) {
|
||||||
|
return "```{$type} applied{$matches[2]}";
|
||||||
|
}
|
||||||
|
return $matches[0];
|
||||||
|
}, $originalMsg);
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
return Base::retError("未找到可应用的规则");
|
||||||
|
}
|
||||||
|
|
||||||
|
$msg->msg = $updatedMsg;
|
||||||
|
$msg->save();
|
||||||
|
//
|
||||||
|
return Base::retSuccess("success");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/dialog/sticker/search 56. 搜索在线表情
|
* @api {get} api/dialog/sticker/search 56. 搜索在线表情
|
||||||
*
|
*
|
||||||
|
|||||||
@ -3220,6 +3220,89 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async applyCreateTask(event, el) {
|
||||||
|
const currentTarget = event.target;
|
||||||
|
if (currentTarget.classList.contains('applying') || currentTarget.classList.contains('applied')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentTarget.classList.add('applying')
|
||||||
|
|
||||||
|
if (this.dialogData.group_type !== 'project') {
|
||||||
|
currentTarget.classList.remove('applying')
|
||||||
|
$A.modalError('只有在项目中才能创建任务')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = event.target;
|
||||||
|
while (target) {
|
||||||
|
if (target.classList.contains('apply-create-task')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (target.classList.contains('dialog-scroller')) {
|
||||||
|
target = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
target = target.parentElement;
|
||||||
|
}
|
||||||
|
if (!target) {
|
||||||
|
currentTarget.classList.remove('applying')
|
||||||
|
$A.modalError('未找到任务内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.dialogData.group_info) {
|
||||||
|
currentTarget.classList.remove('applying')
|
||||||
|
$A.modalError('项目不存在')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTaskElements = el.querySelectorAll('.apply-create-task');
|
||||||
|
const taskIndex = Array.from(allTaskElements).indexOf(target);
|
||||||
|
const taskList = Array.from(target.querySelectorAll('li'))
|
||||||
|
.map(item => {
|
||||||
|
const title = item.querySelector('.title')?.innerText?.trim();
|
||||||
|
if (!title) return null;
|
||||||
|
|
||||||
|
const desc = item.querySelector('.desc')?.innerText?.trim() || '';
|
||||||
|
const content = desc ? desc.split('\n')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(line => `<p>${line.trim()}</p>`)
|
||||||
|
.join('') : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
project_id: this.dialogData.group_info.id,
|
||||||
|
name: title,
|
||||||
|
content
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const results = await Promise.all(taskList.map(item =>
|
||||||
|
this.$store.dispatch("taskAdd", item).then(
|
||||||
|
success => ({ success: true, data: success }),
|
||||||
|
error => ({ success: false, error: error })
|
||||||
|
)
|
||||||
|
));
|
||||||
|
const successTasks = results.filter(r => r.success).map(r => r.data);
|
||||||
|
const failedTasks = results.filter(r => !r.success).map(r => r.error);
|
||||||
|
if (failedTasks.length > 0) {
|
||||||
|
$A.modalError(`成功创建 ${successTasks.length} 个任务,${failedTasks.length} 个任务创建失败`);
|
||||||
|
} else {
|
||||||
|
$A.messageSuccess(`成功创建 ${successTasks.length} 个任务`);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTarget.classList.remove('applying')
|
||||||
|
currentTarget.classList.add('applied')
|
||||||
|
|
||||||
|
await this.$store.dispatch("call", {
|
||||||
|
url: 'dialog/msg/applied',
|
||||||
|
data: {
|
||||||
|
type: 'CreateTask',
|
||||||
|
index: taskIndex,
|
||||||
|
msg_id: this.operateItem.id
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
openTranslationMenu(event) {
|
openTranslationMenu(event) {
|
||||||
const list = Object.keys(languageList).map(item => ({
|
const list = Object.keys(languageList).map(item => ({
|
||||||
label: languageList[item],
|
label: languageList[item],
|
||||||
@ -3325,6 +3408,13 @@ export default {
|
|||||||
}
|
}
|
||||||
const {target, clientX} = event
|
const {target, clientX} = event
|
||||||
|
|
||||||
|
// 创建任务
|
||||||
|
if (target.classList.contains('apply-create-task-button')) {
|
||||||
|
this.operateItem = this.findMsgByElement(el)
|
||||||
|
this.applyCreateTask(event, el)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 点击切换翻译
|
// 点击切换翻译
|
||||||
if (target.classList.contains('translation-label')) {
|
if (target.classList.contains('translation-label')) {
|
||||||
this.operateItem = this.findMsgByElement(el)
|
this.operateItem = this.findMsgByElement(el)
|
||||||
|
|||||||
177
resources/assets/js/store/markdown.js
vendored
177
resources/assets/js/store/markdown.js
vendored
@ -9,6 +9,12 @@ import mdKatex from "@traptitech/markdown-it-katex";
|
|||||||
const MarkdownUtils = {
|
const MarkdownUtils = {
|
||||||
mdi: null,
|
mdi: null,
|
||||||
mds: null,
|
mds: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析Markdown
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
formatMsg: (text) => {
|
formatMsg: (text) => {
|
||||||
const array = text.match(/<img\s+[^>]*?>/g);
|
const array = text.match(/<img\s+[^>]*?>/g);
|
||||||
if (array) {
|
if (array) {
|
||||||
@ -18,11 +24,181 @@ const MarkdownUtils = {
|
|||||||
}
|
}
|
||||||
return text
|
return text
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高亮代码块
|
||||||
|
* @param {*} str
|
||||||
|
* @param {*} lang
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
highlightBlock: (str, lang = '') => {
|
highlightBlock: (str, lang = '') => {
|
||||||
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${$A.L('复制')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
|
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${$A.L('复制')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MarkdownPluginUtils = {
|
||||||
|
// 配置选项
|
||||||
|
config: {
|
||||||
|
maxTitleLength: 200,
|
||||||
|
maxDescLength: 1000,
|
||||||
|
maxItems: 200,
|
||||||
|
defaultTitle: '创建任务'
|
||||||
|
},
|
||||||
|
|
||||||
|
// HTML转义函数
|
||||||
|
escapeHtml(unsafe) {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
},
|
||||||
|
|
||||||
|
// 验证输入
|
||||||
|
validateInput(value, maxLength) {
|
||||||
|
if (!value) return '';
|
||||||
|
if (typeof value !== 'string') return '';
|
||||||
|
if (value.length > maxLength) {
|
||||||
|
return value.substring(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 解析任务项
|
||||||
|
parseTaskItems(content) {
|
||||||
|
const items = [];
|
||||||
|
let currentItem = {};
|
||||||
|
let itemCount = 0;
|
||||||
|
|
||||||
|
content.forEach(line => {
|
||||||
|
line = line.trim();
|
||||||
|
if (!line) {
|
||||||
|
if (Object.keys(currentItem).length > 0) {
|
||||||
|
items.push(currentItem);
|
||||||
|
currentItem = {};
|
||||||
|
itemCount++;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemCount >= this.config.maxItems) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [key, ...valueParts] = line.split(':');
|
||||||
|
const value = valueParts.join(':').trim();
|
||||||
|
|
||||||
|
if (key === 'title' && value) {
|
||||||
|
if (Object.keys(currentItem).length > 0) {
|
||||||
|
items.push(currentItem);
|
||||||
|
currentItem = {};
|
||||||
|
itemCount++;
|
||||||
|
}
|
||||||
|
currentItem.title = this.validateInput(value, this.config.maxTitleLength);
|
||||||
|
} else if (key === 'desc' && value) {
|
||||||
|
currentItem.desc = this.validateInput(value, this.config.maxDescLength);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(currentItem).length > 0 && itemCount < this.config.maxItems) {
|
||||||
|
items.push(currentItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 生成HTML
|
||||||
|
generateTaskHtml(items, status = null) {
|
||||||
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = [
|
||||||
|
'<div class="apply-create-task">',
|
||||||
|
'<ul>'
|
||||||
|
];
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
if (item.title) {
|
||||||
|
html.push(`<li>`);
|
||||||
|
html.push(`<div class="task-index">${index + 1}.</div>`);
|
||||||
|
html.push(`<div class="task-item">`);
|
||||||
|
html.push(`<div class="title">${this.escapeHtml(item.title)}</div>`);
|
||||||
|
if (item.desc) {
|
||||||
|
html.push(`<div class="desc">${this.escapeHtml(item.desc)}</div>`);
|
||||||
|
}
|
||||||
|
html.push('</div>');
|
||||||
|
html.push('</li>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
html.push(
|
||||||
|
'</ul>',
|
||||||
|
`<div class="apply-button"><div class="apply-create-task-button ${status||''}">${this.escapeHtml($A.L(this.config.defaultTitle))}</div></div>`,
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
return html.join('\n');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改初始化插件函数
|
||||||
|
initCreateTaskPlugin(md) {
|
||||||
|
md.block.ruler.before('fence', 'create_task', (state, startLine, endLine, silent) => {
|
||||||
|
try {
|
||||||
|
const start = state.bMarks[startLine] + state.tShift[startLine];
|
||||||
|
const max = state.eMarks[startLine];
|
||||||
|
|
||||||
|
const match = state.src.slice(start, max).trim().match(/^```\s*CreateTask\s*(applying|applied)?$/);
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextLine = startLine + 1;
|
||||||
|
let content = [];
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
while (nextLine < endLine) {
|
||||||
|
const line = state.src.slice(state.bMarks[nextLine], state.eMarks[nextLine]).trim();
|
||||||
|
if (line === '```') {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
content.push(line);
|
||||||
|
nextLine++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 token 并设置为空字符串内容
|
||||||
|
const token = state.push('html_block', '', 0);
|
||||||
|
|
||||||
|
// 如果有内容,则解析并生成HTML
|
||||||
|
if (content.length > 0) {
|
||||||
|
const items = this.parseTaskItems(content);
|
||||||
|
const html = this.generateTaskHtml(items, match[1]);
|
||||||
|
token.content = html || '';
|
||||||
|
} else {
|
||||||
|
token.content = ''; // 空内容直接返回空字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
token.map = [startLine, nextLine + 1];
|
||||||
|
state.line = nextLine + 1;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in create_task parser:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export function MarkdownConver(text) {
|
export function MarkdownConver(text) {
|
||||||
if (text === '...') {
|
if (text === '...') {
|
||||||
return '<div class="input-blink"></div>'
|
return '<div class="input-blink"></div>'
|
||||||
@ -41,6 +217,7 @@ export function MarkdownConver(text) {
|
|||||||
})
|
})
|
||||||
MarkdownUtils.mdi.use(mila, {attrs: {target: '_blank', rel: 'noopener'}})
|
MarkdownUtils.mdi.use(mila, {attrs: {target: '_blank', rel: 'noopener'}})
|
||||||
MarkdownUtils.mdi.use(mdKatex, {blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000'})
|
MarkdownUtils.mdi.use(mdKatex, {blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000'})
|
||||||
|
MarkdownPluginUtils.initCreateTaskPlugin(MarkdownUtils.mdi);
|
||||||
}
|
}
|
||||||
return MarkdownUtils.formatMsg(MarkdownUtils.mdi.render(text))
|
return MarkdownUtils.formatMsg(MarkdownUtils.mdi.render(text))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2037,6 +2037,97 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.apply-create-task {
|
||||||
|
min-width: 160px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.task-index {
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
.task-item {
|
||||||
|
line-height: 18px;
|
||||||
|
.title,
|
||||||
|
.desc {
|
||||||
|
word-break: break-all;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
padding-top: 4px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.apply-button {
|
||||||
|
display: inline-block;
|
||||||
|
user-select: none;
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #515a6e;
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: #dcdee2;
|
||||||
|
cursor: pointer;
|
||||||
|
&:before {
|
||||||
|
font-family: "taskfont", "serif" !important;
|
||||||
|
content: "\e6f2";
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
&.applying,
|
||||||
|
&.applied {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
&.applying {
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid #dddd;
|
||||||
|
border-bottom-color: $primary-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: pureing-rotation 0.75s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.applied {
|
||||||
|
color: #c5c8ce;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-color: #dcdee2;
|
||||||
|
&:before {
|
||||||
|
content: "\e684";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.window-touch) {
|
body:not(.window-touch) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user