feat: 添加收藏备注功能

- 在 UsersController 中新增 favorite__remark 方法,支持用户修改收藏的备注
- 在 UserFavorite 模型中添加更新备注的逻辑
- 新增数据库迁移以添加备注字段
- 更新前端组件以支持备注的显示和编辑
- 优化收藏操作的用户体验
This commit is contained in:
kuaifan 2025-09-24 18:15:03 +08:00
parent 89bdd86f14
commit a268391e68
10 changed files with 277 additions and 57 deletions

View File

@ -3188,6 +3188,58 @@ class UsersController extends AbstractController
return Base::retSuccess($message, $result);
}
/**
* @api {post} api/users/favorite/remark 47-1. 修改收藏备注
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName favorite__remark
*
* @apiParam {String} type 收藏类型 (task/project/file/message)
* @apiParam {Number} id 收藏对象ID
* @apiParam {String} remark 收藏备注(<=255个字符)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function favorite__remark()
{
$user = User::auth();
//
$type = trim(Request::input('type'));
$id = intval(Request::input('id'));
$remark = trim(Request::input('remark', ''));
if (!$type || $id <= 0) {
return Base::retError('参数错误');
}
$allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE, UserFavorite::TYPE_MESSAGE];
if (!in_array($type, $allowedTypes)) {
return Base::retError('无效的收藏类型');
}
if ($remark === '') {
return Base::retError('请输入修改备注');
}
if (mb_strlen($remark) > 255) {
return Base::retError('备注最多支持255个字符');
}
$favorite = UserFavorite::updateRemark($user->userid, $type, $id, $remark);
if (!$favorite) {
return Base::retError('收藏记录不存在');
}
return Base::retSuccess('修改备注成功', [
'remark' => $favorite->remark,
]);
}
/**
* @api {post} api/users/favorites/clean 48. 清理用户收藏
*

View File

@ -44,6 +44,7 @@ class UserFavorite extends AbstractModel
'userid',
'favoritable_type',
'favoritable_id',
'remark',
];
/**
@ -79,16 +80,42 @@ class UserFavorite extends AbstractModel
if ($favorite) {
// 取消收藏
$favorite->delete();
return ['favorited' => false, 'action' => 'removed'];
} else {
// 添加收藏
self::create([
'userid' => $userid,
'favoritable_type' => $type,
'favoritable_id' => $id,
]);
return ['favorited' => true, 'action' => 'added'];
return ['favorited' => false, 'action' => 'removed', 'remark' => ''];
}
// 添加收藏
$favorite = self::create([
'userid' => $userid,
'favoritable_type' => $type,
'favoritable_id' => $id,
]);
return ['favorited' => true, 'action' => 'added', 'remark' => $favorite->remark ?? ''];
}
/**
* 更新收藏备注
* @param int $userid
* @param string $type
* @param int $id
* @param string $remark
* @return static|null
*/
public static function updateRemark($userid, $type, $id, $remark)
{
$favorite = self::whereUserid($userid)
->whereFavoritableType($type)
->whereFavoritableId($id)
->first();
if (!$favorite) {
return null;
}
$favorite->remark = $remark;
$favorite->save();
return $favorite;
}
/**
@ -192,6 +219,7 @@ class UserFavorite extends AbstractModel
'flow_item_status' => $flowItemStatus,
'flow_item_color' => $flowItemColor,
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
'remark' => $favorite->remark,
];
}
}
@ -211,6 +239,7 @@ class UserFavorite extends AbstractModel
'desc' => $project->desc,
'archived_at' => $project->archived_at,
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
'remark' => $favorite->remark,
];
}
}
@ -231,6 +260,7 @@ class UserFavorite extends AbstractModel
'size' => $file->size,
'pid' => $file->pid,
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
'remark' => $favorite->remark,
];
}
}
@ -263,6 +293,7 @@ class UserFavorite extends AbstractModel
'userid' => $message->userid,
'type' => $message->type,
'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'),
'remark' => $favorite->remark,
];
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddRemarkToUserFavoritesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('user_favorites', function (Blueprint $table) {
if (!Schema::hasColumn('user_favorites', 'remark')) {
$table->string('remark', 255)->default('')->after('favoritable_id')->comment('收藏备注');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('user_favorites', function (Blueprint $table) {
if (Schema::hasColumn('user_favorites', 'remark')) {
$table->dropColumn('remark');
}
});
}
}

View File

@ -108,6 +108,11 @@ router.afterEach(() => {
store.commit('route/loading', false);
});
// 消息配置
ViewUI.Message.config({
duration: 2.5
});
// 加载路由
Vue.prototype.goForward = function(route, isReplace, autoBroadcast = true) {
if ($A.Ready && $A.isSubElectron && autoBroadcast) {

View File

@ -4079,25 +4079,22 @@ export default {
if (this.operateVisible) {
return
}
this.$store.dispatch("toggleFavorite", {
type: 'message',
id: this.operateItem.id
}).then(({data, msg}) => {
}).then(({data}) => {
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

View File

@ -66,10 +66,11 @@
<script>
import SearchButton from "../../../components/SearchButton.vue";
import QuickEdit from "../../../components/QuickEdit.vue";
export default {
name: "FavoriteManagement",
components: {SearchButton},
components: {SearchButton, QuickEdit},
data() {
return {
loadIng: 0,
@ -118,6 +119,53 @@ export default {
]);
}
},
{
title: this.$L('备注'),
key: 'remark',
minWidth: 160,
render: (h, {row}) => {
return h('QuickEdit', {
props: {
value: row.remark || '',
attrTitle: row.remark || '',
alwaysIcon: true,
},
on: {
'on-update': (val, cb) => {
const remark = (val || '').trim();
if (!remark) {
$A.messageWarning(this.$L('请输入修改备注'));
cb();
return;
}
this.$store.dispatch('call', {
url: 'users/favorite/remark',
data: {
type: row.type,
id: row.id,
remark,
},
method: 'post',
}).then(({data, msg}) => {
const newRemark = data && typeof data.remark !== 'undefined' ? data.remark : remark;
row.remark = newRemark;
const target = this.allData.find(item => item.id === row.id && item.type === row.type);
if (target) {
target.remark = newRemark;
}
$A.messageSuccess(msg || this.$L('操作成功'));
cb();
}).catch(({msg}) => {
$A.modalError(msg || this.$L('操作失败'));
cb();
});
}
}
}, [
h('AutoTip', row.remark || '-')
]);
}
},
{
title: this.$L('所属项目'),
key: 'project_name',
@ -232,7 +280,7 @@ export default {
getLists() {
this.loadIng++;
this.keyIs = $A.objImplode(this.keys) != "";
this.$store.dispatch("call", {
url: 'users/favorites',
data: {
@ -243,7 +291,7 @@ export default {
}).then(({data}) => {
//
this.allData = [];
//
if (data.data.tasks) {
data.data.tasks.forEach(task => {
@ -259,10 +307,11 @@ export default {
flow_item_status: task.flow_item_status,
flow_item_color: task.flow_item_color,
favorited_at: task.favorited_at,
remark: task.remark || '',
});
});
}
//
if (data.data.projects) {
data.data.projects.forEach(project => {
@ -273,10 +322,11 @@ export default {
desc: project.desc,
archived_at: project.archived_at,
favorited_at: project.favorited_at,
remark: project.remark || '',
});
});
}
//
if (data.data.files) {
data.data.files.forEach(file => {
@ -288,10 +338,11 @@ export default {
size: file.size,
pid: file.pid,
favorited_at: file.favorited_at,
remark: file.remark || '',
});
});
}
//
if (data.data.messages) {
data.data.messages.forEach(message => {
@ -303,10 +354,11 @@ export default {
userid: message.userid,
msg_type: message.type,
favorited_at: message.favorited_at,
remark: message.remark || '',
});
});
}
this.total = data.total || this.allData.length;
this.filterData();
this.noText = '没有相关的收藏';
@ -319,14 +371,14 @@ export default {
filterData() {
let filteredData = this.allData;
//
if (this.keys.name) {
filteredData = filteredData.filter(item => {
return item.name && item.name.toLowerCase().includes(this.keys.name.toLowerCase());
});
}
this.list = filteredData;
},
@ -355,10 +407,10 @@ export default {
break;
case 'file':
this.$router.push({
name: 'manage-file',
name: 'manage-file',
params: {
folderId: item.pid || 0,
fileId: null,
folderId: item.pid || 0,
fileId: null,
shakeId: item.id
}
});
@ -386,10 +438,7 @@ export default {
type: item.type,
id: item.id
}).then(() => {
$A.messageSuccess('取消收藏成功');
this.getLists();
}).catch(({msg}) => {
$A.modalError(msg);
});
}
}

View File

@ -1892,15 +1892,12 @@ export default {
*/
toggleProjectFavorite() {
if (!this.projectData.id) return;
this.$store.dispatch("toggleFavorite", {
type: 'project',
id: this.projectData.id
}).then(({data, msg}) => {
}).then(({data}) => {
this.$set(this.projectData, 'favorited', data.favorited);
$A.messageSuccess(msg);
}).catch(({msg}) => {
$A.modalError(msg || this.$L('操作失败'));
});
},
@ -1909,7 +1906,7 @@ export default {
*/
checkProjectFavoriteStatus() {
if (!this.projectData.id) return;
this.$store.dispatch("checkFavoriteStatus", {
type: 'project',
id: this.projectData.id

View File

@ -505,7 +505,7 @@ export default {
*/
checkFavoriteStatus() {
if (!this.task.id) return;
this.$store.dispatch("checkFavoriteStatus", {
type: 'task',
id: this.task.id
@ -521,16 +521,13 @@ export default {
*/
toggleFavorite() {
if (!this.task.id) return;
this.$store.dispatch("toggleFavorite", {
type: 'task',
id: this.task.id
}).then(({data, msg}) => {
}).then(({data}) => {
this.isFavorited = data.favorited;
this.hide();
$A.messageSuccess(msg);
}).catch(({msg}) => {
$A.messageError(msg || '操作失败');
});
}
},

View File

@ -392,7 +392,7 @@
<div>
<div style="margin:-10px 0 8px">{{$L('文件名称')}}: {{linkData.name}}</div>
<Input ref="linkInput" v-model="linkData.url" type="textarea" :rows="2" @on-focus="linkFocus" readonly/>
<!-- 游客访问权限控制 -->
<div style="margin:12px 0">
<Checkbox v-model="linkData.guest_access" @on-change="onGuestAccessChange">
@ -403,7 +403,7 @@
{{$L('警告:任何人都可通过此链接访问文件')}}
</div>
</div>
<div class="form-tip" style="padding-top:6px">
{{$L('可通过此链接浏览文件。')}}
<Poptip
@ -2087,11 +2087,11 @@ export default {
*/
toggleFileFavorite(item) {
if (!item.id || item.type === 'folder') return;
this.$store.dispatch("toggleFavorite", {
type: 'file',
id: item.id
}).then(({data, msg}) => {
}).then(({data}) => {
const fileIndex = this.fileList.findIndex(file => file.id === item.id);
if (fileIndex > -1) {
this.$set(this.fileList[fileIndex], 'favorited', data.favorited);
@ -2099,9 +2099,6 @@ export default {
if (this.contextMenuItem.id === item.id) {
this.$set(this.contextMenuItem, 'favorited', data.favorited);
}
$A.messageSuccess(msg);
}).catch(({msg}) => {
$A.modalError(msg || this.$L('操作失败'));
});
},
@ -2110,7 +2107,7 @@ export default {
*/
checkSingleFileFavoriteStatus(file) {
if (!file.id || file.type === 'folder') return;
this.$store.dispatch("checkFavoriteStatus", {
type: 'file',
id: file.id

View File

@ -2898,13 +2898,72 @@ export default {
* @param {object} params {type: 'task|project|file|message', id: number}
*/
toggleFavorite({dispatch}, {type, id}) {
return dispatch('call', {
url: 'users/favorite/toggle',
data: {
type: type,
id: id
},
method: 'post',
return new Promise((resolve, reject) => {
dispatch('call', {
url: 'users/favorite/toggle',
data: {
type: type,
id: id
},
method: 'post',
}).then(result => {
resolve(result)
//
const {data, msg} = result
if (!data.favorited) {
$A.messageSuccess(msg);
return
}
$A.Message.success({
duration: 5,
render: h => {
return h('span', [
h('span', $A.L(msg)),
h('a', {
style: {
marginLeft: '8px'
},
on: {
click: () => {
const currentRemark = data && typeof data.remark === 'string' ? data.remark : '';
$A.modalInput({
title: $A.L('修改备注'),
placeholder: $A.L('请输入修改备注'),
okText: $A.L('保存'),
value: currentRemark,
onOk: (inputValue) => {
const remark = typeof inputValue === 'string' ? inputValue.trim() : '';
if (!remark) {
return $A.L('请输入修改备注');
}
return new Promise((resolveRemark, rejectRemark) => {
dispatch('call', {
url: 'users/favorite/remark',
data: {
type,
id,
remark,
},
method: 'post',
}).then(({msg}) => {
$A.messageSuccess(msg || $A.L('操作成功'));
resolveRemark();
}).catch(({msg}) => {
rejectRemark(msg || $A.L('操作失败'));
});
});
}
});
}
}
}, $A.L('修改备注')),
])
}
});
}).catch(({msg}) => {
$A.modalError(msg || this.$L('操作失败'));
reject()
});
});
},