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

View File

@ -38,6 +38,7 @@ class UserFavorite extends AbstractModel
const TYPE_TASK = 'task';
const TYPE_PROJECT = 'project';
const TYPE_FILE = 'file';
const TYPE_MESSAGE = 'message';
protected $fillable = [
'userid',
@ -126,13 +127,15 @@ class UserFavorite extends AbstractModel
$data = [
'tasks' => [],
'projects' => [],
'files' => []
'files' => [],
'messages' => []
];
// 分组收集ID
$taskIds = [];
$projectIds = [];
$fileIds = [];
$messageIds = [];
foreach ($favorites->items() as $favorite) {
switch ($favorite->favoritable_type) {
@ -145,6 +148,9 @@ class UserFavorite extends AbstractModel
case self::TYPE_FILE:
$fileIds[] = $favorite->favoritable_id;
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 [
'data' => $data,
'total' => $favorites->total(),

View File

@ -376,6 +376,10 @@
<i class="taskfont">&#xe61e;</i>
<span>{{ $L(operateItem.tag ? '取消标注' : '标注') }}</span>
</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')">
<i class="taskfont">&#xe7b8;</i>
<span>{{ $L('新任务') }}</span>
@ -3135,6 +3139,9 @@ export default {
})
}
}
if (this.operateVisible) {
this.checkMessageFavoriteStatus(this.operateItem);
}
requestAnimationFrame(() => {
this.operateItem.clientX = event.clientX
this.operateItem.clientY = event.clientY
@ -3286,6 +3293,10 @@ export default {
this.onTag()
break;
case "favorite":
this.onFavorite()
break;
case "newTask":
let content = $A.formatMsgBasic(this.operateItem.msg.text)
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) {
if (val === 'user') {
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="project">{{$L('项目')}}</Option>
<Option value="file">{{$L('文件')}}</Option>
<Option value="message">{{$L('消息')}}</Option>
</Select>
</div>
</li>
@ -85,16 +86,19 @@ export default {
const typeMap = {
'task': this.$L('任务'),
'project': this.$L('项目'),
'file': this.$L('文件')
'file': this.$L('文件'),
'message': this.$L('消息')
};
const color = {
'task': 'primary',
'project': 'success',
'file': 'warning'
'task': 'success',
'project': '#f87cbd',
'file': 'warning',
'message': 'primary'
};
return h('Tag', {
class: 'favorite-type-tag',
props: {
color: color[row.type] || 'default'
color: color[row.type] || 'primary'
}
}, 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.filterData();
this.noText = '没有相关的收藏';
@ -349,6 +368,16 @@ export default {
}, 600);
this.$emit('on-close');
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 {object} params {type: 'task|project|file', id: number}
* @param {object} params {type: 'task|project|file|message', id: number}
*/
checkFavoriteStatus({dispatch}, {type, id}) {
return dispatch('call', {
@ -2868,7 +2868,7 @@ export default {
/**
* 切换收藏状态
* @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}) {
return dispatch('call', {
@ -2884,7 +2884,7 @@ export default {
/**
* 批量检查收藏状态
* @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}) {
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%);
}
}
}