feat(frontend): 支持快捷指令跨组拖拽并升级网络历史图
新增快捷指令跨标签组拖拽能力,支持将未标记命令直接拖入 目标标签组,并修正 manual/name/last_used 排序按钮状态映射 将状态监控内存与网络卡片的响应式阈值统一收紧到 250px, 同时用可悬浮查看最近采样点的 canvas 历史图替换网络趋势线
This commit is contained in:
@@ -120,43 +120,45 @@
|
||||
</section>
|
||||
|
||||
<section class="monitor-module monitor-module--network">
|
||||
<div class="network-module__hero">
|
||||
<div class="monitor-module__heading">
|
||||
<div>
|
||||
<span class="monitor-module__eyebrow">{{ t('statusMonitor.networkLabel') }}</span>
|
||||
<h5 class="monitor-module__title">{{ t('statusMonitor.networkLabel') }}</h5>
|
||||
</div>
|
||||
<div class="network-module__sparkline" aria-hidden="true">
|
||||
<svg viewBox="0 0 160 30" preserveAspectRatio="none">
|
||||
<path class="network-module__sparkline-path network-module__sparkline-path--up" :d="networkUpSparklinePath"></path>
|
||||
<path class="network-module__sparkline-path network-module__sparkline-path--down" :d="networkDownSparklinePath"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="monitor-module__pill">{{ t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitLabel }) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="network-table">
|
||||
<div class="network-table__header">
|
||||
<span>{{ networkInterfaceDisplay }}</span>
|
||||
<span>{{ t('statusMonitor.downloadLabel') }} / {{ t('statusMonitor.uploadLabel') }}</span>
|
||||
</div>
|
||||
<div class="network-table__columns">
|
||||
<span></span>
|
||||
<span>{{ t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitLabel }) }}</span>
|
||||
<span>{{ t('statusMonitor.totalTrafficLabel') }}</span>
|
||||
</div>
|
||||
<div class="module-split module-split--network">
|
||||
<StatusMonitorNetworkHistoryChart
|
||||
:download-history="currentNetRxHistory"
|
||||
:upload-history="currentNetTxHistory"
|
||||
/>
|
||||
|
||||
<div class="network-stat-stack">
|
||||
<article
|
||||
v-for="item in networkFlowItems"
|
||||
:key="item.key"
|
||||
:class="['network-stat', `network-stat--${item.tone}`]"
|
||||
>
|
||||
<span class="network-stat__label">
|
||||
<i :class="['fas', item.icon]"></i>
|
||||
<span>{{ item.label }}</span>
|
||||
</span>
|
||||
<span class="network-stat__value">{{ item.value }}</span>
|
||||
<span class="network-stat__total">{{ item.totalValue }}</span>
|
||||
</article>
|
||||
<div class="network-table">
|
||||
<div class="network-table__header">
|
||||
<span>{{ networkInterfaceDisplay }}</span>
|
||||
<span>{{ t('statusMonitor.downloadLabel') }} / {{ t('statusMonitor.uploadLabel') }}</span>
|
||||
</div>
|
||||
<div class="network-table__columns">
|
||||
<span></span>
|
||||
<span>{{ t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitLabel }) }}</span>
|
||||
<span>{{ t('statusMonitor.totalTrafficLabel') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="network-stat-stack">
|
||||
<article
|
||||
v-for="item in networkFlowItems"
|
||||
:key="item.key"
|
||||
:class="['network-stat', `network-stat--${item.tone}`]"
|
||||
>
|
||||
<span class="network-stat__label">
|
||||
<i :class="['fas', item.icon]"></i>
|
||||
<span>{{ item.label }}</span>
|
||||
</span>
|
||||
<span class="network-stat__value">{{ item.value }}</span>
|
||||
<span class="network-stat__total">{{ item.totalValue }}</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -273,6 +275,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import ProcessManagerModal from './ProcessManagerModal.vue';
|
||||
import StatusCharts from './StatusCharts.vue';
|
||||
import StatusMonitorNetworkHistoryChart from './StatusMonitorNetworkHistoryChart.vue';
|
||||
import { useSessionStore } from '../stores/session.store';
|
||||
import { useSettingsStore } from '../stores/settings.store';
|
||||
import { useConnectionsStore } from '../stores/connections.store';
|
||||
@@ -554,24 +557,6 @@ const networkRateUnitLabel = computed(() => {
|
||||
return maxRate >= 1024 * 1024 ? 'MB/s' : 'KB/s';
|
||||
});
|
||||
|
||||
const networkHistoryScale = computed(() => {
|
||||
const values = [
|
||||
...currentNetRxHistory.value.map(value => value ?? 0),
|
||||
...currentNetTxHistory.value.map(value => value ?? 0),
|
||||
];
|
||||
return Math.max(...values, 1);
|
||||
});
|
||||
|
||||
const networkDownSparklinePath = computed(() => {
|
||||
const samples = currentNetRxHistory.value.slice(-24).map(value => Math.max(0, ((value ?? 0) / networkHistoryScale.value) * 100));
|
||||
return buildSparklinePath(samples, 160, 30, 22);
|
||||
});
|
||||
|
||||
const networkUpSparklinePath = computed(() => {
|
||||
const samples = currentNetTxHistory.value.slice(-24).map(value => Math.max(0, ((value ?? 0) / networkHistoryScale.value) * 100));
|
||||
return buildSparklinePath(samples, 160, 30, 22);
|
||||
});
|
||||
|
||||
const diskDeviceAccent = computed(() => {
|
||||
const raw = currentServerStatus.value?.diskDevice;
|
||||
|
||||
@@ -1014,6 +999,16 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.module-split--memory {
|
||||
grid-template-columns: minmax(110px, 0.88fr) minmax(0, 1.12fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.module-split--network {
|
||||
grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.memory-ring-panel,
|
||||
.disk-device-card,
|
||||
.disk-io-card,
|
||||
@@ -1085,6 +1080,12 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.memory-stat-stack {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
align-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.memory-stat__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1123,46 +1124,10 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.network-module__hero {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(96px, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.network-module__sparkline {
|
||||
height: 30px;
|
||||
min-width: 0;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.1);
|
||||
}
|
||||
|
||||
.network-module__sparkline svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.network-module__sparkline-path {
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.network-module__sparkline-path--up {
|
||||
stroke: #34d399;
|
||||
filter: drop-shadow(0 0 6px rgba(52, 211, 153, 0.28));
|
||||
}
|
||||
|
||||
.network-module__sparkline-path--down {
|
||||
stroke: #60a5fa;
|
||||
filter: drop-shadow(0 0 6px rgba(96, 165, 250, 0.24));
|
||||
}
|
||||
|
||||
.network-table {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
@@ -1475,12 +1440,6 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.memory-stat-stack {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
align-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.disk-summary-table__head span,
|
||||
.disk-summary-table__row span {
|
||||
text-align: left;
|
||||
@@ -1489,7 +1448,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
|
||||
@container (max-width: 250px) {
|
||||
.module-split--memory,
|
||||
.network-module__hero,
|
||||
.module-split--network,
|
||||
.disk-compact-top {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1537,10 +1496,6 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
.process-summary-strip {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.memory-stat-stack {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<div ref="chartHostRef" class="network-history-chart">
|
||||
<div class="network-history-chart__header">
|
||||
<div>
|
||||
<p class="network-history-chart__subtitle">{{ t('statusMonitor.networkHistoryRecentPoints', { count: displayPointCount }) }}</p>
|
||||
<h6 class="network-history-chart__title">
|
||||
{{ t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitIsMB ? 'MB/s' : 'KB/s' }) }}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div class="network-history-chart__legend">
|
||||
<span class="network-history-chart__legend-item">
|
||||
<span class="network-history-chart__legend-dot network-history-chart__legend-dot--download"></span>
|
||||
{{ t('statusMonitor.downloadLabel') }}
|
||||
</span>
|
||||
<span class="network-history-chart__legend-item">
|
||||
<span class="network-history-chart__legend-dot network-history-chart__legend-dot--upload"></span>
|
||||
{{ t('statusMonitor.uploadLabel') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="network-history-chart__canvas">
|
||||
<Line :data="networkChartData" :options="networkChartOptions" :key="chartKey" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Line } from 'vue-chartjs';
|
||||
import {
|
||||
CategoryScale,
|
||||
Chart as ChartJS,
|
||||
type ChartOptions,
|
||||
Legend,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
type TooltipItem,
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
);
|
||||
|
||||
const DISPLAY_POINTS = 24;
|
||||
const KB_TO_MB_THRESHOLD = 1024;
|
||||
|
||||
const props = defineProps({
|
||||
downloadHistory: {
|
||||
type: Array as PropType<readonly (number | null)[]>,
|
||||
required: true,
|
||||
},
|
||||
uploadHistory: {
|
||||
type: Array as PropType<readonly (number | null)[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const chartHostRef = ref<HTMLElement | null>(null);
|
||||
const chartKey = ref(0);
|
||||
let chartResizeObserver: ResizeObserver | null = null;
|
||||
let lastChartHostWidth = 0;
|
||||
|
||||
const recentDownloadHistory = computed(() => props.downloadHistory.slice(-DISPLAY_POINTS));
|
||||
const recentUploadHistory = computed(() => props.uploadHistory.slice(-DISPLAY_POINTS));
|
||||
const displayPointCount = computed(() => Math.max(recentDownloadHistory.value.length, recentUploadHistory.value.length, DISPLAY_POINTS));
|
||||
|
||||
const peakHistoryRateKB = computed(() => {
|
||||
const values = [
|
||||
...recentDownloadHistory.value.map(value => (value ?? 0) / 1024),
|
||||
...recentUploadHistory.value.map(value => (value ?? 0) / 1024),
|
||||
];
|
||||
return Math.max(...values, 0);
|
||||
});
|
||||
|
||||
const networkRateUnitIsMB = computed(() => peakHistoryRateKB.value >= KB_TO_MB_THRESHOLD);
|
||||
|
||||
const chartDivisor = computed(() => (networkRateUnitIsMB.value ? 1024 * 1024 : 1024));
|
||||
|
||||
const chartPrecision = computed(() => (networkRateUnitIsMB.value ? 2 : 1));
|
||||
|
||||
const chartLabels = computed(() =>
|
||||
Array.from({ length: displayPointCount.value }, (_, index) => `${index + 1}`),
|
||||
);
|
||||
|
||||
const networkChartData = computed(() => ({
|
||||
labels: chartLabels.value,
|
||||
datasets: [
|
||||
{
|
||||
label: t('statusMonitor.networkDownloadLabelUnit', { unit: networkRateUnitIsMB.value ? 'MB/s' : 'KB/s' }),
|
||||
data: recentDownloadHistory.value.map(value =>
|
||||
value === null || value === undefined ? null : Number((value / chartDivisor.value).toFixed(chartPrecision.value)),
|
||||
),
|
||||
borderColor: 'rgba(96, 165, 250, 1)',
|
||||
backgroundColor: 'rgba(96, 165, 250, 0.18)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
tension: 0.24,
|
||||
spanGaps: true,
|
||||
},
|
||||
{
|
||||
label: t('statusMonitor.networkUploadLabelUnit', { unit: networkRateUnitIsMB.value ? 'MB/s' : 'KB/s' }),
|
||||
data: recentUploadHistory.value.map(value =>
|
||||
value === null || value === undefined ? null : Number((value / chartDivisor.value).toFixed(chartPrecision.value)),
|
||||
),
|
||||
borderColor: 'rgba(52, 211, 153, 1)',
|
||||
backgroundColor: 'rgba(52, 211, 153, 0.16)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
tension: 0.24,
|
||||
spanGaps: true,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const suggestedYAxisMax = computed(() => {
|
||||
const allValues = [
|
||||
...recentDownloadHistory.value,
|
||||
...recentUploadHistory.value,
|
||||
].filter((value): value is number => value !== null && value !== undefined && Number.isFinite(value))
|
||||
.map(value => value / chartDivisor.value);
|
||||
|
||||
const currentMax = Math.max(...allValues, 0);
|
||||
if (currentMax === 0) {
|
||||
return networkRateUnitIsMB.value ? 1 : 100;
|
||||
}
|
||||
|
||||
if (networkRateUnitIsMB.value) {
|
||||
return Math.max(1, Math.ceil(currentMax * 1.2));
|
||||
}
|
||||
|
||||
if (currentMax <= 100) {
|
||||
return Math.max(10, Math.ceil((currentMax * 1.2) / 10) * 10);
|
||||
}
|
||||
|
||||
if (currentMax <= 500) {
|
||||
return Math.ceil((currentMax * 1.2) / 50) * 50;
|
||||
}
|
||||
|
||||
return Math.ceil((currentMax * 1.2) / 100) * 100;
|
||||
});
|
||||
|
||||
const networkChartOptions = computed<ChartOptions<'line'>>(() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
displayColors: true,
|
||||
callbacks: {
|
||||
title: () => '',
|
||||
label: (context: TooltipItem<'line'>) => {
|
||||
const label = context.dataset.label ?? '';
|
||||
const value = context.parsed.y;
|
||||
if (value === null) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return `${label}: ${Number(value).toFixed(chartPrecision.value)} ${networkRateUnitIsMB.value ? 'MB/s' : 'KB/s'}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
min: 0,
|
||||
max: suggestedYAxisMax.value,
|
||||
ticks: {
|
||||
color: '#8fa0b3',
|
||||
callback: value => {
|
||||
const numericValue = Number(value);
|
||||
return Number.isFinite(numericValue) ? numericValue.toFixed(networkRateUnitIsMB.value ? 2 : 0) : '';
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(148, 163, 184, 0.12)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const rerenderChart = (): void => {
|
||||
chartKey.value += 1;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [recentDownloadHistory.value, recentUploadHistory.value, suggestedYAxisMax.value],
|
||||
() => {
|
||||
rerenderChart();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const host = chartHostRef.value;
|
||||
if (!host || typeof ResizeObserver === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
lastChartHostWidth = Math.round(host.getBoundingClientRect().width);
|
||||
chartResizeObserver = new ResizeObserver(entries => {
|
||||
const entry = entries[0];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextWidth = Math.round(entry.contentRect.width);
|
||||
if (!nextWidth || Math.abs(nextWidth - lastChartHostWidth) < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastChartHostWidth = nextWidth;
|
||||
nextTick(() => {
|
||||
rerenderChart();
|
||||
});
|
||||
});
|
||||
|
||||
chartResizeObserver.observe(host);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
chartResizeObserver?.disconnect();
|
||||
chartResizeObserver = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.network-history-chart {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.08);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.02)),
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.06), transparent 62%);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.network-history-chart__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.network-history-chart__subtitle {
|
||||
margin: 0;
|
||||
color: #8fa0b3;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.network-history-chart__title {
|
||||
margin: 6px 0 0;
|
||||
color: #f8fbff;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.network-history-chart__legend {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.network-history-chart__legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #d9e5f1;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.network-history-chart__legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.network-history-chart__legend-dot--download {
|
||||
background: rgba(96, 165, 250, 1);
|
||||
}
|
||||
|
||||
.network-history-chart__legend-dot--upload {
|
||||
background: rgba(52, 211, 153, 1);
|
||||
}
|
||||
|
||||
.network-history-chart__canvas {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
height: 164px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.network-history-chart__canvas :deep(canvas) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
@container (max-width: 300px) {
|
||||
.network-history-chart__header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.network-history-chart__legend {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -688,6 +688,7 @@
|
||||
"cpuUsageTitle": "CPU Usage",
|
||||
"memoryUsageTitleUnit": "Memory Usage ({unit})",
|
||||
"networkSpeedTitleUnit": "Network Speed ({unit})",
|
||||
"networkHistoryRecentPoints": "Latest {count} samples",
|
||||
"cpuUsageLabel": "CPU Usage (%)",
|
||||
"memoryUsageLabelUnit": "Memory Usage ({unit})",
|
||||
"networkDownloadLabelUnit": "Download ({unit})",
|
||||
|
||||
@@ -1423,6 +1423,7 @@
|
||||
"cpuUsageTitle": "CPU使用率",
|
||||
"memoryUsageTitleUnit": "メモリ使用状況 ({unit})",
|
||||
"networkSpeedTitleUnit": "ネットワーク速度 ({unit})",
|
||||
"networkHistoryRecentPoints": "直近 {count} 件のサンプル",
|
||||
"cpuUsageLabel": "CPU使用率 (%)",
|
||||
"memoryUsageLabelUnit": "メモリ使用量 ({unit})",
|
||||
"networkDownloadLabelUnit": "ダウンロード ({unit})",
|
||||
|
||||
@@ -688,6 +688,7 @@
|
||||
"cpuUsageTitle": "CPU 使用率",
|
||||
"memoryUsageTitleUnit": "内存使用情况 ({unit})",
|
||||
"networkSpeedTitleUnit": "网络速度 ({unit})",
|
||||
"networkHistoryRecentPoints": "最近 {count} 个采样点",
|
||||
"cpuUsageLabel": "CPU 使用率 (%)",
|
||||
"memoryUsageLabelUnit": "内存使用 ({unit})",
|
||||
"networkDownloadLabelUnit": "下载 ({unit})",
|
||||
|
||||
@@ -343,12 +343,15 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
command: string,
|
||||
tagIds?: number[],
|
||||
variables?: Record<string, string>,
|
||||
notifySuccess = true,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await apiClient.put(`/quick-commands/${id}`, { name, command, tagIds, variables });
|
||||
clearQuickCommandsCache();
|
||||
await fetchQuickCommands();
|
||||
uiNotificationsStore.showSuccess('快捷指令已更新');
|
||||
if (notifySuccess) {
|
||||
uiNotificationsStore.showSuccess('快捷指令已更新');
|
||||
}
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCommandsStore] 更新快捷指令失败:', err);
|
||||
|
||||
@@ -464,6 +464,30 @@ const moveById = <T extends { id: number }>(items: T[], sourceId: number, target
|
||||
return clonedItems;
|
||||
};
|
||||
|
||||
const insertOrMoveById = <T extends { id: number }>(
|
||||
items: T[],
|
||||
sourceId: number,
|
||||
targetId: number,
|
||||
getSourceItem?: () => T | undefined,
|
||||
): T[] => {
|
||||
const clonedItems = [...items];
|
||||
const sourceIndex = clonedItems.findIndex((item) => item.id === sourceId);
|
||||
const targetIndex = clonedItems.findIndex((item) => item.id === targetId);
|
||||
|
||||
if (targetIndex === -1) {
|
||||
return clonedItems;
|
||||
}
|
||||
|
||||
const sourceItem = sourceIndex === -1 ? getSourceItem?.() : clonedItems.splice(sourceIndex, 1)[0];
|
||||
if (!sourceItem) {
|
||||
return clonedItems;
|
||||
}
|
||||
|
||||
const nextTargetIndex = clonedItems.findIndex((item) => item.id === targetId);
|
||||
clonedItems.splice(nextTargetIndex === -1 ? clonedItems.length : nextTargetIndex, 0, sourceItem);
|
||||
return clonedItems;
|
||||
};
|
||||
|
||||
const isGroupDropTarget = (tagId: number | null): boolean =>
|
||||
tagId !== null && groupDropTargetTagId.value === tagId;
|
||||
|
||||
@@ -544,7 +568,15 @@ const handleCommandDragOver = (commandId: number, groupTagId: number | null) =>
|
||||
return;
|
||||
}
|
||||
|
||||
if (draggingCommand.value.groupTagId !== groupTagId || draggingCommand.value.commandId === commandId) {
|
||||
if (draggingCommand.value.commandId === commandId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
showQuickCommandTagsBoolean.value
|
||||
&& draggingCommand.value.groupTagId !== groupTagId
|
||||
&& groupTagId === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,51 +584,115 @@ const handleCommandDragOver = (commandId: number, groupTagId: number | null) =>
|
||||
};
|
||||
|
||||
const handleCommandDrop = async (commandId: number, groupTagId: number | null) => {
|
||||
if (!draggingCommand.value || dragDisabledBySearch.value) {
|
||||
const activeDraggingCommand = draggingCommand.value;
|
||||
|
||||
if (!activeDraggingCommand || dragDisabledBySearch.value) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (draggingCommand.value.groupTagId !== groupTagId || draggingCommand.value.commandId === commandId) {
|
||||
if (activeDraggingCommand.commandId === commandId) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceGroupTagId = activeDraggingCommand.groupTagId;
|
||||
|
||||
if (
|
||||
showQuickCommandTagsBoolean.value
|
||||
&& sourceGroupTagId !== groupTagId
|
||||
&& groupTagId === null
|
||||
) {
|
||||
uiNotificationsStore.showInfo(t('quickCommands.dragMoveToUntaggedUnsupported', '暂不支持把已标记命令拖入“未标记”分组。'));
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
let currentCommands: QuickCommandFE[] = [];
|
||||
if (showQuickCommandTagsBoolean.value) {
|
||||
currentCommands = filteredAndGroupedCommands.value.find((group) => group.tagId === groupTagId)?.commands ?? [];
|
||||
} else {
|
||||
currentCommands = flatFilteredCommands.value;
|
||||
}
|
||||
|
||||
const reorderedCommands = moveById(currentCommands, draggingCommand.value.commandId, commandId);
|
||||
if (showQuickCommandTagsBoolean.value) {
|
||||
if (groupTagId !== null) {
|
||||
await quickCommandsStore.reorderCommandsInTag(groupTagId, reorderedCommands.map((item) => item.id));
|
||||
if (!showQuickCommandTagsBoolean.value || sourceGroupTagId === groupTagId) {
|
||||
if (showQuickCommandTagsBoolean.value) {
|
||||
currentCommands = filteredAndGroupedCommands.value.find((group) => group.tagId === groupTagId)?.commands ?? [];
|
||||
} else {
|
||||
const reorderedUntaggedIds = reorderedCommands.map((item) => item.id);
|
||||
const globalCommandIds = [...quickCommandsStore.quickCommandsList]
|
||||
.sort((a, b) => (a.sort_order - b.sort_order) || (a.id - b.id))
|
||||
.map((command) => command.id);
|
||||
|
||||
let untaggedIndex = 0;
|
||||
const mergedCommandIds = globalCommandIds.map((existingCommandId) => {
|
||||
const command = quickCommandsStore.quickCommandsList.find((item) => item.id === existingCommandId);
|
||||
if (!command || command.tagIds.length > 0) {
|
||||
return existingCommandId;
|
||||
}
|
||||
|
||||
const nextUntaggedId = reorderedUntaggedIds[untaggedIndex];
|
||||
untaggedIndex += 1;
|
||||
return nextUntaggedId ?? existingCommandId;
|
||||
});
|
||||
|
||||
await quickCommandsStore.reorderQuickCommands(mergedCommandIds);
|
||||
currentCommands = flatFilteredCommands.value;
|
||||
}
|
||||
} else {
|
||||
await quickCommandsStore.reorderQuickCommands(reorderedCommands.map((item) => item.id));
|
||||
|
||||
const reorderedCommands = moveById(currentCommands, activeDraggingCommand.commandId, commandId);
|
||||
|
||||
if (showQuickCommandTagsBoolean.value) {
|
||||
if (groupTagId !== null) {
|
||||
await quickCommandsStore.reorderCommandsInTag(groupTagId, reorderedCommands.map((item) => item.id));
|
||||
} else {
|
||||
const reorderedUntaggedIds = reorderedCommands.map((item) => item.id);
|
||||
const globalCommandIds = [...quickCommandsStore.quickCommandsList]
|
||||
.sort((a, b) => (a.sort_order - b.sort_order) || (a.id - b.id))
|
||||
.map((command) => command.id);
|
||||
|
||||
let untaggedIndex = 0;
|
||||
const mergedCommandIds = globalCommandIds.map((existingCommandId) => {
|
||||
const command = quickCommandsStore.quickCommandsList.find((item) => item.id === existingCommandId);
|
||||
if (!command || command.tagIds.length > 0) {
|
||||
return existingCommandId;
|
||||
}
|
||||
|
||||
const nextUntaggedId = reorderedUntaggedIds[untaggedIndex];
|
||||
untaggedIndex += 1;
|
||||
return nextUntaggedId ?? existingCommandId;
|
||||
});
|
||||
|
||||
await quickCommandsStore.reorderQuickCommands(mergedCommandIds);
|
||||
}
|
||||
} else {
|
||||
await quickCommandsStore.reorderQuickCommands(reorderedCommands.map((item) => item.id));
|
||||
}
|
||||
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupTagId === null) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceCommand = quickCommandsStore.quickCommandsList.find((item) => item.id === activeDraggingCommand.commandId);
|
||||
if (!sourceCommand) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTagIds = Array.from(
|
||||
new Set([
|
||||
...sourceCommand.tagIds.filter((tagId) => tagId !== sourceGroupTagId),
|
||||
groupTagId,
|
||||
]),
|
||||
);
|
||||
|
||||
const updateSuccess = await quickCommandsStore.updateQuickCommand(
|
||||
sourceCommand.id,
|
||||
sourceCommand.name,
|
||||
sourceCommand.command,
|
||||
nextTagIds,
|
||||
sourceCommand.variables ?? undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
if (!updateSuccess) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshedSourceCommand =
|
||||
quickCommandsStore.quickCommandsList.find((item) => item.id === sourceCommand.id)
|
||||
?? { ...sourceCommand, tagIds: nextTagIds };
|
||||
const targetCommands = filteredAndGroupedCommands.value.find((group) => group.tagId === groupTagId)?.commands ?? [];
|
||||
const reorderedTargetCommands = insertOrMoveById(
|
||||
targetCommands,
|
||||
refreshedSourceCommand.id,
|
||||
commandId,
|
||||
() => refreshedSourceCommand,
|
||||
);
|
||||
|
||||
await quickCommandsStore.reorderCommandsInTag(groupTagId, reorderedTargetCommands.map((item) => item.id));
|
||||
resetDragState();
|
||||
};
|
||||
|
||||
@@ -745,13 +841,20 @@ const toggleGroup = (groupName: string) => {
|
||||
|
||||
// 计算排序按钮的 title 和 icon
|
||||
const sortButtonTitle = computed(() => {
|
||||
if (sortBy.value === 'manual') {
|
||||
return t('quickCommands.sortByManual', '按手动顺序排序');
|
||||
}
|
||||
|
||||
return sortBy.value === 'name'
|
||||
? t('quickCommands.sortByName', '按名称排序')
|
||||
: t('quickCommands.sortByLastUsed', '按最近使用排序');
|
||||
});
|
||||
|
||||
const sortButtonIcon = computed(() => {
|
||||
// 使用 Font Awesome 图标示例
|
||||
if (sortBy.value === 'manual') {
|
||||
return 'fas fa-grip-lines';
|
||||
}
|
||||
|
||||
return sortBy.value === 'name' ? 'fas fa-sort-alpha-down' : 'fas fa-clock';
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user