初始提交

This commit is contained in:
2024-12-11 22:53:46 +08:00
commit 3d8e041fd6
32 changed files with 4098 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# uTools虚拟列表实现
模版插件的自定义实现,用于扩展模版插件的可能性

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/fox.png" type="image/png">
<title>utools-recent-projects-next</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "utools-recent-projects-next",
"private": true,
"version": "4.0.0",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"licia": "^1.46.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/node": "^22.10.1",
"@vitejs/plugin-vue": "^5.2.1",
"sass-embedded": "^1.82.0",
"typescript": "^5.0.2",
"utools-api-types": "^5.2.0",
"vite": "^6.0.1"
}
}

1060
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

BIN
public/fox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

26
public/plugin.json Normal file
View File

@@ -0,0 +1,26 @@
{
"pluginName": "书签和历史记录 Next",
"author": "lanyuanxiaoyao",
"homepage": "http://lanyuanxiaoyao.com",
"description": "快速查找打开书签和历史记录,支持 vscodesublimejetbrainsAndroid StudioGoLandIDEAPyCharmWebstorm等系列WPSOfficeWordExcelPowerpointLibreOfficeXcodeChromeFirefoxSafariEdgeOperaBraveYandex等主流浏览器",
"version": "4.0.0",
"logo": "fox.png",
"preload": "preload.js",
"pluginSetting": {
"single": true
},
"development":{
"main": "http://localhost:5173/index.html"
},
"features": [
{
"code": "setting",
"explain": "「书签与历史记录」插件配置界面",
"icon": "fox.png",
"cmds": [
"Setting",
"设置"
]
}
]
}

0
public/preload.js Normal file
View File

10
src/App.vue Normal file
View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/config')
</script>
<template>
<router-view />
</template>

View File

@@ -0,0 +1,558 @@
<script setup lang="ts">
import VirtualList from './VirtualList.vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import type { ListItem, MenuItem } from '@/types'
/**
* 组件属性接口
*/
interface Props {
/** 列表数据 */
data: ListItem[]
/** 菜单项配置 */
menuItems: MenuItem[]
}
const props = withDefaults(defineProps<Props>(), {})
// ===== 工具栏配置常量 =====
/** 是否显示工具栏 */
const SHOW_TOOLBAR = true
/** 工具栏高度(像素) */
const TOOLBAR_HEIGHT = 40
/**
* 组件事件定义
*/
const emit = defineEmits<{
/** 选中项变化事件 */
select: [item: ListItem]
/** 点击项目事件 */
click: [item: ListItem]
/** 右键菜单事件 */
'context-menu': [data: { item: ListItem }]
}>()
// ===== 状态管理 =====
/** 当前选中的列表项 */
const selectedItem = ref<ListItem | null>(null)
/** 菜单显示状态 */
const showMenu = ref<boolean>(false)
/** 列表冻结状态 */
const listFrozen = ref<boolean>(false)
/** 临时禁用鼠标悬停状态 */
const temporaryDisableHover = ref<boolean>(false)
/** 当前选中的菜单项索引 */
const selectedMenuIndex = ref<number>(-1)
/** Toast 显示状态 */
const toastVisible = ref<boolean>(false)
/** Toast 消息内容 */
const toastMessage = ref<string>('')
// ===== 定时器 =====
/** 鼠标悬停禁用定时器 */
let hoverDisableTimer: number | null = null
/** Toast 显示定时器 */
let toastTimer: number | null = null
// ===== 事件处理函数 =====
/**
* 处理列表项选中
* @param item 选中的列表项
*/
const handleSelect = (item: ListItem): void => {
selectedItem.value = item
emit('select', item)
}
/**
* 处理列表项点击
* @param item 点击的列表项
*/
const handleClick = (item: ListItem): void => {
emit('click', item)
}
/**
* 显示右键菜单
* @param data 包含目标列表项的数据对象
*/
const showContextMenu = (data: { item: ListItem }): void => {
const { item } = data
handleSelect(item)
showMenu.value = true
listFrozen.value = true
emit('context-menu', data)
}
/**
* 关闭菜单并处理相关状态
*/
const closeMenu = (): void => {
showMenu.value = false
listFrozen.value = false
selectedMenuIndex.value = -1
// 临时禁用鼠标悬停,防止菜单关闭时立即触发悬停效果
temporaryDisableHover.value = true
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
hoverDisableTimer = window.setTimeout(() => {
temporaryDisableHover.value = false
}, 300)
}
/**
* 处理菜单项点击
* @param item 被点击的菜单项
*/
const handleMenuClick = (item: MenuItem): void => {
item.action(selectedItem.value)
closeMenu()
}
/**
* 处理菜单触发器点击
* 控制菜单的显示/隐藏状态
* @param e 鼠标事件对象
*/
const handleMenuTriggerClick = (e: MouseEvent): void => {
e.stopPropagation()
if (showMenu.value) {
closeMenu()
} else {
if (props.data.length > 0) {
// 如果有数据,选择当前选中项或第一项
const itemToSelect = selectedItem.value || props.data[0]
showContextMenu({ item: itemToSelect })
} else {
// 如果没有数据,仅显示菜单
showMenu.value = true
listFrozen.value = true
}
}
}
/**
* 处理键盘事件
* - 右方向键:打开菜单
* - 左方向键/ESC关闭菜单
* - 上下方向键:在菜单打开时导航菜单项
* - 回车键:在菜单打开时选择当前菜单项
* @param e 键盘事件对象
*/
const handleKeyDown = (e: KeyboardEvent): void => {
if (e.key === 'ArrowRight' && !showMenu.value && selectedItem.value) {
e.preventDefault()
handleMenuTriggerClick(new MouseEvent('click'))
} else if ((e.key === 'ArrowLeft' || e.key === 'Escape') && showMenu.value) {
e.preventDefault()
closeMenu()
} else if (showMenu.value) {
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
// 向上选择菜单项,到顶部时循环到底部
selectedMenuIndex.value =
selectedMenuIndex.value <= 0
? props.menuItems.length - 1
: selectedMenuIndex.value - 1
break
case 'ArrowDown':
e.preventDefault()
// 向下选择菜单项,到底部时循环到顶部
selectedMenuIndex.value =
selectedMenuIndex.value >= props.menuItems.length - 1
? 0
: selectedMenuIndex.value + 1
break
case 'Enter':
e.preventDefault()
if (selectedMenuIndex.value >= 0) {
handleMenuClick(props.menuItems[selectedMenuIndex.value])
}
break
}
}
}
/**
* 显示 Toast 消息
* @param message 要显示的消息内容
*/
const showToast = (message: string): void => {
if (toastTimer) clearTimeout(toastTimer)
toastMessage.value = message
toastVisible.value = true
toastTimer = window.setTimeout(() => {
toastVisible.value = false
}, 3000)
}
// ===== 生命周期钩子 =====
/**
* 组件挂载时添加全局事件监听
*/
onMounted(() => {
document.addEventListener('click', closeMenu)
document.addEventListener('keydown', handleKeyDown)
})
/**
* 组件卸载前清理事件监听和定时器
*/
onBeforeUnmount(() => {
document.removeEventListener('click', closeMenu)
document.removeEventListener('keydown', handleKeyDown)
if (toastTimer) clearTimeout(toastTimer)
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
})
// ===== 过渡钩子 =====
/**
* 过渡开始前的处理
* 确保元素在离开过渡开始时可见
* @param el 过渡元素
*/
const onBeforeLeave = (el: Element): void => {
(el as HTMLElement).style.display = 'block'
}
/**
* 过渡结束后的处理
* 重置元素的显示状态
* @param el 过渡元素
*/
const onAfterLeave = (el: Element): void => {
(el as HTMLElement).style.display = ''
}
</script>
<template>
<!-- 主容器阻止默认右键菜单 -->
<div class="project-list-container" @contextmenu.prevent>
<!-- 主要内容区域 -->
<div class="main-content">
<VirtualList
:data="data"
:frozen="listFrozen"
:disable-hover="showMenu || temporaryDisableHover"
@select="handleSelect"
@click="handleClick"
@contextmenu="showContextMenu"
@showToast="showToast"
/>
</div>
<!-- 工具栏 -->
<div v-if="SHOW_TOOLBAR" class="toolbar" :style="{ height: `${TOOLBAR_HEIGHT}px` }">
<div class="toolbar-content">
<div class="total-count"> {{ data.length }} </div>
<div class="toolbar-spacer"></div>
<div
class="menu-trigger"
:class="{ active: showMenu }"
@click="handleMenuTriggerClick"
>
<svg viewBox="0 0 24 24" width="16" height="16">
<circle cx="12" cy="6" r="2" fill="currentColor" />
<circle cx="12" cy="12" r="2" fill="currentColor" />
<circle cx="12" cy="18" r="2" fill="currentColor" />
</svg>
<transition
name="menu"
@before-leave="onBeforeLeave"
@after-leave="onAfterLeave"
>
<div v-if="showMenu" class="popup-menu" @click.stop>
<div v-if="selectedItem" class="selected-item-info">
<div class="selected-item-header">
<div class="selected-item-icon">
<img
v-if="selectedItem.icon"
:src="selectedItem.icon"
:alt="selectedItem.name"
width="24"
height="24"
>
<svg v-else viewBox="0 0 24 24" width="16" height="16">
<circle cx="12" cy="12" r="10" fill="rgba(0, 0, 0, 0.38)" />
</svg>
</div>
<div class="selected-item-name">{{ selectedItem.name }}</div>
</div>
<div class="selected-item-content">
<div class="selected-item-description">{{ selectedItem.description }}</div>
<div class="selected-item-tags" v-if="selectedItem.tags?.length">
<span
v-for="tag in selectedItem.tags"
:key="tag.id"
class="selected-item-tag"
:style="{ backgroundColor: tag.color }"
>
{{ tag.name }}
</span>
</div>
</div>
</div>
<div class="menu-divider"></div>
<div
v-for="(item, index) in menuItems"
:key="item.id"
class="menu-item"
:class="{ 'menu-item-selected': index === selectedMenuIndex }"
@click="handleMenuClick(item)"
>
{{ item.label }}
</div>
</div>
</transition>
</div>
</div>
</div>
<!-- Toast 消息 -->
<transition name="toast">
<div v-if="toastVisible" class="toast">
{{ toastMessage }}
</div>
</transition>
</div>
</template>
<style scoped>
/* 主容器:使用 flex 布局实现垂直方向的布局结构 */
.project-list-container {
height: 100%;
display: flex;
flex-direction: column;
}
/* 主内容区域:占用剩余空间并防止内容溢出 */
.main-content {
flex: 1;
min-height: 0;
}
/* 工具栏:固定高度,顶部边框和阴影效果 */
.toolbar {
height: 40px;
background-color: #fff;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 1;
}
/* 工具栏内容:水平布局,两端对齐 */
.toolbar-content {
height: 100%;
padding: 0 16px;
display: flex;
align-items: center;
}
/* 总数显示:次要文本样式 */
.total-count {
font-size: 13px;
color: rgba(0, 0, 0, 0.6);
}
/* 弹性间隔:用于推开左右两侧的内容 */
.toolbar-spacer {
flex: 1;
}
/* 菜单触发器:圆形按钮样式 */
.menu-trigger {
position: relative;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: rgba(0, 0, 0, 0.54);
border-radius: 6px;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 菜单触发器悬停和激活状态 */
.menu-trigger:hover,
.menu-trigger.active {
background-color: rgba(0, 0, 0, 0.04);
color: rgba(0, 0, 0, 0.87);
}
/* 弹出菜单:浮动面板样式 */
.popup-menu {
position: absolute;
bottom: 100%;
right: 0;
margin-bottom: 23px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
min-width: 320px;
padding: 8px 0;
z-index: 1000;
transform-origin: top right;
}
/* 菜单动画相关样式 */
.menu-enter-active,
.menu-leave-active {
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.menu-enter-from,
.menu-leave-to {
opacity: 0;
transform: scale(0.95);
}
.menu-enter-to,
.menu-leave-from {
opacity: 1;
transform: scale(1);
}
/* 菜单分割线 */
.menu-divider {
height: 1px;
background-color: rgba(0, 0, 0, 0.12);
margin: 6px 0;
}
/* 菜单项:交互式列表项样式 */
.menu-item {
padding: 10px 16px;
margin: 0 4px;
font-size: 13px;
color: rgba(0, 0, 0, 0.87);
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 8px;
display: flex;
align-items: center;
font-weight: 400;
}
/* 菜单项悬停和选中状态 */
.menu-item:hover,
.menu-item-selected {
background-color: rgba(0, 0, 0, 0.04);
color: rgba(0, 0, 0, 0.87);
}
/* 选中项信息区域 */
.selected-item-info {
padding: 12px 16px;
margin: 0 4px 4px 4px;
border-radius: 8px;
}
/* 选中项头部区域 */
.selected-item-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
/* 选中项图标容器 */
.selected-item-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(0, 0, 0, 0.54);
overflow: hidden;
}
/* 选中项图标图片 */
.selected-item-icon img {
width: 24px;
height: 24px;
object-fit: contain;
}
/* 选中项名称 */
.selected-item-name {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 选中项内容区域 */
.selected-item-content {
display: flex;
flex-direction: column;
gap: 8px;
}
/* 选中项描述 */
.selected-item-description {
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
word-break: break-word;
overflow-wrap: break-word;
line-height: 1.5;
}
/* 选中项标签容器 */
.selected-item-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
/* 选中项标签样式 */
.selected-item-tag {
font-size: 10px;
padding: 2px 6px;
border-radius: 12px;
font-weight: 400;
white-space: nowrap;
letter-spacing: 0.4px;
text-transform: uppercase;
color: #fff !important;
opacity: 0.9;
}
/* Toast 消息容器:居中显示的浮动提示 */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #323232;
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 1000;
pointer-events: none;
max-width: 80%;
text-align: center;
}
/* Toast 动画相关样式 */
.toast-enter-active,
.toast-leave-active {
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.toast-enter-from,
.toast-leave-to {
opacity: 0;
transform: translate(-50%, calc(-50% - 20px));
}
</style>

View File

@@ -0,0 +1,581 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import type { ListItem } from '@/types'
interface Props {
data: ListItem[]
height?: string | number
frozen?: boolean
disableHover?: boolean
}
const props = withDefaults(defineProps<Props>(), {
height: '100%',
frozen: false,
disableHover: false
})
const emit = defineEmits<{
select: [item: ListItem]
click: [item: ListItem]
contextmenu: [data: { item: ListItem }]
showToast: [message: string]
}>()
// ===== 列表配置常量 =====
/** 列表项基础高度(像素) */
const ITEM_HEIGHT = 50
/** 列表项内边距总和(像素) */
const ITEM_PADDING = 16
/** 上下缓冲区域的项目数量 */
const BUFFER_COUNT = 100
/** 滚动防抖时间(毫秒) */
const SCROLL_DEBOUNCE_TIME = 16
/** 虚拟滚动阈值,少于此数量则直接渲染全部 */
const VIRTUALIZATION_THRESHOLD = 500
// 计算列表项实际总高度
const totalItemHeight = computed(() =>
ITEM_HEIGHT + ITEM_PADDING
)
// 响应式状态管理
const containerHeight = ref<number>(0)
const startIndex = ref<number>(0)
const selectedIndex = ref<number>(0)
const isKeyboardNavigating = ref<boolean>(false)
let keyboardTimer: number | null = null
// DOM 引用
const containerRef = ref<HTMLElement | null>(null)
const listRef = ref<HTMLElement | null>(null)
// 计算可视区域能显示的列表项数量
const visibleCount = computed(() =>
Math.floor(containerHeight.value / totalItemHeight.value)
)
/**
* 是否需要虚拟滚动
* 当数据量小于阈值时直接渲染全部
*/
const needVirtualization = computed<boolean>(() =>
props.data.length > VIRTUALIZATION_THRESHOLD
)
// 计算当前需要渲染的数据
const visibleData = computed(() => {
// 数据量小于阈值时,直接返回全部数据
if (!needVirtualization.value) {
return props.data.map((item, index) => ({
...item,
index
}))
}
// 否则使用虚拟滚动
const visibleStart = Math.max(0, startIndex.value - BUFFER_COUNT)
const visibleEnd = Math.min(
props.data.length,
startIndex.value + visibleCount.value + BUFFER_COUNT
)
return props.data.slice(visibleStart, visibleEnd).map((item, index) => ({
...item,
index: visibleStart + index
}))
})
// 计算列表偏移量
const offsetY = computed(() =>
needVirtualization.value
? Math.max(0, (startIndex.value - BUFFER_COUNT) * totalItemHeight.value)
: 0
)
// 计算虚拟列表总高度
const phantomHeight = computed(() =>
needVirtualization.value
? props.data.length * totalItemHeight.value
: 'auto'
)
// 选择处理
const handleSelect = (item: ListItem): void => {
if (props.frozen) return
selectedIndex.value = item.index ?? 0
emit('select', item)
}
/**
* 处理列表项点击
* @param item 点击的列表项
*/
const handleClick = (item: ListItem): void => {
if (props.frozen) return
handleSelect(item)
emit('click', item)
}
/**
* 更新容器高度
* 从父元素或窗口获取实际高度
*/
function updateContainerHeight(): void {
if (containerRef.value) {
const parentHeight = containerRef.value.parentElement?.clientHeight
containerHeight.value = parentHeight || window.innerHeight
}
}
/** ResizeObserver 实例,用于监听容器尺寸变化 */
let resizeObserver: ResizeObserver | null = null
/**
* 防抖函数
* 在指定延迟后执行函数,如果在延迟期间再次调用则重置延迟
* @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)
timer = window.setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
/**
* 处理滚动事件(已防抖)
* 根据滚动位置更新可视区的起始索引
*/
const handleScroll = debounce(() => {
// 不需要虚拟滚动时直接返回
if (!needVirtualization.value) return
if (!containerRef.value) return
const scrollTop = containerRef.value.scrollTop
const newStartIndex = Math.floor(scrollTop / totalItemHeight.value)
if (newStartIndex !== startIndex.value) {
startIndex.value = newStartIndex
}
}, SCROLL_DEBOUNCE_TIME)
/** 鼠标悬停禁用状态 */
const disableHover = ref<boolean>(false)
/** 鼠标悬停禁用定时器 */
let hoverTimer: number | null = null
/**
* 处理鼠标移入事件
* 在非键盘导航状态下更新选中项
* @param index 目标项的索引
*/
function handleMouseEnter(index: number): void {
if (props.frozen || disableHover.value || props.disableHover) return
if (!isKeyboardNavigating.value) {
selectedIndex.value = index
emit('select', { ...props.data[index], index })
ensureSelectedItemVisible()
}
}
/**
* 记录到达列表边界的时间
* 用于实现边界循环导航的延迟判断
*/
const lastReachBoundaryTime = ref<{ top: number; bottom: number }>({
top: 0,
bottom: 0
})
/**
* 处理键盘事件
* 实现键盘导航、边界循环和项目选择功能
* @param e 键盘事件对象
*/
function handleKeyDown(e: KeyboardEvent): void {
if (props.frozen) return
if (e.key === 'Enter') {
if (selectedIndex.value >= 0 && selectedIndex.value < props.data.length) {
const item = props.data[selectedIndex.value]
emit('click', { ...item, index: selectedIndex.value })
}
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault()
const isFirstItem = selectedIndex.value === 0
const isLastItem = selectedIndex.value === props.data.length - 1
const now = Date.now()
if (e.key === 'ArrowUp' && isFirstItem) {
const timeSinceLastTop = now - lastReachBoundaryTime.value.top
if (timeSinceLastTop < 5000) {
disableHover.value = true
selectedIndex.value = props.data.length - 1
emit('select', { ...props.data[selectedIndex.value], index: selectedIndex.value })
ensureSelectedItemVisible()
if (hoverTimer) clearTimeout(hoverTimer)
hoverTimer = window.setTimeout(() => {
disableHover.value = false
}, 1000)
} else {
lastReachBoundaryTime.value.top = now
emit('showToast', '已经到列表顶端,再次点击将回到列表底端')
}
return
}
if (e.key === 'ArrowDown' && isLastItem) {
const timeSinceLastBottom = now - lastReachBoundaryTime.value.bottom
if (timeSinceLastBottom < 5000) {
disableHover.value = true
selectedIndex.value = 0
emit('select', { ...props.data[selectedIndex.value], index: selectedIndex.value })
ensureSelectedItemVisible()
if (hoverTimer) clearTimeout(hoverTimer)
hoverTimer = window.setTimeout(() => {
disableHover.value = false
}, 1000)
} else {
lastReachBoundaryTime.value.bottom = now
emit('showToast', '已经到列表底端,再次点击将回到列表顶端')
}
return
}
isKeyboardNavigating.value = true
if (keyboardTimer) clearTimeout(keyboardTimer)
if (e.key === 'ArrowUp') {
const nextIndex = Math.max(0, selectedIndex.value - 1)
selectedIndex.value = nextIndex
emit('select', { ...props.data[nextIndex], index: nextIndex })
const itemTop = nextIndex * totalItemHeight.value
if (itemTop < (containerRef.value?.scrollTop ?? 0) && containerRef.value) {
containerRef.value.scrollTop = itemTop
}
} else {
const nextIndex = Math.min(props.data.length - 1, selectedIndex.value + 1)
selectedIndex.value = nextIndex
emit('select', { ...props.data[nextIndex], index: nextIndex })
const itemBottom = (nextIndex + 1) * totalItemHeight.value
const scrollBottom = (containerRef.value?.scrollTop ?? 0) + containerHeight.value
if (itemBottom > scrollBottom && containerRef.value) {
containerRef.value.scrollTop = itemBottom - containerHeight.value
}
}
keyboardTimer = window.setTimeout(() => {
isKeyboardNavigating.value = false
}, 1000)
}
}
/**
* 保持选中项在可视区域内
* 如果选中项不可见,则滚动到合适位置
*/
function ensureSelectedItemVisible(): void {
if (!containerRef.value) return
const containerTop = containerRef.value.scrollTop
const containerHeight = containerRef.value.clientHeight
const itemTop = selectedIndex.value * totalItemHeight.value
const itemBottom = (selectedIndex.value + 1) * totalItemHeight.value
const scrollBottom = containerTop + containerHeight
if (itemTop < containerTop) {
containerRef.value.scrollTop = itemTop
} else if (itemBottom > scrollBottom) {
containerRef.value.scrollTop = itemBottom - containerHeight
}
}
/**
* 处理右键点击事件
* @param e 鼠标事件对象
* @param item 目标列表项
*/
const handleContextMenu = (e: MouseEvent, item: ListItem): void => {
if (props.frozen) return
e.preventDefault()
emit('contextmenu', { item })
}
// ===== 生命周期钩子 =====
onMounted(() => {
if (containerRef.value) {
// 添加滚动事件监听
containerRef.value.addEventListener('scroll', handleScroll)
// 创建并启用 ResizeObserver
resizeObserver = new ResizeObserver(() => {
updateContainerHeight()
})
if (containerRef.value.parentElement) {
resizeObserver.observe(containerRef.value.parentElement)
}
}
// 初始化容器高度
updateContainerHeight()
// 添加全局事件监听
window.addEventListener('resize', updateContainerHeight)
window.addEventListener('keydown', handleKeyDown)
})
onBeforeUnmount(() => {
// 清理所有事件监听和定时器
if (containerRef.value) {
containerRef.value.removeEventListener('scroll', handleScroll)
}
if (resizeObserver) {
resizeObserver.disconnect()
}
window.removeEventListener('resize', updateContainerHeight)
window.removeEventListener('keydown', handleKeyDown)
if (keyboardTimer) clearTimeout(keyboardTimer)
if (hoverTimer) clearTimeout(hoverTimer)
})
</script>
<template>
<!-- 虚拟列表容器 -->
<div
ref="containerRef"
class="virtual-list-container"
:class="{ 'list-frozen': frozen }"
:style="{ height: '100%' }"
tabindex="0"
>
<!-- 虚拟高度占位元素 -->
<div
ref="listRef"
class="virtual-list-phantom"
:style="{ height: needVirtualization ? `${phantomHeight}px` : 'auto' }"
>
<!-- 实际渲染的列表内容 -->
<div
class="virtual-list"
:style="needVirtualization ? { transform: `translateY(${offsetY}px)` } : {}"
>
<!-- 列表项 -->
<div
v-for="item in visibleData"
:key="item.index"
class="list-item"
:class="{ 'selected': selectedIndex === item.index }"
:style="{ height: `${ITEM_HEIGHT}px` }"
@mouseenter="handleMouseEnter(item.index)"
@click="handleClick(item)"
@contextmenu="handleContextMenu($event, item)"
>
<slot name="item" :item="item">
<!-- 默认列表项样式 -->
<div class="default-item">
<!-- 项目图标 -->
<div class="item-icon">
<img
v-if="item.icon"
:src="item.icon"
:alt="item.name"
width="32"
height="32"
>
<svg v-else viewBox="0 0 24 24" width="16" height="16">
<circle cx="12" cy="12" r="10" fill="rgba(0, 0, 0, 0.38)" />
</svg>
</div>
<!-- 项目内容 -->
<div class="item-content">
<!-- 标题行名称和标签 -->
<div class="item-header">
<div class="item-name">{{ item.name }}</div>
<!-- 标签列表 -->
<div class="item-tags" v-if="item.tags?.length">
<span
v-for="tag in item.tags"
:key="tag.id"
class="item-tag"
:style="{ backgroundColor: tag.color }"
>
{{ tag.name }}
</span>
</div>
</div>
<!-- 路径信息 -->
<div class="item-description">
<div class="item-description-text">{{ item.description }}</div>
</div>
</div>
</div>
</slot>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 虚拟列表容器:可滚动区域 */
.virtual-list-container {
overflow-y: auto;
position: relative;
background-color: #fafafa;
width: 100%;
height: 100%;
outline: none;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
/* 添加以下样式来隐藏 Webkit 浏览器的滚动条 */
.virtual-list-container::-webkit-scrollbar {
display: none;
}
/* 拟高度容器:用于保持滚动条比例 */
.virtual-list-phantom {
position: relative;
width: 100%;
}
/* 实际列表内容容器:通过 transform 实现位置偏移 */
.virtual-list {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
/* 列表项基础样式 */
.list-item {
padding: 8px 12px 8px 8px;
display: flex;
align-items: center;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
background-color: white;
border-radius: 4px;
margin: 0 4px;
}
/* 选中状态样式 */
.list-item.selected {
background-color: rgba(0, 0, 0, 0.04);
}
/* 列表项布局容器 */
.default-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
/* 图标容器 */
.item-icon {
flex-shrink: 0;
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(0, 0, 0, 0.54);
overflow: hidden;
}
/* 图标图片样式 */
.item-icon img {
width: 32px;
height: 32px;
object-fit: contain;
}
/* 内容区域布局 */
.item-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
/* 标题行布局 */
.item-header {
display: flex;
align-items: center;
min-width: 0;
}
/* 项目名称样式 */
.item-name {
font-size: 14px;
color: rgba(0, 0, 0, 0.87);
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
min-width: 0;
margin-right: auto;
}
/* 信息行布局 */
.item-description {
display: flex;
gap: 4px;
}
/* 描述文本样式 */
.item-description-text {
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
/* 标签容器样式 */
.item-tags {
display: flex;
gap: 6px;
flex-wrap: nowrap;
flex-shrink: 0;
overflow: hidden;
margin-left: 12px;
}
/* 单个标签样式 */
.item-tag {
font-size: 10px;
padding: 2px 6px;
border-radius: 12px;
font-weight: 400;
white-space: nowrap;
flex-shrink: 0;
letter-spacing: 0.4px;
text-transform: uppercase;
color: #fff !important;
opacity: 0.9;
}
/* 列表冻结状态样式 */
.list-frozen {
pointer-events: none;
opacity: 0.38;
user-select: none;
}
</style>

7
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

176
src/i18n/EnUs.ts Normal file
View File

@@ -0,0 +1,176 @@
import {Sentence} from './index'
export class EnUs implements Sentence {
readonly activateCountPerDay: string = 'Activate Plugin Per Day'
readonly activatePluginToday: string = 'Today Activate Plugin'
readonly activatePluginTotal: string = 'Total Activate Plugin'
readonly auto: string = 'Auto'
readonly beta: string = 'Beta'
readonly betaDesc: string = `beta means that this adaption\nis in preview version\nI can't test all the use scenes\nI need you submit the feedback \nwhen you meet some error`
readonly browser: string = 'Browser'
readonly browserPathDescPrefix: string = 'file usually at: '
readonly buyNow: string = 'Purchase'
readonly buySuccess: string = 'Purchase for plugin member successfully'
readonly catalogueSearchPlaceHolder: string = 'Search and filter adapter'
readonly catsxpBrowser: string = 'Catsxp'
readonly centBrowser: string = 'CentBrowser'
readonly clearStatisticsWarning: string = 'Disable statistics will clear all statistics\' data that record before, please check again'
readonly cloud: string = 'Global'
readonly cloudDesc: string = 'Sync this setting on device that\nlogin with the same account'
readonly configFileAt: string = 'Config file usually at'
readonly configPrefix: string = 'Configure'
readonly configSuffix: string = 'file path'
readonly copyOrderId: string = 'Copy order ID'
readonly cssBackground: string = 'CSS background attribute'
readonly cssBackgroundDesc: string = 'egurl(xxx) center center no-repeat'
readonly cssBackgroundPlaceholder: string = 'CSS background attribute'
readonly deepinBrowser: string = 'Deepin'
readonly deviceToday: string = 'Device Today'
readonly deviceTotal: string = 'Device Total'
readonly devTip: string = 'In development mode now'
readonly emptyTipsDesc: string = 'If you are not configure this program, please configure it in the setting page. Click this to setting page'
readonly emptyTipsTitle: string = 'Found nothing'
readonly enableDesc: string = 'Closing this option will be as if this application is not configured'
readonly enableLabel: string = 'Enable'
readonly enableStatistics: string = 'Enable'
readonly enableStatisticsDesc: string = 'Plugin will record the count of access software by it, and provide charts to show the records. Using records\' data only be save in uTools cloud'
readonly enhanceConfig: string = 'Advanced Options'
readonly enhanceConfigChipDesc: string = 'If you cannot understand this option\ndo not change it'
readonly enhanceConfigDesc: string = 'Enable this option, it will show advanced options, you can configure the plugin in more behavior.'
readonly error: string = 'Error'
readonly errorArgs: string = 'Arguments error, feedback please'
readonly errorInfoToClipboard: string = 'Error message is copied to clipboard'
readonly evernote: string = 'Evernote'
readonly evernoteUserId: string = 'User ID'
readonly evernoteUserIdDesc: string = 'About "How to find my evernote user id" is in 「https://yuanliao.info/d/3978-300/80」'
readonly executorFileAt: string = 'Executor file usually at'
readonly executorPrefix: string = 'Configure'
readonly executorSuffix: string = 'executable file path'
readonly fetchOrderError: string = 'Fetch order id error'
readonly file: string = 'File'
readonly fileOpening: string = 'Opening'
readonly filePathInMatch: string = 'Add file path to index of search'
readonly filePathInMatchDesc: string = 'Enable this option would add file path to index of search for history that type is file, it is useful for who is disturbed by file path when search can close this option.'
readonly filePathNonExistsTips: string = 'File path is non-exists'
readonly fileSelectorPlaceholder: string = 'Click the input to select file'
readonly filterNonExistsFiles: string = 'Filter non-exists\' files'
readonly filterNonExistsFilesDesc: string = 'The plugin will display the history content, just like the software itself, but if you want the plugin to filter out the non-exists files, then you can consider enabling this option.'
readonly fullUrlInMatch: string = 'Add full url to index of search'
readonly fullUrlInMatchDesc: string = 'Enable this option would add full url to index of search for history that type is URL, it is useful for who want to search the query params of the url.'
readonly getFavicon: string = 'Get favicon for website'
readonly getFaviconDesc: string = 'Enable this option, the plugin would use the 「f1.allesedv.com」 to get the website icon and show it in the result instead of the browser icon, but the API may slow; Plugin need to send the URL to this API, the privacy problem is also best considering within'
readonly getFileIcon: string = 'Get file icon from system'
readonly getFileIconDesc: string = 'Enable this option, Plugin would get the file icon from system and display it in the result, but this will affect some performance, which is not recommended on low-performance machines.'
readonly getProjectsError: string = 'Error for getting projects, check settings please'
readonly here: string = 'here'
readonly historyLimit: string = 'History limit'
readonly historyLimitDesc: string = 'Set the max of history item, but you need to know that it configure single browser\'s max, if you set 200 at this, and have configured 2 browser, finally you will get 400 history item. 「Unlimited」will greatly reduce the loading performance.'
readonly huaweiBrowser: string = 'Huawei'
readonly inputDirectlyPlaceholder: string = 'Input the file path'
readonly inputPathDirectly: string = 'Input path directly'
readonly inputPathDirectlyDesc: string = 'Enable this option to input the \npath directly instead of the file selector. But it is also easy for running error because the path is incorrect.'
readonly languageSetting: string = 'Language'
readonly languageSettingDesc: string = 'Although uTools has almost no foreign users, you can also choose language.'
readonly liebaoBrowser: string = 'Liebao'
readonly local: string = 'Local'
readonly localDesc: string = 'The setting is active on current device only'
readonly logo: string = 'Logo'
readonly logoDesc: string = 'Enable the option will show the special logo picture in background of result path.'
readonly maxthonBrowser: string = 'Maxthon'
readonly member: string = 'Member'
readonly memberPrefix: string = 'You have be '
readonly moreStatistics: string = 'More Statistics'
readonly moreStatisticsComing: string = 'More statistics will coming...'
readonly multiMatch: string = 'Multi keyword match'
readonly multiMatchDesc: string = 'Enable this option, you can input multi keyword that split by 「Space」 to match the projects.'
readonly nativeId: string = 'Native ID'
readonly nativeIdDesc: string = `Native ID is used to identify the prefix of the configuration item.\nNative configuration could be \nfound in「账号与数据」`
readonly needLogin: string = 'You need to login first'
readonly needReboot: string = 'Reboot plugin needed'
readonly needRebootDesc: string = 'Close all window of plugin and reopen it'
readonly nonExistsFileOrDeleted: string = 'Path non-exists or deleted'
readonly nonExistsPathOrCancel: string = 'Path non-exists or you cancel the selector'
readonly notifyFileOpen: string = 'Notify when file is opened'
readonly notifyFileOpenDesc: string = 'Enable this option, The system notification is pop-up when the project is turned on, and some software opens the project requires a certain startup time, which is intended to help the user confirm the operating status of the plugin.'
readonly onlyProvideForMember: string = 'Only for member(VIP)'
readonly onlyProvideForPluginMember: string = 'Only for plugin member'
readonly opacity: string = 'Opacity'
readonly opacityDesc: string = 'Opacity for the logo picture, 0% is transparent and 100% is totally not transparent.'
readonly openInNew: string = 'Open In New Window'
readonly openInNewDesc: string = 'Always open in new window if target is folder'
readonly orderId: string = 'Order ID'
readonly orderInfo: string = 'Plugin Member Order'
readonly orderTime: string = 'Order Time'
readonly outPluginImmediately: string = 'Out plugin after open project'
readonly outPluginImmediatelyDesc: string = 'Enable this option could let you do any uTools\' behavior after open a project conveniently. If you would like to open multi projects continuously, you can disable this option.'
readonly pathNotFound: string = 'Path not found'
readonly picture: string = 'Picture'
readonly pictureDesc: string = 'It provide some in-built picture and you can set a picture path(Simple Custom) or edit the css(Total Custom) for logo.'
readonly picturePath: string = 'Picture Path'
readonly picturePathDesc: string = 'Picture path for logo, it support local path, Base64, and url.'
readonly picturePathPlaceholder: string = 'Local path、Base64 or URL'
readonly pinyinIndex: string = 'Pinyin & First Letter Index'
readonly pinyinIndexDesc: string = 'Enable this option, it will create the Pinyin and Pinyin first letter index for results, but decrease the performance'
readonly placeholder: string = 'Search, support pinyin and first letter.'
readonly plugin: string = 'Plugin'
readonly pluginSetting: string = 'Setting'
readonly ready: string = 'Complete'
readonly reload: string = 'Reload'
readonly requestMoreApplication: string = 'Apply More Adaption'
readonly safariBookmarkDesc: string = 'Safari bookmark path is not need to configure.'
readonly search: string = 'Search'
readonly settingBetaDesc: string = 'The setting is in preview\nIt may be remove in the feature'
readonly settingDocument: string = 'Document'
readonly shortcuts: string = 'Shortcuts'
readonly showBookmarkCatalogue: string = 'Show bookmark catalogue'
readonly showBookmarkCatalogueDesc: string = 'Enable this option would show the catalogue of the bookmark item before the result item\'s title, it is useful for who want to search the keyword in catalogue name'
readonly simpleCustom: string = 'Simple Custom'
readonly smartTag: string = 'Smart Tag'
readonly softwareAccessCount: string = 'Software Access'
readonly softwareAccessCountByHour: string = 'Software Access By Hour'
readonly sourceCodeRepository: string = 'Source Code'
readonly statistics: string = 'Statistics'
readonly surveyPrefix: string = 'Plugin usage survey, fill out a questionnaire to help plugins do better. Click'
readonly surveySuffix: string = 'to open it'
readonly systemInformation: string = 'System Information'
readonly systemUser: string = 'System User'
readonly systemVersion: string = 'System Version'
readonly tag: string = 'Tag'
readonly theme: string = 'Theme'
readonly toBeMember: string = 'Purchase for plugin member'
readonly toBeMemberDesc: string = 'For all VIP features'
readonly toClipboard: string = 'copied to clipboard'
readonly totalCustom: string = 'Total Custom'
readonly twinkstarBrowser: string = 'Twinkstar'
readonly unknownError: string = 'Unknown error, feedback please'
readonly unknownInputError: string = 'Unknown input error'
readonly unready: string = 'Incomplete'
readonly unSupportTipsDesc: string = 'The current keyword corresponds to the historical project index does not support the current platform. If you affect your daily operation, you can disable in the plugin details.'
readonly unSupportTipsTitle: string = 'Feature is not support this platform'
readonly useCount: string = 'Use Count'
readonly utoolsVersion: string = 'uTools Version'
readonly xiaobaiBrowser: string = 'Xiaobai'
readonly homepage: string = 'Homepage'
readonly clickToOpen: string = 'Click to open'
readonly roundTips: string = 'Round! Round!'
readonly roundCloseTips: string = 'Easter egg is closed'
readonly unlimited: string = 'Unlimited'
readonly decreasePerformance: string = 'Decrease performance'
readonly decreasePerformanceDesc: string = 'The Option may \ndecrease performance'
readonly bought: string = 'Bought'
readonly openPluginMemberDialog: string = 'About Member'
readonly fusionMode: string = 'Fusion Mode'
readonly fusionModeDesc: string = 'Add feature「Browser Fusion」, you could search browser bookmark and history together'
readonly configFolderSuffix: string = 'folder path'
readonly configFolderAt: string = 'Config folder usually at'
readonly offlineVersion: string = 'Offline'
readonly pluginVersion: string = 'Plugin Version'
readonly snakeIndex: string = 'Snake Index'
readonly snakeIndexDesc: string = 'Provide first letter index for snake name, like「hello_world」would provide「hw」in index keysIt could decrease index performance and now only effect to developments\' project name, enable this option if you really need it.'
readonly kebabIndex: string = 'Kebab Index'
readonly kebabIndexDesc: string = 'Provide first letter index for kebab name, like「hello-world」would provide「hw」in index keysIt could decrease index performance and now only effect to developments\' project name, enable this option if you really need it.'
readonly camelCaseIndex: string = 'Pascal & Camel Index'
readonly camelCaseIndexDesc: string = 'Provide first letter index for pascal or camel case name, like「HelloWorld」or 「helloWorld」would provide「hw」in index keysIt could decrease index performance and now only effect to developments\' project name, enable this option if you really need it.'
readonly sortByAccessTime: string = 'Sort by access time'
readonly sortByAccessTimeDesc: string = 'Because the projects is not sorted by VSCode in it\'s database, this option would help you to sort the projects by access time that is form system. But the time sometimes is not work very well that you need to think about it.'
}

177
src/i18n/ZhCn.ts Normal file
View File

@@ -0,0 +1,177 @@
import {Sentence} from './index'
export class ZhCn implements Sentence {
readonly activateCountPerDay: string = '插件激活次数 / 日'
readonly activatePluginToday: string = '今日激活插件次数'
readonly activatePluginTotal: string = '总激活插件次数'
readonly auto: string = '自动'
readonly beta: string = '测试'
readonly betaDesc: string = `beta 意味着这个功能处于试验阶段\n但我无法测试所有使用场景\n需要你在遇到无法正常使用的时候积极向我反馈`
readonly browser: string = '浏览器'
readonly browserPathDescPrefix: string = '文件通常放在: '
readonly buyNow: string = '立即开通'
readonly buySuccess: string = '插件会员开通成功'
readonly catalogueSearchPlaceHolder: string = '搜索过滤软件适配项'
readonly catsxpBrowser: string = '猫眼浏览器'
readonly centBrowser: string = '百分浏览器'
readonly clearStatisticsWarning: string = '关闭统计功能将清除当前存储的统计数据,请确认'
readonly cloud: string = '全局'
readonly cloudDesc: string = '同账号的设备同步该设置'
readonly configFileAt: string = '配置文件通常放在'
readonly configPrefix: string = '设置'
readonly configSuffix: string = '文件路径'
readonly copyOrderId: string = '复制订单 ID'
readonly cssBackground: string = 'CSS background 属性'
readonly cssBackgroundDesc: string = '直接填写 background 的值不需要加其他前缀后缀和花括号例如url(xxx) center center no-repeat'
readonly cssBackgroundPlaceholder: string = '填写 background 的值'
readonly deepinBrowser: string = '深度浏览器'
readonly deviceToday: string = '今日使用设备数'
readonly deviceTotal: string = '总使用设备数'
readonly devTip: string = '当前处于开发模式'
readonly emptyTipsDesc: string = '如果你还没有设置该软件的相关配置,请先在 Setting 关键字中设置相关配置内容,点击可跳转设置界面'
readonly emptyTipsTitle: string = '似乎什么都找不到'
readonly enableDesc: string = '关闭这个选项将如同没有配置这个应用适配一样'
readonly enableLabel: string = '是否启用该应用'
readonly enableStatistics: string = '启用统计'
readonly enableStatisticsDesc: string = '插件将按一定的规则统计你使用的软件次数,并给予图表反馈,您的使用数据仅保存在 uTools 云同步中'
readonly enhanceConfig: string = '高级设置'
readonly enhanceConfigChipDesc: string = '如果你无法清楚理解该设置的作用\n请勿随意更改'
readonly enhanceConfigDesc: string = '启动该选项将显示更多高级设置,可以对插件的功能进行更细致的调节'
readonly error: string = '有错误'
readonly errorArgs: string = '参数错误,请向作者反馈'
readonly errorInfoToClipboard: string = '错误信息已复制到剪贴板'
readonly evernote: string = '印象笔记'
readonly evernoteUserId: string = '用户 ID'
readonly evernoteUserIdDesc: string = '在「https://yuanliao.info/d/3978-300/80」查看如何获取印象笔记用户 ID'
readonly executorFileAt: string = '可执行文件通常放在'
readonly executorPrefix: string = '设置'
readonly executorSuffix: string = '可执行程序路径'
readonly fetchOrderError: string = '订单信息获取失败'
readonly file: string = '文件'
readonly fileOpening: string = '正在打开项目'
readonly filePathInMatch: string = '文件路径加入搜索匹配'
readonly filePathInMatchDesc: string = '启动该选项将对涉及本地文件类型的历史记录,把文件的全路径包含在可搜索的关键字中,对于文件路径内容容易产生搜索干扰的用户,可以酌情关闭该设置'
readonly filePathNonExistsTips: string = '文件路径不存在'
readonly fileSelectorPlaceholder: string = '点击输入框选择路径'
readonly filterNonExistsFiles: string = '过滤不存在的文件'
readonly filterNonExistsFilesDesc: string = '插件会如实显示历史记录内容,如同软件本身一样,但如果你希望插件替你将不存在的文件过滤掉,那么可以考虑启用该选项'
readonly fullUrlInMatch: string = '完整 URL 加入搜索匹配'
readonly fullUrlInMatchDesc: string = '启动该选项将会对设置 URL 类型的历史记录,把完整的 URL 包含在可搜索的关键字中,目前只将 URL 域名部分加入搜索,无法搜索到 Query 部分的参数,需要的用户可以酌情开启该设置'
readonly getFavicon: string = '获取 favicon'
readonly getFaviconDesc: string = '启动该选项可以使用互联网提供的「f1.allesedv.com」来获取网站图标显示在结果里代替浏览器图标但该 API 较慢; 另由于需要将网址传到该 API隐私问题也最好考虑在内'
readonly getFileIcon: string = '获取文件图标'
readonly getFileIconDesc: string = '启动该选项可以在文件型历史记录的结果里显示系统文件图标作为 Icon但这会影响一些性能在低性能的机器上不建议开启'
readonly getProjectsError: string = '获取项目记录错误,请检查配置'
readonly here: string = '这里'
readonly historyLimit: string = '历史记录获取条数'
readonly historyLimitDesc: string = '可以设置历史记录获取的最大数量值得注意的是这是单个浏览器的限制而不是总的数量如果设置数量为200浏览器配置了2个那么最大获取历史记录的数量为400设置「无限制」会导致加载速度大幅下降'
readonly huaweiBrowser: string = '华为浏览器'
readonly inputDirectlyPlaceholder: string = '输入文件路径'
readonly inputPathDirectly: string = '直接输入路径'
readonly inputPathDirectlyDesc: string = '启动该选项可直接在路径框中输入路径而非使用文件选择器,特殊情况可能会带来方便,但也容易因为人为输入失误导致插件运行错误。(重启插件指完全退出插件后再次打开设置)'
readonly languageSetting: string = '语言设置'
readonly languageSettingDesc: string = '尽管 uTools 几乎没有国外用户,但还是可以选择其他语言'
readonly liebaoBrowser: string = '猎豹浏览器'
readonly local: string = '本地'
readonly localDesc: string = '该设置仅本地生效'
readonly logo: string = 'Logo'
readonly logoDesc: string = '启用改选项将会显示指定的 Logo 图案在历史记录结果页面的背景中'
readonly maxthonBrowser: string = '傲游浏览器'
readonly member: string = '会员'
readonly memberPrefix: string = '您已成为尊贵的'
readonly moreStatistics: string = '查看更多统计'
readonly moreStatisticsComing: string = '更多图表正在施工中...'
readonly multiMatch: string = '多关键词匹配'
readonly multiMatchDesc: string = '启用该选项可以在搜索项目时使用「空格」分隔多个关键字,只有同时满足多个关键词时才能被检索出来,关键词可乱序输入'
readonly nativeId: string = 'Native ID'
readonly nativeIdDesc: string = `Native ID 用于标识配置文件项的前缀\n本机配置文件可以在「账号与数据」中找到`
readonly needLogin: string = '开通需要先登录 uTools'
readonly needReboot: string = '重启插件生效'
readonly needRebootDesc: string = '完全关闭插件所有页面再重新打开'
readonly nonExistsFileOrDeleted: string = '路径指示的文件不存在或已被删除'
readonly nonExistsPathOrCancel: string = '路径不存在或是您主动取消选择'
readonly notifyFileOpen: string = '项目打开通知'
readonly notifyFileOpenDesc: string = '启动该选项会在打开项目时弹出系统通知,部分软件打开项目需要一定的启动时间,该设置旨在帮助用户确认插件的运行状态'
readonly onlyProvideForMember: string = '功能仅限会员使用'
readonly onlyProvideForPluginMember: string = '仅限插件会员使用'
readonly opacity: string = '不透明度'
readonly opacityDesc: string = 'Logo 图案的不透明度,即 0% 为完全透明100% 为完全不透明'
readonly openInNew: string = '新窗口打开'
readonly openInNewDesc: string = '如果打开的是文件夹,无论是否打开该选项,都将在新窗口打开'
readonly orderId: string = '订单 ID'
readonly orderInfo: string = '插件会员支付信息'
readonly orderTime: string = '订单时间'
readonly outPluginImmediately: string = '打开项目后立即退出插件'
readonly outPluginImmediatelyDesc: string = '启动改选项可以方便在打开项目后马上开始其他 uTools 操作,而无需再次退出本插件,如果你需要连续打开多个项目,也可以关闭该选项'
readonly pathNotFound: string = '文件不存在'
readonly picture: string = '图案'
readonly pictureDesc: string = 'Logo 图案支持内置的几款图案和自定义图案,自定义图案分为直接填写图片地址(简单自定义)和直接填写 CSS 属性(完全自定义),后者适合有经验的用户实现更多的自定义属性'
readonly picturePath: string = '图片路径'
readonly picturePathDesc: string = 'Logo 图片路径支持本地图片无法云同步、Base64、网络 URL拥有多个设备的用户建议使用后两个本地图片无法跨平台同步'
readonly picturePathPlaceholder: string = '填写图片文件路径、Base64 或 URL'
readonly pinyinIndex: string = '使用拼音及拼音首字母搜索'
readonly pinyinIndexDesc: string = '启动该选项会使插件生成书签和历史记录名称的拼音和拼音首字母索引供搜索,但在结果多的时候会影响插件打开后的首次检索速度'
readonly placeholder: string = '快速搜索结果,支持全拼和拼音首字母'
readonly plugin: string = '插件'
readonly pluginSetting: string = '插件配置'
readonly ready: string = '已配置'
readonly reload: string = '重新加载'
readonly requestMoreApplication: string = '适配更多软件'
readonly safariBookmarkDesc: string = 'Safari 书签配置固定,无需额外配置'
readonly search: string = '搜索'
readonly settingBetaDesc: string = '该设置处于预览\n之后可能会被移除'
readonly settingDocument: string = '配置文档'
readonly shortcuts: string = '快捷指令'
readonly showBookmarkCatalogue: string = '显示书签目录'
readonly showBookmarkCatalogueDesc: string = '启动改选项可以在浏览器书签结果的名称前加入书签的目录层级显示,对于希望搜索匹配目录中的关键字的用户很有用'
readonly simpleCustom: string = '简单自定义'
readonly smartTag: string = 'AI 标签'
readonly softwareAccessCount: string = '总软件使用次数'
readonly softwareAccessCountByHour: string = '每小时软件使用次数'
readonly sourceCodeRepository: string = '源码主页'
readonly statistics: string = '使用统计'
readonly surveyPrefix: string = '插件使用情况小调查,填个问卷帮助插件做得更好。点击'
readonly surveySuffix: string = '打开问卷'
readonly systemInformation: string = '系统信息'
readonly systemUser: string = 'System 用户'
readonly systemVersion: string = 'System 版本'
readonly tag: string = '标签'
readonly theme: string = '主题'
readonly toBeMember: string = '开通插件会员'
readonly toBeMemberDesc: string = '获得全部 VIP 功能'
readonly toClipboard: string = '已复制到剪贴板'
readonly totalCustom: string = '完全自定义'
readonly twinkstarBrowser: string = '星愿浏览器'
readonly unknownError: string = '未知错误,请向作者反馈'
readonly unknownInputError: string = '出现未知的输入错误'
readonly unready: string = '待完善'
readonly unSupportTipsDesc: string = '当前关键字对应的历史项目索引不支持当前平台,如果影响了你的日常操作,可以在插件详情中禁用'
readonly unSupportTipsTitle: string = '该关键字不支持当前平台'
readonly useCount: string = '使用次数'
readonly utoolsVersion: string = 'uTools 版本'
readonly xiaobaiBrowser: string = '小白浏览器'
readonly homepage: string = '官网'
readonly clickToOpen: string = '点击打开'
readonly roundTips: string = '转起来!转起来!'
readonly roundCloseTips: string = '彩蛋已关闭'
readonly unlimited: string = '无限制'
readonly decreasePerformance: string = '影响性能'
readonly decreasePerformanceDesc: string = '该选项可能会影响插件性能,请谨慎设置'
readonly bought: string = '已开通'
readonly openPluginMemberDialog: string = '打开会员说明'
readonly fusionMode: string = '融合模式'
readonly fusionModeDesc: string = '增加「Browser Fusion」关键字可以同时查询书签和历史记录配置浏览器过多将影响加载性能'
readonly configFolderSuffix: string = '文件夹路径'
readonly configFolderAt: string = '配置文件夹通常放在'
readonly offlineVersion: string = '离线版'
readonly pluginVersion: string = '插件版本'
readonly snakeIndex: string = 'Snake 索引'
readonly snakeIndexDesc: string = '适配蛇形「Snake」命名法的首字母索引如「hello_world」会得到「hw」的索引可能会降低插件索引性能目前仅对编程类软件生效请按需开启'
readonly kebabIndex: string = 'Kebab 索引'
readonly kebabIndexDesc: string = '适配烤串「Kebab」命名法的首字母索引如「hello-world」会得到「hw」的索引可能会降低插件索引性能目前仅对编程类软件生效请按需开启'
readonly camelCaseIndex: string = 'Pascal & Camel 索引'
readonly camelCaseIndexDesc: string = '适配帕斯卡「Pascal」或驼峰「Camel」命名法的首字母索引如「HelloWorld」或「helloWorld」会得到「hw」的索引可能会降低插件索引性能目前仅对编程类软件生效请按需开启'
readonly sortByAccessTime: string = '按最后访问时间排序项目'
readonly sortByAccessTimeDesc: string = '由于 VSCode 配置文件项不存在项目访问时间,并且不按最后访问顺序排序,可以使用系统的最后访问时间替代,但与实际情况存在有一定的偏差,请酌情使用'
}

362
src/i18n/index.ts Normal file
View File

@@ -0,0 +1,362 @@
import {I18n} from 'licia'
import {EnUs} from './EnUs'
import {ZhCn} from './ZhCn'
export const sentenceKey = {
activateCountPerDay: 'activateCountPerDay',
activatePluginToday: 'activatePluginToday',
activatePluginTotal: 'activatePluginTotal',
auto: 'auto',
beta: 'beta',
betaDesc: 'betaDesc',
browser: 'browser',
browserPathDescPrefix: 'browserPathDescPrefix',
buyNow: 'buyNow',
buySuccess: 'buySuccess',
catalogueSearchPlaceHolder: 'catalogueSearchPlaceHolder',
catsxpBrowser: 'catsxpBrowser',
centBrowser: 'centBrowser',
clearStatisticsWarning: 'clearStatisticsWarning',
cloud: 'cloud',
cloudDesc: 'cloudDesc',
configFileAt: 'configFileAt',
configPrefix: 'configPrefix',
configSuffix: 'configSuffix',
copyOrderId: 'copyOrderId',
cssBackground: 'cssBackground',
cssBackgroundDesc: 'cssBackgroundDesc',
cssBackgroundPlaceholder: 'cssBackgroundPlaceholder',
deepinBrowser: 'deepinBrowser',
deviceToday: 'deviceToday',
deviceTotal: 'deviceTotal',
devTip: 'devTip',
emptyTipsDesc: 'emptyTipsDesc',
emptyTipsTitle: 'emptyTipsTitle',
enableDesc: 'enableDesc',
enableLabel: 'enableLabel',
enableStatistics: 'enableStatistics',
enableStatisticsDesc: 'enableStatisticsDesc',
enhanceConfig: 'enhanceConfig',
enhanceConfigChipDesc: 'enhanceConfigChipDesc',
enhanceConfigDesc: 'enhanceConfigDesc',
error: 'error',
errorArgs: 'errorArgs',
errorInfoToClipboard: 'errorInfoToClipboard',
evernote: 'evernote',
evernoteUserId: 'evernoteUserId',
evernoteUserIdDesc: 'evernoteUserIdDesc',
evernoteUserIdPlaceholder: 'evernoteUserIdPlaceholder',
executorFileAt: 'executorFileAt',
executorPrefix: 'executorPrefix',
executorSuffix: 'executorSuffix',
fetchOrderError: 'fetchOrderError',
file: 'file',
fileOpening: 'fileOpening',
filePathInMatch: 'filePathInMatch',
filePathInMatchDesc: 'filePathInMatchDesc',
filePathNonExistsTips: 'filePathNonExistsTips',
fileSelectorPlaceholder: 'fileSelectorPlaceholder',
filterNonExistsFiles: 'filterNonExistsFiles',
filterNonExistsFilesDesc: 'filterNonExistsFilesDesc',
fullUrlInMatch: 'fullUrlInMatch',
fullUrlInMatchDesc: 'fullUrlInMatchDesc',
getFavicon: 'getFavicon',
getFaviconDesc: 'getFaviconDesc',
getFileIcon: 'getFileIcon',
getFileIconDesc: 'getFileIconDesc',
getProjectsError: 'getProjectsError',
here: 'here',
historyLimit: 'historyLimit',
historyLimitDesc: 'historyLimitDesc',
huaweiBrowser: 'huaweiBrowser',
inputDirectlyPlaceholder: 'inputDirectlyPlaceholder',
inputPathDirectly: 'inputPathDirectly',
inputPathDirectlyDesc: 'inputPathDirectlyDesc',
languageSetting: 'languageSetting',
languageSettingDesc: 'languageSettingDesc',
liebaoBrowser: 'liebaoBrowser',
local: 'local',
localDesc: 'localDesc',
logo: 'logo',
logoDesc: 'logoDesc',
maxthonBrowser: 'maxthonBrowser',
member: 'member',
memberPrefix: 'memberPrefix',
moreStatistics: 'moreStatistics',
moreStatisticsComing: 'moreStatisticsComing',
multiMatch: 'multiMatch',
multiMatchDesc: 'multiMatchDesc',
nativeId: 'nativeId',
nativeIdDesc: 'nativeIdDesc',
needLogin: 'needLogin',
needReboot: 'needReboot',
needRebootDesc: 'needRebootDesc',
nonExistsFileOrDeleted: 'nonExistsFileOrDeleted',
nonExistsPathOrCancel: 'nonExistsPathOrCancel',
notifyFileOpen: 'notifyFileOpen',
notifyFileOpenDesc: 'notifyFileOpenDesc',
onlyProvideForMember: 'onlyProvideForMember',
onlyProvideForPluginMember: 'onlyProvideForPluginMember',
opacity: 'opacity',
opacityDesc: 'opacityDesc',
openInNew: 'openInNew',
openInNewDesc: 'openInNewDesc',
orderId: 'orderId',
orderInfo: 'orderInfo',
orderTime: 'orderTime',
outPluginImmediately: 'outPluginImmediately',
outPluginImmediatelyDesc: 'outPluginImmediatelyDesc',
pathNotFound: 'pathNotFound',
picture: 'picture',
pictureDesc: 'pictureDesc',
picturePath: 'picturePath',
picturePathDesc: 'picturePathDesc',
picturePathPlaceholder: 'picturePathPlaceholder',
pinyinIndex: 'pinyinIndex',
pinyinIndexDesc: 'pinyinIndexDesc',
placeholder: 'placeholder',
plugin: 'plugin',
pluginSetting: 'pluginSetting',
ready: 'ready',
reload: 'reload',
requestMoreApplication: 'requestMoreApplication',
safariBookmarkDesc: 'safariBookmarkDesc',
search: 'search',
settingBetaDesc: 'settingBetaDesc',
settingDocument: 'settingDocument',
shortcuts: 'shortcuts',
showBookmarkCatalogue: 'showBookmarkCatalogue',
showBookmarkCatalogueDesc: 'showBookmarkCatalogueDesc',
simpleCustom: 'simpleCustom',
smartTag: 'smartTag',
softwareAccessCount: 'softwareAccessCount',
softwareAccessCountByHour: 'softwareAccessCountByHour',
sourceCodeRepository: 'sourceCodeRepository',
statistics: 'statistics',
surveyPrefix: 'surveyPrefix',
surveySuffix: 'surveySuffix',
systemInformation: 'systemInformation',
systemUser: 'systemUser',
systemVersion: 'systemVersion',
tag: 'tag',
theme: 'theme',
toBeMember: 'toBeMember',
toBeMemberDesc: 'toBeMemberDesc',
toClipboard: 'toClipboard',
totalCustom: 'totalCustom',
twinkstarBrowser: 'twinkstarBrowser',
unknownError: 'unknownError',
unknownInputError: 'unknownInputError',
unready: 'unready',
unSupportTipsDesc: 'unSupportTipsDesc',
unSupportTipsTitle: 'unSupportTipsTitle',
useCount: 'useCount',
utoolsVersion: 'utoolsVersion',
xiaobaiBrowser: 'xiaobaiBrowser',
homepage: 'homepage',
clickToOpen: 'clickToOpen',
roundTips: 'roundTips',
roundCloseTips: 'roundCloseTips',
unlimited: 'unlimited',
decreasePerformance: 'decreasePerformance',
decreasePerformanceDesc: 'decreasePerformanceDesc',
bought: 'bought',
openPluginMemberDialog: 'openPluginMemberDialog',
fusionMode: 'fusionMode',
fusionModeDesc: 'fusionModeDesc',
configFolderSuffix: 'configFolderSuffix',
configFolderAt: 'configFolderAt',
offlineVersion: 'offlineVersion',
pluginVersion: 'pluginVersion',
snakeIndex: 'snakeIndex',
snakeIndexDesc: 'snakeIndexDesc',
kebabIndex: 'kebabIndex',
kebabIndexDesc: 'kebabIndexDesc',
camelCaseIndex: 'camelCaseIndex',
camelCaseIndexDesc: 'camelCaseIndexDesc',
sortByAccessTime: 'sortByAccessTime',
sortByAccessTimeDesc: 'sortByAccessTimeDesc',
}
export interface Sentence {
readonly activateCountPerDay: string
readonly activatePluginToday: string
readonly activatePluginTotal: string
readonly auto: string
readonly beta: string
readonly betaDesc: string
readonly browser: string
readonly browserPathDescPrefix: string
readonly buyNow: string
readonly buySuccess: string
readonly catalogueSearchPlaceHolder: string
readonly catsxpBrowser: string
readonly centBrowser: string
readonly clearStatisticsWarning: string
readonly cloud: string
readonly cloudDesc: string
readonly configFileAt: string
readonly configPrefix: string
readonly configSuffix: string
readonly copyOrderId: string
readonly cssBackground: string
readonly cssBackgroundDesc: string
readonly cssBackgroundPlaceholder: string
readonly deepinBrowser: string
readonly deviceToday: string
readonly deviceTotal: string
readonly devTip: string
readonly emptyTipsDesc: string
readonly emptyTipsTitle: string
readonly enableDesc: string
readonly enableLabel: string
readonly enableStatistics: string
readonly enableStatisticsDesc: string
readonly enhanceConfig: string
readonly enhanceConfigChipDesc: string
readonly enhanceConfigDesc: string
readonly error: string
readonly errorArgs: string
readonly errorInfoToClipboard: string
readonly evernote: string
readonly evernoteUserId: string
readonly evernoteUserIdDesc: string
readonly executorFileAt: string
readonly executorPrefix: string
readonly executorSuffix: string
readonly fetchOrderError: string
readonly file: string
readonly fileOpening: string
readonly filePathInMatch: string
readonly filePathInMatchDesc: string
readonly filePathNonExistsTips: string
readonly fileSelectorPlaceholder: string
readonly filterNonExistsFiles: string
readonly filterNonExistsFilesDesc: string
readonly fullUrlInMatch: string
readonly fullUrlInMatchDesc: string
readonly getFavicon: string
readonly getFaviconDesc: string
readonly getFileIcon: string
readonly getFileIconDesc: string
readonly getProjectsError: string
readonly here: string
readonly historyLimit: string
readonly historyLimitDesc: string
readonly huaweiBrowser: string
readonly inputDirectlyPlaceholder: string
readonly inputPathDirectly: string
readonly inputPathDirectlyDesc: string
readonly languageSetting: string
readonly languageSettingDesc: string
readonly liebaoBrowser: string
readonly local: string
readonly localDesc: string
readonly logo: string
readonly logoDesc: string
readonly maxthonBrowser: string
readonly member: string
readonly memberPrefix: string
readonly moreStatistics: string
readonly moreStatisticsComing: string
readonly multiMatch: string
readonly multiMatchDesc: string
readonly nativeId: string
readonly nativeIdDesc: string
readonly needLogin: string
readonly needReboot: string
readonly needRebootDesc: string
readonly nonExistsFileOrDeleted: string
readonly nonExistsPathOrCancel: string
readonly notifyFileOpen: string
readonly notifyFileOpenDesc: string
readonly onlyProvideForMember: string
readonly onlyProvideForPluginMember: string
readonly opacity: string
readonly opacityDesc: string
readonly openInNew: string
readonly openInNewDesc: string
readonly orderId: string
readonly orderInfo: string
readonly orderTime: string
readonly outPluginImmediately: string
readonly outPluginImmediatelyDesc: string
readonly pathNotFound: string
readonly picture: string
readonly pictureDesc: string
readonly picturePath: string
readonly picturePathDesc: string
readonly picturePathPlaceholder: string
readonly pinyinIndex: string
readonly pinyinIndexDesc: string
readonly placeholder: string
readonly plugin: string
readonly pluginSetting: string
readonly ready: string
readonly reload: string
readonly requestMoreApplication: string
readonly safariBookmarkDesc: string
readonly search: string
readonly settingBetaDesc: string
readonly settingDocument: string
readonly shortcuts: string
readonly showBookmarkCatalogue: string
readonly showBookmarkCatalogueDesc: string
readonly simpleCustom: string
readonly smartTag: string
readonly softwareAccessCount: string
readonly softwareAccessCountByHour: string
readonly sourceCodeRepository: string
readonly statistics: string
readonly surveyPrefix: string
readonly surveySuffix: string
readonly systemInformation: string
readonly systemUser: string
readonly systemVersion: string
readonly tag: string
readonly theme: string
readonly toBeMember: string
readonly toBeMemberDesc: string
readonly toClipboard: string
readonly totalCustom: string
readonly twinkstarBrowser: string
readonly unknownError: string
readonly unknownInputError: string
readonly unready: string
readonly unSupportTipsDesc: string
readonly unSupportTipsTitle: string
readonly useCount: string
readonly utoolsVersion: string
readonly xiaobaiBrowser: string
readonly homepage: string
readonly clickToOpen: string
readonly roundTips: string
readonly roundCloseTips: string
readonly unlimited: string
readonly decreasePerformance: string
readonly decreasePerformanceDesc: string
readonly bought: string
readonly openPluginMemberDialog: string
readonly fusionMode: string
readonly fusionModeDesc: string
readonly configFolderSuffix: string
readonly configFolderAt: string
readonly offlineVersion: string
readonly pluginVersion: string
readonly snakeIndex: string
readonly snakeIndexDesc: string
readonly kebabIndex: string
readonly kebabIndexDesc: string
readonly camelCaseIndex: string
readonly camelCaseIndexDesc: string
readonly sortByAccessTime: string
readonly sortByAccessTimeDesc: string
}
let languageData = new I18n('zh-CN', {
'zh-CN': { ...(new ZhCn()) },
'en-US': { ...(new EnUs()) },
})
languageData.locale(navigator.language)
export const i18n = languageData

8
src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './style.css'
const app = createApp(App)
app.use(router)
app.mount('#app')

133
src/pages/Config.vue Normal file
View File

@@ -0,0 +1,133 @@
<script setup lang="ts">
import { ref } from 'vue'
// 配置项状态
const config = ref({
theme: 'light',
language: 'zh-CN',
autoSave: true,
notifications: true
})
// 处理配置更改
const handleConfigChange = () => {
// 这里可以添加保存配置的逻辑
console.log('配置已更新:', config.value)
}
</script>
<template>
<div class="config-container">
<h2 class="config-title">系统设置</h2>
<div class="config-section">
<div class="config-item">
<label>主题</label>
<select v-model="config.theme">
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
<div class="config-item">
<label>语言</label>
<select v-model="config.language">
<option value="zh-CN">中文</option>
<option value="en-US">English</option>
</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"
>
保存设置
</button>
</div>
</div>
</template>
<style scoped>
.config-container {
padding: 24px;
max-width: 600px;
margin: 0 auto;
}
.config-title {
font-size: 24px;
font-weight: 500;
color: #333;
margin-bottom: 24px;
}
.config-section {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.config-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.config-item:last-child {
border-bottom: none;
}
.config-item label {
font-size: 14px;
color: #333;
}
.config-item select {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
min-width: 120px;
}
.config-actions {
margin-top: 24px;
text-align: right;
}
.save-button {
background: #1976d2;
color: white;
border: none;
padding: 8px 24px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.save-button:hover {
background: #1565c0;
}
</style>

62
src/pages/Project.vue Normal file
View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { ref, type Ref } from 'vue'
import ProjectList from '@/components/ProjectList.vue'
import type { ListItem, MenuItem } from '@/types'
// 生成模拟数据
const listData: Ref<ListItem[]> = ref([])
const initListData = () => {
let samplePic = 'https://aisuda.bce.baidu.com/amis/static/favicon_b3b0647.png'
listData.value = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `project-${i + 1}`,
description: `这是项目 ${i + 1} 的详细描述信息`,
icon: samplePic,
tags: [
...(i % 2 === 0 ? [{ id: 1, name: 'Vue', color: '#42b883' }] : []),
...(i % 3 === 0 ? [{ id: 2, name: 'TypeScript', color: '#3178c6' }] : []),
...(i % 4 === 0 ? [{ id: 3, name: 'React', color: '#149eca' }] : []),
...(i % 5 === 0 ? [{ id: 4, name: 'Node.js', color: '#539e43' }] : []),
],
}))
}
initListData()
// 自定义菜单项
const menuItems: MenuItem[] = [
{
id: 'refresh',
label: '刷新列表',
action: (item) => {
console.log('当前选中项:', item)
},
},
{
id: 'clear',
label: '清空选择',
action: (item) => {
console.log('清空前选中项:', item)
},
},
{
id: 'export',
label: '导出数据',
action: (item) => {
console.log('当前选中项:', item)
},
},
]
// 列表项点击事件处理
const handleItemClick = (item: ListItem): void => {
console.log('点击列表项:', item)
}
</script>
<template>
<ProjectList
:data="listData"
:menu-items="menuItems"
@click="handleItemClick"
/>
</template>

View File

@@ -0,0 +1,452 @@
import { i18n, sentenceKey } from "@/i18n"
import { Context } from "./context"
import { Executor } from "./executor"
import { Platform } from "./helper"
import { isEmpty, isEqual } from "licia"
/**
* 选项
*
* 对 uTools 模板插件中结果项的定义, 增加了 id 和 searchKey 的适配, 方便使用
*/
export interface Item {
readonly id: string
readonly title: string
readonly description: string
readonly icon: string
readonly searchKey: Array<string>
}
/**
* 对选项的简单抽象实现, 方便使用
*/
export abstract class ItemImpl implements Item {
readonly id: string
readonly title: string
readonly description: string
readonly icon: string
readonly searchKey: Array<string>
score?: number
protected constructor(id: string, title: string, description: string, icon: string, searchKey: Array<string>) {
this.id = id
this.title = title
this.description = description
this.icon = icon
this.searchKey = searchKey
}
}
/**
* 项目选项
*
* 查询历史记录后的结果, 增加了用于打开该历史的 command 命令
*/
export abstract class ProjectItemImpl extends ItemImpl {
exists: boolean
command: Executor
protected constructor(id: string, title: string, description: string, icon: string, searchKey: Array<string>, exists: boolean, command: Executor) {
super(id, title, description, icon, searchKey)
this.exists = exists
this.command = command
}
}
export abstract class DatetimeProjectItemImpl extends ProjectItemImpl {
datetime: number
protected constructor(id: string, title: string, description: string, icon: string, searchKey: Array<string>, exists: boolean, command: Executor, datetime: number) {
super(id, title, description, icon, searchKey, exists, command)
this.datetime = datetime
}
}
/**
* 配置项的类型
*/
export enum SettingType {
/**
* 输入文本类型的配置
*/
plain,
/**
* 输入路径类型的配置
*/
path,
/**
* 开关类型的配置
*/
switch,
}
export interface SettingProperties {
readonly openFile: boolean
readonly openDirectory: boolean
readonly treatPackageAsDirectory: boolean
readonly filters: Array<{ name: string, extensions: Array<string> }>
}
export class DefaultSettingProperties implements SettingProperties {
readonly openFile: boolean = true
readonly openDirectory: boolean = false
readonly treatPackageAsDirectory: boolean = true
readonly filters: Array<{ name: string, extensions: Array<string> }> = []
}
export class SelectMacAppSettingProperties extends DefaultSettingProperties {
override readonly treatPackageAsDirectory: boolean = false
}
export interface SettingItem {
readonly type: SettingType
readonly id: string
readonly name: string
readonly value: string | boolean
readonly description?: string | DescriptionGetter
readonly placeholder?: string | DescriptionGetter
readonly properties: SettingProperties
}
export abstract class SettingItemImpl implements SettingItem {
readonly type: SettingType
readonly id: string
readonly name: string
readonly value: string | boolean
readonly description?: string | DescriptionGetter
readonly placeholder?: string | DescriptionGetter
readonly properties: SettingProperties
protected constructor(
type: SettingType,
id: string,
name: string,
value: string | boolean,
description?: string | DescriptionGetter,
placeholder?: string | DescriptionGetter,
properties: SettingProperties = new DefaultSettingProperties(),
) {
this.type = type
this.id = id
this.name = name
this.value = value
this.description = description
this.placeholder = placeholder
this.properties = properties
}
}
export class PlainSettingItem extends SettingItemImpl {
constructor(id: string, name: string, value: string, description?: string | DescriptionGetter, placeholder?: string | DescriptionGetter, properties?: SettingProperties) {
super(SettingType.plain, id, name, value, description, placeholder, properties)
}
}
export class InputSettingItem extends SettingItemImpl {
constructor(id: string, name: string, value: string, description?: string | DescriptionGetter, properties?: SettingProperties) {
super(SettingType.path, id, name, value, description, undefined, properties)
}
}
export class SwitchSettingItem extends SettingItemImpl {
constructor(id: string, name: string, value: boolean, description?: string | DescriptionGetter, properties?: SettingProperties) {
super(SettingType.switch, id, name, value, description, undefined, properties)
}
}
export enum ApplicationConfigState {
empty,
undone,
done,
error,
}
export const GROUP_BROWSER_HISTORY = 'Browser History'
export const GROUP_BROWSER_BOOKMARK = 'Browser Bookmark'
export const GROUP_EDITOR = 'Editor'
export const GROUP_NOTES = 'Notes'
export const GROUP_IDE = 'IDE'
export const GROUP_OFFICE = 'Office'
export const GROUP_SYSTEM = 'System'
export const GROUP_OTHER = 'Other'
export type NameGetter = (context?: Context) => string
export type DescriptionGetter = (context?: Context) => string | undefined
export enum Vip {
// 没有会员
none,
// uTools 会员
uTools,
// 插件会员
plugin,
}
/**
* 应用
*/
export interface Application<P extends ProjectItemImpl> {
readonly id: string
readonly name: string | NameGetter
readonly homepage: string
readonly icon: string
readonly type: string
readonly platform: Array<Platform>
readonly group: string
readonly description: string | DescriptionGetter
readonly beta: boolean
readonly vip: Vip
enabled: boolean
update: (nativeId: string) => void
generateSettingItems: (context: Context, nativeId: string) => Array<SettingItem>
generateProjectItems: (context: Context) => Promise<Array<P>>
isFinishConfig: (context: Context) => Promise<ApplicationConfigState>
}
/**
* 应用实现
* 定义了获取 configId 和 executorId 的方法
*/
export abstract class ApplicationImpl<P extends ProjectItemImpl> implements Application<P> {
readonly id: string
readonly name: string | NameGetter
readonly homepage: string
readonly icon: string
readonly type: string
readonly platform: Array<Platform>
readonly group: string
readonly description: string | DescriptionGetter
readonly beta: boolean
readonly vip: Vip = Vip.none
enabled: boolean = false
protected constructor(id: string, name: string | NameGetter, homepage: string = '', icon: string, type: string, platform: Array<Platform>, group: string = 'default', description: string | DescriptionGetter = '', beta: boolean = false, vip: Vip = Vip.none) {
this.id = id
this.name = name
this.homepage = homepage
this.icon = icon
this.type = type
this.platform = platform
this.group = group
this.description = description
this.beta = beta
this.vip = vip
}
enable() { return this.enabled }
disEnable() { return !this.enable() }
update(nativeId: string) {
this.enabled = utools.dbStorage.getItem(this.enabledId(nativeId)) ?? false
}
enabledId(nativeId: string): string {
return `${nativeId}/${this.id}-enabled`
}
// Enable Setting 作为顶层设置项固定显示在 SettingCard 里, 不在额外设置, 为了保留兼容性
// SettingCard 中还是会引用 ApplicationImpl 中设置的 EnableId 信息
// 此外 application 的 enable 也需要自身更新, 所以不适合提取赋值方法到其他地方
// 综上, 这个提取操作只能作为一个比较不美观的操作作为 Setting Item 的特例
generateSettingItems(context: Context, nativeId: string): Array<SettingItem> {
return []
}
abstract generateProjectItems(context: Context): Promise<Array<P>>
async isFinishConfig(context: Context): Promise<ApplicationConfigState> {
if (this.disEnable())
return ApplicationConfigState.empty
return ApplicationConfigState.done
}
protected async nonExistsPath(path: string): Promise<boolean> {
return await nonExistsToRead(path)
}
protected async existsPath(path: string): Promise<boolean> {
return await existsToRead(path)
}
}
export interface ApplicationCache<P extends ProjectItemImpl> {
cache: Array<P>
isNew(): Promise<boolean>
generateCacheProjectItems(context: Context): Promise<Array<P>>
clearCache(): Promise<void>
}
export abstract class ApplicationCacheImpl<P extends ProjectItemImpl> extends ApplicationImpl<P> implements ApplicationCache<P> {
cache: Array<P> = []
abstract generateCacheProjectItems(context: Context): Promise<Array<P>>
abstract isNew(): Promise<boolean>
override async generateProjectItems(context: Context): Promise<Array<P>> {
if (await this.isNew()) {
this.cache = await this.generateCacheProjectItems(context)
}
return this.cache
}
async clearCache(): Promise<void> {
this.cache = []
}
}
export abstract class ApplicationConfigImpl<P extends ProjectItemImpl> extends ApplicationImpl<P> {
protected readonly configFilename: string
protected config: string = ''
constructor(id: string, name: string | NameGetter, homepage: string = '', icon: string, type: string, platform: Array<Platform>, group: string = 'default', description: string | DescriptionGetter = '', beta: boolean = false, configFilename: string, vip: Vip = Vip.none) {
super(id, name, homepage, icon, type, platform, group, description, beta, vip)
this.configFilename = configFilename
}
override update(nativeId: string) {
super.update(nativeId)
this.config = utools.dbStorage.getItem(this.configId(nativeId)) ?? ''
}
configId(nativeId: string): string {
return `${nativeId}/${this.id}-config`
}
configSettingItem(context: Context, nativeId: string): SettingItem {
return new InputSettingItem(
this.configId(nativeId),
`${i18n.t(sentenceKey.configPrefix)} ${getName(this.name)}${this.configFilename}${i18n.t(sentenceKey.configSuffix)}`,
this.config,
undefined,
this.configSettingItemProperties(),
)
}
configSettingItemProperties(): SettingProperties {
return new DefaultSettingProperties()
}
override generateSettingItems(context: Context, nativeId: string): Array<SettingItem> {
return [
...super.generateSettingItems(context, nativeId),
this.configSettingItem(context, nativeId),
]
}
override async isFinishConfig(context: Context): Promise<ApplicationConfigState> {
if (this.disEnable())
return ApplicationConfigState.empty
if (isEmpty(this.config)) {
return ApplicationConfigState.undone
} else if (await this.nonExistsPath(this.config)) {
return ApplicationConfigState.error
} else {
return ApplicationConfigState.done
}
}
abstract defaultConfigPath(): string
}
export abstract class ApplicationCacheConfigImpl<P extends ProjectItemImpl> extends ApplicationConfigImpl<P> implements ApplicationCache<P> {
cache: Array<P> = []
private sign: string = ''
abstract generateCacheProjectItems(context: Context): Promise<Array<P>>
async isNew(): Promise<boolean> {
let last = this.sign
this.sign = await signCalculateAsync(this.config)
return isEmpty(last) ? true : !isEqual(this.sign, last)
}
override async generateProjectItems(context: Context): Promise<Array<P>> {
if (await this.isNew()) {
this.cache = await this.generateCacheProjectItems(context)
}
return this.cache
}
async clearCache(): Promise<void> {
this.cache = []
}
}
export abstract class ApplicationConfigAndExecutorImpl<P extends ProjectItemImpl> extends ApplicationConfigImpl<P> {
protected executor: string = ''
override update(nativeId: string) {
super.update(nativeId)
this.executor = utools.dbStorage.getItem(this.executorId(nativeId)) ?? ''
}
executorId(nativeId: string): string {
return `${nativeId}/${this.id}-executor`
}
executorSettingItem(context: Context, nativeId: string): SettingItem {
return new InputSettingItem(
this.executorId(nativeId),
`${i18n.t(sentenceKey.executorPrefix)} ${getName(this.name)} ${i18n.t(sentenceKey.executorSuffix)}`,
this.executor,
undefined,
this.executorSettingItemProperties(),
)
}
executorSettingItemProperties(): SettingProperties {
return new DefaultSettingProperties()
}
override generateSettingItems(context: Context, nativeId: string): Array<SettingItem> {
return [
...super.generateSettingItems(context, nativeId),
this.executorSettingItem(context, nativeId),
]
}
override async isFinishConfig(context: Context): Promise<ApplicationConfigState> {
if (this.disEnable())
return ApplicationConfigState.empty
if (isEmpty(this.config) || isEmpty(this.executor)) {
return ApplicationConfigState.undone
} else if ((await this.nonExistsPath(this.config)) || (await this.nonExistsPath(this.executor))) {
return ApplicationConfigState.error
} else {
return ApplicationConfigState.done
}
}
abstract defaultExecutorPath(): string
}
export abstract class ApplicationCacheConfigAndExecutorImpl<P extends ProjectItemImpl> extends ApplicationConfigAndExecutorImpl<P> implements ApplicationCache<P> {
cache: Array<P> = []
private sign: string = ''
abstract generateCacheProjectItems(context: Context): Promise<Array<P>>
async isNew(): Promise<boolean> {
let last = this.sign
this.sign = await signCalculateAsync(this.config)
return isEmpty(last) ? true : !isEqual(this.sign, last)
}
override async generateProjectItems(context: Context): Promise<Array<P>> {
if (await this.isNew()) {
this.cache = await this.generateCacheProjectItems(context)
}
return this.cache
}
async clearCache(): Promise<void> {
this.cache = []
}
}

View File

@@ -0,0 +1,35 @@
export class Configuration {
readonly isDev: boolean
readonly languageSetting: string = 'auto'
readonly enableFilterNonExistsFiles: boolean = false
readonly enableGetFaviconFromNet: boolean = false
readonly enableGetFileIcon: boolean = false
readonly enableOpenNotification: boolean = false
readonly enableEditPathInputDirectly: boolean = false
readonly enablePinyinIndex: boolean = true
readonly browserHistoryLimit: number = 100
readonly enableEnhanceConfig: boolean = false
readonly enableShowBookmarkCatalogue: boolean = true
readonly enableFilePathInMatch: boolean = true
readonly enableFullUrlInMatch: boolean = false
// 全局配置
readonly enableStatistic: boolean = false
readonly enableInputSwitch: boolean = false
readonly inputSwitchToolPath: string = ''
readonly enableMultiMatch: boolean = false
// 主题
readonly enableThemeLogo: boolean = false
readonly enableThemeColor: boolean = false
readonly enableOutPluginImmediately: boolean = true
readonly enableRoundRound: boolean = false
readonly enableBrowserFusion: boolean = false
readonly enableSnakeIndex: boolean = false
readonly enableKebabIndex: boolean = false
readonly enableCamelCaseIndex: boolean = false
constructor() {
this.isDev = utools.isDev() ?? false
}
}

View File

@@ -0,0 +1,12 @@
import { Configuration } from "./configuration"
import { Helper, UtoolsHelper } from "./helper"
export class Context {
readonly helper: Helper
readonly configuration: Configuration
constructor() {
this.helper = new UtoolsHelper()
this.configuration = new Configuration()
}
}

View File

@@ -0,0 +1,173 @@
import { isEmpty, isNil, trim } from "licia"
import { Context } from "./context"
import { i18n, sentenceKey } from "@/i18n"
import { Platform } from "./helper"
/**
* 命令执行器
*/
export interface Executor {
readonly command: string
readonly context: Context
execute: () => void
}
/**
* 没有命令执行器
*
* 用于表示没有执行器的概念, 用于提示性的结果项
*/
export class NoExecutor implements Executor {
readonly command: string
readonly context: Context
constructor(context: Context) {
this.context = context
this.command = ''
}
execute(): void {
}
}
/**
* 命令行执行器
*
* 使用 exec 命令在终端中执行指定的语句
*/
export class ShellExecutor implements Executor {
readonly command: string
readonly context: Context
constructor(context: Context, command: string) {
this.context = context
this.command = command
}
execute(): void {
if (isEmpty(this.command)) {
this.context.helper.errorNotify(i18n.t(sentenceKey.errorArgs))
return
}
exec(this.command, { windowsHide: true }, error => {
if (this.context.configuration.isDev) {
console.log('error', error)
}
if (isNil(error)) {
if (this.context.configuration.enableOutPluginImmediately) {
utools.hideMainWindow()
utools.outPlugin()
}
} else {
this.context.helper.errorNotify(error?.message ?? i18n.t(sentenceKey.unknownError))
}
})
}
}
export class NohupShellExecutor extends ShellExecutor {
constructor(context: Context, executor: string, path?: string, args?: string) {
let platform: Platform = context.helper.platform()
switch (platform) {
case Platform.darwin:
case Platform.linux:
super(context, `nohup "${executor}" ${isNil(path) ? '' : `"${path}"`} ${args ?? ''} > /dev/null 2>&1 &`)
break
case Platform.win32:
super(context, `powershell.exe -command "Start-Process -FilePath '${executor}' -ArgumentList '"""${path}"""'"`)
break
case Platform.unknown:
super(context, '')
break
}
}
}
/**
* Electron 执行器
*
* 使用 shell.openExternal 打开指定的链接, 即使用默认程度打开
*/
export class ElectronExecutor implements Executor {
readonly command: string
readonly context: Context
constructor(context: Context, command: string) {
this.context = context
this.command = command
}
execute(): void {
if (isEmpty(this.command)) {
this.context.helper.errorNotify(i18n.t(sentenceKey.errorArgs))
return
}
shell.openExternal(this.command)
.then(() => {
if (this.context.configuration.enableOutPluginImmediately) {
utools.hideMainWindow()
utools.outPlugin()
}
})
.catch(error => {
this.context.helper.errorNotify(error?.message ?? i18n.t(sentenceKey.unknownError))
})
}
}
export class ElectronPathExecutor implements Executor {
readonly command: string
readonly context: Context
constructor(context: Context, command: string) {
this.context = context
this.command = command
}
execute(): void {
if (isEmpty(this.command)) {
this.context.helper.errorNotify(i18n.t(sentenceKey.errorArgs))
return
}
shell.openPath(this.command)
.then(message => {
if (isEmpty(trim(message))) {
if (this.context.configuration.enableOutPluginImmediately) {
utools.hideMainWindow()
utools.outPlugin()
}
} else {
this.context.helper.infoNotify(message)
}
})
.catch(error => {
this.context.helper.errorNotify(error?.message ?? i18n.t(sentenceKey.unknownError))
})
}
}
export class UtoolsExecutor implements Executor {
readonly command: string
readonly context: Context
constructor(context: Context, command: string) {
this.context = context
this.command = command
}
execute(): void {
if (isEmpty(this.command)) {
this.context.helper.errorNotify(i18n.t(sentenceKey.errorArgs))
return
}
try {
utools.shellOpenExternal(this.command)
if (this.context.configuration.enableOutPluginImmediately) {
utools.hideMainWindow()
utools.outPlugin()
}
} catch (error: any) {
this.context.helper.errorNotify(error?.message ?? i18n.t(sentenceKey.unknownError))
}
}
}

View File

@@ -0,0 +1,71 @@
import { toStr } from "licia"
export enum Platform {
win32,
darwin,
linux,
unknown,
}
export const PLATFORM_ALL: Array<Platform> = [
Platform.win32,
Platform.darwin,
Platform.linux,
]
export const PLATFORM_NO_MACOS: Array<Platform> = [
Platform.win32,
Platform.linux,
]
export const PLATFORM_NO_LINUX: Array<Platform> = [
Platform.win32,
Platform.darwin,
]
export const PLATFORM_WINDOWS: Array<Platform> = [
Platform.win32,
]
export const PLATFORM_MACOS: Array<Platform> = [
Platform.darwin,
]
export const PLATFORM_LINUX: Array<Platform> = [
Platform.linux,
]
export interface Helper {
infoNotify(message: any): void
errorNotify(message: any): void
warningNotify(message: any): void
platform(): Platform
}
export class UtoolsHelper implements Helper {
infoNotify(message: any): void {
console.log('[Info]', message)
utools.showNotification(`[Info] ${toStr(message)}`)
}
warningNotify(message: any): void {
console.warn('[Warn]', message)
utools.showNotification(`[Warn] ${toStr(message)}`)
}
errorNotify(message: any): void {
console.error('[Error]', message)
utools.showNotification(`[Error] ${toStr(message)}`)
}
private isMacOS = () => {
// @ts-ignore
return isNil(utools.isMacOS) ? utools.isMacOs() : utools.isMacOS()
}
platform(): Platform {
if (utools.isWindows()) return Platform.win32
else if (this.isMacOS()) return Platform.darwin
else if (utools.isLinux()) return Platform.linux
else return Platform.unknown
}
}

View File

27
src/router/index.ts Normal file
View File

@@ -0,0 +1,27 @@
import { createWebHashHistory } from 'vue-router'
import { createRouter } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/project'
},
{
path: '/project',
name: 'project',
component: () => import('@/pages/Project.vue')
},
{
path: '/config',
name: 'config',
component: () => import('@/pages/Config.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router

10
src/style.css Normal file
View File

@@ -0,0 +1,10 @@
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
#app {
height: 100%;
}

41
src/types/index.ts Normal file
View File

@@ -0,0 +1,41 @@
/**
* 标签信息接口
*/
export interface Tag {
/** 标签唯一标识 */
id: number
/** 标签名称 */
name: string
/** 标签颜色(十六进制颜色值) */
color: string
}
/**
* 列表项数据接口
*/
export interface ListItem {
/** 项目唯一标识 */
id: number
/** 项目名称 */
name: string
/** 项目描述 */
description: string
/** 项目图标 URL */
icon?: string
/** 项目标签列表 */
tags?: Tag[]
/** 在虚拟列表中的索引位置 */
index?: number
}
/**
* 菜单项接口
*/
export interface MenuItem {
/** 菜单项唯一标识 */
id: string
/** 菜单项显示文本 */
label: string
/** 菜单项点击处理函数,接收当前选中的列表项作为参数 */
action: (item: ListItem | null) => void
}

37
tsconfig.json Normal file
View 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/*"
]
},
"types": [
"vue-router",
"node",
"utools-api-types"
]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
}

15
vite.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig(({ command, mode }) => {
return {
plugins: [vue()],
base: mode === 'production' ? './' : '/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
}
})