Files
Thing/src/modules/devtools/DevToolsModule.vue
T
2026-09-22 19:10:16 +08:00

181 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { ArrowLeft, Search } from '@lucide/vue'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { useSearchStore } from '@/stores/searchStore'
import { registerAllTools, TOOLS_META } from './tools'
import {
getAllTools, getTool, searchTools,
CATEGORY_ORDER, CATEGORY_LABEL, type DevTool
} from './registry'
const searchStore = useSearchStore()
registerAllTools()
const allTools = getAllTools()
// ===== 列表视图:搜索 + 分类筛选 =====
const query = ref('')
const category = ref<'all' | DevTool['category']>('all')
const filteredTools = computed(() => {
const list = searchTools(query.value)
if (category.value === 'all') return list
return list.filter(t => t.category === category.value)
})
interface ToolGroup {
category: DevTool['category']
label: string
tools: DevTool[]
}
const groupedTools = computed<ToolGroup[]>(() => {
if (category.value !== 'all') {
return [{
category: category.value as DevTool['category'],
label: CATEGORY_LABEL[category.value as DevTool['category']],
tools: filteredTools.value
}]
}
const groups: ToolGroup[] = []
for (const cat of CATEGORY_ORDER) {
const tools = filteredTools.value.filter(t => t.category === cat)
if (tools.length > 0) groups.push({ category: cat, label: CATEGORY_LABEL[cat], tools })
}
return groups
})
// ===== 详情视图 =====
const selectedId = ref<string | null>(null)
const selectedTool = computed<DevTool | null>(() => {
if (!selectedId.value) return null
return getTool(selectedId.value) ?? null
})
function openTool(id: string) {
selectedId.value = id
}
// ===== 全局搜索跳转:工具索引 → 打开对应工具 =====
onMounted(() => {
const actions = new Map<number, () => void>()
TOOLS_META.forEach((t, i) => {
actions.set(i, () => openTool(t.id))
})
searchStore.registerActions('devtools', actions)
})
watch(selectedId, () => {
query.value = ''
})
</script>
<template>
<!--
# 滚动归属只有主滚动区滚2026-09-22 由内滚动改为外滚动
此前本模块的列表页与详情页各自套一层 `ScrollArea``h-full overflow-hidden`
的模块根 + 内部滚动), monitor / music / clipboard / 设置页的形态不一致 ——
那些模块的模块根是 `min-h-full p-*`整页在 ModuleContainer 的主滚动区里滚
两套滚动归属混用会带来一串副作用Tab /工具条要不要固定内边距该加在
滚动容器根还是内层卡片阴影被视口裁`-mx-3 px-3` 就是在补这个)。
现在统一到外滚动模块根 `min-h-full p-5`内容自然增高滚动交给主滚动区
p-5 属于**滚动内容**模块根在主滚动区内部),因此卡片阴影四周都有余量
不再需要任何负外边距补偿
工具自身需要限高的地方正则结果diff 输出等仍保留各自的 `max-h-*` 内滚动
盒子 —— 那是一块有边界的输出区」,与页面级滚动不是一回事
-->
<div class="min-h-full p-5 flex flex-col">
<!-- 列表视图 -->
<template v-if="!selectedTool">
<div class="flex items-center gap-3 mb-4 flex-wrap">
<div class="relative flex-1 min-w-[180px] max-w-xs">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input v-model="query" placeholder="搜索工具..." class="h-8 pl-8 text-sm" />
</div>
<!--
分类筛选选中态用主题色软底 SegmentedNav 及其余模块的筛选 chip 同一
口径)。此前写的是 `bg-secondary text-foreground` —— 灰底黑字读起来像
禁用」,也是全项目唯一用实心灰表达选中的地方
-->
<Button
v-for="cat in (['all', ...CATEGORY_ORDER] as const)"
:key="cat"
size="sm"
variant="ghost"
class="h-7 px-2.5 text-xs"
:class="
category === cat
? 'bg-primary-soft font-medium text-primary-soft-text'
: 'text-fg-secondary hover:text-foreground'
"
@click="category = cat as typeof category"
>
{{ cat === 'all' ? '全部' : CATEGORY_LABEL[cat as DevTool['category']] }}
</Button>
<span class="text-xs text-muted-foreground ml-auto">{{ (category === 'all' ? allTools : filteredTools).length }} 个工具</span>
</div>
<!-- 空态flex-1 撑住剩余高度短页面时也能垂直居中 -->
<div
v-if="filteredTools.length === 0"
class="flex-1 flex items-center justify-center text-muted-foreground text-sm"
>
没有找到匹配的工具
</div>
<!-- 按分类分组 -->
<div v-else>
<div v-for="group in groupedTools" :key="group.category" class="mb-5">
<div class="flex items-center gap-2 mb-2">
<span class="text-xs font-medium text-muted-foreground">{{ group.label }}</span>
<Separator class="flex-1" />
</div>
<div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3">
<Card
v-for="tool in group.tools"
:key="tool.id"
class="cursor-pointer transition-all hover:border-primary/50 hover:shadow-sm !py-0 !gap-0"
@click="openTool(tool.id)"
>
<CardContent class="p-4 flex flex-col gap-2">
<div class="size-9 rounded-lg bg-primary/10 text-primary flex items-center justify-center">
<component :is="tool.icon" class="size-5" />
</div>
<div class="flex flex-col gap-0.5">
<span class="text-sm font-medium leading-tight">{{ tool.name }}</span>
<span class="text-xs text-muted-foreground leading-snug line-clamp-2">{{ tool.description }}</span>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</template>
<!-- 工具详情视图 -->
<template v-else>
<div class="flex items-center gap-3 mb-4">
<Button size="sm" variant="ghost" class="gap-1 h-8" @click="selectedId = null">
<ArrowLeft class="size-4" />
返回
</Button>
<div class="size-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center">
<component :is="selectedTool.icon" class="size-4" />
</div>
<div class="flex flex-col">
<span class="text-sm font-medium leading-tight">{{ selectedTool.name }}</span>
<span class="text-xs text-muted-foreground leading-snug">{{ selectedTool.description }}</span>
</div>
</div>
<component :is="selectedTool.component" />
</template>
</div>
</template>