Files
tyc-uniapp_V2/src/pages/toolbox/category.vue
2026-05-21 14:51:06 +08:00

538 lines
12 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 { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { toolboxCategories, toolboxItems, getCategoryAllTools } from '@/config/toolboxRegistry'
definePage({
style: {
navigationBarTitleText: '分类工具',
navigationStyle: 'default',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
})
const categoryKey = ref('')
const category = ref<any>(null)
const tools = ref<any[]>([])
const isAllCategories = ref(false)
/** 当前选中的 tab 索引 */
const activeTab = ref(0)
/** 搜索关键词 */
const searchKeyword = ref('')
/** 当前选中分类的完整数据 */
const activeCategory = computed(() => toolboxCategories[activeTab.value] || toolboxCategories[0])
/** 当前选中分类的全部工具 */
const activeTools = computed(() => getCategoryAllTools(activeCategory.value.key))
/** 是否正在搜索(有关键词) */
const isSearching = computed(() => searchKeyword.value.trim().length > 0)
/** 搜索结果:从所有工具中匹配名称 / 描述 / key */
const searchResults = computed(() => {
const kw = searchKeyword.value.trim().toLowerCase()
if (!kw) return []
return toolboxItems.filter(item =>
item.name.toLowerCase().includes(kw)
|| item.desc.toLowerCase().includes(kw)
|| item.key.toLowerCase().includes(kw),
)
})
onLoad((query) => {
const key = (query?.category as string) || ''
categoryKey.value = key
if (key === 'all') {
isAllCategories.value = true
uni.setNavigationBarTitle({ title: '全部分类' })
return
}
const cat = toolboxCategories.find(c => c.key === key)
if (cat) {
category.value = cat
tools.value = getCategoryAllTools(key)
uni.setNavigationBarTitle({ title: cat.name })
}
})
function switchTab(idx: number) {
activeTab.value = idx
searchKeyword.value = ''
}
function clearSearch() {
searchKeyword.value = ''
}
function goTool(key: string) {
uni.navigateTo({
url: `/pages/toolbox/query?key=${encodeURIComponent(key)}`,
})
}
</script>
<template>
<view class="page-root">
<!-- 全部分类视图 -->
<template v-if="isAllCategories">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrap">
<view class="search-icon i-carbon-search" />
<input
class="search-input"
type="text"
placeholder="搜索工具名称"
placeholder-class="search-placeholder"
:value="searchKeyword"
confirm-type="search"
@input="searchKeyword = $event.detail.value"
/>
<view v-if="isSearching" class="search-clear i-carbon-close-filled" @tap="clearSearch" />
</view>
</view>
<!-- Tab 搜索时隐藏 -->
<view v-show="!isSearching" class="tab-bar-wrap">
<scroll-view scroll-x class="tab-scroll" :scroll-into-view="'tab-' + activeTab" :scroll-with-animation="true">
<view class="tab-bar">
<view
v-for="(cat, idx) in toolboxCategories"
:id="'tab-' + idx"
:key="cat.key"
class="tab-item"
:class="{ 'tab-item--active': activeTab === idx }"
@tap="switchTab(idx)"
>
<text class="tab-text" :class="{ 'tab-text--active': activeTab === idx }">{{ cat.name }}</text>
</view>
</view>
</scroll-view>
<view class="tab-indicator-track">
<view
class="tab-indicator"
:style="{ width: `${100 / toolboxCategories.length}%`, transform: `translateX(${activeTab * 100}%)` }"
/>
</view>
</view>
<!-- 搜索结果 / 工具网格 -->
<scroll-view scroll-y class="tool-scrollarea">
<view class="tool-grid-page">
<!-- 搜索结果视图 -->
<template v-if="isSearching">
<view v-if="searchResults.length === 0" class="search-empty">
<view class="search-empty-icon i-carbon-search" />
<text class="search-empty-text">未找到相关工具</text>
</view>
<template v-else>
<view class="grid-header">
<text class="grid-header-name">搜索结果</text>
<text class="grid-header-count"> {{ searchResults.length }} </text>
</view>
<view class="tool-grid">
<view
v-for="item in searchResults"
:key="item.key"
class="tool-cell"
@tap="goTool(item.key)"
>
<view class="tool-cell-icon-wrap" style="background: #eef4ff">
<view :class="['tool-cell-icon', item.icon]" style="color: #1768ff" />
</view>
<text class="tool-cell-name">{{ item.name }}</text>
</view>
</view>
</template>
</template>
<!-- 正常 Tab 工具网格 -->
<template v-else>
<view class="grid-header">
<view class="grid-header-dot" :style="{ background: activeCategory.color }" />
<text class="grid-header-name">{{ activeCategory.name }}</text>
<text class="grid-header-count"> {{ activeTools.length }} </text>
</view>
<view class="tool-grid">
<view
v-for="item in activeTools"
:key="item.key"
class="tool-cell"
@tap="goTool(item.key)"
>
<view class="tool-cell-icon-wrap" :style="{ background: `${activeCategory.color}12` }">
<view :class="['tool-cell-icon', item.icon]" :style="{ color: activeCategory.color }" />
</view>
<text class="tool-cell-name">{{ item.name }}</text>
</view>
</view>
</template>
</view>
</scroll-view>
</template>
<!-- 单分类视图 -->
<template v-else-if="category">
<scroll-view scroll-y class="scrollarea">
<view class="page">
<view class="cat-header">
<view class="cat-icon-large" :style="{ background: `${category.color}15` }">
<view :class="['icon', category.icon]" :style="{ color: category.color }" />
</view>
<view class="cat-info">
<text class="cat-name">{{ category.name }}</text>
<text class="cat-count"> {{ tools.length }} 个工具</text>
</view>
</view>
<view class="tool-list">
<view
v-for="item in tools"
:key="item.key"
class="tool-item"
@tap="goTool(item.key)"
>
<view class="item-icon-wrap" :style="{ background: category ? `${category.color}12` : '#e8f0fe' }">
<view :class="['item-icon', item.icon]" :style="{ color: category?.color || '#1768ff' }" />
</view>
<view class="item-content">
<text class="item-name">{{ item.name }}</text>
<text class="item-desc">{{ item.desc }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</template>
</view>
</template>
<style scoped lang="scss">
.page-root {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #f8faff 0%, #f3f5fb 100%);
}
/* ============ 搜索栏 ============ */
.search-bar {
padding: 16rpx 24rpx 12rpx;
background: #fff;
flex-shrink: 0;
}
.search-input-wrap {
display: flex;
align-items: center;
height: 72rpx;
background: #f5f6fa;
border-radius: 36rpx;
padding: 0 24rpx;
gap: 12rpx;
}
.search-icon {
font-size: 30rpx;
color: #c0c4cc;
flex-shrink: 0;
}
.search-input {
flex: 1;
font-size: 26rpx;
color: #1d2129;
height: 72rpx;
line-height: 72rpx;
}
.search-placeholder {
color: #c0c4cc;
font-size: 26rpx;
}
.search-clear {
font-size: 30rpx;
color: #c0c4cc;
flex-shrink: 0;
padding: 8rpx;
}
.search-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0 80rpx;
}
.search-empty-icon {
font-size: 80rpx;
color: #dcdfe6;
margin-bottom: 20rpx;
}
.search-empty-text {
font-size: 26rpx;
color: #86909c;
}
/* ============ Tab 栏 ============ */
.tab-bar-wrap {
background: #fff;
flex-shrink: 0;
border-bottom: 1rpx solid #f0f1f5;
}
.tab-scroll {
width: 100%;
white-space: nowrap;
}
.tab-bar {
display: flex;
height: 88rpx;
line-height: 88rpx;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0 8rpx;
position: relative;
}
.tab-text {
font-size: 28rpx;
color: #86909c;
transition: color 0.25s, font-weight 0.25s;
}
.tab-text--active {
color: #1d2129;
font-weight: 600;
}
/* 下划线指示器 */
.tab-indicator-track {
height: 6rpx;
position: relative;
background: transparent;
}
.tab-indicator {
position: absolute;
top: 0;
left: 0;
height: 6rpx;
border-radius: 3rpx;
background: #1768ff;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* ============ 工具网格区域 ============ */
.tool-scrollarea {
flex: 1;
min-height: 0;
}
.tool-grid-page {
padding: 24rpx 24rpx 40rpx;
box-sizing: border-box;
}
.grid-header {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 24rpx;
padding: 0 4rpx;
}
.grid-header-dot {
width: 12rpx;
height: 12rpx;
border-radius: 4rpx;
}
.grid-header-name {
font-size: 28rpx;
font-weight: 600;
color: #1d2129;
}
.grid-header-count {
font-size: 22rpx;
color: #86909c;
margin-left: auto;
}
/* 4列网格 */
.tool-grid {
display: flex;
flex-wrap: wrap;
}
.tool-cell {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 4rpx;
box-sizing: border-box;
}
.tool-cell:active {
opacity: 0.7;
}
.tool-cell-icon-wrap {
width: 80rpx;
height: 80rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
.tool-cell-icon {
font-size: 38rpx;
}
.tool-cell-name {
font-size: 22rpx;
color: #333;
text-align: center;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
/* ============ 单分类详情 ============ */
.scrollarea {
flex: 1;
min-height: 0;
height: 0;
}
.page {
padding: 24rpx 24rpx 40rpx;
box-sizing: border-box;
}
.cat-header {
display: flex;
align-items: center;
gap: 24rpx;
padding: 28rpx 32rpx;
background: linear-gradient(145deg, #ffffff 0%, #f7f8ff 100%);
border-radius: 24rpx;
border: 1rpx solid #e5e6f0;
margin-bottom: 24rpx;
box-shadow: 0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
}
.cat-icon-large {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
font-size: 44rpx;
}
.cat-info {
flex: 1;
}
.cat-name {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
margin-bottom: 6rpx;
}
.cat-count {
display: block;
font-size: 24rpx;
color: #86909c;
}
.tool-list {
background: linear-gradient(145deg, #ffffff 0%, #f7f8ff 100%);
border-radius: 24rpx;
border: 1rpx solid #e5e6f0;
overflow: hidden;
box-shadow: 0 16rpx 40rpx rgba(15, 35, 52, 0.04),
0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
}
.tool-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx 28rpx;
border-bottom: 1rpx solid #f2f3f5;
}
.tool-item:last-child {
border-bottom: none;
}
.tool-item:active {
background: #f7f8fa;
}
.item-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.item-icon {
font-size: 34rpx;
}
.item-content {
flex: 1;
min-width: 0;
}
.item-name {
display: block;
font-size: 28rpx;
font-weight: 500;
color: #1d2129;
margin-bottom: 4rpx;
}
.item-desc {
display: block;
font-size: 22rpx;
color: #86909c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>