feat: 扩展收藏功能,支持消息类型的收藏

- 在 UserFavorite 模型中添加消息类型常量
- 更新 UsersController,支持消息的收藏、切换和状态检查
- 修改前端 Vue 组件以实现消息的收藏操作和状态显示
- 优化收藏管理界面,支持消息类型的展示与处理
This commit is contained in:
kuaifan 2025-09-23 09:48:06 +08:00
parent 18a922b5cd
commit 73ca4b1ea5
6 changed files with 149 additions and 17 deletions

View File

@ -2835,7 +2835,7 @@ class UsersController extends AbstractController
* @apiGroup users * @apiGroup users
* @apiName favorites * @apiName favorites
* *
* @apiParam {String} [type] 收藏类型过滤 (task/project/file) * @apiParam {String} [type] 收藏类型过滤 (task/project/file/message)
* @apiParam {Number} [page=1] 页码 * @apiParam {Number} [page=1] 页码
* @apiParam {Number} [pagesize=20] 每页数量 * @apiParam {Number} [pagesize=20] 每页数量
* *
@ -2852,7 +2852,7 @@ class UsersController extends AbstractController
$pageSize = min(intval(Request::input('pagesize', 20)), 100); $pageSize = min(intval(Request::input('pagesize', 20)), 100);
// //
// 验证收藏类型 // 验证收藏类型
$allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE]; $allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE, UserFavorite::TYPE_MESSAGE];
if ($type && !in_array($type, $allowedTypes)) { if ($type && !in_array($type, $allowedTypes)) {
return Base::retError('无效的收藏类型'); return Base::retError('无效的收藏类型');
} }
@ -2870,7 +2870,7 @@ class UsersController extends AbstractController
* @apiGroup users * @apiGroup users
* @apiName favorite__toggle * @apiName favorite__toggle
* *
* @apiParam {String} type 收藏类型 (task/project/file) * @apiParam {String} type 收藏类型 (task/project/file/message)
* @apiParam {Number} id 收藏对象ID * @apiParam {Number} id 收藏对象ID
* *
* @apiSuccess {Number} ret 返回状态码1正确、0错误 * @apiSuccess {Number} ret 返回状态码1正确、0错误
@ -2889,7 +2889,7 @@ class UsersController extends AbstractController
} }
// //
// 验证收藏类型 // 验证收藏类型
$allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE]; $allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE, UserFavorite::TYPE_MESSAGE];
if (!in_array($type, $allowedTypes)) { if (!in_array($type, $allowedTypes)) {
return Base::retError('无效的收藏类型'); return Base::retError('无效的收藏类型');
} }
@ -2914,6 +2914,12 @@ class UsersController extends AbstractController
return Base::retError('文件不存在'); return Base::retError('文件不存在');
} }
break; break;
case UserFavorite::TYPE_MESSAGE:
$object = WebSocketDialogMsg::whereId($id)->first();
if (!$object) {
return Base::retError('消息不存在');
}
break;
} }
// //
$result = UserFavorite::toggleFavorite($user->userid, $type, $id); $result = UserFavorite::toggleFavorite($user->userid, $type, $id);
@ -2930,7 +2936,7 @@ class UsersController extends AbstractController
* @apiGroup users * @apiGroup users
* @apiName favorites__clean * @apiName favorites__clean
* *
* @apiParam {String} [type] 收藏类型 (task/project/file),不传则清理全部 * @apiParam {String} [type] 收藏类型 (task/project/file/message),不传则清理全部
* *
* @apiSuccess {Number} ret 返回状态码1正确、0错误 * @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {String} msg 返回信息(错误描述)
@ -2944,7 +2950,7 @@ class UsersController extends AbstractController
// //
// 验证收藏类型 // 验证收藏类型
if ($type) { if ($type) {
$allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE]; $allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE, UserFavorite::TYPE_MESSAGE];
if (!in_array($type, $allowedTypes)) { if (!in_array($type, $allowedTypes)) {
return Base::retError('无效的收藏类型'); return Base::retError('无效的收藏类型');
} }
@ -2964,7 +2970,7 @@ class UsersController extends AbstractController
* @apiGroup users * @apiGroup users
* @apiName favorite__check * @apiName favorite__check
* *
* @apiParam {String} type 收藏类型 (task/project/file) * @apiParam {String} type 收藏类型 (task/project/file/message)
* @apiParam {Number} id 收藏对象ID * @apiParam {Number} id 收藏对象ID
* *
* @apiSuccess {Number} ret 返回状态码1正确、0错误 * @apiSuccess {Number} ret 返回状态码1正确、0错误
@ -2983,7 +2989,7 @@ class UsersController extends AbstractController
} }
// //
// 验证收藏类型 // 验证收藏类型
$allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE]; $allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE, UserFavorite::TYPE_MESSAGE];
if (!in_array($type, $allowedTypes)) { if (!in_array($type, $allowedTypes)) {
return Base::retError('无效的收藏类型'); return Base::retError('无效的收藏类型');
} }

View File

@ -38,6 +38,7 @@ class UserFavorite extends AbstractModel
const TYPE_TASK = 'task'; const TYPE_TASK = 'task';
const TYPE_PROJECT = 'project'; const TYPE_PROJECT = 'project';
const TYPE_FILE = 'file'; const TYPE_FILE = 'file';
const TYPE_MESSAGE = 'message';
protected $fillable = [ protected $fillable = [
'userid', 'userid',
@ -126,13 +127,15 @@ class UserFavorite extends AbstractModel
$data = [ $data = [
'tasks' => [], 'tasks' => [],
'projects' => [], 'projects' => [],
'files' => [] 'files' => [],
'messages' => []
]; ];
// 分组收集ID // 分组收集ID
$taskIds = []; $taskIds = [];
$projectIds = []; $projectIds = [];
$fileIds = []; $fileIds = [];
$messageIds = [];
foreach ($favorites->items() as $favorite) { foreach ($favorites->items() as $favorite) {
switch ($favorite->favoritable_type) { switch ($favorite->favoritable_type) {
@ -145,6 +148,9 @@ class UserFavorite extends AbstractModel
case self::TYPE_FILE: case self::TYPE_FILE:
$fileIds[] = $favorite->favoritable_id; $fileIds[] = $favorite->favoritable_id;
break; break;
case self::TYPE_MESSAGE:
$messageIds[] = $favorite->favoritable_id;
break;
} }
} }
@ -230,6 +236,38 @@ class UserFavorite extends AbstractModel
} }
} }
if (!empty($messageIds)) {
$messages = WebSocketDialogMsg::select([
'id', 'dialog_id', 'userid', 'type', 'msg', 'created_at'
])->whereIn('id', $messageIds)->get()->keyBy('id');
foreach ($favorites->items() as $favorite) {
if ($favorite->favoritable_type === self::TYPE_MESSAGE && isset($messages[$favorite->favoritable_id])) {
$message = $messages[$favorite->favoritable_id];
// 使用 previewTextMsg 获取消息预览文本
$previewText = '';
if ($message->msg && is_array($message->msg)) {
$previewText = WebSocketDialogMsg::previewTextMsg($message->msg);
}
// 如果没有预览文本,使用消息类型作为标题
if (empty($previewText)) {
$previewText = '[' . ucfirst($message->type) . ']';
}
$data['messages'][] = [
'id' => $message->id,
'name' => $previewText,
'dialog_id' => $message->dialog_id,
'userid' => $message->userid,
'type' => $message->type,
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
];
}
}
}
return [ return [
'data' => $data, 'data' => $data,
'total' => $favorites->total(), 'total' => $favorites->total(),

View File

@ -376,6 +376,10 @@
<i class="taskfont">&#xe61e;</i> <i class="taskfont">&#xe61e;</i>
<span>{{ $L(operateItem.tag ? '取消标注' : '标注') }}</span> <span>{{ $L(operateItem.tag ? '取消标注' : '标注') }}</span>
</li> </li>
<li @click="onOperate('favorite')">
<i class="taskfont">{{ operateItem.favorited ? '&#xe683;' : '&#xe679;' }}</i>
<span>{{ $L(operateItem.favorited ? '取消收藏' : '收藏') }}</span>
</li>
<li v-if="actionPermission(operateItem, 'newTask')" @click="onOperate('newTask')"> <li v-if="actionPermission(operateItem, 'newTask')" @click="onOperate('newTask')">
<i class="taskfont">&#xe7b8;</i> <i class="taskfont">&#xe7b8;</i>
<span>{{ $L('新任务') }}</span> <span>{{ $L('新任务') }}</span>
@ -3135,6 +3139,9 @@ export default {
}) })
} }
} }
if (this.operateVisible) {
this.checkMessageFavoriteStatus(this.operateItem);
}
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.operateItem.clientX = event.clientX this.operateItem.clientX = event.clientX
this.operateItem.clientY = event.clientY this.operateItem.clientY = event.clientY
@ -3286,6 +3293,10 @@ export default {
this.onTag() this.onTag()
break; break;
case "favorite":
this.onFavorite()
break;
case "newTask": case "newTask":
let content = $A.formatMsgBasic(this.operateItem.msg.text) let content = $A.formatMsgBasic(this.operateItem.msg.text)
content = content.replace(/<img[^>]*?src=(["'])([^"']+?)(_thumb\.(png|jpg|jpeg))?\1[^>]*?>/g, `<img src="$2">`) content = content.replace(/<img[^>]*?src=(["'])([^"']+?)(_thumb\.(png|jpg|jpeg))?\1[^>]*?>/g, `<img src="$2">`)
@ -4064,6 +4075,47 @@ export default {
}); });
}, },
onFavorite() {
if (this.operateVisible) {
return
}
this.$store.dispatch("toggleFavorite", {
type: 'message',
id: this.operateItem.id
}).then(({data, msg}) => {
this.$set(this.operateItem, 'favorited', data.favorited);
const message = this.dialogMsgs.find(msg => msg.id === this.operateItem.id);
if (message) {
this.$set(message, 'favorited', data.favorited);
}
this.$Message.success(msg);
}).catch(({msg}) => {
$A.messageError(msg);
});
},
checkMessageFavoriteStatus(message) {
if (!message.id) return;
this.$store.dispatch("checkFavoriteStatus", {
type: 'message',
id: message.id
}).then(({data}) => {
this.$set(this.operateItem, 'favorited', data.favorited || false);
const msgInList = this.dialogMsgs.find(msg => msg.id === message.id);
if (msgInList) {
this.$set(msgInList, 'favorited', data.favorited || false);
}
}).catch(() => {
this.$set(this.operateItem, 'favorited', false);
const msgInList = this.dialogMsgs.find(msg => msg.id === message.id);
if (msgInList) {
this.$set(msgInList, 'favorited', false);
}
});
},
onTypeChange(val) { onTypeChange(val) {
if (val === 'user') { if (val === 'user') {
if (this.todoSettingData.userids.length === 0 && this.todoSettingData.quick_value.length > 0) { if (this.todoSettingData.userids.length === 0 && this.todoSettingData.quick_value.length > 0) {

View File

@ -18,6 +18,7 @@
<Option value="task">{{$L('任务')}}</Option> <Option value="task">{{$L('任务')}}</Option>
<Option value="project">{{$L('项目')}}</Option> <Option value="project">{{$L('项目')}}</Option>
<Option value="file">{{$L('文件')}}</Option> <Option value="file">{{$L('文件')}}</Option>
<Option value="message">{{$L('消息')}}</Option>
</Select> </Select>
</div> </div>
</li> </li>
@ -85,16 +86,19 @@ export default {
const typeMap = { const typeMap = {
'task': this.$L('任务'), 'task': this.$L('任务'),
'project': this.$L('项目'), 'project': this.$L('项目'),
'file': this.$L('文件') 'file': this.$L('文件'),
'message': this.$L('消息')
}; };
const color = { const color = {
'task': 'primary', 'task': 'success',
'project': 'success', 'project': '#f87cbd',
'file': 'warning' 'file': 'warning',
'message': 'primary'
}; };
return h('Tag', { return h('Tag', {
class: 'favorite-type-tag',
props: { props: {
color: color[row.type] || 'default' color: color[row.type] || 'primary'
} }
}, typeMap[row.type] || row.type); }, typeMap[row.type] || row.type);
} }
@ -288,6 +292,21 @@ export default {
}); });
} }
//
if (data.data.messages) {
data.data.messages.forEach(message => {
this.allData.push({
id: message.id,
type: 'message',
name: message.name,
dialog_id: message.dialog_id,
userid: message.userid,
msg_type: message.type,
favorited_at: message.favorited_at,
});
});
}
this.total = data.total || this.allData.length; this.total = data.total || this.allData.length;
this.filterData(); this.filterData();
this.noText = '没有相关的收藏'; this.noText = '没有相关的收藏';
@ -349,6 +368,16 @@ export default {
}, 600); }, 600);
this.$emit('on-close'); this.$emit('on-close');
break; break;
case 'message':
this.$store.dispatch("openDialog", item.dialog_id).then(() => {
this.$store.state.dialogSearchMsgId = item.id;
if (this.$route.name === 'manage-messenger') {
this.$emit('on-close');
}
}).catch(({msg}) => {
$A.modalError(msg || this.$L('打开会话失败'));
});
break;
} }
}, },

View File

@ -2851,7 +2851,7 @@ export default {
/** /**
* 检查收藏状态 * 检查收藏状态
* @param dispatch * @param dispatch
* @param {object} params {type: 'task|project|file', id: number} * @param {object} params {type: 'task|project|file|message', id: number}
*/ */
checkFavoriteStatus({dispatch}, {type, id}) { checkFavoriteStatus({dispatch}, {type, id}) {
return dispatch('call', { return dispatch('call', {
@ -2868,7 +2868,7 @@ export default {
/** /**
* 切换收藏状态 * 切换收藏状态
* @param dispatch * @param dispatch
* @param {object} params {type: 'task|project|file', id: number} * @param {object} params {type: 'task|project|file|message', id: number}
*/ */
toggleFavorite({dispatch}, {type, id}) { toggleFavorite({dispatch}, {type, id}) {
return dispatch('call', { return dispatch('call', {
@ -2884,7 +2884,7 @@ export default {
/** /**
* 批量检查收藏状态 * 批量检查收藏状态
* @param dispatch * @param dispatch
* @param {object} params {type: 'task|project|file', items: array} * @param {object} params {type: 'task|project|file|message', items: array}
*/ */
checkFavoritesStatus({dispatch}, {type, items}) { checkFavoritesStatus({dispatch}, {type, items}) {
if (!Array.isArray(items) || items.length === 0) { if (!Array.isArray(items) || items.length === 0) {

View File

@ -758,4 +758,11 @@ body.dark-mode-reverse {
} }
} }
} }
.favorite-type-tag {
.ivu-tag-text {
-webkit-filter: invert(100%);
filter: invert(100%);
}
}
} }