🚧 Optimize
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
|
||||
import { SystemService } from '@/../bindings/voidraft/internal/services';
|
||||
import type { MemoryStats } from '@/../bindings/voidraft/internal/services';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
|
||||
import {computed, nextTick, onMounted, onUnmounted, ref, watch} from 'vue';
|
||||
import type {MemoryStats} from '@/../bindings/voidraft/internal/services';
|
||||
import {SystemService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useThemeStore} from '@/stores/themeStore';
|
||||
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||
|
||||
const { t } = useI18n();
|
||||
const {t} = useI18n();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
// 响应式状态
|
||||
const memoryStats = ref<MemoryStats | null>(null);
|
||||
const formattedMemory = ref('');
|
||||
const isLoading = ref(true);
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// 存储历史数据点 (最近60个数据点)
|
||||
const historyData = ref<number[]>([]);
|
||||
@@ -21,209 +22,188 @@ const maxDataPoints = 60;
|
||||
// 动态最大内存值(MB),初始为200MB,会根据实际使用动态调整
|
||||
const maxMemoryMB = ref(200);
|
||||
|
||||
// 使用themeStore获取当前主题
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// 使用 computed 获取当前主题状态
|
||||
const isDarkTheme = computed(() => {
|
||||
const theme = themeStore.currentTheme;
|
||||
if (theme === SystemThemeType.SystemThemeAuto) {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
return theme === SystemThemeType.SystemThemeDark;
|
||||
const {currentTheme} = themeStore;
|
||||
return currentTheme === SystemThemeType.SystemThemeDark;
|
||||
});
|
||||
|
||||
// 监听主题变化,重新绘制图表
|
||||
watch(() => themeStore.currentTheme, () => {
|
||||
nextTick(() => drawChart());
|
||||
nextTick(drawChart);
|
||||
});
|
||||
|
||||
// 静默错误处理包装器
|
||||
const withSilentErrorHandling = async <T>(
|
||||
operation: () => Promise<T>,
|
||||
fallback?: T
|
||||
): Promise<T | undefined> => {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (_error) {
|
||||
// 静默处理错误,不输出到控制台
|
||||
return fallback;
|
||||
}
|
||||
// 格式化内存显示函数
|
||||
const formatMemorySize = (heapMB: number): string => {
|
||||
if (heapMB < 1) return `${(heapMB * 1024).toFixed(0)}K`;
|
||||
if (heapMB < 100) return `${heapMB.toFixed(1)}M`;
|
||||
return `${heapMB.toFixed(0)}M`;
|
||||
};
|
||||
|
||||
// 获取内存统计信息
|
||||
const fetchMemoryStats = async () => {
|
||||
const stats = await withSilentErrorHandling(() => SystemService.GetMemoryStats());
|
||||
|
||||
if (!stats) {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchMemoryStats = async (): Promise<void> => {
|
||||
try {
|
||||
const stats = await SystemService.GetMemoryStats();
|
||||
|
||||
memoryStats.value = stats;
|
||||
|
||||
|
||||
// 格式化内存显示 - 主要显示堆内存使用量
|
||||
const heapMB = (stats.heapInUse / 1024 / 1024);
|
||||
if (heapMB < 1) {
|
||||
formattedMemory.value = `${(heapMB * 1024).toFixed(0)}K`;
|
||||
} else if (heapMB < 100) {
|
||||
formattedMemory.value = `${heapMB.toFixed(1)}M`;
|
||||
} else {
|
||||
formattedMemory.value = `${heapMB.toFixed(0)}M`;
|
||||
}
|
||||
|
||||
const heapMB = stats.heapInUse / (1024 * 1024);
|
||||
formattedMemory.value = formatMemorySize(heapMB);
|
||||
|
||||
// 自动调整最大内存值,确保图表能够显示更大范围
|
||||
if (heapMB > maxMemoryMB.value * 0.8) {
|
||||
// 如果内存使用超过当前最大值的80%,则将最大值调整为当前使用值的2倍
|
||||
maxMemoryMB.value = Math.ceil(heapMB * 2);
|
||||
}
|
||||
|
||||
|
||||
// 添加新数据点到历史记录 - 使用动态最大值计算百分比
|
||||
const memoryUsagePercent = Math.min((heapMB / maxMemoryMB.value) * 100, 100);
|
||||
historyData.value.push(memoryUsagePercent);
|
||||
|
||||
// 保持最大数据点数量
|
||||
if (historyData.value.length > maxDataPoints) {
|
||||
historyData.value.shift();
|
||||
}
|
||||
|
||||
historyData.value = [...historyData.value, memoryUsagePercent].slice(-maxDataPoints);
|
||||
|
||||
// 更新图表
|
||||
drawChart();
|
||||
|
||||
} catch {
|
||||
// 静默处理错误
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 绘制实时曲线图 - 简化版
|
||||
const drawChart = () => {
|
||||
if (!canvasRef.value || historyData.value.length === 0) return;
|
||||
|
||||
const canvas = canvasRef.value;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// 设置canvas尺寸
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width * window.devicePixelRatio;
|
||||
canvas.height = rect.height * window.devicePixelRatio;
|
||||
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// 根据主题选择合适的颜色 - 更柔和的颜色
|
||||
const gridColor = isDarkTheme.value ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.07)';
|
||||
const lineColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.6)' : 'rgba(37, 99, 235, 0.6)';
|
||||
const fillColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.05)' : 'rgba(37, 99, 235, 0.05)';
|
||||
const pointColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.8)' : 'rgba(37, 99, 235, 0.8)';
|
||||
|
||||
// 绘制背景网格 - 更加柔和
|
||||
// 获取主题相关颜色配置
|
||||
const getThemeColors = () => {
|
||||
const isDark = isDarkTheme.value;
|
||||
return {
|
||||
grid: isDark ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.07)',
|
||||
line: isDark ? 'rgba(74, 158, 255, 0.6)' : 'rgba(37, 99, 235, 0.6)',
|
||||
fill: isDark ? 'rgba(74, 158, 255, 0.05)' : 'rgba(37, 99, 235, 0.05)',
|
||||
point: isDark ? 'rgba(74, 158, 255, 0.8)' : 'rgba(37, 99, 235, 0.8)'
|
||||
};
|
||||
};
|
||||
|
||||
// 绘制网格背景
|
||||
const drawGrid = (ctx: CanvasRenderingContext2D, width: number, height: number, colors: ReturnType<typeof getThemeColors>): void => {
|
||||
ctx.strokeStyle = colors.grid;
|
||||
ctx.lineWidth = 0.5;
|
||||
|
||||
// 水平网格线
|
||||
for (let i = 0; i <= 4; i++) {
|
||||
const y = (height / 4) * i;
|
||||
ctx.strokeStyle = gridColor;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
|
||||
// 垂直网格线
|
||||
for (let i = 0; i <= 6; i++) {
|
||||
const x = (width / 6) * i;
|
||||
ctx.strokeStyle = gridColor;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// 绘制平滑曲线路径
|
||||
const drawSmoothPath = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
data: number[],
|
||||
startX: number,
|
||||
stepX: number,
|
||||
height: number,
|
||||
fillArea = false
|
||||
): void => {
|
||||
if (data.length < 2) return;
|
||||
|
||||
const firstY = height - (data[0] / 100) * height;
|
||||
|
||||
ctx.beginPath();
|
||||
if (fillArea) ctx.moveTo(startX, height);
|
||||
ctx.moveTo(startX, firstY);
|
||||
|
||||
// 使用二次贝塞尔曲线绘制平滑路径
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const x = startX + i * stepX;
|
||||
const y = height - (data[i] / 100) * height;
|
||||
|
||||
if (i === 1) {
|
||||
ctx.lineTo(x, y);
|
||||
} else {
|
||||
const prevX = startX + (i - 1) * stepX;
|
||||
const prevY = height - (data[i - 1] / 100) * height;
|
||||
const cpX = (prevX + x) / 2;
|
||||
const cpY = (prevY + y) / 2;
|
||||
ctx.quadraticCurveTo(cpX, cpY, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
if (fillArea) {
|
||||
const lastX = startX + (data.length - 1) * stepX;
|
||||
ctx.lineTo(lastX, height);
|
||||
ctx.closePath();
|
||||
}
|
||||
};
|
||||
|
||||
// 绘制实时曲线图
|
||||
const drawChart = (): void => {
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas || historyData.value.length === 0) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// 设置canvas尺寸
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio;
|
||||
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const {width, height} = rect;
|
||||
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// 获取主题颜色
|
||||
const colors = getThemeColors();
|
||||
|
||||
// 绘制背景网格
|
||||
drawGrid(ctx, width, height, colors);
|
||||
|
||||
if (historyData.value.length < 2) return;
|
||||
|
||||
|
||||
// 计算数据点位置
|
||||
const dataLength = historyData.value.length;
|
||||
const stepX = width / (maxDataPoints - 1);
|
||||
const startX = width - (dataLength - 1) * stepX;
|
||||
|
||||
// 绘制填充区域 - 更柔和的填充
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, height);
|
||||
|
||||
// 移动到第一个数据点
|
||||
const firstY = height - (historyData.value[0] / 100) * height;
|
||||
ctx.lineTo(startX, firstY);
|
||||
|
||||
// 绘制数据点路径 - 使用曲线连接点,确保连续性
|
||||
for (let i = 1; i < dataLength; i++) {
|
||||
const x = startX + i * stepX;
|
||||
const y = height - (historyData.value[i] / 100) * height;
|
||||
|
||||
// 使用贝塞尔曲线平滑连接
|
||||
if (i < dataLength - 1) {
|
||||
const nextX = startX + (i + 1) * stepX;
|
||||
const nextY = height - (historyData.value[i + 1] / 100) * height;
|
||||
const cpX1 = x - stepX / 4;
|
||||
const cpY1 = y;
|
||||
const cpX2 = x + stepX / 4;
|
||||
const cpY2 = nextY;
|
||||
|
||||
// 使用三次贝塞尔曲线平滑连接点
|
||||
ctx.bezierCurveTo(cpX1, cpY1, cpX2, cpY2, nextX, nextY);
|
||||
i++; // 跳过下一个点,因为已经在曲线中处理了
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// 完成填充路径
|
||||
const lastX = startX + (dataLength - 1) * stepX;
|
||||
ctx.lineTo(lastX, height);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = fillColor;
|
||||
|
||||
// 绘制填充区域
|
||||
drawSmoothPath(ctx, historyData.value, startX, stepX, height, true);
|
||||
ctx.fillStyle = colors.fill;
|
||||
ctx.fill();
|
||||
|
||||
// 绘制主曲线 - 平滑连续的曲线
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, firstY);
|
||||
|
||||
// 重新绘制曲线路径,但这次只绘制线条
|
||||
for (let i = 1; i < dataLength; i++) {
|
||||
const x = startX + i * stepX;
|
||||
const y = height - (historyData.value[i] / 100) * height;
|
||||
|
||||
// 使用贝塞尔曲线平滑连接
|
||||
if (i < dataLength - 1) {
|
||||
const nextX = startX + (i + 1) * stepX;
|
||||
const nextY = height - (historyData.value[i + 1] / 100) * height;
|
||||
const cpX1 = x - stepX / 4;
|
||||
const cpY1 = y;
|
||||
const cpX2 = x + stepX / 4;
|
||||
const cpY2 = nextY;
|
||||
|
||||
// 使用三次贝塞尔曲线平滑连接点
|
||||
ctx.bezierCurveTo(cpX1, cpY1, cpX2, cpY2, nextX, nextY);
|
||||
i++; // 跳过下一个点,因为已经在曲线中处理了
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.strokeStyle = lineColor;
|
||||
|
||||
// 绘制主曲线
|
||||
drawSmoothPath(ctx, historyData.value, startX, stepX, height);
|
||||
ctx.strokeStyle = colors.line;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.stroke();
|
||||
|
||||
|
||||
// 绘制当前值的高亮点
|
||||
const lastX = startX + (dataLength - 1) * stepX;
|
||||
const lastY = height - (historyData.value[dataLength - 1] / 100) * height;
|
||||
|
||||
|
||||
// 外圈
|
||||
ctx.fillStyle = pointColor;
|
||||
ctx.fillStyle = colors.point;
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
|
||||
// 内圈
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.beginPath();
|
||||
@@ -232,72 +212,32 @@ const drawChart = () => {
|
||||
};
|
||||
|
||||
// 手动触发GC
|
||||
const triggerGC = async () => {
|
||||
const success = await withSilentErrorHandling(() => SystemService.TriggerGC());
|
||||
|
||||
if (success) {
|
||||
// 延迟一下再获取新的统计信息
|
||||
setTimeout(fetchMemoryStats, 100);
|
||||
const triggerGC = async (): Promise<void> => {
|
||||
try {
|
||||
await SystemService.TriggerGC();
|
||||
} catch (error) {
|
||||
console.error("Failed to trigger GC: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (historyData.value.length > 0) {
|
||||
nextTick(() => drawChart());
|
||||
}
|
||||
};
|
||||
|
||||
// 仅监听系统主题变化
|
||||
const setupSystemThemeListener = () => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleSystemThemeChange = () => {
|
||||
// 仅当设置为auto时才响应系统主题变化
|
||||
if (themeStore.currentTheme === SystemThemeType.SystemThemeAuto) {
|
||||
nextTick(() => drawChart());
|
||||
}
|
||||
};
|
||||
|
||||
// 添加监听器
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||
}
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
if (mediaQuery.removeEventListener) {
|
||||
mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchMemoryStats();
|
||||
// 每1秒更新一次内存信息
|
||||
// 每3秒更新一次内存信息
|
||||
intervalId = setInterval(fetchMemoryStats, 3000);
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// 设置系统主题监听器(仅用于auto模式)
|
||||
const cleanupThemeListener = setupSystemThemeListener();
|
||||
|
||||
// 在卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
cleanupThemeListener();
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
intervalId && clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="memory-monitor" @click="triggerGC" :title="`${t('monitor.memory')}: ${formattedMemory} | ${t('monitor.clickToClean')}`">
|
||||
<div class="memory-monitor" @click="triggerGC"
|
||||
:title="`${t('monitor.memory')}: ${formattedMemory} | ${t('monitor.clickToClean')}`">
|
||||
<div class="monitor-info">
|
||||
<div class="memory-label">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
<span>{{ t('monitor.memory') }}</span>
|
||||
@@ -306,10 +246,10 @@ onMounted(() => {
|
||||
<div class="memory-loading" v-else>--</div>
|
||||
</div>
|
||||
<div class="chart-area">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="memory-chart"
|
||||
:class="{ 'loading': isLoading }"
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="memory-chart"
|
||||
:class="{ 'loading': isLoading }"
|
||||
></canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,28 +263,28 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
|
||||
|
||||
&:hover {
|
||||
.monitor-info {
|
||||
.memory-label {
|
||||
color: var(--selection-text);
|
||||
}
|
||||
|
||||
|
||||
.memory-value {
|
||||
color: var(--toolbar-text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chart-area .memory-chart {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.monitor-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
|
||||
.memory-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -353,18 +293,18 @@ onMounted(() => {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
|
||||
svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
span {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.memory-value, .memory-loading {
|
||||
color: var(--toolbar-text-secondary);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
@@ -372,26 +312,26 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
.memory-loading {
|
||||
opacity: 0.5;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chart-area {
|
||||
height: 48px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
|
||||
|
||||
.memory-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
|
||||
&.loading {
|
||||
opacity: 0.3;
|
||||
}
|
||||
@@ -407,4 +347,4 @@ onMounted(() => {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick, shallowRef, readonly, effectScope, onScopeDispose } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useEditorStore } from '@/stores/editorStore';
|
||||
import { type SupportedLanguage } from '@/views/editor/extensions/codeblock/types';
|
||||
@@ -8,56 +8,46 @@ import { getActiveNoteBlock } from '@/views/editor/extensions/codeblock/state';
|
||||
import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands';
|
||||
|
||||
const { t } = useI18n();
|
||||
const editorStore = useEditorStore();
|
||||
const editorStore = readonly(useEditorStore());
|
||||
|
||||
// 组件状态
|
||||
const showLanguageMenu = ref(false);
|
||||
const showLanguageMenu = shallowRef(false);
|
||||
const searchQuery = ref('');
|
||||
const searchInputRef = ref<HTMLInputElement>();
|
||||
|
||||
// 支持的语言列表
|
||||
const supportedLanguages = getAllSupportedLanguages();
|
||||
|
||||
// 动态生成语言显示名称映射
|
||||
const languageNames = computed(() => {
|
||||
// 优化语言数据处理
|
||||
const languageData = computed(() => {
|
||||
const supportedLanguages = getAllSupportedLanguages();
|
||||
const names: Record<string, string> = {
|
||||
auto: 'Auto',
|
||||
text: 'Plain Text'
|
||||
};
|
||||
|
||||
LANGUAGES.forEach(lang => {
|
||||
names[lang.token] = lang.name;
|
||||
});
|
||||
|
||||
return names;
|
||||
});
|
||||
|
||||
// 动态生成语言别名映射
|
||||
const languageAliases = computed(() => {
|
||||
const aliases: Record<string, string> = {
|
||||
auto: 'auto',
|
||||
text: 'txt'
|
||||
};
|
||||
|
||||
// 一次遍历完成所有映射
|
||||
LANGUAGES.forEach(lang => {
|
||||
// 使用语言名称的小写作为别名
|
||||
names[lang.token] = lang.name;
|
||||
aliases[lang.token] = lang.name.toLowerCase();
|
||||
});
|
||||
|
||||
return aliases;
|
||||
return {
|
||||
supportedLanguages,
|
||||
names,
|
||||
aliases
|
||||
};
|
||||
});
|
||||
|
||||
// 当前活动块的语言信息
|
||||
const currentBlockLanguage = ref<{ name: SupportedLanguage; auto: boolean }>({
|
||||
const currentBlockLanguage = shallowRef<{ name: SupportedLanguage; auto: boolean }>({
|
||||
name: 'text',
|
||||
auto: false
|
||||
});
|
||||
|
||||
// 事件监听器引用
|
||||
const eventListeners = ref<{
|
||||
updateListener?: () => void;
|
||||
selectionUpdateListener?: () => void;
|
||||
}>({});
|
||||
// 事件监听器管理
|
||||
let editorScope: ReturnType<typeof effectScope> | null = null;
|
||||
|
||||
// 更新当前块语言信息
|
||||
const updateCurrentBlockLanguage = () => {
|
||||
@@ -81,7 +71,7 @@ const updateCurrentBlockLanguage = () => {
|
||||
currentBlockLanguage.value = newLanguage;
|
||||
}
|
||||
} else {
|
||||
if (currentBlockLanguage.value.name !== 'text' || currentBlockLanguage.value.auto !== false) {
|
||||
if (currentBlockLanguage.value.name !== 'text' || currentBlockLanguage.value.auto) {
|
||||
currentBlockLanguage.value = { name: 'text', auto: false };
|
||||
}
|
||||
}
|
||||
@@ -93,57 +83,47 @@ const updateCurrentBlockLanguage = () => {
|
||||
|
||||
// 清理事件监听器
|
||||
const cleanupEventListeners = () => {
|
||||
if (editorStore.editorView?.dom && eventListeners.value.updateListener) {
|
||||
const dom = editorStore.editorView.dom;
|
||||
dom.removeEventListener('click', eventListeners.value.updateListener);
|
||||
dom.removeEventListener('keyup', eventListeners.value.updateListener);
|
||||
dom.removeEventListener('keydown', eventListeners.value.updateListener);
|
||||
dom.removeEventListener('focus', eventListeners.value.updateListener);
|
||||
dom.removeEventListener('mouseup', eventListeners.value.updateListener);
|
||||
|
||||
if (eventListeners.value.selectionUpdateListener) {
|
||||
dom.removeEventListener('selectionchange', eventListeners.value.selectionUpdateListener);
|
||||
}
|
||||
if (editorScope) {
|
||||
editorScope.stop();
|
||||
editorScope = null;
|
||||
}
|
||||
eventListeners.value = {};
|
||||
};
|
||||
|
||||
// 设置事件监听器
|
||||
// 设置事件监听器 - 使用 effectScope 管理
|
||||
const setupEventListeners = (view: any) => {
|
||||
cleanupEventListeners();
|
||||
|
||||
// 监听编辑器状态更新
|
||||
const updateListener = () => {
|
||||
// 使用 requestAnimationFrame 确保在下一帧更新,性能更好
|
||||
requestAnimationFrame(() => {
|
||||
updateCurrentBlockLanguage();
|
||||
editorScope = effectScope();
|
||||
editorScope.run(() => {
|
||||
// 监听编辑器状态更新
|
||||
const updateListener = () => {
|
||||
// 使用 requestAnimationFrame 确保在下一帧更新
|
||||
requestAnimationFrame(() => {
|
||||
updateCurrentBlockLanguage();
|
||||
});
|
||||
};
|
||||
|
||||
// 监听关键事件:光标位置变化、文档变化、焦点变化
|
||||
view.dom.addEventListener('click', updateListener);
|
||||
view.dom.addEventListener('keyup', updateListener);
|
||||
view.dom.addEventListener('keydown', updateListener);
|
||||
view.dom.addEventListener('focus', updateListener);
|
||||
view.dom.addEventListener('mouseup', updateListener);
|
||||
view.dom.addEventListener('selectionchange', updateListener);
|
||||
|
||||
// 在 scope 销毁时清理
|
||||
onScopeDispose(() => {
|
||||
view.dom.removeEventListener('click', updateListener);
|
||||
view.dom.removeEventListener('keyup', updateListener);
|
||||
view.dom.removeEventListener('keydown', updateListener);
|
||||
view.dom.removeEventListener('focus', updateListener);
|
||||
view.dom.removeEventListener('mouseup', updateListener);
|
||||
view.dom.removeEventListener('selectionchange', updateListener);
|
||||
});
|
||||
};
|
||||
|
||||
// 监听选择变化
|
||||
const selectionUpdateListener = () => {
|
||||
requestAnimationFrame(() => {
|
||||
updateCurrentBlockLanguage();
|
||||
});
|
||||
};
|
||||
|
||||
// 保存监听器引用
|
||||
eventListeners.value = { updateListener, selectionUpdateListener };
|
||||
|
||||
// 监听关键事件:光标位置变化、文档变化、焦点变化
|
||||
view.dom.addEventListener('click', updateListener);
|
||||
view.dom.addEventListener('keyup', updateListener);
|
||||
view.dom.addEventListener('keydown', updateListener);
|
||||
view.dom.addEventListener('focus', updateListener);
|
||||
view.dom.addEventListener('mouseup', updateListener); // 鼠标选择结束
|
||||
|
||||
// 监听编辑器的选择变化事件
|
||||
if (view.dom.addEventListener) {
|
||||
view.dom.addEventListener('selectionchange', selectionUpdateListener);
|
||||
}
|
||||
|
||||
// 立即更新一次当前状态
|
||||
updateCurrentBlockLanguage();
|
||||
|
||||
// 立即更新一次当前状态
|
||||
updateCurrentBlockLanguage();
|
||||
});
|
||||
};
|
||||
|
||||
// 监听编辑器状态变化
|
||||
@@ -159,16 +139,18 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 过滤后的语言列表
|
||||
// 过滤后的语言列表 - 优化搜索性能
|
||||
const filteredLanguages = computed(() => {
|
||||
const { supportedLanguages, names, aliases } = languageData.value;
|
||||
|
||||
if (!searchQuery.value) {
|
||||
return supportedLanguages;
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return supportedLanguages.filter(langId => {
|
||||
const name = languageNames.value[langId];
|
||||
const alias = languageAliases.value[langId];
|
||||
const name = names[langId];
|
||||
const alias = aliases[langId];
|
||||
return langId.toLowerCase().includes(query) ||
|
||||
(name && name.toLowerCase().includes(query)) ||
|
||||
(alias && alias.toLowerCase().includes(query));
|
||||
@@ -191,7 +173,7 @@ const closeLanguageMenu = () => {
|
||||
searchQuery.value = '';
|
||||
};
|
||||
|
||||
// 选择语言
|
||||
// 选择语言 - 优化性能
|
||||
const selectLanguage = (languageId: SupportedLanguage) => {
|
||||
if (!editorStore.editorView) {
|
||||
closeLanguageMenu();
|
||||
@@ -203,18 +185,9 @@ const selectLanguage = (languageId: SupportedLanguage) => {
|
||||
const state = view.state;
|
||||
const dispatch = view.dispatch;
|
||||
|
||||
let targetLanguage: string;
|
||||
let autoDetect: boolean;
|
||||
|
||||
if (languageId === 'auto') {
|
||||
// 设置为自动检测
|
||||
targetLanguage = 'text';
|
||||
autoDetect = true;
|
||||
} else {
|
||||
// 设置为指定语言,关闭自动检测
|
||||
targetLanguage = languageId;
|
||||
autoDetect = false;
|
||||
}
|
||||
const [targetLanguage, autoDetect] = languageId === 'auto'
|
||||
? ['text', true]
|
||||
: [languageId, false];
|
||||
|
||||
// 使用修复后的函数来更改语言
|
||||
const success = changeCurrentBlockLanguage(state as any, dispatch, targetLanguage, autoDetect);
|
||||
@@ -231,72 +204,75 @@ const selectLanguage = (languageId: SupportedLanguage) => {
|
||||
closeLanguageMenu();
|
||||
};
|
||||
|
||||
// 点击外部关闭
|
||||
// 全局事件处理器 - 使用 effectScope 管理
|
||||
const globalScope = effectScope();
|
||||
|
||||
// 点击外部关闭 - 只在菜单打开时处理
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!showLanguageMenu.value) return;
|
||||
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.block-language-selector')) {
|
||||
closeLanguageMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// 键盘事件处理
|
||||
// 键盘事件处理 - 只在菜单打开时处理
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (!showLanguageMenu.value) return;
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
closeLanguageMenu();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 在 setup 阶段就设置全局事件监听器
|
||||
globalScope.run(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
onScopeDispose(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 立即更新一次当前语言状态
|
||||
updateCurrentBlockLanguage();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
globalScope.stop();
|
||||
cleanupEventListeners();
|
||||
});
|
||||
|
||||
// 获取当前语言的显示名称
|
||||
const getCurrentLanguageName = computed(() => {
|
||||
// 优化计算属性
|
||||
const languageDisplayInfo = computed(() => {
|
||||
const lang = currentBlockLanguage.value;
|
||||
if (lang.auto) {
|
||||
return `${lang.name} (auto)`;
|
||||
}
|
||||
return lang.name;
|
||||
const displayName = lang.auto ? `${lang.name} (auto)` : lang.name;
|
||||
const displayLanguage = lang.auto ? 'auto' : lang.name;
|
||||
|
||||
return {
|
||||
name: displayName,
|
||||
language: displayLanguage
|
||||
};
|
||||
});
|
||||
|
||||
// 获取当前显示的语言选项
|
||||
const getCurrentDisplayLanguage = computed(() => {
|
||||
const lang = currentBlockLanguage.value;
|
||||
if (lang.auto) {
|
||||
return 'auto';
|
||||
}
|
||||
return lang.name;
|
||||
});
|
||||
|
||||
// 滚动到当前选择的语言
|
||||
// 滚动到当前选择的语言 - 优化性能
|
||||
const scrollToCurrentLanguage = () => {
|
||||
nextTick(() => {
|
||||
const currentLang = getCurrentDisplayLanguage.value;
|
||||
const selectorElement = document.querySelector('.block-language-selector');
|
||||
|
||||
if (!selectorElement) return;
|
||||
|
||||
const languageList = selectorElement.querySelector('.language-list') as HTMLElement;
|
||||
const activeOption = selectorElement.querySelector(`.language-option[data-language="${currentLang}"]`) as HTMLElement;
|
||||
|
||||
if (languageList && activeOption) {
|
||||
// 使用 scrollIntoView 进行平滑滚动
|
||||
activeOption.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'nearest',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
const currentLang = languageDisplayInfo.value.language;
|
||||
const activeOption = document.querySelector(`.language-option[data-language="${currentLang}"]`) as HTMLElement;
|
||||
|
||||
if (activeOption) {
|
||||
// 使用 scrollIntoView 进行平滑滚动
|
||||
activeOption.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'nearest',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -314,7 +290,7 @@ const scrollToCurrentLanguage = () => {
|
||||
<polyline points="8 6 2 12 8 18"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="language-name">{{ getCurrentLanguageName }}</span>
|
||||
<span class="language-name">{{ languageDisplayInfo.name }}</span>
|
||||
<span class="arrow" :class="{ 'open': showLanguageMenu }">▲</span>
|
||||
</button>
|
||||
|
||||
@@ -341,11 +317,11 @@ const scrollToCurrentLanguage = () => {
|
||||
v-for="language in filteredLanguages"
|
||||
:key="language"
|
||||
class="language-option"
|
||||
:class="{ 'active': getCurrentDisplayLanguage === language }"
|
||||
:class="{ 'active': languageDisplayInfo.language === language }"
|
||||
:data-language="language"
|
||||
@click="selectLanguage(language)"
|
||||
>
|
||||
<span class="language-name">{{ languageNames[language] || language }}</span>
|
||||
<span class="language-name">{{ languageData.names[language] || language }}</span>
|
||||
<span class="language-alias">{{ language }}</span>
|
||||
</div>
|
||||
|
||||
@@ -517,4 +493,4 @@ const scrollToCurrentLanguage = () => {
|
||||
background-color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -82,7 +82,7 @@ const formatTime = (dateString: string | null) => {
|
||||
// 核心操作
|
||||
const openMenu = async () => {
|
||||
documentStore.openDocumentSelector();
|
||||
await documentStore.updateDocuments();
|
||||
await documentStore.getDocumentMetaList();
|
||||
await nextTick();
|
||||
inputRef.value?.focus();
|
||||
};
|
||||
@@ -158,7 +158,7 @@ const saveEdit = async () => {
|
||||
|
||||
try {
|
||||
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
||||
await documentStore.updateDocuments();
|
||||
await documentStore.getDocumentMetaList();
|
||||
} catch (error) {
|
||||
console.error('Failed to update document:', error);
|
||||
} finally {
|
||||
@@ -191,7 +191,7 @@ const handleDelete = async (doc: Document, event: Event) => {
|
||||
|
||||
const deleteSuccess = await documentStore.deleteDocument(doc.id);
|
||||
if (deleteSuccess) {
|
||||
await documentStore.updateDocuments();
|
||||
await documentStore.getDocumentMetaList();
|
||||
// 如果删除的是当前文档,切换到第一个文档
|
||||
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
||||
const firstDoc = documentStore.documentList[0];
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
|
||||
import {computed, onMounted, onUnmounted, ref, watch, shallowRef, readonly, toRefs, effectScope, onScopeDispose} from 'vue';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import {useWindowStore} from '@/stores/windowStore';
|
||||
import {useSystemStore} from '@/stores/systemStore';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import {useRouter} from 'vue-router';
|
||||
import BlockLanguageSelector from './BlockLanguageSelector.vue';
|
||||
import DocumentSelector from './DocumentSelector.vue';
|
||||
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
|
||||
import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages';
|
||||
import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode';
|
||||
import {createDebounce} from '@/common/utils/debounce';
|
||||
|
||||
const editorStore = useEditorStore();
|
||||
const configStore = useConfigStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const windowStore = useWindowStore();
|
||||
const systemStore = useSystemStore();
|
||||
const editorStore = readonly(useEditorStore());
|
||||
const configStore = readonly(useConfigStore());
|
||||
const updateStore = readonly(useUpdateStore());
|
||||
const windowStore = readonly(useWindowStore());
|
||||
const systemStore = readonly(useSystemStore());
|
||||
const {t} = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
// 当前块是否支持格式化的响应式状态
|
||||
const canFormatCurrentBlock = ref(false);
|
||||
const isLoaded = shallowRef(false);
|
||||
|
||||
// 窗口置顶状态 - 合并配置和临时状态
|
||||
const { documentStats } = toRefs(editorStore);
|
||||
const { config } = toRefs(configStore);
|
||||
|
||||
// 窗口置顶状态
|
||||
const isCurrentWindowOnTop = computed(() => {
|
||||
return configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
|
||||
return config.value.general.alwaysOnTop || systemStore.isWindowOnTop;
|
||||
});
|
||||
|
||||
// 切换窗口置顶状态
|
||||
const toggleAlwaysOnTop = async () => {
|
||||
const currentlyOnTop = isCurrentWindowOnTop.value;
|
||||
|
||||
|
||||
if (currentlyOnTop) {
|
||||
// 如果当前是置顶状态,彻底关闭所有置顶
|
||||
if (configStore.config.general.alwaysOnTop) {
|
||||
if (config.value.general.alwaysOnTop) {
|
||||
await configStore.setAlwaysOnTop(false);
|
||||
}
|
||||
await systemStore.setWindowOnTop(false);
|
||||
@@ -57,9 +60,8 @@ const formatCurrentBlock = () => {
|
||||
formatBlockContent(editorStore.editorView);
|
||||
};
|
||||
|
||||
// 格式化按钮状态更新
|
||||
// 格式化按钮状态更新 - 使用更高效的检查逻辑
|
||||
const updateFormatButtonState = () => {
|
||||
// 安全检查
|
||||
const view = editorStore.editorView;
|
||||
if (!view) {
|
||||
canFormatCurrentBlock.value = false;
|
||||
@@ -67,156 +69,158 @@ const updateFormatButtonState = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取活动块和语言信息
|
||||
const state = view.state;
|
||||
const activeBlock = getActiveNoteBlock(state as any);
|
||||
|
||||
// 提前返回,减少不必要的计算
|
||||
if (!activeBlock) {
|
||||
canFormatCurrentBlock.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查块和语言格式化支持
|
||||
canFormatCurrentBlock.value = !!(
|
||||
activeBlock &&
|
||||
getLanguage(activeBlock.language.name as any)?.prettier
|
||||
);
|
||||
const language = getLanguage(activeBlock.language.name as any);
|
||||
canFormatCurrentBlock.value = Boolean(language?.prettier);
|
||||
} catch (error) {
|
||||
console.warn('Error checking format capability:', error);
|
||||
canFormatCurrentBlock.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建带300ms防抖的更新函数
|
||||
const debouncedUpdateFormatButton = (() => {
|
||||
let timeout: number | null = null;
|
||||
// 创建带1s防抖的更新函数
|
||||
const { debouncedFn: debouncedUpdateFormat, cancel: cancelDebounce } = createDebounce(
|
||||
updateFormatButtonState,
|
||||
{ delay: 1000 }
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = window.setTimeout(() => {
|
||||
updateFormatButtonState();
|
||||
timeout = null;
|
||||
}, 1000);
|
||||
};
|
||||
})();
|
||||
// 使用 effectScope 管理编辑器事件监听器
|
||||
const editorScope = effectScope();
|
||||
let cleanupListeners: (() => void)[] = [];
|
||||
|
||||
// 编辑器事件管理
|
||||
// 优化的事件监听器管理
|
||||
const setupEditorListeners = (view: any) => {
|
||||
if (!view?.dom) return [];
|
||||
|
||||
const events = [
|
||||
{type: 'click', handler: updateFormatButtonState},
|
||||
{type: 'keyup', handler: debouncedUpdateFormatButton},
|
||||
{type: 'focus', handler: updateFormatButtonState}
|
||||
];
|
||||
// 使用对象缓存事件处理器,避免重复创建
|
||||
const eventHandlers = {
|
||||
click: updateFormatButtonState,
|
||||
keyup: debouncedUpdateFormat,
|
||||
focus: updateFormatButtonState
|
||||
} as const;
|
||||
|
||||
// 注册所有事件
|
||||
events.forEach(event => view.dom.addEventListener(event.type, event.handler));
|
||||
const events = Object.entries(eventHandlers).map(([type, handler]) => ({
|
||||
type,
|
||||
handler,
|
||||
cleanup: () => view.dom.removeEventListener(type, handler)
|
||||
}));
|
||||
|
||||
// 返回清理函数数组
|
||||
return events.map(event =>
|
||||
() => view.dom.removeEventListener(event.type, event.handler)
|
||||
);
|
||||
// 批量注册事件
|
||||
events.forEach(event => view.dom.addEventListener(event.type, event.handler, { passive: true }));
|
||||
|
||||
return events.map(event => event.cleanup);
|
||||
};
|
||||
|
||||
// 监听编辑器视图变化
|
||||
let cleanupListeners: (() => void)[] = [];
|
||||
|
||||
watch(
|
||||
() => editorStore.editorView,
|
||||
(newView) => {
|
||||
// 清理旧监听器
|
||||
cleanupListeners.forEach(cleanup => cleanup());
|
||||
cleanupListeners = [];
|
||||
// 在 scope 中管理副作用
|
||||
editorScope.run(() => {
|
||||
// 清理旧监听器
|
||||
cleanupListeners.forEach(cleanup => cleanup());
|
||||
cleanupListeners = [];
|
||||
|
||||
if (newView) {
|
||||
// 初始更新状态
|
||||
updateFormatButtonState();
|
||||
// 设置新监听器
|
||||
cleanupListeners = setupEditorListeners(newView);
|
||||
} else {
|
||||
canFormatCurrentBlock.value = false;
|
||||
}
|
||||
if (newView) {
|
||||
// 初始更新状态
|
||||
updateFormatButtonState();
|
||||
// 设置新监听器
|
||||
cleanupListeners = setupEditorListeners(newView);
|
||||
} else {
|
||||
canFormatCurrentBlock.value = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
{immediate: true}
|
||||
{ immediate: true, flush: 'post' }
|
||||
);
|
||||
|
||||
// 组件生命周期
|
||||
const isLoaded = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
isLoaded.value = true;
|
||||
// 首次更新格式化状态
|
||||
updateFormatButtonState();
|
||||
await systemStore.setWindowOnTop(isCurrentWindowOnTop.value);
|
||||
});
|
||||
|
||||
// 使用 onScopeDispose 确保 scope 清理
|
||||
onScopeDispose(() => {
|
||||
cleanupListeners.forEach(cleanup => cleanup());
|
||||
cleanupListeners = [];
|
||||
cancelDebounce();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理所有事件监听器
|
||||
cleanupListeners.forEach(cleanup => cleanup());
|
||||
cleanupListeners = [];
|
||||
// 停止 effect scope
|
||||
editorScope.stop();
|
||||
// 清理防抖函数
|
||||
cancelDebounce();
|
||||
});
|
||||
|
||||
// 组件加载后初始化置顶状态
|
||||
watch(isLoaded, async (loaded) => {
|
||||
if (loaded) {
|
||||
// 应用合并后的置顶状态
|
||||
const shouldBeOnTop = configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
|
||||
try {
|
||||
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
|
||||
} catch (error) {
|
||||
console.error('Failed to apply window pin state:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听配置变化,同步窗口状态
|
||||
watch(
|
||||
() => isCurrentWindowOnTop.value,
|
||||
async (shouldBeOnTop) => {
|
||||
try {
|
||||
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
|
||||
} catch (error) {
|
||||
console.error('Failed to sync window pin state:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 更新按钮处理
|
||||
const handleUpdateButtonClick = async () => {
|
||||
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
|
||||
// 开始下载更新
|
||||
const { hasUpdate, isUpdating, updateSuccess } = updateStore;
|
||||
|
||||
if (hasUpdate && !isUpdating && !updateSuccess) {
|
||||
await updateStore.applyUpdate();
|
||||
} else if (updateStore.updateSuccess) {
|
||||
// 更新成功后,点击重启
|
||||
} else if (updateSuccess) {
|
||||
await updateStore.restartApplication();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新按钮标题计算属性
|
||||
const updateButtonTitle = computed(() => {
|
||||
if (updateStore.isChecking) return t('settings.checking');
|
||||
if (updateStore.isUpdating) return t('settings.updating');
|
||||
if (updateStore.updateSuccess) return t('settings.updateSuccessRestartRequired');
|
||||
if (updateStore.hasUpdate) return `${t('settings.newVersionAvailable')}: ${updateStore.updateResult?.latestVersion || ''}`;
|
||||
const { isChecking, isUpdating, updateSuccess, hasUpdate, updateResult } = updateStore;
|
||||
|
||||
if (isChecking) return t('settings.checking');
|
||||
if (isUpdating) return t('settings.updating');
|
||||
if (updateSuccess) return t('settings.updateSuccessRestartRequired');
|
||||
if (hasUpdate) return `${t('settings.newVersionAvailable')}: ${updateResult?.latestVersion || ''}`;
|
||||
return '';
|
||||
});
|
||||
|
||||
// 统计数据的计算属性
|
||||
const statsData = computed(() => ({
|
||||
lines: documentStats.value.lines,
|
||||
characters: documentStats.value.characters,
|
||||
selectedCharacters: documentStats.value.selectedCharacters
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar-container">
|
||||
<div class="statistics">
|
||||
<span class="stat-item" :title="t('toolbar.editor.lines')">{{ t('toolbar.editor.lines') }}: <span
|
||||
class="stat-value">{{
|
||||
editorStore.documentStats.lines
|
||||
}}</span></span>
|
||||
<span class="stat-item" :title="t('toolbar.editor.characters')">{{ t('toolbar.editor.characters') }}: <span
|
||||
class="stat-value">{{
|
||||
editorStore.documentStats.characters
|
||||
}}</span></span>
|
||||
<span class="stat-item" :title="t('toolbar.editor.selected')"
|
||||
v-if="editorStore.documentStats.selectedCharacters > 0">
|
||||
{{ t('toolbar.editor.selected') }}: <span class="stat-value">{{
|
||||
editorStore.documentStats.selectedCharacters
|
||||
}}</span>
|
||||
<span class="stat-item" :title="t('toolbar.editor.lines')">
|
||||
{{ t('toolbar.editor.lines') }}:
|
||||
<span class="stat-value">{{ statsData.lines }}</span>
|
||||
</span>
|
||||
<span class="stat-item" :title="t('toolbar.editor.characters')">
|
||||
{{ t('toolbar.editor.characters') }}:
|
||||
<span class="stat-value">{{ statsData.characters }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="statsData.selectedCharacters > 0"
|
||||
class="stat-item"
|
||||
:title="t('toolbar.editor.selected')"
|
||||
>
|
||||
{{ t('toolbar.editor.selected') }}:
|
||||
<span class="stat-value">{{ statsData.selectedCharacters }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span class="font-size" :title="t('toolbar.fontSizeTooltip')" @click="() => configStore.resetFontSize()">
|
||||
{{ configStore.config.editing.fontSize }}px
|
||||
<span
|
||||
class="font-size"
|
||||
:title="t('toolbar.fontSizeTooltip')"
|
||||
@click="configStore.resetFontSize"
|
||||
>
|
||||
{{ config.editing.fontSize }}px
|
||||
</span>
|
||||
|
||||
<!-- 文档选择器 -->
|
||||
@@ -302,7 +306,6 @@ const updateButtonTitle = computed(() => {
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
<button v-if="windowStore.isMainWindow" class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
Reference in New Issue
Block a user