Compare commits
3 Commits
90ccd8d6d0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 57a669df71 | |||
| f8df3d2681 | |||
| 8f1964c374 |
@@ -9,12 +9,10 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mousetrap": "^1.6.5",
|
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mousetrap": "^1.6.15",
|
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"sass-embedded": "^1.82.0",
|
"sass-embedded": "^1.82.0",
|
||||||
|
|||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -8,9 +8,6 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
mousetrap:
|
|
||||||
specifier: ^1.6.5
|
|
||||||
version: 1.6.5
|
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.13
|
specifier: ^3.5.13
|
||||||
version: 3.5.13(typescript@5.7.2)
|
version: 3.5.13(typescript@5.7.2)
|
||||||
@@ -18,9 +15,6 @@ importers:
|
|||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0(vue@3.5.13(typescript@5.7.2))
|
version: 4.5.0(vue@3.5.13(typescript@5.7.2))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/mousetrap':
|
|
||||||
specifier: ^1.6.15
|
|
||||||
version: 1.6.15
|
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.10.1
|
specifier: ^22.10.1
|
||||||
version: 22.10.1
|
version: 22.10.1
|
||||||
@@ -304,9 +298,6 @@ packages:
|
|||||||
'@types/estree@1.0.6':
|
'@types/estree@1.0.6':
|
||||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||||
|
|
||||||
'@types/mousetrap@1.6.15':
|
|
||||||
resolution: {integrity: sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw==}
|
|
||||||
|
|
||||||
'@types/node@22.10.1':
|
'@types/node@22.10.1':
|
||||||
resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==}
|
resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==}
|
||||||
|
|
||||||
@@ -385,9 +376,6 @@ packages:
|
|||||||
magic-string@0.30.15:
|
magic-string@0.30.15:
|
||||||
resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==}
|
resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==}
|
||||||
|
|
||||||
mousetrap@1.6.5:
|
|
||||||
resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==}
|
|
||||||
|
|
||||||
nanoid@3.3.8:
|
nanoid@3.3.8:
|
||||||
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -766,8 +754,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.6': {}
|
'@types/estree@1.0.6': {}
|
||||||
|
|
||||||
'@types/mousetrap@1.6.15': {}
|
|
||||||
|
|
||||||
'@types/node@22.10.1':
|
'@types/node@22.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.20.0
|
undici-types: 6.20.0
|
||||||
@@ -881,8 +867,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
mousetrap@1.6.5: {}
|
|
||||||
|
|
||||||
nanoid@3.3.8: {}
|
nanoid@3.3.8: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
router.push('/project')
|
router.push('/config')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
import VirtualList from './VirtualList.vue'
|
import VirtualList from './VirtualList.vue'
|
||||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
|
||||||
import type { ListItem, MenuItem } from '@/types'
|
import type { ListItem, MenuItem } from '@/types'
|
||||||
import Mousetrap from 'mousetrap'
|
import { keyboardManager } from '@/utils/KeyboardManager'
|
||||||
import 'mousetrap/plugins/global-bind/mousetrap-global-bind'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件属性接口
|
* 组件属性接口
|
||||||
@@ -54,7 +53,7 @@ const toastMessage = ref<string>('')
|
|||||||
/** 是否正在使用键盘导航菜单 */
|
/** 是否正在使用键盘导航菜单 */
|
||||||
const isKeyboardNavigation = ref<boolean>(false)
|
const isKeyboardNavigation = ref<boolean>(false)
|
||||||
/** 快捷键处理函数映射 */
|
/** 快捷键处理函数映射 */
|
||||||
const shortcutHandlers = new Map<string, () => void>()
|
const shortcutHandlers = new Map<string, (e: KeyboardEvent) => void>()
|
||||||
|
|
||||||
// ===== 定时器 =====
|
// ===== 定时器 =====
|
||||||
/** 鼠标悬停禁用定时器 */
|
/** 鼠标悬停禁用定时器 */
|
||||||
@@ -105,12 +104,10 @@ const closeMenu = (): void => {
|
|||||||
isKeyboardNavigation.value = false
|
isKeyboardNavigation.value = false
|
||||||
|
|
||||||
// 清除快捷键绑定
|
// 清除快捷键绑定
|
||||||
shortcutHandlers.forEach((_, shortcut) => {
|
keyboardManager.disable()
|
||||||
Mousetrap.unbind(shortcut)
|
|
||||||
})
|
|
||||||
shortcutHandlers.clear()
|
shortcutHandlers.clear()
|
||||||
|
|
||||||
// 临时禁用鼠标悬停,防止菜单关闭时立即触发悬停效果
|
// 临时禁用鼠标悬停,防止菜单关闭时<EFBFBD><EFBFBD><EFBFBD>即触发悬停效果
|
||||||
temporaryDisableHover.value = true
|
temporaryDisableHover.value = true
|
||||||
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
||||||
hoverDisableTimer = window.setTimeout(() => {
|
hoverDisableTimer = window.setTimeout(() => {
|
||||||
@@ -158,7 +155,7 @@ const handleMenuTriggerClick = (e: MouseEvent): void => {
|
|||||||
* - 左方向键/ESC:关闭菜单
|
* - 左方向键/ESC:关闭菜单
|
||||||
* - 上下方向键:在菜单打开时导航菜单项
|
* - 上下方向键:在菜单打开时导航菜单项
|
||||||
* - 回车键:在菜单打开时选择当前菜单项
|
* - 回车键:在菜单打开时选择当前菜单项
|
||||||
* @param e 键盘事件对象
|
* @param e 键盘事件<EFBFBD><EFBFBD>象
|
||||||
*/
|
*/
|
||||||
const handleKeyDown = (e: KeyboardEvent): void => {
|
const handleKeyDown = (e: KeyboardEvent): void => {
|
||||||
if (e.key === 'ArrowRight' && !showMenu.value && selectedItem.value) {
|
if (e.key === 'ArrowRight' && !showMenu.value && selectedItem.value) {
|
||||||
@@ -181,7 +178,7 @@ const handleKeyDown = (e: KeyboardEvent): void => {
|
|||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
isKeyboardNavigation.value = true
|
isKeyboardNavigation.value = true
|
||||||
// 向下选<EFBFBD><EFBFBD>菜单项,到底部时循环到顶部
|
// 向下选择菜单项,到底部时循环到顶部
|
||||||
selectedMenuIndex.value =
|
selectedMenuIndex.value =
|
||||||
selectedMenuIndex.value >= currentMenuItems.value.length - 1
|
selectedMenuIndex.value >= currentMenuItems.value.length - 1
|
||||||
? 0
|
? 0
|
||||||
@@ -232,10 +229,7 @@ onBeforeUnmount(() => {
|
|||||||
document.removeEventListener('keydown', handleKeyDown)
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
if (toastTimer) clearTimeout(toastTimer)
|
if (toastTimer) clearTimeout(toastTimer)
|
||||||
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
||||||
// 清除所有快捷键绑定
|
keyboardManager.disable()
|
||||||
shortcutHandlers.forEach((_, shortcut) => {
|
|
||||||
Mousetrap.unbind(shortcut)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== 过渡钩子 =====
|
// ===== 过渡钩子 =====
|
||||||
@@ -257,46 +251,31 @@ const onAfterLeave = (el: Element): void => {
|
|||||||
(el as HTMLElement).style.display = ''
|
(el as HTMLElement).style.display = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在导入后添加 Mousetrap 配置
|
|
||||||
Mousetrap.prototype.stopCallback = (e: KeyboardEvent, element: HTMLElement, combo: string) => {
|
|
||||||
// 当菜单打开时,总是响应快捷键
|
|
||||||
if (showMenu.value) {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 否则使用默认行为
|
|
||||||
return element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 bindShortcuts 函数
|
// 修改 bindShortcuts 函数
|
||||||
const bindShortcuts = (): void => {
|
const bindShortcuts = (): void => {
|
||||||
// 清除之前的绑定
|
// 清除之前的绑定
|
||||||
shortcutHandlers.forEach((_, shortcut) => {
|
shortcutHandlers.forEach((_, shortcut) => {
|
||||||
Mousetrap.unbind(shortcut)
|
keyboardManager.unbind(shortcut)
|
||||||
})
|
})
|
||||||
shortcutHandlers.clear()
|
shortcutHandlers.clear()
|
||||||
|
|
||||||
// 只在菜单打开时绑定快捷键
|
// 只在菜单打开时绑定快捷键
|
||||||
if (showMenu.value) {
|
if (showMenu.value) {
|
||||||
|
keyboardManager.enable()
|
||||||
currentMenuItems.value.forEach((item) => {
|
currentMenuItems.value.forEach((item) => {
|
||||||
if (item.shortcut) {
|
if (item.shortcut) {
|
||||||
const handler = (e: ExtendedKeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
// 阻止浏览器默认行为
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
if (selectedItem.value || !item.action.toString().includes('selectedItem')) {
|
if (selectedItem.value || !item.action.toString().includes('selectedItem')) {
|
||||||
item.action(selectedItem.value)
|
item.action(selectedItem.value)
|
||||||
// 执行后关闭菜单
|
|
||||||
closeMenu()
|
closeMenu()
|
||||||
}
|
}
|
||||||
return false // 阻止事件冒泡
|
|
||||||
}
|
}
|
||||||
shortcutHandlers.set(item.shortcut, handler)
|
shortcutHandlers.set(item.shortcut, handler)
|
||||||
Mousetrap.bindGlobal(item.shortcut, handler)
|
keyboardManager.bind(item.shortcut, handler)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
keyboardManager.disable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -438,7 +417,7 @@ const bindShortcuts = (): void => {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 工具栏:固定高度,顶部边框和阴影效果 */
|
/* 工具栏固定高度,顶部边框和阴影效果 */
|
||||||
.toolbar {
|
.toolbar {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|||||||
@@ -1,66 +1,166 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { ConfigGroup, TextConfigItem, SelectConfigItem, SwitchConfigItem, ConfigItemType } from '@/types/config'
|
||||||
|
import { ConfigManager, LocalStorageConfigStore } from '@/utils/ConfigStore'
|
||||||
|
|
||||||
// 配置项状态
|
const configManager = ConfigManager.getInstance(new LocalStorageConfigStore())
|
||||||
const config = ref({
|
|
||||||
theme: 'light',
|
|
||||||
language: 'zh-CN',
|
|
||||||
autoSave: true,
|
|
||||||
notifications: true
|
|
||||||
})
|
|
||||||
|
|
||||||
// 处理配置更改
|
// 防抖函数
|
||||||
const handleConfigChange = () => {
|
const debounce = (fn: Function, delay: number) => {
|
||||||
// 这里可以添加保存配置的逻辑
|
let timer: number | null = null
|
||||||
console.log('配置已更新:', config.value)
|
return (...args: any[]) => {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
fn(...args)
|
||||||
|
timer = null
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理文本配置项实时更新
|
||||||
|
const handleTextConfigChange = debounce((item: TextConfigItem, group: ConfigGroup) => {
|
||||||
|
configManager.updateConfig({
|
||||||
|
[`${group.key}.${item.key}`]: item.value
|
||||||
|
})
|
||||||
|
console.log('配置已更新:', configManager.getAllConfig())
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
// 处理单个配置项更改
|
||||||
|
const handleConfigItemChange = (item: any, group: ConfigGroup) => {
|
||||||
|
configManager.updateConfig({
|
||||||
|
[`${group.key}.${item.key}`]: item.value
|
||||||
|
})
|
||||||
|
console.log('配置已更新:', configManager.getAllConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理配置组启用状态变更
|
||||||
|
const handleGroupEnableChange = (group: ConfigGroup) => {
|
||||||
|
configManager.updateConfig({
|
||||||
|
[`${group.key}.enabled`]: group.enabled
|
||||||
|
})
|
||||||
|
console.log('配置已更新:', configManager.getAllConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用构造函数定义配置组
|
||||||
|
const configGroups = ref([
|
||||||
|
new ConfigGroup('appearance', '外观设置', false, [
|
||||||
|
new TextConfigItem(
|
||||||
|
'projectName',
|
||||||
|
'项目名称',
|
||||||
|
'设置新建项目时的默认项目名称',
|
||||||
|
'',
|
||||||
|
'请输入默认项目名称'
|
||||||
|
),
|
||||||
|
new SelectConfigItem(
|
||||||
|
'theme',
|
||||||
|
'主题',
|
||||||
|
'切换应用的显示主题',
|
||||||
|
[
|
||||||
|
{ label: '浅色', value: 'light' },
|
||||||
|
{ label: '深色', value: 'dark' }
|
||||||
|
],
|
||||||
|
'light'
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
|
||||||
|
new ConfigGroup('system', '系统设置', true, [
|
||||||
|
new SelectConfigItem(
|
||||||
|
'language',
|
||||||
|
'语言',
|
||||||
|
'设置应用界面显示的语言',
|
||||||
|
[
|
||||||
|
{ label: '中文', value: 'zh-CN' },
|
||||||
|
{ label: 'English', value: 'en-US' }
|
||||||
|
],
|
||||||
|
'zh-CN'
|
||||||
|
),
|
||||||
|
new SwitchConfigItem(
|
||||||
|
'autoSave',
|
||||||
|
'自动保存',
|
||||||
|
'开启后将自动保存您的修改',
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new SwitchConfigItem(
|
||||||
|
'notifications',
|
||||||
|
'通知提醒',
|
||||||
|
'是否显示系统通知提醒',
|
||||||
|
true
|
||||||
|
)
|
||||||
|
])
|
||||||
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="config-container">
|
<div class="config-container">
|
||||||
<h2 class="config-title">系统设置</h2>
|
<h2 class="config-title">系统设置</h2>
|
||||||
|
|
||||||
<div class="config-section">
|
<div
|
||||||
<div class="config-item">
|
v-for="(group, groupIndex) in configGroups"
|
||||||
<label>主题</label>
|
:key="groupIndex"
|
||||||
<select v-model="config.theme">
|
class="config-section"
|
||||||
<option value="light">浅色</option>
|
>
|
||||||
<option value="dark">深色</option>
|
<div class="group-header">
|
||||||
</select>
|
<h3 class="group-title">{{ group.title }}</h3>
|
||||||
|
<label class="switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="group.enabled"
|
||||||
|
@change="handleGroupEnableChange(group)"
|
||||||
|
>
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-item">
|
<div
|
||||||
<label>语言</label>
|
v-if="group.enabled"
|
||||||
<select v-model="config.language">
|
v-for="(item, itemIndex) in group.items"
|
||||||
<option value="zh-CN">中文</option>
|
:key="itemIndex"
|
||||||
<option value="en-US">English</option>
|
class="config-item"
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-item">
|
|
||||||
<label>自动保存</label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
v-model="config.autoSave"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-item">
|
|
||||||
<label>通知提醒</label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
v-model="config.notifications"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-actions">
|
|
||||||
<button
|
|
||||||
class="save-button"
|
|
||||||
@click="handleConfigChange"
|
|
||||||
>
|
>
|
||||||
保存设置
|
<div class="item-info">
|
||||||
</button>
|
<div class="item-label">{{ item.label }}</div>
|
||||||
|
<div v-if="item.description" class="item-description">
|
||||||
|
{{ item.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文本输入 -->
|
||||||
|
<div v-if="item.type === ConfigItemType.TEXT" class="input-wrapper">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="item.value"
|
||||||
|
:placeholder="(item as any).placeholder"
|
||||||
|
@input="handleTextConfigChange(item, group)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 开关 -->
|
||||||
|
<label v-else-if="item.type === ConfigItemType.SWITCH" class="switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="item.value"
|
||||||
|
@change="handleConfigItemChange(item, group)"
|
||||||
|
>
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- 下拉选择 -->
|
||||||
|
<div v-else-if="item.type === ConfigItemType.SELECT" class="select-wrapper">
|
||||||
|
<select
|
||||||
|
v-model="item.value"
|
||||||
|
@change="handleConfigItemChange(item, group)"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="option in (item as any).options"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="select-arrow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -82,8 +182,21 @@ const handleConfigChange = () => {
|
|||||||
.config-section {
|
.config-section {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-item {
|
.config-item {
|
||||||
@@ -91,43 +204,114 @@ const handleConfigChange = () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-item:last-child {
|
.item-info {
|
||||||
border-bottom: none;
|
flex: 1;
|
||||||
|
margin-right: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-item label {
|
.item-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-item select {
|
.item-description {
|
||||||
padding: 6px 12px;
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框通用包装器 */
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
transition: all 0.2s;
|
||||||
min-width: 120px;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-actions {
|
.input-wrapper input:focus {
|
||||||
margin-top: 24px;
|
border-color: #1976d2;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-button {
|
/* 开关样式 */
|
||||||
background: #1976d2;
|
.switch {
|
||||||
color: white;
|
position: relative;
|
||||||
border: none;
|
display: inline-block;
|
||||||
padding: 8px 24px;
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
left: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input:checked + .slider {
|
||||||
|
background-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input:checked + .slider:before {
|
||||||
|
transform: translateX(20px);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉选择框样式 */
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
appearance: none;
|
||||||
|
background: white;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-button:hover {
|
.select-wrapper select:focus {
|
||||||
background: #1565c0;
|
border-color: #1976d2;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -26,7 +26,7 @@ const initListData = () => {
|
|||||||
action: (item) => {
|
action: (item) => {
|
||||||
console.log('打开项目:', item?.name)
|
console.log('打开项目:', item?.name)
|
||||||
},
|
},
|
||||||
shortcut: 'ctrl+o'
|
shortcut: 'alt+o'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'edit',
|
id: 'edit',
|
||||||
|
|||||||
74
src/types/config.ts
Normal file
74
src/types/config.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// 配置项类型枚举
|
||||||
|
export enum ConfigItemType {
|
||||||
|
TEXT = 'text',
|
||||||
|
SWITCH = 'switch',
|
||||||
|
SELECT = 'select'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础配置项接口
|
||||||
|
export interface BaseConfigItem {
|
||||||
|
type: ConfigItemType
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
value: any
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文本配置项
|
||||||
|
export class TextConfigItem implements BaseConfigItem {
|
||||||
|
readonly type = ConfigItemType.TEXT
|
||||||
|
value: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public key: string,
|
||||||
|
public label: string,
|
||||||
|
public description: string,
|
||||||
|
defaultValue: string = '',
|
||||||
|
public placeholder?: string
|
||||||
|
) {
|
||||||
|
this.value = defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开关配置项
|
||||||
|
export class SwitchConfigItem implements BaseConfigItem {
|
||||||
|
readonly type = ConfigItemType.SWITCH
|
||||||
|
value: boolean
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public key: string,
|
||||||
|
public label: string,
|
||||||
|
public description: string,
|
||||||
|
defaultValue: boolean = false
|
||||||
|
) {
|
||||||
|
this.value = defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择配置项
|
||||||
|
export class SelectConfigItem implements BaseConfigItem {
|
||||||
|
readonly type = ConfigItemType.SELECT
|
||||||
|
value: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public key: string,
|
||||||
|
public label: string,
|
||||||
|
public description: string,
|
||||||
|
public options: Array<{ label: string; value: string }>,
|
||||||
|
defaultValue: string
|
||||||
|
) {
|
||||||
|
this.value = defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConfigItem = TextConfigItem | SelectConfigItem | SwitchConfigItem
|
||||||
|
|
||||||
|
// 配置组
|
||||||
|
export class ConfigGroup {
|
||||||
|
constructor(
|
||||||
|
public key: string,
|
||||||
|
public title: string,
|
||||||
|
public enabled: boolean,
|
||||||
|
public items: ConfigItem[]
|
||||||
|
) {}
|
||||||
|
}
|
||||||
98
src/utils/ConfigStore.ts
Normal file
98
src/utils/ConfigStore.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// 配置存储接口
|
||||||
|
export interface ConfigStore {
|
||||||
|
get<T>(key: string): T | undefined
|
||||||
|
set<T>(key: string, value: T): void
|
||||||
|
getAll(): Record<string, any>
|
||||||
|
clear(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内存存储实现
|
||||||
|
export class MemoryConfigStore implements ConfigStore {
|
||||||
|
private store: Record<string, any> = {}
|
||||||
|
|
||||||
|
get<T>(key: string): T | undefined {
|
||||||
|
return this.store[key] as T
|
||||||
|
}
|
||||||
|
|
||||||
|
set<T>(key: string, value: T): void {
|
||||||
|
this.store[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): Record<string, any> {
|
||||||
|
return { ...this.store }
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.store = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalStorage存储实现
|
||||||
|
export class LocalStorageConfigStore implements ConfigStore {
|
||||||
|
private readonly prefix = 'app_config.'
|
||||||
|
|
||||||
|
private getKey(key: string): string {
|
||||||
|
return this.prefix + key
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T>(key: string): T | undefined {
|
||||||
|
const value = localStorage.getItem(this.getKey(key))
|
||||||
|
return value ? JSON.parse(value) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
set<T>(key: string, value: T): void {
|
||||||
|
localStorage.setItem(this.getKey(key), JSON.stringify(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): Record<string, any> {
|
||||||
|
const result: Record<string, any> = {}
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i)
|
||||||
|
if (key?.startsWith(this.prefix)) {
|
||||||
|
const realKey = key.slice(this.prefix.length)
|
||||||
|
result[realKey] = JSON.parse(localStorage.getItem(key) || '{}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
for (let i = localStorage.length - 1; i >= 0; i--) {
|
||||||
|
const key = localStorage.key(i)
|
||||||
|
if (key?.startsWith(this.prefix)) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置管理器
|
||||||
|
export class ConfigManager {
|
||||||
|
private static instance: ConfigManager
|
||||||
|
private store: ConfigStore
|
||||||
|
|
||||||
|
private constructor(store: ConfigStore) {
|
||||||
|
this.store = store
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(store: ConfigStore = new MemoryConfigStore()): ConfigManager {
|
||||||
|
if (!ConfigManager.instance) {
|
||||||
|
ConfigManager.instance = new ConfigManager(store)
|
||||||
|
}
|
||||||
|
return ConfigManager.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig(config: Record<string, any>): void {
|
||||||
|
Object.entries(config).forEach(([key, value]) => {
|
||||||
|
this.store.set(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfig<T>(key: string): T | undefined {
|
||||||
|
return this.store.get<T>(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllConfig(): Record<string, any> {
|
||||||
|
return this.store.getAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/utils/KeyboardManager.ts
Normal file
116
src/utils/KeyboardManager.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
type KeyboardCallback = (e: KeyboardEvent) => void
|
||||||
|
|
||||||
|
export class KeyboardManager {
|
||||||
|
private shortcuts: Map<string, KeyboardCallback> = new Map()
|
||||||
|
private pressedKeys: Set<string> = new Set()
|
||||||
|
private enabled: boolean = false
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.handleKeyDown = this.handleKeyDown.bind(this)
|
||||||
|
this.handleKeyUp = this.handleKeyUp.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用快捷键监听
|
||||||
|
*/
|
||||||
|
enable(): void {
|
||||||
|
if (!this.enabled) {
|
||||||
|
window.addEventListener('keydown', this.handleKeyDown)
|
||||||
|
window.addEventListener('keyup', this.handleKeyUp)
|
||||||
|
this.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用快捷键监听
|
||||||
|
*/
|
||||||
|
disable(): void {
|
||||||
|
if (this.enabled) {
|
||||||
|
window.removeEventListener('keydown', this.handleKeyDown)
|
||||||
|
window.removeEventListener('keyup', this.handleKeyUp)
|
||||||
|
this.enabled = false
|
||||||
|
this.pressedKeys.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定快捷键
|
||||||
|
*/
|
||||||
|
bind(shortcut: string, callback: KeyboardCallback): void {
|
||||||
|
this.shortcuts.set(this.normalizeShortcut(shortcut), callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑快捷键
|
||||||
|
*/
|
||||||
|
unbind(shortcut: string): void {
|
||||||
|
this.shortcuts.delete(this.normalizeShortcut(shortcut))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有快捷键绑定
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.shortcuts.clear()
|
||||||
|
this.pressedKeys.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeyDown(e: KeyboardEvent): void {
|
||||||
|
// 忽略在输入框中的按键
|
||||||
|
if (this.shouldIgnoreInput(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this.normalizeKey(e.key.toLowerCase())
|
||||||
|
this.pressedKeys.add(key)
|
||||||
|
|
||||||
|
const currentShortcut = Array.from(this.pressedKeys).sort().join('+')
|
||||||
|
const callback = this.shortcuts.get(currentShortcut)
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
callback(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeyUp(e: KeyboardEvent): void {
|
||||||
|
const key = this.normalizeKey(e.key.toLowerCase())
|
||||||
|
this.pressedKeys.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeKey(key: string): string {
|
||||||
|
const keyMap: Record<string, string> = {
|
||||||
|
'control': 'ctrl',
|
||||||
|
'command': 'cmd',
|
||||||
|
'meta': 'cmd',
|
||||||
|
'escape': 'esc',
|
||||||
|
' ': 'space',
|
||||||
|
'arrowup': 'up',
|
||||||
|
'arrowdown': 'down',
|
||||||
|
'arrowleft': 'left',
|
||||||
|
'arrowright': 'right',
|
||||||
|
}
|
||||||
|
return keyMap[key] || key
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeShortcut(shortcut: string): string {
|
||||||
|
return shortcut
|
||||||
|
.toLowerCase()
|
||||||
|
.split('+')
|
||||||
|
.map(key => this.normalizeKey(key.trim()))
|
||||||
|
.sort()
|
||||||
|
.join('+')
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldIgnoreInput(e: KeyboardEvent): boolean {
|
||||||
|
const element = e.target as HTMLElement
|
||||||
|
return element.tagName === 'INPUT' ||
|
||||||
|
element.tagName === 'TEXTAREA' ||
|
||||||
|
element.tagName === 'SELECT' ||
|
||||||
|
element.isContentEditable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例实例
|
||||||
|
export const keyboardManager = new KeyboardManager()
|
||||||
@@ -23,8 +23,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"types": [
|
"types": [
|
||||||
"vue-router",
|
"vue-router"
|
||||||
"mousetrap"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
Reference in New Issue
Block a user