改为typescript支持
This commit is contained in:
@@ -16,6 +16,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@types/node": "^22.10.1",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
"vite": "^6.0.1"
|
"vite": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
pnpm-lock.yaml
generated
50
pnpm-lock.yaml
generated
@@ -10,14 +10,20 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.13
|
specifier: ^3.5.13
|
||||||
version: 3.5.13
|
version: 3.5.13(typescript@5.7.2)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^22.10.1
|
||||||
|
version: 22.10.1
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1(vite@6.0.3)(vue@3.5.13)
|
version: 5.2.1(vite@6.0.3(@types/node@22.10.1))(vue@3.5.13(typescript@5.7.2))
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.0.2
|
||||||
|
version: 5.7.2
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.3
|
version: 6.0.3(@types/node@22.10.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -283,6 +289,9 @@ packages:
|
|||||||
'@types/estree@1.0.6':
|
'@types/estree@1.0.6':
|
||||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz}
|
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz}
|
||||||
|
|
||||||
|
'@types/node@22.10.1':
|
||||||
|
resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==, tarball: https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz}
|
||||||
|
|
||||||
'@vitejs/plugin-vue@5.2.1':
|
'@vitejs/plugin-vue@5.2.1':
|
||||||
resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==, tarball: https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz}
|
resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==, tarball: https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
@@ -363,6 +372,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, tarball: https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, tarball: https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
typescript@5.7.2:
|
||||||
|
resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==, tarball: https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
undici-types@6.20.0:
|
||||||
|
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz}
|
||||||
|
|
||||||
vite@6.0.3:
|
vite@6.0.3:
|
||||||
resolution: {integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==, tarball: https://registry.npmjs.org/vite/-/vite-6.0.3.tgz}
|
resolution: {integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==, tarball: https://registry.npmjs.org/vite/-/vite-6.0.3.tgz}
|
||||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
@@ -559,10 +576,14 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.6': {}
|
'@types/estree@1.0.6': {}
|
||||||
|
|
||||||
'@vitejs/plugin-vue@5.2.1(vite@6.0.3)(vue@3.5.13)':
|
'@types/node@22.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 6.0.3
|
undici-types: 6.20.0
|
||||||
vue: 3.5.13
|
|
||||||
|
'@vitejs/plugin-vue@5.2.1(vite@6.0.3(@types/node@22.10.1))(vue@3.5.13(typescript@5.7.2))':
|
||||||
|
dependencies:
|
||||||
|
vite: 6.0.3(@types/node@22.10.1)
|
||||||
|
vue: 3.5.13(typescript@5.7.2)
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.13':
|
'@vue/compiler-core@3.5.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -610,11 +631,11 @@ snapshots:
|
|||||||
'@vue/shared': 3.5.13
|
'@vue/shared': 3.5.13
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
'@vue/server-renderer@3.5.13(vue@3.5.13)':
|
'@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.7.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/compiler-ssr': 3.5.13
|
'@vue/compiler-ssr': 3.5.13
|
||||||
'@vue/shared': 3.5.13
|
'@vue/shared': 3.5.13
|
||||||
vue: 3.5.13
|
vue: 3.5.13(typescript@5.7.2)
|
||||||
|
|
||||||
'@vue/shared@3.5.13': {}
|
'@vue/shared@3.5.13': {}
|
||||||
|
|
||||||
@@ -695,18 +716,25 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
vite@6.0.3:
|
typescript@5.7.2: {}
|
||||||
|
|
||||||
|
undici-types@6.20.0: {}
|
||||||
|
|
||||||
|
vite@6.0.3(@types/node@22.10.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.24.0
|
esbuild: 0.24.0
|
||||||
postcss: 8.4.49
|
postcss: 8.4.49
|
||||||
rollup: 4.28.1
|
rollup: 4.28.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/node': 22.10.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
vue@3.5.13:
|
vue@3.5.13(typescript@5.7.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/compiler-dom': 3.5.13
|
'@vue/compiler-dom': 3.5.13
|
||||||
'@vue/compiler-sfc': 3.5.13
|
'@vue/compiler-sfc': 3.5.13
|
||||||
'@vue/runtime-dom': 3.5.13
|
'@vue/runtime-dom': 3.5.13
|
||||||
'@vue/server-renderer': 3.5.13(vue@3.5.13)
|
'@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.7.2))
|
||||||
'@vue/shared': 3.5.13
|
'@vue/shared': 3.5.13
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.7.2
|
||||||
|
|||||||
11
src/App.vue
11
src/App.vue
@@ -1,8 +1,9 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import ProjectList from './components/ProjectList.vue'
|
import ProjectList from './components/ProjectList.vue'
|
||||||
|
import type { ListItem, MenuItem, ListConfig } from '@/types'
|
||||||
|
|
||||||
// 生成模拟数据
|
// 生成模拟数据
|
||||||
const listData = Array.from({ length: 789 }, (_, i) => ({
|
const listData: ListItem[] = Array.from({ length: 789 }, (_, i) => ({
|
||||||
id: i,
|
id: i,
|
||||||
name: `project-${i + 1}`,
|
name: `project-${i + 1}`,
|
||||||
path: `/Users/lanyuanxiaoyao/Project/IdeaProjects/project-${i + 1}`,
|
path: `/Users/lanyuanxiaoyao/Project/IdeaProjects/project-${i + 1}`,
|
||||||
@@ -15,7 +16,7 @@ const listData = Array.from({ length: 789 }, (_, i) => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// 自定义配置
|
// 自定义配置
|
||||||
const listConfig = {
|
const listConfig: ListConfig = {
|
||||||
itemHeight: 50,
|
itemHeight: 50,
|
||||||
itemPadding: 16,
|
itemPadding: 16,
|
||||||
bufferCount: 10,
|
bufferCount: 10,
|
||||||
@@ -23,7 +24,7 @@ const listConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 自定义菜单项
|
// 自定义菜单项
|
||||||
const menuItems = [
|
const menuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
id: 'refresh',
|
id: 'refresh',
|
||||||
label: '刷新列表',
|
label: '刷新列表',
|
||||||
@@ -48,7 +49,7 @@ const menuItems = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
// 列表项点击事件处理
|
// 列表项点击事件处理
|
||||||
const handleItemClick = (item) => {
|
const handleItemClick = (item: ListItem): void => {
|
||||||
console.log('点击列表项:', item)
|
console.log('点击列表项:', item)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,89 +1,86 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import VirtualList from './VirtualList.vue'
|
import VirtualList from './VirtualList.vue'
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import type { ListItem, MenuItem, ListConfig } from '@/types'
|
||||||
|
|
||||||
const props = defineProps({
|
/**
|
||||||
// 列表数据
|
* 组件属性接口
|
||||||
data: {
|
*/
|
||||||
type: Array,
|
interface Props {
|
||||||
required: true
|
/** 列表数据 */
|
||||||
},
|
data: ListItem[]
|
||||||
// 虚拟列表配置
|
/** 虚拟列表配置 */
|
||||||
config: {
|
config: ListConfig
|
||||||
type: Object,
|
/** 菜单项配置 */
|
||||||
default: () => ({
|
menuItems: MenuItem[]
|
||||||
itemHeight: 50,
|
/** 是否显示工具栏 */
|
||||||
itemPadding: 16,
|
showToolbar?: boolean
|
||||||
bufferCount: 10,
|
/** 工具栏高度 */
|
||||||
scrollDebounceTime: 16,
|
toolbarHeight?: number
|
||||||
})
|
|
||||||
},
|
|
||||||
// 菜单项配置
|
|
||||||
menuItems: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [
|
|
||||||
{
|
|
||||||
id: 'refresh',
|
|
||||||
label: '刷新列表',
|
|
||||||
action: (selectedItem) => console.log('刷新列表', selectedItem)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'clear',
|
|
||||||
label: '清空选择',
|
|
||||||
action: (selectedItem) => console.log('清空选择', selectedItem)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'export',
|
|
||||||
label: '导出数据',
|
|
||||||
action: (selectedItem) => console.log('导出数据', selectedItem)
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
// 是否显示工具栏
|
|
||||||
showToolbar: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
// 工具栏高度
|
|
||||||
toolbarHeight: {
|
|
||||||
type: Number,
|
|
||||||
default: 40
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
showToolbar: true,
|
||||||
|
toolbarHeight: 40
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定义事件
|
/**
|
||||||
const emit = defineEmits([
|
* 组件事件定义
|
||||||
'select', // 选中项变化
|
*/
|
||||||
'click', // 点击项目
|
const emit = defineEmits<{
|
||||||
'context-menu', // 右键菜单
|
/** 选中项变化事件 */
|
||||||
'update:selected' // 支持 v-model:selected
|
select: [item: ListItem]
|
||||||
])
|
/** 点击项目事件 */
|
||||||
|
click: [item: ListItem]
|
||||||
|
/** 右键菜单事件 */
|
||||||
|
'context-menu': [data: { item: ListItem }]
|
||||||
|
}>()
|
||||||
|
|
||||||
// 状态管理
|
// ===== 状态管理 =====
|
||||||
const selectedItem = ref(null)
|
/** 当前选中的列表项 */
|
||||||
const showMenu = ref(false)
|
const selectedItem = ref<ListItem | null>(null)
|
||||||
const listFrozen = ref(false)
|
/** 菜单显示状态 */
|
||||||
const temporaryDisableHover = ref(false)
|
const showMenu = ref<boolean>(false)
|
||||||
const selectedMenuIndex = ref(-1)
|
/** 列表冻结状态 */
|
||||||
const toastVisible = ref(false)
|
const listFrozen = ref<boolean>(false)
|
||||||
const toastMessage = ref('')
|
/** 临时禁用鼠标悬停状态 */
|
||||||
|
const temporaryDisableHover = ref<boolean>(false)
|
||||||
|
/** 当前选中的菜单项索引 */
|
||||||
|
const selectedMenuIndex = ref<number>(-1)
|
||||||
|
/** Toast 显示状态 */
|
||||||
|
const toastVisible = ref<boolean>(false)
|
||||||
|
/** Toast 消息内容 */
|
||||||
|
const toastMessage = ref<string>('')
|
||||||
|
|
||||||
// 定时器
|
// ===== 定时器 =====
|
||||||
let hoverDisableTimer = null
|
/** 鼠标悬停禁用定时器 */
|
||||||
let toastTimer = null
|
let hoverDisableTimer: number | null = null
|
||||||
|
/** Toast 显示定时器 */
|
||||||
|
let toastTimer: number | null = null
|
||||||
|
|
||||||
// 处理函数
|
// ===== 事件处理函数 =====
|
||||||
const handleSelect = (item) => {
|
/**
|
||||||
|
* 处理列表项选中
|
||||||
|
* @param item 选中的列表项
|
||||||
|
*/
|
||||||
|
const handleSelect = (item: ListItem): void => {
|
||||||
selectedItem.value = item
|
selectedItem.value = item
|
||||||
emit('select', item)
|
emit('select', item)
|
||||||
emit('update:selected', item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = (item) => {
|
/**
|
||||||
|
* 处理列表项点击
|
||||||
|
* @param item 点击的列表项
|
||||||
|
*/
|
||||||
|
const handleClick = (item: ListItem): void => {
|
||||||
emit('click', item)
|
emit('click', item)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showContextMenu = (data) => {
|
/**
|
||||||
|
* 显示右键菜单
|
||||||
|
* @param data 包含目标列表项的数据对象
|
||||||
|
*/
|
||||||
|
const showContextMenu = (data: { item: ListItem }): void => {
|
||||||
const { item } = data
|
const { item } = data
|
||||||
handleSelect(item)
|
handleSelect(item)
|
||||||
showMenu.value = true
|
showMenu.value = true
|
||||||
@@ -91,42 +88,65 @@ const showContextMenu = (data) => {
|
|||||||
emit('context-menu', data)
|
emit('context-menu', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeMenu = () => {
|
/**
|
||||||
|
* 关闭菜单并处理相关状态
|
||||||
|
*/
|
||||||
|
const closeMenu = (): void => {
|
||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
listFrozen.value = false
|
listFrozen.value = false
|
||||||
selectedMenuIndex.value = -1
|
selectedMenuIndex.value = -1
|
||||||
|
|
||||||
|
// 临时禁用鼠标悬停,防止菜单关闭时立即触发悬停效果
|
||||||
temporaryDisableHover.value = true
|
temporaryDisableHover.value = true
|
||||||
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
||||||
hoverDisableTimer = setTimeout(() => {
|
hoverDisableTimer = window.setTimeout(() => {
|
||||||
temporaryDisableHover.value = false
|
temporaryDisableHover.value = false
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMenuClick = (item) => {
|
/**
|
||||||
item.action?.(selectedItem.value)
|
* 处理菜单项点击
|
||||||
|
* @param item 被点击的菜单项
|
||||||
|
*/
|
||||||
|
const handleMenuClick = (item: MenuItem): void => {
|
||||||
|
item.action(selectedItem.value)
|
||||||
closeMenu()
|
closeMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMenuTriggerClick = (e) => {
|
/**
|
||||||
|
* 处理菜单触发器点击
|
||||||
|
* 控制菜单的显示/隐藏状态
|
||||||
|
* @param e 鼠标事件对象
|
||||||
|
*/
|
||||||
|
const handleMenuTriggerClick = (e: MouseEvent): void => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (showMenu.value) {
|
if (showMenu.value) {
|
||||||
closeMenu()
|
closeMenu()
|
||||||
} else {
|
} else {
|
||||||
if (props.data.length > 0) {
|
if (props.data.length > 0) {
|
||||||
|
// 如果有数据,选择当前选中项或第一项
|
||||||
const itemToSelect = selectedItem.value || props.data[0]
|
const itemToSelect = selectedItem.value || props.data[0]
|
||||||
showContextMenu({ item: itemToSelect })
|
showContextMenu({ item: itemToSelect })
|
||||||
} else {
|
} else {
|
||||||
|
// 如果没有数据,仅显示菜单
|
||||||
showMenu.value = true
|
showMenu.value = true
|
||||||
listFrozen.value = true
|
listFrozen.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e) => {
|
/**
|
||||||
|
* 处理键盘事件
|
||||||
|
* - 右方向键:打开菜单
|
||||||
|
* - 左方向键/ESC:关闭菜单
|
||||||
|
* - 上下方向键:在菜单打开时导航菜单项
|
||||||
|
* - 回车键:在菜单打开时选择当前菜单项
|
||||||
|
* @param e 键盘事件对象
|
||||||
|
*/
|
||||||
|
const handleKeyDown = (e: KeyboardEvent): void => {
|
||||||
if (e.key === 'ArrowRight' && !showMenu.value && selectedItem.value) {
|
if (e.key === 'ArrowRight' && !showMenu.value && selectedItem.value) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleMenuTriggerClick(e)
|
handleMenuTriggerClick(new MouseEvent('click'))
|
||||||
} else if ((e.key === 'ArrowLeft' || e.key === 'Escape') && showMenu.value) {
|
} else if ((e.key === 'ArrowLeft' || e.key === 'Escape') && showMenu.value) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
closeMenu()
|
closeMenu()
|
||||||
@@ -134,6 +154,7 @@ const handleKeyDown = (e) => {
|
|||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
// 向上选择菜单项,到顶部时循环到底部
|
||||||
selectedMenuIndex.value =
|
selectedMenuIndex.value =
|
||||||
selectedMenuIndex.value <= 0
|
selectedMenuIndex.value <= 0
|
||||||
? props.menuItems.length - 1
|
? props.menuItems.length - 1
|
||||||
@@ -141,6 +162,7 @@ const handleKeyDown = (e) => {
|
|||||||
break
|
break
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
// 向下选择菜单项,到底部时循环到顶部
|
||||||
selectedMenuIndex.value =
|
selectedMenuIndex.value =
|
||||||
selectedMenuIndex.value >= props.menuItems.length - 1
|
selectedMenuIndex.value >= props.menuItems.length - 1
|
||||||
? 0
|
? 0
|
||||||
@@ -156,21 +178,31 @@ const handleKeyDown = (e) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const showToast = (message) => {
|
/**
|
||||||
|
* 显示 Toast 消息
|
||||||
|
* @param message 要显示的消息内容
|
||||||
|
*/
|
||||||
|
const showToast = (message: string): void => {
|
||||||
if (toastTimer) clearTimeout(toastTimer)
|
if (toastTimer) clearTimeout(toastTimer)
|
||||||
toastMessage.value = message
|
toastMessage.value = message
|
||||||
toastVisible.value = true
|
toastVisible.value = true
|
||||||
toastTimer = setTimeout(() => {
|
toastTimer = window.setTimeout(() => {
|
||||||
toastVisible.value = false
|
toastVisible.value = false
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生命周期钩子
|
// ===== 生命周期钩子 =====
|
||||||
|
/**
|
||||||
|
* 组件挂载时添加全局事件监听
|
||||||
|
*/
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', closeMenu)
|
document.addEventListener('click', closeMenu)
|
||||||
document.addEventListener('keydown', handleKeyDown)
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件卸载前清理事件监听和定时器
|
||||||
|
*/
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('click', closeMenu)
|
document.removeEventListener('click', closeMenu)
|
||||||
document.removeEventListener('keydown', handleKeyDown)
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
@@ -178,18 +210,30 @@ onBeforeUnmount(() => {
|
|||||||
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 过渡钩子
|
// ===== 过渡钩子 =====
|
||||||
const onBeforeLeave = (el) => {
|
/**
|
||||||
el.style.display = 'block'
|
* 过渡开始前的处理
|
||||||
|
* 确保元素在离开过渡开始时可见
|
||||||
|
* @param el 过渡元素
|
||||||
|
*/
|
||||||
|
const onBeforeLeave = (el: Element): void => {
|
||||||
|
(el as HTMLElement).style.display = 'block'
|
||||||
}
|
}
|
||||||
|
|
||||||
const onAfterLeave = (el) => {
|
/**
|
||||||
el.style.display = ''
|
* 过渡结束后的处理
|
||||||
|
* 重置元素的显示状态
|
||||||
|
* @param el 过渡元素
|
||||||
|
*/
|
||||||
|
const onAfterLeave = (el: Element): void => {
|
||||||
|
(el as HTMLElement).style.display = ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- 主容器,阻止默认右键菜单 -->
|
||||||
<div class="project-list-container" @contextmenu.prevent>
|
<div class="project-list-container" @contextmenu.prevent>
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<VirtualList
|
<VirtualList
|
||||||
:data="data"
|
:data="data"
|
||||||
@@ -202,6 +246,8 @@ const onAfterLeave = (el) => {
|
|||||||
@showToast="showToast"
|
@showToast="showToast"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具栏 -->
|
||||||
<div v-if="showToolbar" class="toolbar" :style="{ height: `${toolbarHeight}px` }">
|
<div v-if="showToolbar" class="toolbar" :style="{ height: `${toolbarHeight}px` }">
|
||||||
<div class="toolbar-content">
|
<div class="toolbar-content">
|
||||||
<div class="total-count">共 {{ data.length }} 项</div>
|
<div class="total-count">共 {{ data.length }} 项</div>
|
||||||
@@ -241,6 +287,8 @@ const onAfterLeave = (el) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast 消息 -->
|
||||||
<transition name="toast">
|
<transition name="toast">
|
||||||
<div v-if="toastVisible" class="toast">
|
<div v-if="toastVisible" class="toast">
|
||||||
{{ toastMessage }}
|
{{ toastMessage }}
|
||||||
@@ -250,27 +298,30 @@ const onAfterLeave = (el) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 保持原有样式,但将 .app-container 改为 .project-list-container */
|
/* 主容器:使用 flex 布局实现垂直方向的布局结构 */
|
||||||
.project-list-container {
|
.project-list-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 主内容区域:占用剩余空间并防止内容溢出 */
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0; /* 重要:防止内容溢出 */
|
min-height: 0; /* 关键:确保 flex 布局下内容不会溢出 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 工具栏:固定高度,顶部边框和阴影效果 */
|
||||||
.toolbar {
|
.toolbar {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-top: 1px solid #e8e8e8;
|
border-top: 1px solid #e8e8e8;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.05);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1; /* 确保阴影显示在内容之上 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 工具栏内容:水平布局,两端对齐 */
|
||||||
.toolbar-content {
|
.toolbar-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
@@ -278,15 +329,18 @@ const onAfterLeave = (el) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 总数显示:次要文本样式 */
|
||||||
.total-count {
|
.total-count {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 弹性间隔:用于推开左右两侧的内容 */
|
||||||
.toolbar-spacer {
|
.toolbar-spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 菜单触发器:圆形按钮样式 */
|
||||||
.menu-trigger {
|
.menu-trigger {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
@@ -300,16 +354,14 @@ const onAfterLeave = (el) => {
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-trigger:hover {
|
/* 菜单触发器悬停和激活状态 */
|
||||||
background-color: #f5f5f5;
|
.menu-trigger:hover,
|
||||||
color: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-trigger.active {
|
.menu-trigger.active {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 弹出菜单:浮动面板样式 */
|
||||||
.popup-menu {
|
.popup-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
@@ -323,11 +375,11 @@ const onAfterLeave = (el) => {
|
|||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px); /* 背景模糊效果 */
|
||||||
transform-origin: top right;
|
transform-origin: top right; /* 动画原点 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 添加 transition 相关样式 */
|
/* 菜单动画相关样式 */
|
||||||
.menu-enter-active,
|
.menu-enter-active,
|
||||||
.menu-leave-active {
|
.menu-leave-active {
|
||||||
transition: all 0.2s ease-out;
|
transition: all 0.2s ease-out;
|
||||||
@@ -345,12 +397,14 @@ const onAfterLeave = (el) => {
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 菜单分割线 */
|
||||||
.menu-divider {
|
.menu-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: rgba(0, 0, 0, 0.06);
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 菜单项:交互式列表项样式 */
|
||||||
.menu-item {
|
.menu-item {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
@@ -364,17 +418,21 @@ const onAfterLeave = (el) => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item:hover, .menu-item-selected {
|
/* 菜单项悬停和选中状态 */
|
||||||
|
.menu-item:hover,
|
||||||
|
.menu-item-selected {
|
||||||
background-color: rgba(79, 70, 229, 0.06);
|
background-color: rgba(79, 70, 229, 0.06);
|
||||||
color: #4f46e5;
|
color: #4f46e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 选中项信息区域 */
|
||||||
.selected-item-info {
|
.selected-item-info {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin: 0 4px 4px 4px;
|
margin: 0 4px 4px 4px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 选中项名称 */
|
||||||
.selected-item-name {
|
.selected-item-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -382,6 +440,7 @@ const onAfterLeave = (el) => {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 选中项路径 */
|
||||||
.selected-item-path {
|
.selected-item-path {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -389,7 +448,7 @@ const onAfterLeave = (el) => {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 添加 Toast 相关样式 */
|
/* Toast 消息容器:居中显示的浮动提示 */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -401,11 +460,12 @@ const onAfterLeave = (el) => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: none;
|
pointer-events: none; /* 防止干扰鼠标事件 */
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toast 动画相关样式 */
|
||||||
.toast-enter-active,
|
.toast-enter-active,
|
||||||
.toast-leave-active {
|
.toast-leave-active {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
@@ -414,6 +474,6 @@ const onAfterLeave = (el) => {
|
|||||||
.toast-enter-from,
|
.toast-enter-from,
|
||||||
.toast-leave-to {
|
.toast-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translate(-50%, -40%);
|
transform: translate(-50%, -40%); /* 添加垂直位移效果 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,63 +1,45 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import type { ListItem, ListConfig } from '@/types'
|
||||||
|
|
||||||
const props = defineProps({
|
interface Props {
|
||||||
// 列表数据
|
data: ListItem[]
|
||||||
data: {
|
config: ListConfig
|
||||||
type: Array,
|
height?: string | number
|
||||||
required: true
|
frozen?: boolean
|
||||||
},
|
disableHover?: boolean
|
||||||
// 配置参数
|
|
||||||
config: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
itemHeight: 50, // 列表项基础高度
|
|
||||||
itemPadding: 16, // 列表项上下padding总和
|
|
||||||
bufferCount: 2, // 上下缓冲区域的项目数量
|
|
||||||
scrollDebounceTime: 16 // 滚动防抖时间
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// 容器高度
|
|
||||||
height: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: '100%'
|
|
||||||
},
|
|
||||||
frozen: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
disableHover: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
height: '100%',
|
||||||
|
frozen: false,
|
||||||
|
disableHover: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['select', 'click', 'contextmenu', 'showToast'])
|
const emit = defineEmits<{
|
||||||
|
select: [item: ListItem]
|
||||||
|
click: [item: ListItem]
|
||||||
|
contextmenu: [data: { item: ListItem }]
|
||||||
|
showToast: [message: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
// 计算列表项实际总高度
|
// 计算列表项实际总高度
|
||||||
const totalItemHeight = computed(() =>
|
const totalItemHeight = computed(() =>
|
||||||
props.config.itemHeight + props.config.itemPadding
|
props.config.itemHeight + props.config.itemPadding
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
// 响应式状态管理
|
||||||
* 响应式状态管理
|
const containerHeight = ref<number>(0)
|
||||||
*/
|
const startIndex = ref<number>(0)
|
||||||
const containerHeight = ref(0) // 容器高度
|
const selectedIndex = ref<number>(0)
|
||||||
const startIndex = ref(0) // 可视区域起始索引
|
const isKeyboardNavigating = ref<boolean>(false)
|
||||||
const selectedIndex = ref(0) // 当前选中项索引
|
let keyboardTimer: number | null = null
|
||||||
const isKeyboardNavigating = ref(false) // 键盘导航状态
|
|
||||||
let keyboardTimer = null // 键盘导航定时器
|
|
||||||
|
|
||||||
/**
|
// DOM 引用
|
||||||
* DOM 引用
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
*/
|
const listRef = ref<HTMLElement | null>(null)
|
||||||
const containerRef = ref(null)
|
|
||||||
const listRef = ref(null)
|
|
||||||
|
|
||||||
/**
|
// 计算可视区域能显示的列表项数量
|
||||||
* 计算属性
|
|
||||||
*/
|
|
||||||
// 计算可区域能显示的列表项数量
|
|
||||||
const visibleCount = computed(() =>
|
const visibleCount = computed(() =>
|
||||||
Math.floor(containerHeight.value / totalItemHeight.value)
|
Math.floor(containerHeight.value / totalItemHeight.value)
|
||||||
)
|
)
|
||||||
@@ -86,64 +68,79 @@ const phantomHeight = computed(() =>
|
|||||||
props.data.length * totalItemHeight.value
|
props.data.length * totalItemHeight.value
|
||||||
)
|
)
|
||||||
|
|
||||||
// ... 其他方法保持不变,只需将 CONFIG 替换为 props.config ...
|
|
||||||
|
|
||||||
// 选择处理
|
// 选择处理
|
||||||
const handleSelect = (item) => {
|
const handleSelect = (item: ListItem): void => {
|
||||||
if (props.frozen) return // 如果列表被冻结,不响应选择
|
if (props.frozen) return
|
||||||
selectedIndex.value = item.index
|
selectedIndex.value = item.index ?? 0
|
||||||
emit('select', item)
|
emit('select', item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击处理
|
/**
|
||||||
const handleClick = (item) => {
|
* 处理列表项点击
|
||||||
if (props.frozen) return // 如果列表被冻结,不响应点击
|
* @param item 点击的列表项
|
||||||
handleSelect(item) // 点击时同时触发选中
|
*/
|
||||||
|
const handleClick = (item: ListItem): void => {
|
||||||
|
if (props.frozen) return
|
||||||
|
handleSelect(item)
|
||||||
emit('click', item)
|
emit('click', item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改更新容器高度的方法
|
/**
|
||||||
function updateContainerHeight() {
|
* 更新容器高度
|
||||||
|
* 从父元素或窗口获取实际高度
|
||||||
|
*/
|
||||||
|
function updateContainerHeight(): void {
|
||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
const parentHeight = containerRef.value.parentElement?.clientHeight
|
const parentHeight = containerRef.value.parentElement?.clientHeight
|
||||||
containerHeight.value = parentHeight || window.innerHeight
|
containerHeight.value = parentHeight || window.innerHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加 ResizeObserver 来监听父容器尺寸变化
|
/** ResizeObserver 实例,用于监听容器尺寸变化 */
|
||||||
let resizeObserver = null
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
// 添加防抖函数
|
/**
|
||||||
function debounce(fn, delay) {
|
* 防抖函数
|
||||||
let timer = null
|
* 在指定延迟后执行函数,如果在延迟期间再次调用则重置延迟
|
||||||
return function (...args) {
|
* @param fn 需要防抖的函数
|
||||||
|
* @param delay 延迟时间(毫秒)
|
||||||
|
*/
|
||||||
|
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): (...args: Parameters<T>) => void {
|
||||||
|
let timer: number | null = null
|
||||||
|
return function (this: any, ...args: Parameters<T>) {
|
||||||
if (timer) clearTimeout(timer)
|
if (timer) clearTimeout(timer)
|
||||||
timer = setTimeout(() => {
|
timer = window.setTimeout(() => {
|
||||||
fn.apply(this, args)
|
fn.apply(this, args)
|
||||||
}, delay)
|
}, delay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动处理
|
/**
|
||||||
|
* 处理滚动事件(已防抖)
|
||||||
|
* 根据滚动位置更新可视区域的起始索引
|
||||||
|
*/
|
||||||
const handleScroll = debounce(() => {
|
const handleScroll = debounce(() => {
|
||||||
if (!containerRef.value) return
|
if (!containerRef.value) return
|
||||||
const scrollTop = containerRef.value.scrollTop
|
const scrollTop = containerRef.value.scrollTop
|
||||||
// 计算新的起始索引
|
|
||||||
const newStartIndex = Math.floor(scrollTop / totalItemHeight.value)
|
const newStartIndex = Math.floor(scrollTop / totalItemHeight.value)
|
||||||
|
|
||||||
// 只有当起始索引发生变化时才更新
|
|
||||||
if (newStartIndex !== startIndex.value) {
|
if (newStartIndex !== startIndex.value) {
|
||||||
startIndex.value = newStartIndex
|
startIndex.value = newStartIndex
|
||||||
}
|
}
|
||||||
}, props.config.scrollDebounceTime)
|
}, props.config.scrollDebounceTime)
|
||||||
|
|
||||||
// 添加一个状态来控制是否禁用鼠标悬浮
|
/** 鼠标悬停禁用状态 */
|
||||||
const disableHover = ref(false)
|
const disableHover = ref<boolean>(false)
|
||||||
let hoverTimer = null
|
/** 鼠标悬停禁用定时器 */
|
||||||
|
let hoverTimer: number | null = null
|
||||||
|
|
||||||
// 修改鼠标移入处理函数
|
/**
|
||||||
function handleMouseEnter(index) {
|
* 处理鼠标移入事件
|
||||||
if (props.frozen || disableHover.value || props.disableHover) return // 增加 props.disableHover 的判断
|
* 在非键盘导航状态下更新选中项
|
||||||
|
* @param index 目标项的索引
|
||||||
|
*/
|
||||||
|
function handleMouseEnter(index: number): void {
|
||||||
|
if (props.frozen || disableHover.value || props.disableHover) return
|
||||||
if (!isKeyboardNavigating.value) {
|
if (!isKeyboardNavigating.value) {
|
||||||
selectedIndex.value = index
|
selectedIndex.value = index
|
||||||
emit('select', { ...props.data[index], index })
|
emit('select', { ...props.data[index], index })
|
||||||
@@ -151,14 +148,21 @@ function handleMouseEnter(index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加记录上次到达边界的时间
|
/**
|
||||||
const lastReachBoundaryTime = ref({
|
* 记录到达列表边界的时间
|
||||||
|
* 用于实现边界循环导航的延迟判断
|
||||||
|
*/
|
||||||
|
const lastReachBoundaryTime = ref<{ top: number; bottom: number }>({
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0
|
bottom: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// 修改键盘事件处理函数
|
/**
|
||||||
function handleKeyDown(e) {
|
* 处理键盘事件
|
||||||
|
* 实现键盘导航、边界循环和项目选择功能
|
||||||
|
* @param e 键盘事件对象
|
||||||
|
*/
|
||||||
|
function handleKeyDown(e: KeyboardEvent): void {
|
||||||
if (props.frozen) return
|
if (props.frozen) return
|
||||||
|
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
@@ -176,14 +180,12 @@ function handleKeyDown(e) {
|
|||||||
if (e.key === 'ArrowUp' && isFirstItem) {
|
if (e.key === 'ArrowUp' && isFirstItem) {
|
||||||
const timeSinceLastTop = now - lastReachBoundaryTime.value.top
|
const timeSinceLastTop = now - lastReachBoundaryTime.value.top
|
||||||
if (timeSinceLastTop < 5000) {
|
if (timeSinceLastTop < 5000) {
|
||||||
// 5秒内再次点击,跳转到底部
|
disableHover.value = true
|
||||||
disableHover.value = true // 禁用鼠标悬浮
|
|
||||||
selectedIndex.value = props.data.length - 1
|
selectedIndex.value = props.data.length - 1
|
||||||
emit('select', { ...props.data[selectedIndex.value], index: selectedIndex.value })
|
emit('select', { ...props.data[selectedIndex.value], index: selectedIndex.value })
|
||||||
ensureSelectedItemVisible()
|
ensureSelectedItemVisible()
|
||||||
// 1秒后恢复鼠标悬浮功能
|
|
||||||
if (hoverTimer) clearTimeout(hoverTimer)
|
if (hoverTimer) clearTimeout(hoverTimer)
|
||||||
hoverTimer = setTimeout(() => {
|
hoverTimer = window.setTimeout(() => {
|
||||||
disableHover.value = false
|
disableHover.value = false
|
||||||
}, 1000)
|
}, 1000)
|
||||||
} else {
|
} else {
|
||||||
@@ -196,14 +198,12 @@ function handleKeyDown(e) {
|
|||||||
if (e.key === 'ArrowDown' && isLastItem) {
|
if (e.key === 'ArrowDown' && isLastItem) {
|
||||||
const timeSinceLastBottom = now - lastReachBoundaryTime.value.bottom
|
const timeSinceLastBottom = now - lastReachBoundaryTime.value.bottom
|
||||||
if (timeSinceLastBottom < 5000) {
|
if (timeSinceLastBottom < 5000) {
|
||||||
// 5秒内再次点击,跳转到顶部
|
disableHover.value = true
|
||||||
disableHover.value = true // 禁用鼠标悬浮
|
|
||||||
selectedIndex.value = 0
|
selectedIndex.value = 0
|
||||||
emit('select', { ...props.data[selectedIndex.value], index: selectedIndex.value })
|
emit('select', { ...props.data[selectedIndex.value], index: selectedIndex.value })
|
||||||
ensureSelectedItemVisible()
|
ensureSelectedItemVisible()
|
||||||
// 1秒后恢复鼠标悬浮功能
|
|
||||||
if (hoverTimer) clearTimeout(hoverTimer)
|
if (hoverTimer) clearTimeout(hoverTimer)
|
||||||
hoverTimer = setTimeout(() => {
|
hoverTimer = window.setTimeout(() => {
|
||||||
disableHover.value = false
|
disableHover.value = false
|
||||||
}, 1000)
|
}, 1000)
|
||||||
} else {
|
} else {
|
||||||
@@ -213,7 +213,6 @@ function handleKeyDown(e) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原有的上下键处理逻辑
|
|
||||||
isKeyboardNavigating.value = true
|
isKeyboardNavigating.value = true
|
||||||
if (keyboardTimer) clearTimeout(keyboardTimer)
|
if (keyboardTimer) clearTimeout(keyboardTimer)
|
||||||
|
|
||||||
@@ -223,7 +222,7 @@ function handleKeyDown(e) {
|
|||||||
emit('select', { ...props.data[nextIndex], index: nextIndex })
|
emit('select', { ...props.data[nextIndex], index: nextIndex })
|
||||||
|
|
||||||
const itemTop = nextIndex * totalItemHeight.value
|
const itemTop = nextIndex * totalItemHeight.value
|
||||||
if (itemTop < containerRef.value?.scrollTop && containerRef.value) {
|
if (itemTop < (containerRef.value?.scrollTop ?? 0) && containerRef.value) {
|
||||||
containerRef.value.scrollTop = itemTop
|
containerRef.value.scrollTop = itemTop
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -232,51 +231,57 @@ function handleKeyDown(e) {
|
|||||||
emit('select', { ...props.data[nextIndex], index: nextIndex })
|
emit('select', { ...props.data[nextIndex], index: nextIndex })
|
||||||
|
|
||||||
const itemBottom = (nextIndex + 1) * totalItemHeight.value
|
const itemBottom = (nextIndex + 1) * totalItemHeight.value
|
||||||
const scrollBottom = (containerRef.value?.scrollTop || 0) + containerHeight.value
|
const scrollBottom = (containerRef.value?.scrollTop ?? 0) + containerHeight.value
|
||||||
if (itemBottom > scrollBottom && containerRef.value) {
|
if (itemBottom > scrollBottom && containerRef.value) {
|
||||||
containerRef.value.scrollTop = itemBottom - containerHeight.value
|
containerRef.value.scrollTop = itemBottom - containerHeight.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyboardTimer = setTimeout(() => {
|
keyboardTimer = window.setTimeout(() => {
|
||||||
isKeyboardNavigating.value = false
|
isKeyboardNavigating.value = false
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保选项可见
|
/**
|
||||||
function ensureSelectedItemVisible() {
|
* 确保选中项在可视区域内
|
||||||
|
* 如果选中项不可见,则滚动到合适位置
|
||||||
|
*/
|
||||||
|
function ensureSelectedItemVisible(): void {
|
||||||
if (!containerRef.value) return
|
if (!containerRef.value) return
|
||||||
|
|
||||||
const containerTop = containerRef.value.scrollTop
|
const containerTop = containerRef.value.scrollTop
|
||||||
const containerHeight = containerRef.value.clientHeight
|
const containerHeight = containerRef.value.clientHeight
|
||||||
|
|
||||||
// 计算项目的顶部和底部位置
|
|
||||||
const itemTop = selectedIndex.value * totalItemHeight.value
|
const itemTop = selectedIndex.value * totalItemHeight.value
|
||||||
const itemBottom = (selectedIndex.value + 1) * totalItemHeight.value
|
const itemBottom = (selectedIndex.value + 1) * totalItemHeight.value
|
||||||
const scrollBottom = containerTop + containerHeight
|
const scrollBottom = containerTop + containerHeight
|
||||||
|
|
||||||
if (itemTop < containerTop) {
|
if (itemTop < containerTop) {
|
||||||
// 如果项目顶部超出可视区域,滚动到顶部对齐
|
|
||||||
containerRef.value.scrollTop = itemTop
|
containerRef.value.scrollTop = itemTop
|
||||||
} else if (itemBottom > scrollBottom) {
|
} else if (itemBottom > scrollBottom) {
|
||||||
// 如果项目底部超出可视区域,滚动到底部对齐
|
|
||||||
containerRef.value.scrollTop = itemBottom - containerHeight
|
containerRef.value.scrollTop = itemBottom - containerHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改右键点击处理函数
|
/**
|
||||||
const handleContextMenu = (e, item) => {
|
* 处理右键点击事件
|
||||||
if (props.frozen) return // 如果列表被冻结,不响应右键点击
|
* @param e 鼠标事件对象
|
||||||
e.preventDefault() // 阻止默认右键菜单
|
* @param item 目标列表项
|
||||||
emit('contextmenu', { item }) // 不需要传递事件对象
|
*/
|
||||||
|
const handleContextMenu = (e: MouseEvent, item: ListItem): void => {
|
||||||
|
if (props.frozen) return
|
||||||
|
e.preventDefault()
|
||||||
|
emit('contextmenu', { item })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 生命周期钩子 =====
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
|
// 添加滚动事件监听
|
||||||
containerRef.value.addEventListener('scroll', handleScroll)
|
containerRef.value.addEventListener('scroll', handleScroll)
|
||||||
|
|
||||||
// 创建 ResizeObserver 监听父容器尺寸变化
|
// 创建并启用 ResizeObserver
|
||||||
resizeObserver = new ResizeObserver(() => {
|
resizeObserver = new ResizeObserver(() => {
|
||||||
updateContainerHeight()
|
updateContainerHeight()
|
||||||
})
|
})
|
||||||
@@ -286,34 +291,32 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化容器高度
|
||||||
updateContainerHeight()
|
updateContainerHeight()
|
||||||
|
// 添加全局事件监听
|
||||||
window.addEventListener('resize', updateContainerHeight)
|
window.addEventListener('resize', updateContainerHeight)
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
// 清理所有事件监听和定时器
|
||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
containerRef.value.removeEventListener('scroll', handleScroll)
|
containerRef.value.removeEventListener('scroll', handleScroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理 ResizeObserver
|
|
||||||
if (resizeObserver) {
|
if (resizeObserver) {
|
||||||
resizeObserver.disconnect()
|
resizeObserver.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener('resize', updateContainerHeight)
|
window.removeEventListener('resize', updateContainerHeight)
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
if (keyboardTimer) {
|
if (keyboardTimer) clearTimeout(keyboardTimer)
|
||||||
clearTimeout(keyboardTimer)
|
if (hoverTimer) clearTimeout(hoverTimer)
|
||||||
}
|
|
||||||
if (hoverTimer) {
|
|
||||||
clearTimeout(hoverTimer)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- 虚拟列表容器 -->
|
||||||
<div
|
<div
|
||||||
ref="containerRef"
|
ref="containerRef"
|
||||||
class="virtual-list-container"
|
class="virtual-list-container"
|
||||||
@@ -321,15 +324,18 @@ onBeforeUnmount(() => {
|
|||||||
:style="{ height: '100%' }"
|
:style="{ height: '100%' }"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
<!-- 虚拟高度占位元素 -->
|
||||||
<div
|
<div
|
||||||
ref="listRef"
|
ref="listRef"
|
||||||
class="virtual-list-phantom"
|
class="virtual-list-phantom"
|
||||||
:style="{ height: `${phantomHeight}px` }"
|
:style="{ height: `${phantomHeight}px` }"
|
||||||
>
|
>
|
||||||
|
<!-- 实际渲染的列表内容 -->
|
||||||
<div
|
<div
|
||||||
class="virtual-list"
|
class="virtual-list"
|
||||||
:style="{ transform: `translateY(${offsetY}px)` }"
|
:style="{ transform: `translateY(${offsetY}px)` }"
|
||||||
>
|
>
|
||||||
|
<!-- 列表项 -->
|
||||||
<div
|
<div
|
||||||
v-for="item in visibleData"
|
v-for="item in visibleData"
|
||||||
:key="item.index"
|
:key="item.index"
|
||||||
@@ -343,14 +349,18 @@ onBeforeUnmount(() => {
|
|||||||
<slot name="item" :item="item">
|
<slot name="item" :item="item">
|
||||||
<!-- 默认列表项样式 -->
|
<!-- 默认列表项样式 -->
|
||||||
<div class="default-item">
|
<div class="default-item">
|
||||||
|
<!-- 项目图标 -->
|
||||||
<div class="item-icon">
|
<div class="item-icon">
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||||
<circle cx="12" cy="12" r="10" fill="#1a1a1a" />
|
<circle cx="12" cy="12" r="10" fill="#1a1a1a" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 项目内容 -->
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
|
<!-- 标题行:名称和标签 -->
|
||||||
<div class="item-header">
|
<div class="item-header">
|
||||||
<div class="item-name">{{ item.name }}</div>
|
<div class="item-name">{{ item.name }}</div>
|
||||||
|
<!-- 标签列表 -->
|
||||||
<div class="item-tags" v-if="item.tags?.length">
|
<div class="item-tags" v-if="item.tags?.length">
|
||||||
<span
|
<span
|
||||||
v-for="tag in item.tags"
|
v-for="tag in item.tags"
|
||||||
@@ -362,6 +372,7 @@ onBeforeUnmount(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 路径信息 -->
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
<div class="item-path">{{ item.path }}</div>
|
<div class="item-path">{{ item.path }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -375,6 +386,7 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 虚拟列表容器:可滚动区域 */
|
||||||
.virtual-list-container {
|
.virtual-list-container {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
@@ -384,11 +396,13 @@ onBeforeUnmount(() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 虚拟高度容器:用于保持滚动条比例 */
|
||||||
.virtual-list-phantom {
|
.virtual-list-phantom {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 实际列表内容容器:通过 transform 实现位置偏移 */
|
||||||
.virtual-list {
|
.virtual-list {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -396,6 +410,7 @@ onBeforeUnmount(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 列表项基础样式 */
|
||||||
.list-item {
|
.list-item {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -405,12 +420,12 @@ onBeforeUnmount(() => {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 简单的选中高亮效果 */
|
/* 选中状态样式 */
|
||||||
.list-item.selected {
|
.list-item.selected {
|
||||||
background-color: rgba(64, 169, 255, 0.15);
|
background-color: rgba(64, 169, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 默认列表项样式 */
|
/* 列表项布局容器 */
|
||||||
.default-item {
|
.default-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -418,6 +433,7 @@ onBeforeUnmount(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 图标容器 */
|
||||||
.item-icon {
|
.item-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@@ -427,6 +443,7 @@ onBeforeUnmount(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 内容区域布局 */
|
||||||
.item-content {
|
.item-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -435,6 +452,7 @@ onBeforeUnmount(() => {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标题行布局 */
|
||||||
.item-header {
|
.item-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -442,6 +460,7 @@ onBeforeUnmount(() => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 项目名称样式 */
|
||||||
.item-name {
|
.item-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
@@ -453,11 +472,13 @@ onBeforeUnmount(() => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 信息行布局 */
|
||||||
.item-info {
|
.item-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 路径文本样式 */
|
||||||
.item-path {
|
.item-path {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -467,6 +488,7 @@ onBeforeUnmount(() => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标签容器样式 */
|
||||||
.item-tags {
|
.item-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -475,6 +497,7 @@ onBeforeUnmount(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 单个标签样式 */
|
||||||
.item-tag {
|
.item-tag {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
@@ -484,10 +507,10 @@ onBeforeUnmount(() => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 添加冻结状态的样式 */
|
/* 列表冻结状态样式 */
|
||||||
.list-frozen {
|
.list-frozen {
|
||||||
pointer-events: none; /* 禁用鼠标事件 */
|
pointer-events: none; /* 禁用鼠标事件 */
|
||||||
opacity: 0.7; /* 降低透明度表示冻结状态 */
|
opacity: 0.7; /* 降低透明度 */
|
||||||
user-select: none; /* 禁用文本选择 */
|
user-select: none; /* 禁用文本选择 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
7
src/env.d.ts
vendored
Normal file
7
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { createApp } from 'vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
|
||||||
6
src/main.ts
Normal file
6
src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import type { App as AppType } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const app: AppType = createApp(App)
|
||||||
|
app.mount('#app')
|
||||||
53
src/types/index.ts
Normal file
53
src/types/index.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 标签信息接口
|
||||||
|
*/
|
||||||
|
export interface Tag {
|
||||||
|
/** 标签唯一标识 */
|
||||||
|
id: number
|
||||||
|
/** 标签名称 */
|
||||||
|
name: string
|
||||||
|
/** 标签颜色(十六进制颜色值) */
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表项数据接口
|
||||||
|
*/
|
||||||
|
export interface ListItem {
|
||||||
|
/** 项目唯一标识 */
|
||||||
|
id: number
|
||||||
|
/** 项目名称 */
|
||||||
|
name: string
|
||||||
|
/** 项目路径 */
|
||||||
|
path: string
|
||||||
|
/** 项目标签列表 */
|
||||||
|
tags?: Tag[]
|
||||||
|
/** 在虚拟列表中的索引位置 */
|
||||||
|
index?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 菜单项接口
|
||||||
|
*/
|
||||||
|
export interface MenuItem {
|
||||||
|
/** 菜单项唯一标识 */
|
||||||
|
id: string
|
||||||
|
/** 菜单项显示文本 */
|
||||||
|
label: string
|
||||||
|
/** 菜单项点击处理函数,接收当前选中的列表项作为参数 */
|
||||||
|
action: (item: ListItem | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 虚拟列表配置接口
|
||||||
|
*/
|
||||||
|
export interface ListConfig {
|
||||||
|
/** 列表项基础高度(像素) */
|
||||||
|
itemHeight: number
|
||||||
|
/** 列表项内边距总和(像素) */
|
||||||
|
itemPadding: number
|
||||||
|
/** 上下缓冲区域的项目数量 */
|
||||||
|
bufferCount: number
|
||||||
|
/** 滚动防抖时间(毫秒) */
|
||||||
|
scrollDebounceTime: number
|
||||||
|
}
|
||||||
37
tsconfig.json
Normal file
37
tsconfig.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
14
tsconfig.node.json
Normal file
14
tsconfig.node.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user