perf: 支持项目调整排序

This commit is contained in:
kuaifan 2025-08-19 21:19:45 +08:00
parent 788cae3efe
commit 02275bb417
4 changed files with 164 additions and 23 deletions

View File

@ -169,7 +169,11 @@ class ProjectController extends AbstractController
$builder->where('projects.updated_at', '>', $timerange->updated);
}
//
$list = $builder->orderByDesc('projects.id')->paginate(Base::getPaginate(100, 50));
$list = $builder
->orderByDesc('project_users.top_at')
->orderBy('project_users.sort')
->orderByDesc('projects.id')
->paginate(Base::getPaginate(100, 50));
$list->transform(function (Project $project) use ($getstatistics, $getuserid, $user) {
$array = $project->toArray();
if ($getuserid == 'yes') {
@ -643,6 +647,39 @@ class ProjectController extends AbstractController
return Base::retSuccess('调整成功');
}
/**
* @api {post} api/project/user/sort 47. 项目列表排序
*
* @apiDescription 需要token身份按当前用户对项目进行拖动排序仅影响本人
* @apiVersion 1.0.0
* @apiGroup project
* @apiName user__sort
*
* @apiParam {Array} list 排序后的项目ID列表[12,5,9]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function user__sort()
{
$user = User::auth();
$list = Base::json2array(Request::input('list'));
if (!is_array($list)) {
return Base::retError('参数错误');
}
$index = 0;
foreach ($list as $projectId) {
$projectId = intval($projectId);
if ($projectId <= 0) continue;
ProjectUser::whereUserid($user->userid)
->whereProjectId($projectId)
->update(['sort' => $index]);
$index++;
}
return Base::retSuccess('排序已保存');
}
/**
* @api {get} api/project/exit 11. 退出项目
*

View File

@ -129,6 +129,7 @@ class Project extends AbstractModel
'projects.*',
'project_users.owner',
'project_users.top_at',
'project_users.sort',
])
->leftJoin('project_users', function ($leftJoin) use ($userid) {
$leftJoin
@ -153,6 +154,7 @@ class Project extends AbstractModel
'projects.*',
'project_users.owner',
'project_users.top_at',
'project_users.sort',
])
->join('project_users', 'projects.id', '=', 'project_users.project_id')
->where('project_users.userid', $userid);

View File

@ -128,12 +128,22 @@
</ul>
</div>
<div ref="menuProject" class="menu-project">
<ul v-longpress="handleLongpress">
<Draggable
:list="projectDraggableList"
:animation="150"
:disabled="$isEEUIApp || windowTouch || !!projectKeyValue"
tag="ul"
item-key="id"
draggable="li:not(.pinned)"
handle=".project-h1"
v-longpress="handleLongpress"
@start="projectDragging = true"
@end="onProjectSortEnd">
<li
v-for="(item, key) in projectLists"
v-for="item in projectDraggableList"
:ref="`project_${item.id}`"
:key="key"
:class="classNameProject(item)"
:key="item.id"
:class="[classNameProject(item), item.top_at ? 'pinned' : '']"
:data-id="item.id"
@pointerdown="handleOperation"
@click="toggleRoute('project', {projectId: item.id})">
@ -157,7 +167,7 @@
</div>
</li>
<li v-if="projectKeyLoading > 0" class="loading"><Loading/></li>
</ul>
</Draggable>
</div>
</Scrollbar>
<div
@ -393,6 +403,7 @@ import emitter from "../store/events";
import SearchBox from "../components/SearchBox.vue";
import transformEmojiToHtml from "../utils/emoji";
import {languageName} from "../language";
import Draggable from 'vuedraggable'
export default {
components: {
@ -414,7 +425,8 @@ export default {
TeamManagement,
ProjectArchived,
MicroApps,
ComplaintManagement
ComplaintManagement,
Draggable
},
directives: {longpress, TransferDom},
data() {
@ -450,6 +462,9 @@ export default {
projectKeyLoading: 0,
projectSearchShow: false,
projectDraggableList: [],
projectDragging: false,
openMenu: {},
visibleMenu: false,
@ -675,9 +690,15 @@ export default {
projectLists() {
const {projectKeyValue, cacheProjects} = this;
const data = $A.cloneJSON(cacheProjects).sort((a, b) => {
if (a.top_at || b.top_at) {
//
if (a.top_at !== b.top_at && (a.top_at || b.top_at)) {
return $A.sortDay(b.top_at, a.top_at);
}
//
const as = typeof a.sort === 'number' ? a.sort : Number.MAX_SAFE_INTEGER;
const bs = typeof b.sort === 'number' ? b.sort : Number.MAX_SAFE_INTEGER;
if (as !== bs) return as - bs;
// ID
return b.id - a.id;
});
if (projectKeyValue) {
@ -761,6 +782,15 @@ export default {
immediate: true
},
projectLists: {
handler(val) {
if (!this.projectDragging) {
this.projectDraggableList = val
}
},
immediate: true
},
unreadAndOverdue: {
handler(val) {
if (this.$Electron) {
@ -1030,6 +1060,28 @@ export default {
}
},
onProjectSortEnd() {
//
const nonPinnedItems = this.projectDraggableList.filter(item => !item.top_at)
nonPinnedItems.forEach((item, index) => {
this.$store.dispatch("saveProject", {id: item.id, sort: index})
})
//
this.$store.dispatch("call", {
url: 'project/user/sort',
data: {
list: nonPinnedItems.map(item => item.id)
},
method: 'post',
}).then(({msg}) => {
$A.messageSuccess(msg)
}).catch(({msg}) => {
$A.modalError(msg)
}).finally(() => {
this.projectDragging = false
})
},
onAddTask(params) {
this.addTaskShow = true
this.$nextTick(_ => {
@ -1194,6 +1246,7 @@ export default {
},
}).then(({data}) => {
this.$store.dispatch("saveProject", data);
this.projectDraggableList = this.projectLists
this.$nextTick(() => {
const active = this.$refs.menuProject.querySelector(".active")
if (active) {

View File

@ -12,21 +12,24 @@
</Form>
</div>
</div>
<ul
@scroll="onScroll"
@touchstart="onTouchStart"
v-longpress="handleLongpress">
<template v-if="projectLists.length === 0">
<li v-if="projectKeyLoading > 0" class="loading"><Loading/></li>
<li v-else class="nothing">
{{$L(projectKeyValue ? `没有任何与"${projectKeyValue}"相关的结果` : `没有任何项目`)}}
</li>
</template>
<Draggable
:list="projectDraggableList"
:animation="150"
:disabled="!!projectKeyValue"
tag="ul"
item-key="id"
draggable="li:not(.pinned)"
handle=".project-h1"
@scroll.native="onScroll"
@touchstart.native="onTouchStart"
v-longpress="handleLongpress"
@start="projectDragging = true"
@end="onProjectSortEnd">
<li
v-for="(item, key) in projectLists"
:key="key"
v-for="item in projectDraggableList"
:key="item.id"
:data-id="item.id"
:class="{operate: item.id == operateItem.id && operateVisible}"
:class="[{operate: item.id == operateItem.id && operateVisible}, item.top_at ? 'pinned' : '']"
@pointerdown="handleOperation"
@click="toggleRoute('project', {projectId: item.id})">
<div class="project-item">
@ -55,7 +58,13 @@
</div>
</div>
</li>
</ul>
<template v-if="projectLists.length === 0">
<li v-if="projectKeyLoading > 0" class="loading"><Loading/></li>
<li v-else class="nothing">
{{$L(projectKeyValue ? `没有任何与"${projectKeyValue}"相关的结果` : `没有任何项目`)}}
</li>
</template>
</Draggable>
<div
v-transfer-dom
:data-transfer="true"
@ -84,12 +93,14 @@
<script>
import {mapState} from "vuex";
import Draggable from 'vuedraggable'
import longpress from "../../../directives/longpress";
import TransferDom from "../../../directives/transfer-dom";
import transformEmojiToHtml from "../../../utils/emoji";
export default {
name: "ProjectList",
components: {Draggable},
directives: {longpress, TransferDom},
data() {
return {
@ -99,6 +110,9 @@ export default {
operateStyles: {},
operateVisible: false,
operateItem: {},
projectDraggableList: [],
projectDragging: false,
}
},
@ -108,9 +122,15 @@ export default {
projectLists() {
const {projectKeyValue, cacheProjects} = this;
const data = $A.cloneJSON(cacheProjects).sort((a, b) => {
if (a.top_at || b.top_at) {
//
if (a.top_at !== b.top_at && (a.top_at || b.top_at)) {
return $A.sortDay(b.top_at, a.top_at);
}
//
const as = typeof a.sort === 'number' ? a.sort : Number.MAX_SAFE_INTEGER;
const bs = typeof b.sort === 'number' ? b.sort : Number.MAX_SAFE_INTEGER;
if (as !== bs) return as - bs;
// ID
return b.id - a.id;
});
if (projectKeyValue) {
@ -121,6 +141,14 @@ export default {
},
watch: {
projectLists: {
handler(val) {
if (!this.projectDragging) {
this.projectDraggableList = val
}
},
immediate: true
},
projectKeyValue(val) {
if (val == '') {
return;
@ -141,6 +169,27 @@ export default {
methods: {
transformEmojiToHtml,
onProjectSortEnd() {
//
const nonPinnedItems = this.projectDraggableList.filter(item => !item.top_at)
nonPinnedItems.forEach((item, index) => {
this.$store.dispatch("saveProject", {id: item.id, sort: index})
})
//
this.$store.dispatch("call", {
url: 'project/user/sort',
data: {
list: nonPinnedItems.map(item => item.id)
},
method: 'post',
}).then(({msg}) => {
$A.messageSuccess(msg)
}).catch(({msg}) => {
$A.modalError(msg)
}).finally(() => {
this.projectDragging = false
})
},
searchProject() {
this.projectKeyLoading++;
this.$store.dispatch("getProjects", {