♻️ Refactor code

This commit is contained in:
2025-06-02 13:34:54 +08:00
parent 44f7baad10
commit a516b8973e
53 changed files with 1513 additions and 1094 deletions

View File

@@ -0,0 +1,242 @@
<script setup lang="ts">
import { useConfigStore } from '@/stores/configStore';
import { useI18n } from 'vue-i18n';
import { ref, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import MemoryMonitor from '@/components/monitor/MemoryMonitor.vue';
const { t } = useI18n();
const configStore = useConfigStore();
const router = useRouter();
const route = useRoute();
// 导航配置
const navItems = [
{ id: 'general', icon: '⚙️', route: '/settings/general' },
{ id: 'editing', icon: '✏️', route: '/settings/editing' },
{ id: 'appearance', icon: '🎨', route: '/settings/appearance' },
{ id: 'keyBindings', icon: '⌨️', route: '/settings/key-bindings' },
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
];
const activeNavItem = ref(route.path.split('/').pop() || 'general');
// 处理导航点击
const handleNavClick = (item: typeof navItems[0]) => {
activeNavItem.value = item.id;
router.push(item.route);
};
// 返回编辑器
const goBackToEditor = () => {
router.push('/');
};
</script>
<template>
<div class="settings-container">
<div class="settings-sidebar">
<div class="settings-header">
<div class="header-content">
<button class="back-button" @click="goBackToEditor" :title="t('settings.backToEditor')">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
</button>
<h1>{{ t('settings.title') }}</h1>
</div>
</div>
<div class="settings-nav">
<div
v-for="item in navItems"
:key="item.id"
class="nav-item"
:class="{ active: activeNavItem === item.id }"
@click="handleNavClick(item)"
>
<span class="nav-icon">{{ item.icon }}</span>
<span class="nav-text">{{ t(`settings.${item.id}`) }}</span>
</div>
</div>
<div class="settings-footer">
<div class="memory-info-section">
<div class="section-title">{{ t('settings.systemInfo') }}</div>
<MemoryMonitor />
</div>
</div>
</div>
<div class="settings-content">
<router-view />
</div>
</div>
</template>
<style scoped lang="scss">
.settings-container {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
background-color: #2a2a2a;
color: #e0e0e0;
display: flex;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
.settings-sidebar {
width: 200px;
height: 100%;
background-color: #333333;
border-right: 1px solid #444444;
display: flex;
flex-direction: column;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
.settings-header {
padding: 20px 16px;
border-bottom: 1px solid #444444;
background-color: #2d2d2d;
.header-content {
display: flex;
align-items: center;
gap: 12px;
.back-button {
background: none;
border: none;
color: #a0a0a0;
cursor: pointer;
padding: 6px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:hover {
color: #e0e0e0;
background-color: #3a3a3a;
}
svg {
width: 18px;
height: 18px;
}
}
h1 {
font-size: 18px;
font-weight: 600;
margin: 0;
color: #ffffff;
}
}
}
.settings-nav {
flex: 1;
padding: 10px 0;
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: #333333;
}
&::-webkit-scrollbar-thumb {
background-color: #555555;
border-radius: 4px;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent;
&:hover {
background-color: #3a3a3a;
}
&.active {
background-color: #3c3c3c;
border-left-color: #4a9eff;
font-weight: 500;
}
.nav-icon {
margin-right: 10px;
font-size: 16px;
opacity: 0.9;
}
.nav-text {
font-size: 14px;
}
}
}
.settings-footer {
padding: 12px 16px 16px 16px;
border-top: 1px solid #444444;
background-color: #2d2d2d;
.memory-info-section {
display: flex;
flex-direction: column;
gap: 8px;
.section-title {
font-size: 10px;
color: #777777;
font-weight: 500;
margin-bottom: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
}
}
.settings-content {
flex: 1;
height: 100%;
padding: 24px;
overflow-y: auto;
background-color: #2a2a2a;
&::-webkit-scrollbar {
width: 10px;
}
&::-webkit-scrollbar-track {
background: #2a2a2a;
}
&::-webkit-scrollbar-thumb {
background-color: #555555;
border-radius: 5px;
border: 2px solid #2a2a2a;
}
}
}
// 自定义变量
:root {
--border-color: #444444;
--hover-color: #3a3a3a;
--active-bg: #3c3c3c;
--accent-color: #4a9eff;
--bg-primary: #2a2a2a;
--bg-secondary: #333333;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--text-muted: #777777;
}
</style>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
defineProps<{
title: string;
description?: string;
}>();
</script>
<template>
<div class="setting-item">
<div class="setting-info">
<div class="setting-title">{{ title }}</div>
<div v-if="description" class="setting-description">{{ description }}</div>
</div>
<div class="setting-control">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="scss">
.setting-item {
display: flex;
padding: 14px 0;
border-bottom: 1px solid #444444;
transition: background-color 0.15s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.03);
}
&:last-child {
border-bottom: none;
}
.setting-info {
flex: 1;
padding-right: 20px;
.setting-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: #e0e0e0;
}
.setting-description {
font-size: 12px;
color: #a0a0a0;
line-height: 1.5;
}
}
.setting-control {
width: 200px;
display: flex;
align-items: center;
justify-content: flex-end;
}
}
</style>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
defineProps<{
title: string;
}>();
</script>
<template>
<div class="setting-section">
<h2 class="section-title">{{ title }}</h2>
<div class="section-content">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="scss">
.setting-section {
margin-bottom: 30px;
background-color: #333333;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
.section-title {
font-size: 14px;
font-weight: 600;
margin: 0;
padding: 12px 16px;
background-color: #3d3d3d;
color: #ffffff;
border-bottom: 1px solid #444444;
}
.section-content {
padding: 8px 16px;
}
}
</style>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>();
const toggle = () => {
emit('update:modelValue', !props.modelValue);
};
</script>
<template>
<div class="toggle-switch" :class="{ active: modelValue }" @click="toggle">
<div class="toggle-handle"></div>
</div>
</template>
<style scoped lang="scss">
.toggle-switch {
width: 40px;
height: 20px;
background-color: #555555;
border-radius: 10px;
position: relative;
cursor: pointer;
transition: background-color 0.2s;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
&.active {
background-color: #4a9eff;
.toggle-handle {
transform: translateX(20px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
}
.toggle-handle {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background-color: #f0f0f0;
border-radius: 50%;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
}
</style>

View File

@@ -0,0 +1,235 @@
<script setup lang="ts">
import { useConfigStore } from '@/stores/configStore';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import { LanguageType } from '../../../../bindings/voidraft/internal/models/models';
const { t } = useI18n();
const configStore = useConfigStore();
// 语言选项
const languageOptions = [
{ value: LanguageType.LangZhCN, label: t('languages.zh-CN') },
{ value: LanguageType.LangEnUS, label: t('languages.en-US') },
];
// 更新语言设置
const updateLanguage = async (event: Event) => {
const select = event.target as HTMLSelectElement;
const selectedLanguage = select.value as LanguageType;
try {
// 使用 configStore 的语言设置方法
await configStore.setLanguage(selectedLanguage);
} catch (error) {
console.error('Failed to update language:', error);
}
};
// 主题选择(未实际实现,仅界面展示)
const themeOptions = [
{ id: 'dark', name: '深色', color: '#2a2a2a' },
{ id: 'darker', name: '暗黑', color: '#1a1a1a' },
{ id: 'light', name: '浅色', color: '#f5f5f5' },
{ id: 'blue', name: '蓝调', color: '#1e3a5f' },
];
const selectedTheme = ref('dark');
const selectTheme = (themeId: string) => {
selectedTheme.value = themeId;
};
</script>
<template>
<div class="settings-page">
<SettingSection :title="t('settings.language')">
<SettingItem :title="t('settings.language')" :description="t('settings.restartRequired')">
<select class="select-input" :value="configStore.config.language" @change="updateLanguage">
<option v-for="option in languageOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</SettingItem>
</SettingSection>
<SettingSection :title="t('settings.appearance')">
<div class="theme-selector">
<div class="selector-label">主题</div>
<div class="theme-options">
<div
v-for="theme in themeOptions"
:key="theme.id"
class="theme-option"
:class="{ active: selectedTheme === theme.id }"
@click="selectTheme(theme.id)"
>
<div class="color-preview" :style="{ backgroundColor: theme.color }"></div>
<div class="theme-name">{{ theme.name }}</div>
</div>
</div>
</div>
<div class="editor-preview">
<div class="preview-header">
<div class="preview-title">预览</div>
</div>
<div class="preview-content">
<div class="preview-line"><span class="line-number">1</span><span class="keyword">function</span> <span class="function">example</span>() {</div>
<div class="preview-line"><span class="line-number">2</span> <span class="keyword">const</span> greeting = <span class="string">"Hello, World!"</span>;</div>
<div class="preview-line"><span class="line-number">3</span> <span class="function">console.log</span>(greeting);</div>
<div class="preview-line"><span class="line-number">4</span>}</div>
</div>
</div>
</SettingSection>
</div>
</template>
<style scoped lang="scss">
.settings-page {
max-width: 800px;
}
.select-input {
min-width: 150px;
padding: 8px 12px;
border: 1px solid #555555;
border-radius: 4px;
background-color: #3a3a3a;
color: #e0e0e0;
font-size: 13px;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
padding-right: 30px;
&:focus {
outline: none;
border-color: #4a9eff;
}
option {
background-color: #2a2a2a;
}
}
.theme-selector {
padding: 15px 16px;
.selector-label {
font-size: 14px;
font-weight: 500;
margin-bottom: 15px;
color: #e0e0e0;
}
.theme-options {
display: flex;
gap: 15px;
flex-wrap: wrap;
.theme-option {
width: 100px;
cursor: pointer;
transition: all 0.2s ease;
.color-preview {
height: 60px;
border-radius: 4px;
border: 2px solid transparent;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.theme-name {
margin-top: 6px;
font-size: 13px;
text-align: center;
color: #c0c0c0;
}
&:hover .color-preview {
border-color: rgba(255, 255, 255, 0.3);
}
&.active .color-preview {
border-color: #4a9eff;
}
&.active .theme-name {
color: #ffffff;
}
}
}
}
.editor-preview {
margin: 20px 16px;
background-color: #252525;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
.preview-header {
padding: 10px 16px;
background-color: #353535;
border-bottom: 1px solid #444444;
.preview-title {
font-size: 13px;
color: #b0b0b0;
}
}
.preview-content {
padding: 12px 0;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 14px;
.preview-line {
padding: 3px 16px;
line-height: 1.5;
white-space: pre;
&:hover {
background-color: rgba(255, 255, 255, 0.03);
}
.line-number {
color: #707070;
display: inline-block;
width: 25px;
margin-right: 15px;
text-align: right;
user-select: none;
}
.keyword {
color: #569cd6;
}
.function {
color: #dcdcaa;
}
.string {
color: #ce9178;
}
}
}
}
.coming-soon-placeholder {
padding: 20px;
background-color: #333333;
border-radius: 6px;
color: #a0a0a0;
text-align: center;
font-style: italic;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,451 @@
<script setup lang="ts">
import { useConfigStore } from '@/stores/configStore';
import { FONT_OPTIONS } from '@/stores/configStore';
import { useI18n } from 'vue-i18n';
import { ref, computed, onMounted } from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
import { TabType } from '../../../../bindings/voidraft/internal/models/models';
const { t } = useI18n();
const configStore = useConfigStore();
// 确保配置已加载
onMounted(async () => {
if (!configStore.configLoaded) {
await configStore.loadConfig();
}
});
// 字体选择选项
const fontFamilyOptions = FONT_OPTIONS;
const currentFontFamily = computed(() => configStore.config.fontFamily);
// 字体选择
const handleFontFamilyChange = async (event: Event) => {
const target = event.target as HTMLSelectElement;
const fontFamily = target.value;
if (fontFamily) {
try {
await configStore.setFontFamily(fontFamily);
} catch (error) {
console.error('Failed to set font family:', error);
}
}
};
// 字体粗细选项
const fontWeightOptions = [
{ value: '100', label: '极细 (100)' },
{ value: '200', label: '超细 (200)' },
{ value: '300', label: '细 (300)' },
{ value: 'normal', label: '正常 (400)' },
{ value: '500', label: '中等 (500)' },
{ value: '600', label: '半粗 (600)' },
{ value: 'bold', label: '粗体 (700)' },
{ value: '800', label: '超粗 (800)' },
{ value: '900', label: '极粗 (900)' }
];
// 字体粗细选择
const handleFontWeightChange = async (event: Event) => {
const target = event.target as HTMLSelectElement;
try {
await configStore.setFontWeight(target.value);
} catch (error) {
console.error('Failed to set font weight:', error);
}
};
// 行高控制
const increaseLineHeight = async () => {
const newLineHeight = Math.min(3.0, configStore.config.lineHeight + 0.1);
try {
await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10);
} catch (error) {
console.error('Failed to increase line height:', error);
}
};
const decreaseLineHeight = async () => {
const newLineHeight = Math.max(1.0, configStore.config.lineHeight - 0.1);
try {
await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10);
} catch (error) {
console.error('Failed to decrease line height:', error);
}
};
// 字体大小控制
const increaseFontSize = async () => {
try {
await configStore.increaseFontSize();
} catch (error) {
console.error('Failed to increase font size:', error);
}
};
const decreaseFontSize = async () => {
try {
await configStore.decreaseFontSize();
} catch (error) {
console.error('Failed to decrease font size:', error);
}
};
// Tab类型切换
const tabTypeText = computed(() => {
return configStore.config.tabType === TabType.TabTypeSpaces
? t('settings.spaces')
: t('settings.tabs');
});
// Tab大小增减
const increaseTabSize = async () => {
try {
await configStore.increaseTabSize();
} catch (error) {
console.error('Failed to increase tab size:', error);
}
};
const decreaseTabSize = async () => {
try {
await configStore.decreaseTabSize();
} catch (error) {
console.error('Failed to decrease tab size:', error);
}
};
// Tab相关操作
const handleToggleTabIndent = async () => {
try {
await configStore.toggleTabIndent();
} catch (error) {
console.error('Failed to toggle tab indent:', error);
}
};
const handleToggleTabType = async () => {
try {
await configStore.toggleTabType();
} catch (error) {
console.error('Failed to toggle tab type:', error);
}
};
</script>
<template>
<div class="settings-page">
<SettingSection :title="t('settings.fontSettings')">
<SettingItem
:title="t('settings.fontFamily')"
:description="t('settings.fontFamilyDescription')"
>
<select
class="font-family-select"
:value="currentFontFamily"
@change="handleFontFamilyChange"
>
<option
v-for="option in fontFamilyOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</SettingItem>
<SettingItem
:title="t('settings.fontSize')"
:description="t('settings.fontSizeDescription')"
>
<div class="number-control">
<button @click="decreaseFontSize" class="control-button">-</button>
<span>{{ configStore.config.fontSize }}px</span>
<button @click="increaseFontSize" class="control-button">+</button>
</div>
</SettingItem>
<SettingItem
:title="t('settings.fontWeight')"
:description="t('settings.fontWeightDescription')"
>
<select
class="font-weight-select"
:value="configStore.config.fontWeight"
@change="handleFontWeightChange"
>
<option
v-for="option in fontWeightOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</SettingItem>
<SettingItem
:title="t('settings.lineHeight')"
:description="t('settings.lineHeightDescription')"
>
<div class="number-control">
<button @click="decreaseLineHeight" class="control-button">-</button>
<span>{{ configStore.config.lineHeight.toFixed(1) }}</span>
<button @click="increaseLineHeight" class="control-button">+</button>
</div>
</SettingItem>
<div class="font-preview" :style="{
fontSize: `${configStore.config.fontSize}px`,
fontFamily: configStore.config.fontFamily,
fontWeight: configStore.config.fontWeight,
lineHeight: configStore.config.lineHeight
}">
<div class="preview-label">字体预览</div>
<div class="preview-text">
<span>function example() {</span>
<span class="indent">console.log("Hello, 世界!");</span>
<span class="indent">const message = "鸿蒙字体测试";</span>
<span>}</span>
</div>
</div>
</SettingSection>
<SettingSection :title="t('settings.tabSettings')">
<SettingItem :title="t('settings.tabSize')">
<div class="number-control">
<button @click="decreaseTabSize" class="control-button" :disabled="configStore.config.tabSize <= configStore.tabSize.min">-</button>
<span>{{ configStore.config.tabSize }}</span>
<button @click="increaseTabSize" class="control-button" :disabled="configStore.config.tabSize >= configStore.tabSize.max">+</button>
</div>
</SettingItem>
<SettingItem :title="t('settings.tabType')">
<button class="tab-type-toggle" @click="handleToggleTabType">
{{ tabTypeText }}
</button>
</SettingItem>
<SettingItem :title="t('settings.enableTabIndent')">
<ToggleSwitch
v-model="configStore.config.enableTabIndent"
@update:modelValue="handleToggleTabIndent"
/>
</SettingItem>
</SettingSection>
<SettingSection :title="t('settings.saveOptions')">
<SettingItem :title="t('settings.autoSaveDelay')" :description="'单位:毫秒'">
<input
type="number"
class="number-input"
disabled
:value="5000"
/>
</SettingItem>
<SettingItem :title="t('settings.changeThreshold')" :description="'变更字符超过此阈值时触发保存'">
<input
type="number"
class="number-input"
disabled
:value="500"
/>
</SettingItem>
<SettingItem :title="t('settings.minSaveInterval')" :description="'单位:毫秒'">
<input
type="number"
class="number-input"
disabled
:value="1000"
/>
</SettingItem>
</SettingSection>
</div>
</template>
<style scoped lang="scss">
.settings-page {
max-width: 800px;
}
.number-control {
display: flex;
align-items: center;
gap: 10px;
.control-button {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease;
&:hover:not(:disabled) {
background-color: #444444;
border-color: #666666;
}
&:active:not(:disabled) {
transform: translateY(1px);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
span {
min-width: 50px;
text-align: center;
font-size: 14px;
color: #e0e0e0;
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 4px;
padding: 5px 8px;
}
}
.font-size-preview {
margin: 15px 0 5px 20px;
padding: 15px;
background-color: #252525;
border: 1px solid #444444;
border-radius: 4px;
font-family: 'Consolas', 'Courier New', monospace;
.preview-label {
font-size: 12px;
color: #888888;
margin-bottom: 8px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.preview-text {
display: flex;
flex-direction: column;
span {
line-height: 1.4;
color: #d0d0d0;
}
.indent {
padding-left: 20px;
color: #4a9eff;
}
}
}
.font-preview {
margin: 15px 0 5px 20px;
padding: 15px;
background-color: #252525;
border: 1px solid #444444;
border-radius: 4px;
.preview-label {
font-size: 12px;
color: #888888;
margin-bottom: 8px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.preview-text {
display: flex;
flex-direction: column;
span {
color: #d0d0d0;
}
.indent {
padding-left: 20px;
color: #4a9eff;
}
}
}
.font-family-select,
.font-weight-select {
min-width: 180px;
padding: 8px 12px;
border: 1px solid #555555;
border-radius: 4px;
background-color: #3a3a3a;
color: #e0e0e0;
font-size: 13px;
cursor: pointer;
&:focus {
outline: none;
border-color: #4a9eff;
}
&:hover {
border-color: #666666;
}
option {
background-color: #3a3a3a;
color: #e0e0e0;
padding: 4px 8px;
}
}
.tab-type-toggle {
min-width: 100px;
padding: 8px 15px;
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 13px;
text-align: center;
transition: all 0.2s ease;
&:hover {
background-color: #444444;
border-color: #666666;
}
&:active {
transform: translateY(1px);
}
}
.number-input {
width: 100px;
padding: 8px 12px;
border: 1px solid #555555;
border-radius: 4px;
background-color: #3a3a3a;
color: #a0a0a0;
font-size: 13px;
&:focus {
outline: none;
border-color: #4a9eff;
}
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
}
</style>

View File

@@ -0,0 +1,289 @@
<script setup lang="ts">
import { useConfigStore } from '@/stores/configStore';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
const { t } = useI18n();
const configStore = useConfigStore();
// 选择的键盘修饰键
const selectedModifiers = ref({
ctrl: false,
shift: false,
alt: true,
altgr: false,
win: false
});
// 选择的键
const selectedKey = ref('X');
// 可选键列表
const keyOptions = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'
];
// 更新选择的键
const updateSelectedKey = (event: Event) => {
const select = event.target as HTMLSelectElement;
selectedKey.value = select.value;
};
// 切换修饰键
const toggleModifier = (key: keyof typeof selectedModifiers.value) => {
selectedModifiers.value[key] = !selectedModifiers.value[key];
};
// 重置设置
const resetSettings = async () => {
if (confirm(t('settings.confirmReset'))) {
await configStore.resetConfig();
}
};
</script>
<template>
<div class="settings-page">
<SettingSection :title="t('settings.globalHotkey')">
<SettingItem :title="t('settings.enableGlobalHotkey')">
<ToggleSwitch v-model="configStore.config.alwaysOnTop" /> <!-- 此处使用alwaysOnTop作为示例 -->
</SettingItem>
<div class="hotkey-selector">
<div class="hotkey-modifiers">
<label class="modifier-label" :class="{ active: selectedModifiers.ctrl }">
<input type="checkbox" v-model="selectedModifiers.ctrl" class="hidden-checkbox">
<span class="modifier-key">Ctrl</span>
</label>
<label class="modifier-label" :class="{ active: selectedModifiers.shift }">
<input type="checkbox" v-model="selectedModifiers.shift" class="hidden-checkbox">
<span class="modifier-key">Shift</span>
</label>
<label class="modifier-label" :class="{ active: selectedModifiers.alt }">
<input type="checkbox" v-model="selectedModifiers.alt" class="hidden-checkbox">
<span class="modifier-key">Alt</span>
</label>
<label class="modifier-label" :class="{ active: selectedModifiers.altgr }">
<input type="checkbox" v-model="selectedModifiers.altgr" class="hidden-checkbox">
<span class="modifier-key">AltGr</span>
</label>
<label class="modifier-label" :class="{ active: selectedModifiers.win }">
<input type="checkbox" v-model="selectedModifiers.win" class="hidden-checkbox">
<span class="modifier-key">Win</span>
</label>
</div>
<select class="key-select" v-model="selectedKey">
<option v-for="key in keyOptions" :key="key" :value="key">{{ key }}</option>
</select>
</div>
</SettingSection>
<SettingSection :title="t('settings.window')">
<SettingItem :title="t('settings.showInSystemTray')">
<ToggleSwitch v-model="configStore.config.alwaysOnTop" /> <!-- 需要后端实现 -->
</SettingItem>
<SettingItem :title="t('settings.alwaysOnTop')">
<ToggleSwitch v-model="configStore.config.alwaysOnTop" @update:modelValue="configStore.toggleAlwaysOnTop" />
</SettingItem>
</SettingSection>
<SettingSection :title="t('settings.bufferFiles')">
<SettingItem :title="t('settings.useCustomLocation')">
<ToggleSwitch v-model="configStore.config.alwaysOnTop" /> <!-- 需要后端实现 -->
</SettingItem>
<div class="directory-selector">
<div class="path-display">{{ configStore.config.alwaysOnTop ? 'C:/Custom/Path' : 'Default Location' }}</div>
<button class="select-button">{{ t('settings.selectDirectory') }}</button>
</div>
</SettingSection>
<SettingSection :title="t('settings.dangerZone')">
<div class="danger-zone">
<div class="reset-section">
<div class="reset-info">
<h4>{{ t('settings.resetAllSettings') }}</h4>
<p>{{ t('settings.resetDescription') }}</p>
</div>
<button class="reset-button" @click="resetSettings">
{{ t('settings.reset') }}
</button>
</div>
</div>
</SettingSection>
</div>
</template>
<style scoped lang="scss">
.settings-page {
max-width: 800px;
}
.hotkey-selector {
padding: 15px 0 5px 20px;
.hotkey-modifiers {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
.modifier-label {
cursor: pointer;
.hidden-checkbox {
display: none;
}
.modifier-key {
display: inline-block;
padding: 6px 12px;
background-color: #444444;
border: 1px solid #555555;
border-radius: 4px;
color: #b0b0b0;
font-size: 13px;
transition: all 0.2s ease;
&:hover {
background-color: #4a4a4a;
}
}
&.active .modifier-key {
background-color: #2c5a9e;
color: #ffffff;
border-color: #3a6db1;
}
}
}
.key-select {
min-width: 80px;
padding: 8px 12px;
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 4px;
color: #e0e0e0;
font-size: 13px;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
padding-right: 30px;
&:focus {
outline: none;
border-color: #4a9eff;
}
option {
background-color: #2a2a2a;
}
}
}
.directory-selector {
margin-top: 10px;
padding-left: 20px;
display: flex;
align-items: center;
gap: 10px;
.path-display {
flex: 1;
padding: 8px 12px;
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 4px;
color: #b0b0b0;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.select-button {
padding: 8px 12px;
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
white-space: nowrap;
&:hover {
background-color: #444444;
border-color: #666666;
}
&:active {
transform: translateY(1px);
}
}
}
.danger-zone {
padding: 20px;
background-color: rgba(220, 53, 69, 0.05);
border: 1px solid rgba(220, 53, 69, 0.2);
border-radius: 6px;
margin-top: 10px;
.reset-section {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
.reset-info {
flex: 1;
h4 {
margin: 0 0 6px 0;
color: #ff6b6b;
font-size: 14px;
font-weight: 600;
}
p {
margin: 0;
color: #b0b0b0;
font-size: 13px;
line-height: 1.4;
}
}
.reset-button {
padding: 8px 16px;
background-color: #dc3545;
border: 1px solid #c82333;
border-radius: 4px;
color: #ffffff;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
white-space: nowrap;
&:hover {
background-color: #c82333;
border-color: #bd2130;
}
&:active {
transform: translateY(1px);
}
}
}
}
</style>

View File

@@ -0,0 +1,219 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import SettingSection from '../components/SettingSection.vue';
const { t } = useI18n();
interface KeyBinding {
id: string;
name: string;
keys: string[];
isEditing: boolean;
}
// 示例快捷键列表(仅用于界面展示)
const keyBindings = ref<KeyBinding[]>([
{ id: 'save', name: '保存文档', keys: ['Ctrl', 'S'], isEditing: false },
{ id: 'new', name: '新建文档', keys: ['Ctrl', 'N'], isEditing: false },
{ id: 'open', name: '打开文档', keys: ['Ctrl', 'O'], isEditing: false },
{ id: 'find', name: '查找', keys: ['Ctrl', 'F'], isEditing: false },
{ id: 'replace', name: '替换', keys: ['Ctrl', 'H'], isEditing: false },
]);
// 切换编辑状态
const toggleEdit = (binding: KeyBinding) => {
// 先关闭其他所有编辑中的项
keyBindings.value.forEach(item => {
if (item.id !== binding.id) {
item.isEditing = false;
}
});
// 切换当前项
binding.isEditing = !binding.isEditing;
};
// 编辑模式下按键事件处理
const handleKeyDown = (event: KeyboardEvent, binding: KeyBinding) => {
if (!binding.isEditing) return;
event.preventDefault();
event.stopPropagation();
const newKeys: string[] = [];
if (event.ctrlKey) newKeys.push('Ctrl');
if (event.shiftKey) newKeys.push('Shift');
if (event.altKey) newKeys.push('Alt');
// 获取按键
let keyName = event.key;
if (keyName === ' ') keyName = 'Space';
if (keyName.length === 1) keyName = keyName.toUpperCase();
// 如果有修饰键,就添加主键
if (event.ctrlKey || event.shiftKey || event.altKey) {
if (!['Control', 'Shift', 'Alt'].includes(keyName)) {
newKeys.push(keyName);
}
} else {
// 没有修饰键,直接使用主键
newKeys.push(keyName);
}
// 唯一按键,不增加空字段
if (newKeys.length > 0) {
binding.keys = [...new Set(newKeys)];
}
// 完成编辑
binding.isEditing = false;
};
</script>
<template>
<div class="settings-page">
<SettingSection :title="t('settings.keyBindings')">
<div class="key-bindings-container">
<div class="key-bindings-header">
<div class="command-col">命令</div>
<div class="keybinding-col">快捷键</div>
<div class="action-col">操作</div>
</div>
<div
v-for="binding in keyBindings"
:key="binding.id"
class="key-binding-row"
:class="{ 'is-editing': binding.isEditing }"
@keydown="(e) => handleKeyDown(e, binding)"
tabindex="0"
>
<div class="command-col">{{ binding.name }}</div>
<div class="keybinding-col" :class="{ 'is-editing': binding.isEditing }">
<template v-if="binding.isEditing">
按下快捷键...
</template>
<template v-else>
<span
v-for="(key, index) in binding.keys"
:key="index"
class="key-badge"
>
{{ key }}
</span>
</template>
</div>
<div class="action-col">
<button
class="edit-button"
@click="toggleEdit(binding)"
>
{{ binding.isEditing ? '取消' : '编辑' }}
</button>
</div>
</div>
</div>
</SettingSection>
</div>
</template>
<style scoped lang="scss">
.settings-page {
max-width: 800px;
}
.key-bindings-container {
padding: 10px 16px;
.key-bindings-header {
display: flex;
padding: 0 0 10px 0;
border-bottom: 1px solid #444444;
color: #a0a0a0;
font-size: 13px;
font-weight: 500;
}
.key-binding-row {
display: flex;
padding: 14px 0;
border-bottom: 1px solid #3a3a3a;
align-items: center;
transition: background-color 0.2s ease;
&:focus {
outline: none;
}
&:hover {
background-color: rgba(255, 255, 255, 0.03);
}
&.is-editing {
background-color: rgba(74, 158, 255, 0.1);
}
}
.command-col {
flex: 1;
padding-right: 10px;
font-size: 14px;
}
.keybinding-col {
width: 200px;
display: flex;
gap: 5px;
padding: 0 10px;
&.is-editing {
font-style: italic;
color: #a0a0a0;
}
.key-badge {
background-color: #3a3a3a;
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
border: 1px solid #555555;
}
}
.action-col {
width: 80px;
text-align: right;
.edit-button {
padding: 5px 10px;
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
&:hover {
background-color: #444444;
border-color: #666666;
}
&:active {
transform: translateY(1px);
}
}
}
}
.coming-soon-placeholder {
padding: 20px;
background-color: #333333;
border-radius: 6px;
color: #a0a0a0;
text-align: center;
font-style: italic;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,223 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
const { t } = useI18n();
// 模拟版本数据
const currentVersion = ref('1.0.0');
const isCheckingForUpdates = ref(false);
const updateAvailable = ref(false);
const latestVersion = ref('1.1.0');
const updateNotes = ref([
'优化编辑器性能',
'新增自动保存功能',
'修复多个界面显示问题',
'添加更多编辑器主题'
]);
// 自动检查更新选项
const autoCheckUpdates = ref(true);
// 模拟检查更新
const checkForUpdates = () => {
isCheckingForUpdates.value = true;
// 模拟网络请求延迟
setTimeout(() => {
isCheckingForUpdates.value = false;
updateAvailable.value = true;
}, 1500);
};
// 模拟下载更新
const downloadUpdate = () => {
// 在实际应用中这里会调用后端API下载更新
alert('开始下载更新...');
};
</script>
<template>
<div class="settings-page">
<SettingSection :title="t('settings.updates')">
<div class="update-info">
<div class="version-info">
<div class="current-version">
<span class="label">当前版本:</span>
<span class="version">{{ currentVersion }}</span>
</div>
<button
class="check-button"
@click="checkForUpdates"
:disabled="isCheckingForUpdates"
>
<span v-if="isCheckingForUpdates" class="loading-spinner"></span>
{{ isCheckingForUpdates ? '检查中...' : '检查更新' }}
</button>
</div>
<div v-if="updateAvailable" class="update-available">
<div class="update-header">
<div class="update-title">发现新版本: {{ latestVersion }}</div>
<button class="download-button" @click="downloadUpdate">
下载更新
</button>
</div>
<div class="update-notes">
<div class="notes-title">更新内容:</div>
<ul class="notes-list">
<li v-for="(note, index) in updateNotes" :key="index">
{{ note }}
</li>
</ul>
</div>
</div>
</div>
<SettingItem title="自动检查更新" description="启动应用时自动检查更新">
<ToggleSwitch v-model="autoCheckUpdates" />
</SettingItem>
</SettingSection>
</div>
</template>
<style scoped lang="scss">
.settings-page {
max-width: 800px;
}
.update-info {
padding: 15px 16px;
margin-bottom: 20px;
.version-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
.current-version {
font-size: 14px;
.label {
color: #a0a0a0;
margin-right: 5px;
}
.version {
color: #e0e0e0;
font-weight: 500;
}
}
.check-button {
padding: 8px 16px;
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
&:hover:not(:disabled) {
background-color: #444444;
border-color: #666666;
}
&:active:not(:disabled) {
transform: translateY(1px);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading-spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #e0e0e0;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
}
}
.update-available {
background-color: #2c3847;
border: 1px solid #3a4a5c;
border-radius: 6px;
padding: 16px;
.update-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.update-title {
font-size: 14px;
font-weight: 500;
color: #4a9eff;
}
.download-button {
padding: 8px 16px;
background-color: #2c5a9e;
border: none;
border-radius: 4px;
color: #ffffff;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
&:hover {
background-color: #3867a9;
}
&:active {
transform: translateY(1px);
}
}
}
.update-notes {
.notes-title {
font-size: 13px;
color: #b0b0b0;
margin-bottom: 8px;
}
.notes-list {
margin: 0;
padding-left: 20px;
li {
font-size: 13px;
color: #d0d0d0;
margin-bottom: 6px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
}
</style>