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); $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) { $list->transform(function (Project $project) use ($getstatistics, $getuserid, $user) {
$array = $project->toArray(); $array = $project->toArray();
if ($getuserid == 'yes') { if ($getuserid == 'yes') {
@ -643,6 +647,39 @@ class ProjectController extends AbstractController
return Base::retSuccess('调整成功'); 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. 退出项目 * @api {get} api/project/exit 11. 退出项目
* *

View File

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

View File

@ -128,12 +128,22 @@
</ul> </ul>
</div> </div>
<div ref="menuProject" class="menu-project"> <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 <li
v-for="(item, key) in projectLists" v-for="item in projectDraggableList"
:ref="`project_${item.id}`" :ref="`project_${item.id}`"
:key="key" :key="item.id"
:class="classNameProject(item)" :class="[classNameProject(item), item.top_at ? 'pinned' : '']"
:data-id="item.id" :data-id="item.id"
@pointerdown="handleOperation" @pointerdown="handleOperation"
@click="toggleRoute('project', {projectId: item.id})"> @click="toggleRoute('project', {projectId: item.id})">
@ -157,7 +167,7 @@
</div> </div>
</li> </li>
<li v-if="projectKeyLoading > 0" class="loading"><Loading/></li> <li v-if="projectKeyLoading > 0" class="loading"><Loading/></li>
</ul> </Draggable>
</div> </div>
</Scrollbar> </Scrollbar>
<div <div
@ -393,6 +403,7 @@ import emitter from "../store/events";
import SearchBox from "../components/SearchBox.vue"; import SearchBox from "../components/SearchBox.vue";
import transformEmojiToHtml from "../utils/emoji"; import transformEmojiToHtml from "../utils/emoji";
import {languageName} from "../language"; import {languageName} from "../language";
import Draggable from 'vuedraggable'
export default { export default {
components: { components: {
@ -414,7 +425,8 @@ export default {
TeamManagement, TeamManagement,
ProjectArchived, ProjectArchived,
MicroApps, MicroApps,
ComplaintManagement ComplaintManagement,
Draggable
}, },
directives: {longpress, TransferDom}, directives: {longpress, TransferDom},
data() { data() {
@ -450,6 +462,9 @@ export default {
projectKeyLoading: 0, projectKeyLoading: 0,
projectSearchShow: false, projectSearchShow: false,
projectDraggableList: [],
projectDragging: false,
openMenu: {}, openMenu: {},
visibleMenu: false, visibleMenu: false,
@ -675,9 +690,15 @@ export default {
projectLists() { projectLists() {
const {projectKeyValue, cacheProjects} = this; const {projectKeyValue, cacheProjects} = this;
const data = $A.cloneJSON(cacheProjects).sort((a, b) => { 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); 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; return b.id - a.id;
}); });
if (projectKeyValue) { if (projectKeyValue) {
@ -761,6 +782,15 @@ export default {
immediate: true immediate: true
}, },
projectLists: {
handler(val) {
if (!this.projectDragging) {
this.projectDraggableList = val
}
},
immediate: true
},
unreadAndOverdue: { unreadAndOverdue: {
handler(val) { handler(val) {
if (this.$Electron) { 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) { onAddTask(params) {
this.addTaskShow = true this.addTaskShow = true
this.$nextTick(_ => { this.$nextTick(_ => {
@ -1194,6 +1246,7 @@ export default {
}, },
}).then(({data}) => { }).then(({data}) => {
this.$store.dispatch("saveProject", data); this.$store.dispatch("saveProject", data);
this.projectDraggableList = this.projectLists
this.$nextTick(() => { this.$nextTick(() => {
const active = this.$refs.menuProject.querySelector(".active") const active = this.$refs.menuProject.querySelector(".active")
if (active) { if (active) {

View File

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