From ae5b44e4294da2c03b81bd9c74eb45cbf9568d54 Mon Sep 17 00:00:00 2001
From: 794308525 <794308525@qq.com>
Date: Tue, 11 Aug 2026 17:26:40 +0800
Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=9D=A2?=
=?UTF-8?q?=E6=9D=BF=E5=8D=A1=E7=89=87=E8=87=AA=E5=AE=9A=E4=B9=89=E6=8E=92?=
=?UTF-8?q?=E5=BA=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
增加默认排序、最新添加和最早添加三种切换方式。
支持通过拖柄调整面板卡片顺序,并将默认顺序持久化保存。
增加排序字段自动迁移及后端批量保存接口。
优化拖拽换位动画,减少卡片挤位时的闪动。
---
electron/class/sqlite.js | 4 +-
electron/controller/panel.js | 27 ++-
frontend/electron.route.js | 19 ++
frontend/src/views/panel/controller/index.ts | 7 +
frontend/src/views/panel/index.vue | 223 +++++++++++++++++--
5 files changed, 253 insertions(+), 27 deletions(-)
diff --git a/electron/class/sqlite.js b/electron/class/sqlite.js
index b1cfac6..1414f89 100644
--- a/electron/class/sqlite.js
+++ b/electron/class/sqlite.js
@@ -55,6 +55,7 @@ class Sqlite {
this.checkField('ssh_info', 'os_name', 'TEXT', '"Linux"')
this.checkField('ssh_info', 'mstsc_options', 'TEXT', '"{}"')
this.checkField('ssh_info', 'sort', 'INTEGER', '0')
+ this.checkField('panel_info', 'sort', 'INTEGER', '0')
this.checkField('panel_info', 'ov', 'INTEGER', '-1')
this.checkField('panel_info', 'server_id', 'TEXT', '""')
this.checkField('panel_info', 'current_disk', 'TEXT', '""')
@@ -253,6 +254,7 @@ class Sqlite {
\`server_id\` TEXT DEFAULT "", -- server_id
\`proxy_id\` INTEGER DEFAULT 0, -- 代理ID
\`common_use\` INTEGER DEFAULT 0, -- 常用显示状态 1=显示 0=隐藏
+ \`sort\` INTEGER DEFAULT 0, -- 排序值
\`area\` TEXT DEFAULT "" -- 服务器归属区域
)`;
@@ -1210,4 +1212,4 @@ class Sqlite {
}
}
-module.exports = { Sqlite }
\ No newline at end of file
+module.exports = { Sqlite }
diff --git a/electron/controller/panel.js b/electron/controller/panel.js
index 6e67da7..208d98b 100644
--- a/electron/controller/panel.js
+++ b/electron/controller/panel.js
@@ -243,7 +243,7 @@ class PanelController extends Controller {
}
// 获取面板列表
- result.data = pub.M(this.TABLE).where(where, params).order('panel_id DESC').select();
+ result.data = pub.M(this.TABLE).where(where, params).order('sort DESC, panel_id DESC').select();
global.PanelList = result.data;
for (let i = 0; i < result.data.length; i++) {
@@ -278,6 +278,29 @@ class PanelController extends Controller {
pub.M(this.TABLE).where('panel_id=?', panel_id).update({current_disk: disk_path});
}
+ /**
+ * @name 保存面板默认排序
+ * @param {object} args {
+ * panel_ids: number[] - 按展示顺序排列的面板ID
+ * }
+ */
+ async set_sort(args, event) {
+ const panel_ids = Array.isArray(args.data.panel_ids) ? args.data.panel_ids : [];
+ if (panel_ids.length === 0) return;
+
+ const panel_id_set = new Set(panel_ids);
+ const current_list = pub.M(this.TABLE).order('sort DESC, panel_id DESC').select();
+ const ordered_ids = [...panel_ids];
+ const merged_ids = current_list.map(panel => {
+ return panel_id_set.has(panel.panel_id) ? ordered_ids.shift() : panel.panel_id;
+ });
+ const total = merged_ids.length;
+
+ merged_ids.forEach((panel_id, index) => {
+ pub.M(this.TABLE).where('panel_id=?', panel_id).update({ sort: total - index });
+ });
+ }
+
/**
* @name 添加面板分组
* @param {object} args {
@@ -1246,4 +1269,4 @@ class PanelController extends Controller {
}
PanelController.toString = () => '[class PanelController]';
-module.exports = PanelController;
\ No newline at end of file
+module.exports = PanelController;
diff --git a/frontend/electron.route.js b/frontend/electron.route.js
index da655ae..fcce067 100644
--- a/frontend/electron.route.js
+++ b/frontend/electron.route.js
@@ -57,6 +57,25 @@ const routes = {
},
},
},
+ set_sort: {
+ title: '保存面板默认排序',
+ method: 'ipc',
+ path: 'controller.panel.set_sort',
+ args: {
+ channel: {
+ type: 'string',
+ required: true,
+ description: '通道标识',
+ },
+ data: {
+ panel_ids: {
+ type: 'array',
+ required: true,
+ description: '按展示顺序排列的面板ID',
+ },
+ },
+ },
+ },
record_disk: {
title: '记录面板选中的磁盘',
method: 'ipc',
diff --git a/frontend/src/views/panel/controller/index.ts b/frontend/src/views/panel/controller/index.ts
index b7476dc..0fec7ee 100644
--- a/frontend/src/views/panel/controller/index.ts
+++ b/frontend/src/views/panel/controller/index.ts
@@ -43,6 +43,13 @@ export const record_disk = (parent: { panel_id: number; disk_path: string }) =>
common.send(routes.panel.record_disk.path, parent)
}
+/**
+ * @description 保存面板默认展示顺序
+ */
+export const set_panel_sort = (panel_ids: number[]) => {
+ common.send(routes.panel.set_sort.path, { panel_ids })
+}
+
/**
* @description 获取面板安装脚本列表
*/
diff --git a/frontend/src/views/panel/index.vue b/frontend/src/views/panel/index.vue
index 51ed8a1..a294ff4 100644
--- a/frontend/src/views/panel/index.vue
+++ b/frontend/src/views/panel/index.vue
@@ -8,6 +8,13 @@
}}
{{ pub.lang('导入') }}
{{ pub.lang('导出') }}
+
+
+
+
-
-
-
- {{ item.title }}
- {{ authType[item.ov].name }}
-
-
- [{{ getUrlLink(item.url) }}]
-
-
-
-
- {{ item.title }}
-
+
+
+
+
+
+
+
+ {{ item.title }}
+ {{ authType[item.ov].name }}
+
+
+ [{{ getUrlLink(item.url) }}]
+
+
+
+
+ {{ item.title }}
+
+
@@ -201,6 +226,7 @@
+
@@ -246,7 +272,7 @@ import installResults from './components/AddPanel/installResults.vue'
import AddGroup from '@views/panel/components/AddGroup/index.vue'
import White from '@/assets/images/logo-white.svg'
import { pub, getByteUnit } from '@utils/tools'
-import { record_disk, type Panel_Params } from './controller'
+import { record_disk, set_panel_sort, type Panel_Params } from './controller'
import { checkIp } from '@utils/is'
import { common, routes, ipc } from '@api/http'
@@ -271,6 +297,22 @@ const isActive = ref(false)
const searchServic = ref('')
const firstLoad = ref(true)
const allPanelList = ref([]) as any
+const sortMode = ref<'default' | 'latest' | 'earliest'>('default')
+const sortOptions = [
+ { label: pub.lang('默认排序'), value: 'default' },
+ { label: pub.lang('最新添加'), value: 'latest' },
+ { label: pub.lang('最早添加'), value: 'earliest' },
+]
+const draggedPanelID = ref(null)
+let dragOrderChanged = false
+let dragMoveFrame: number | null = null
+let pendingDragMove: {
+ targetID: number
+ clientX: number
+ clientY: number
+ targetElement: HTMLElement
+} | null = null
+let lastPanelMove = { from: -1, to: -1, time: 0 }
const authType = [
{ name: pub.lang('免费版'), bg: 'bg-[#e7e7e7]', text: 'text-[#909399]' },
@@ -298,17 +340,126 @@ const isShow = computed(() => {
return status
})
+const canDragSort = computed(() => sortMode.value === 'default' && searchServic.value === '')
+const dragHandleTitle = computed(() => {
+ if (sortMode.value !== 'default') return pub.lang('切换到默认排序后可拖动')
+ if (searchServic.value !== '') return pub.lang('清空搜索后可拖动')
+ return pub.lang('拖动排序')
+})
+
// Computed property for showListArray
const showListArray = computed(() => {
+ let list = allPanelList.value
if (searchServic.value !== '') {
- return allPanelList.value.filter(
+ list = allPanelList.value.filter(
(item: any) =>
item.title.includes(searchServic.value) || item.url.includes(searchServic.value)
)
}
- return allPanelList.value
+ if (sortMode.value === 'latest') {
+ return [...list].sort((a: any, b: any) => Number(b.addtime) - Number(a.addtime))
+ }
+ if (sortMode.value === 'earliest') {
+ return [...list].sort((a: any, b: any) => Number(a.addtime) - Number(b.addtime))
+ }
+ return list
})
+const startPanelDrag = (index: number, event: DragEvent) => {
+ if (!canDragSort.value) {
+ event.preventDefault()
+ return
+ }
+ draggedPanelID.value = showListArray.value[index].panel_id
+ dragOrderChanged = false
+ if (event.dataTransfer) {
+ event.dataTransfer.effectAllowed = 'move'
+ event.dataTransfer.setData('text/plain', String(draggedPanelID.value))
+ const handle = event.currentTarget as HTMLElement
+ const card = handle.closest('.panel-card-col') as HTMLElement | null
+ if (card) {
+ const rect = card.getBoundingClientRect()
+ event.dataTransfer.setDragImage(
+ card,
+ Math.max(0, event.clientX - rect.left),
+ Math.max(0, event.clientY - rect.top)
+ )
+ }
+ }
+}
+
+const queuePanelMove = (targetID: number, event: DragEvent) => {
+ if (!canDragSort.value || draggedPanelID.value === null) return
+ pendingDragMove = {
+ targetID,
+ clientX: event.clientX,
+ clientY: event.clientY,
+ targetElement: event.currentTarget as HTMLElement,
+ }
+ if (dragMoveFrame !== null) return
+ dragMoveFrame = requestAnimationFrame(() => {
+ const move = pendingDragMove
+ dragMoveFrame = null
+ pendingDragMove = null
+ if (move) movePanel(move)
+ })
+}
+
+const movePanel = ({ targetID, clientX, clientY, targetElement }: NonNullable) => {
+ if (!canDragSort.value || draggedPanelID.value === null || targetID === draggedPanelID.value) return
+ const currentIndex = allPanelList.value.findIndex(
+ (item: any) => item.panel_id === draggedPanelID.value
+ )
+ const targetIndex = allPanelList.value.findIndex((item: any) => item.panel_id === targetID)
+ if (currentIndex < 0 || targetIndex < 0 || currentIndex === targetIndex) return
+
+ const now = performance.now()
+ if (
+ now - lastPanelMove.time < 180 &&
+ currentIndex === lastPanelMove.to &&
+ targetIndex === lastPanelMove.from
+ ) {
+ return
+ }
+
+ const targetRect = targetElement.getBoundingClientRect()
+ const draggedElement = document.querySelector('.panel-card-col--dragging') as HTMLElement | null
+ const draggedRect = draggedElement?.getBoundingClientRect()
+ const isSameRow = draggedRect
+ ? Math.abs(targetRect.top - draggedRect.top) < Math.min(targetRect.height, draggedRect.height) / 2
+ : true
+ const pointerPosition = isSameRow ? clientX : clientY
+ const targetMiddle = isSameRow
+ ? targetRect.left + targetRect.width / 2
+ : targetRect.top + targetRect.height / 2
+ const isMovingForward = currentIndex < targetIndex
+
+ if ((isMovingForward && pointerPosition < targetMiddle) || (!isMovingForward && pointerPosition > targetMiddle)) {
+ return
+ }
+
+ const [draggedPanel] = allPanelList.value.splice(currentIndex, 1)
+ allPanelList.value.splice(targetIndex, 0, draggedPanel)
+ dragOrderChanged = true
+ lastPanelMove = { from: currentIndex, to: targetIndex, time: now }
+}
+
+const finishPanelDrag = () => {
+ const finalMove = pendingDragMove
+ if (dragMoveFrame !== null) {
+ cancelAnimationFrame(dragMoveFrame)
+ dragMoveFrame = null
+ }
+ pendingDragMove = null
+ if (finalMove) movePanel(finalMove)
+ if (dragOrderChanged) {
+ set_panel_sort(allPanelList.value.map((item: any) => item.panel_id))
+ }
+ dragOrderChanged = false
+ lastPanelMove = { from: -1, to: -1, time: 0 }
+ draggedPanelID.value = null
+}
+
// 获取链接
const getUrlLink = (url: string) => {
if (!url) return ''
@@ -685,6 +836,30 @@ onUnmounted(() => {
font-size: 1.6rem;
}
}
+.panel-card-col {
+ transition: opacity 0.15s ease;
+
+ &--dragging {
+ opacity: 0.25;
+ }
+}
+.panel-card-move {
+ transition: transform 0.2s ease;
+}
+.panel-drag-handle {
+ display: inline-flex;
+ align-items: center;
+ cursor: grab;
+
+ &:active {
+ cursor: grabbing;
+ }
+
+ &--disabled {
+ cursor: not-allowed;
+ opacity: 0.45;
+ }
+}
.disk-card-option {
height: 2rem;
line-height: 2rem;
From 4964b04d54e7ac36b3f85fbfd12f8713e057f02f Mon Sep 17 00:00:00 2001
From: 794308525 <794308525@qq.com>
Date: Tue, 11 Aug 2026 18:07:05 +0800
Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E9=9D=A2?=
=?UTF-8?q?=E6=9D=BF=E9=A1=B6=E9=83=A8=E5=B7=A5=E5=85=B7=E6=A0=8F=E4=B8=8E?=
=?UTF-8?q?=E5=88=86=E7=BB=84=E7=BB=9F=E8=AE=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
重构顶部品牌与操作区域的视觉层级,统一分组、排序控件为带平滑选中背景的胶囊样式,并兼容深色模式。\n\n为分组列表补充面板数量统计,在分组管理中增加默认开启且可持久化的数量显示开关。
---
electron/controller/panel.js | 14 +
frontend/src/store/panel/index.ts | 4 +-
frontend/src/types/base.d.ts | 1 +
.../panel/components/GroupManage/index.vue | 33 +-
frontend/src/views/panel/index.vue | 323 +++++++++++++++---
5 files changed, 331 insertions(+), 44 deletions(-)
diff --git a/electron/controller/panel.js b/electron/controller/panel.js
index 208d98b..41ac315 100644
--- a/electron/controller/panel.js
+++ b/electron/controller/panel.js
@@ -219,6 +219,20 @@ class PanelController extends Controller {
result.groups = result.groups.concat(groups);
}
+ // 统计各分组面板数量,全部分组展示所有面板总数
+ const panel_counts = pub.M(this.TABLE).field('group_id, COUNT(*) AS panel_count').group('group_id').select();
+ const group_count_map = {};
+ let panel_total = 0;
+ panel_counts.forEach(item => {
+ const count = Number(item.panel_count) || 0;
+ group_count_map[item.group_id] = count;
+ panel_total += count;
+ });
+ result.groups = result.groups.map(group => ({
+ ...group,
+ panel_count: group.group_id === -1 ? panel_total : (group_count_map[group.group_id] || 0)
+ }));
+
// 检查分组是否存在
if (group_id !== undefined && group_id != -1){
let is_group_exists = false;
diff --git a/frontend/src/store/panel/index.ts b/frontend/src/store/panel/index.ts
index 1b4c200..539086c 100644
--- a/frontend/src/store/panel/index.ts
+++ b/frontend/src/store/panel/index.ts
@@ -34,6 +34,7 @@ export const usePanelBase = defineStore(
const groupManageVisible = ref(false) // 分组管理弹窗
const isShowIP = ref(true) // 是否显示IP
+ const showGroupCount = ref(true) // 是否显示分组数量
const addPanelVisible = ref(false) // 添加面板弹窗
const isEdit = ref(false) // 是否编辑模式
const panelParams = ref()
@@ -54,6 +55,7 @@ export const usePanelBase = defineStore(
editGroupParams,
groupManageVisible,
isShowIP,
+ showGroupCount,
addPanelVisible,
isEdit,
panelParams,
@@ -64,7 +66,7 @@ export const usePanelBase = defineStore(
},
{
persist: {
- paths: ['isShowIP', 'currentGroupID'],
+ paths: ['isShowIP', 'showGroupCount', 'currentGroupID'],
},
}
)
diff --git a/frontend/src/types/base.d.ts b/frontend/src/types/base.d.ts
index 35b273f..68afe2b 100644
--- a/frontend/src/types/base.d.ts
+++ b/frontend/src/types/base.d.ts
@@ -14,6 +14,7 @@ declare type proxyOptions = {
declare type groupOptions = {
group_id: number
group_name: string
+ panel_count?: number
}
/**
* @description 下拉框格式
diff --git a/frontend/src/views/panel/components/GroupManage/index.vue b/frontend/src/views/panel/components/GroupManage/index.vue
index 4ec753c..ca0d74d 100644
--- a/frontend/src/views/panel/components/GroupManage/index.vue
+++ b/frontend/src/views/panel/components/GroupManage/index.vue
@@ -12,6 +12,15 @@
v-html="pub.lang('分组管理')">
+
+
+
{{ pub.lang('显示分组数量') }}
+
+ {{ pub.lang('在顶部的分组选项中显示面板数量') }}
+
+
+
+
{
}
})
+
diff --git a/frontend/src/views/panel/index.vue b/frontend/src/views/panel/index.vue
index a294ff4..4c71e30 100644
--- a/frontend/src/views/panel/index.vue
+++ b/frontend/src/views/panel/index.vue
@@ -1,25 +1,84 @@
-