工具完成
This commit is contained in:
248
src/views/HomeView.vue
Normal file
248
src/views/HomeView.vue
Normal file
@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-main">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="fixed top-0 left-0 w-full bg-card border-b border-opacity-15 border-primary-500 shadow-lg" style="z-index: 999;">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Logo和标题 -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="text-2xl font-bold text-purple">
|
||||
子归云
|
||||
</div>
|
||||
<div class="text-sm text-secondary hidden sm:block">
|
||||
{{ t('common.tools') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主题和语言切换 -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<ThemeToggle />
|
||||
<LanguageToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 pt-24">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="mb-8">
|
||||
<div class="relative max-w-md mx-auto">
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
type="text"
|
||||
:placeholder="t('common.search') + '...'"
|
||||
class="input-field pl-10 text-center"
|
||||
@keyup.enter="searchTools"
|
||||
>
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<FontAwesomeIcon :icon="['fas', 'search']" class="text-tertiary" />
|
||||
</div>
|
||||
<button
|
||||
v-if="searchTerm"
|
||||
@click="clearSearch"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'times']" class="text-tertiary hover:text-primary transition-colors" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类过滤 -->
|
||||
<div class="mb-8">
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
<button
|
||||
v-for="category in categories"
|
||||
:key="category.code"
|
||||
@click="() => setActiveCategory(category.code)"
|
||||
:class="[
|
||||
'px-4 py-2 rounded-full text-sm font-medium transition-all duration-200',
|
||||
activeCategory === category.code
|
||||
? 'bg-primary-500 text-white shadow-lg'
|
||||
: 'bg-card text-secondary hover:bg-block-hover hover:text-primary'
|
||||
]"
|
||||
>
|
||||
{{ t(`categories.${category.code}`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具网格 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<div
|
||||
v-for="tool in filteredTools"
|
||||
:key="tool.code"
|
||||
@click="() => navigateToTool(tool.code)"
|
||||
class="card p-6 cursor-pointer group animate-fadeIn"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="icon-container">
|
||||
<FontAwesomeIcon
|
||||
:icon="tool.icon"
|
||||
class="text-2xl text-primary-light group-hover:text-primary transition-colors duration-200"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@click.stop="() => toggleFavorite(tool.code)"
|
||||
class="text-tertiary hover:text-warning transition-colors"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="favoriteTools.includes(tool.code) ? ['fas', 'star'] : ['far', 'star']"
|
||||
:class="favoriteTools.includes(tool.code) ? 'text-warning' : ''"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-primary mb-2">
|
||||
{{ getToolTitle(tool.code) }}
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-secondary line-clamp-2">
|
||||
{{ getToolDescription(tool.code) }}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="cat in tool.category.slice(0, 2)"
|
||||
:key="cat"
|
||||
class="px-2 py-1 text-xs rounded bg-block text-tertiary"
|
||||
>
|
||||
{{ t(`categories.${cat}`) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="filteredTools.length === 0"
|
||||
class="text-center py-16"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'search']" class="text-6xl text-tertiary mb-4" />
|
||||
<h3 class="text-xl font-semibold text-secondary mb-2">
|
||||
{{ t('common.noData') }}
|
||||
</h3>
|
||||
<p class="text-tertiary">
|
||||
尝试调整搜索条件或选择其他分类
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 返回顶部按钮 -->
|
||||
<BackToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useLanguage } from '@/composables/useLanguage'
|
||||
import tools from '@/config/tools'
|
||||
import categories from '@/config/categories'
|
||||
import type { Tool } from '@/types/tools'
|
||||
|
||||
// 组件导入
|
||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
import LanguageToggle from '@/components/LanguageToggle.vue'
|
||||
import BackToTop from '@/components/BackToTop.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useLanguage()
|
||||
|
||||
// 响应式状态
|
||||
const searchTerm = ref('')
|
||||
const activeCategory = ref('all')
|
||||
const favoriteTools = ref<string[]>([])
|
||||
|
||||
// 设置激活分类
|
||||
const setActiveCategory = (categoryCode: string) => {
|
||||
activeCategory.value = categoryCode
|
||||
localStorage.setItem('lastActiveCategory', categoryCode)
|
||||
}
|
||||
|
||||
// 过滤工具
|
||||
const filteredTools = computed(() => {
|
||||
let filtered = tools
|
||||
|
||||
// 分类过滤
|
||||
if (activeCategory.value !== 'all') {
|
||||
filtered = filtered.filter(tool => tool.category.includes(activeCategory.value))
|
||||
}
|
||||
|
||||
// 搜索过滤
|
||||
if (searchTerm.value.trim()) {
|
||||
const searchLower = searchTerm.value.toLowerCase().trim()
|
||||
filtered = filtered.filter(tool => {
|
||||
const title = getToolTitle(tool.code).toLowerCase()
|
||||
const description = getToolDescription(tool.code).toLowerCase()
|
||||
const keywords = tool.keywords.join(' ').toLowerCase()
|
||||
|
||||
return title.includes(searchLower) ||
|
||||
description.includes(searchLower) ||
|
||||
keywords.includes(searchLower)
|
||||
})
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
// 获取工具标题
|
||||
const getToolTitle = (toolCode: string) => {
|
||||
return t(`tools.${toolCode}.title`) || toolCode
|
||||
}
|
||||
|
||||
// 获取工具描述
|
||||
const getToolDescription = (toolCode: string) => {
|
||||
return t(`tools.${toolCode}.description`) || ''
|
||||
}
|
||||
|
||||
// 导航到工具页面
|
||||
const navigateToTool = (toolCode: string) => {
|
||||
router.push(`/tools/${toolCode}`)
|
||||
}
|
||||
|
||||
// 搜索工具
|
||||
const searchTools = () => {
|
||||
// 搜索逻辑已经在computed中处理
|
||||
}
|
||||
|
||||
// 清除搜索
|
||||
const clearSearch = () => {
|
||||
searchTerm.value = ''
|
||||
}
|
||||
|
||||
// 切换收藏
|
||||
const toggleFavorite = (toolCode: string) => {
|
||||
const index = favoriteTools.value.indexOf(toolCode)
|
||||
if (index > -1) {
|
||||
favoriteTools.value.splice(index, 1)
|
||||
} else {
|
||||
favoriteTools.value.push(toolCode)
|
||||
}
|
||||
localStorage.setItem('favoriteTools', JSON.stringify(favoriteTools.value))
|
||||
}
|
||||
|
||||
// 从本地存储恢复数据
|
||||
onMounted(() => {
|
||||
// 恢复收藏工具
|
||||
const saved = localStorage.getItem('favoriteTools')
|
||||
if (saved) {
|
||||
favoriteTools.value = JSON.parse(saved)
|
||||
}
|
||||
|
||||
// 恢复激活分类
|
||||
const savedCategory = localStorage.getItem('lastActiveCategory')
|
||||
if (savedCategory) {
|
||||
activeCategory.value = savedCategory
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
41
src/views/NotFoundView.vue
Normal file
41
src/views/NotFoundView.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-main flex items-center justify-center">
|
||||
<div class="text-center px-4">
|
||||
<div class="text-8xl text-primary mb-4">404</div>
|
||||
<h1 class="text-3xl font-bold text-primary mb-4">页面未找到</h1>
|
||||
<p class="text-lg text-secondary mb-8">
|
||||
抱歉,您访问的页面不存在或已被移除。
|
||||
</p>
|
||||
<div class="space-x-4">
|
||||
<button
|
||||
@click="goHome"
|
||||
class="btn-primary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'home']" class="mr-2" />
|
||||
返回首页
|
||||
</button>
|
||||
<button
|
||||
@click="goBack"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'arrow-left']" class="mr-2" />
|
||||
返回上页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
history.back()
|
||||
}
|
||||
</script>
|
197
src/views/ToolView.vue
Normal file
197
src/views/ToolView.vue
Normal file
@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-main">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="fixed top-0 left-0 w-full bg-card border-b border-opacity-15 border-primary-500 shadow-lg" style="z-index: 999;">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- 返回按钮和标题 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="goBack"
|
||||
class="p-2 rounded-lg bg-card hover:bg-block-hover text-secondary hover:text-primary transition-all duration-200"
|
||||
title="返回首页"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'arrow-left']" class="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<div v-if="currentTool" class="icon-container">
|
||||
<FontAwesomeIcon
|
||||
:icon="currentTool.icon"
|
||||
class="text-2xl text-primary-light"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-primary">
|
||||
{{ toolTitle }}
|
||||
</h1>
|
||||
<p class="text-sm text-secondary hidden sm:block">
|
||||
{{ toolDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主题和语言切换 -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<ThemeToggle />
|
||||
<LanguageToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 工具内容区 -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 pt-24">
|
||||
<div v-if="isLoading" class="text-center py-16">
|
||||
<FontAwesomeIcon :icon="['fas', 'spinner']" class="text-4xl text-primary animate-spin mb-4" />
|
||||
<p class="text-secondary">{{ t('common.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="toolNotFound" class="text-center py-16">
|
||||
<FontAwesomeIcon :icon="['fas', 'search']" class="text-6xl text-tertiary mb-4" />
|
||||
<h2 class="text-2xl font-semibold text-secondary mb-2">工具未找到</h2>
|
||||
<p class="text-tertiary mb-6">您访问的工具不存在或暂未实现</p>
|
||||
<button
|
||||
@click="goBack"
|
||||
class="btn-primary"
|
||||
>
|
||||
返回首页
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 动态工具组件 -->
|
||||
<component
|
||||
v-else-if="ToolComponent"
|
||||
:is="ToolComponent"
|
||||
class="animate-fadeIn"
|
||||
/>
|
||||
</main>
|
||||
|
||||
<!-- 返回顶部按钮 -->
|
||||
<BackToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, defineAsyncComponent, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useLanguage } from '@/composables/useLanguage'
|
||||
import tools from '@/config/tools'
|
||||
import type { Tool } from '@/types/tools'
|
||||
|
||||
// 组件导入
|
||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
import LanguageToggle from '@/components/LanguageToggle.vue'
|
||||
import BackToTop from '@/components/BackToTop.vue'
|
||||
|
||||
interface Props {
|
||||
toolCode: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const router = useRouter()
|
||||
const { t } = useLanguage()
|
||||
|
||||
// 响应式状态
|
||||
const isLoading = ref(true)
|
||||
const toolNotFound = ref(false)
|
||||
const ToolComponent = ref<any>(null)
|
||||
|
||||
// 当前工具信息
|
||||
const currentTool = computed((): Tool | undefined => {
|
||||
return tools.find(tool => tool.code === props.toolCode)
|
||||
})
|
||||
|
||||
// 工具标题
|
||||
const toolTitle = computed(() => {
|
||||
if (!currentTool.value) return '未知工具'
|
||||
return t(`tools.${currentTool.value.code}.title`) || currentTool.value.code
|
||||
})
|
||||
|
||||
// 工具描述
|
||||
const toolDescription = computed(() => {
|
||||
if (!currentTool.value) return ''
|
||||
return t(`tools.${currentTool.value.code}.description`) || ''
|
||||
})
|
||||
|
||||
// 返回首页
|
||||
const goBack = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 工具组件映射表
|
||||
const toolComponentMap: Record<string, () => Promise<any>> = {
|
||||
json_formatter: () => import('@/components/tools/JsonFormatter.vue'),
|
||||
timestamp_converter: () => import('@/components/tools/TimestampConverter.vue'),
|
||||
encoding_converter: () => import('@/components/tools/EncodingConverter.vue'),
|
||||
regex_tester: () => import('@/components/tools/RegexTester.vue'),
|
||||
crypto_tools: () => import('@/components/tools/CryptoTools.vue'),
|
||||
color_tools: () => import('@/components/tools/ColorTools.vue'),
|
||||
code_formatter: () => import('@/components/tools/CodeFormatter.vue'),
|
||||
json_editor: () => import('@/components/tools/JsonEditor.vue'),
|
||||
json_converter: () => import('@/components/tools/JsonConverter.vue'),
|
||||
url_encoder: () => import('@/components/tools/UrlEncoder.vue'),
|
||||
unicode_converter: () => import('@/components/tools/UnicodeConverter.vue'),
|
||||
jwt_decoder: () => import('@/components/tools/JwtDecoder.vue'),
|
||||
ip_lookup: () => import('@/components/tools/IpLookup.vue'),
|
||||
date_calculator: () => import('@/components/tools/DateCalculator.vue'),
|
||||
timezone_converter: () => import('@/components/tools/TimezoneConverter.vue'),
|
||||
text_counter: () => import('@/components/tools/TextCounter.vue'),
|
||||
text_space_stripper: () => import('@/components/tools/TextSpaceStripper.vue'),
|
||||
html_markdown_converter: () => import('@/components/tools/HtmlMarkdownConverter.vue'),
|
||||
image_compressor: () => import('@/components/tools/ImageCompressor.vue'),
|
||||
qrcode_generator: () => import('@/components/tools/QrcodeGenerator.vue'),
|
||||
css_gradient_generator: () => import('@/components/tools/CssGradientGenerator.vue'),
|
||||
number_base_converter: () => import('@/components/tools/NumberBaseConverter.vue'),
|
||||
yml_properties_converter: () => import('@/components/tools/YmlPropertiesConverter.vue'),
|
||||
base64_to_image: () => import('@/components/tools/Base64ToImage.vue'),
|
||||
image_watermark: () => import('@/components/tools/ImageWatermark.vue'),
|
||||
image_to_ico: () => import('@/components/tools/ImageToIco.vue'),
|
||||
cron_generator: () => import('@/components/tools/CronGenerator.vue'),
|
||||
http_tester: () => import('@/components/tools/HttpTester.vue'),
|
||||
chrome_bookmark_recovery: () => import('@/components/tools/ChromeBookmarkRecovery.vue')
|
||||
}
|
||||
|
||||
// 加载工具组件
|
||||
const loadToolComponent = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
toolNotFound.value = false
|
||||
|
||||
// 检查工具是否存在
|
||||
if (!currentTool.value) {
|
||||
toolNotFound.value = true
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 检查组件是否存在
|
||||
const componentLoader = toolComponentMap[props.toolCode]
|
||||
if (!componentLoader) {
|
||||
toolNotFound.value = true
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 动态加载组件
|
||||
const component = defineAsyncComponent(componentLoader)
|
||||
ToolComponent.value = component
|
||||
|
||||
isLoading.value = false
|
||||
} catch (error) {
|
||||
console.error('加载工具组件失败:', error)
|
||||
toolNotFound.value = true
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadToolComponent()
|
||||
})
|
||||
|
||||
// 监听路由变化
|
||||
watch(() => props.toolCode, () => {
|
||||
loadToolComponent()
|
||||
})
|
||||
</script>
|
Reference in New Issue
Block a user