619 lines
22 KiB
Vue
619 lines
22 KiB
Vue
<template>
|
||
<div class="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 p-6">
|
||
<!-- 页面头部 -->
|
||
<div class="mb-8" data-aos="fade-down">
|
||
<div class="bg-glass rounded-3xl p-6 border border-white/10">
|
||
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||
<div class="mb-4 md:mb-0">
|
||
<h1 class="text-3xl font-bold text-gradient mb-2">文章管理</h1>
|
||
<p class="text-gray-300">管理和编辑您的文章内容</p>
|
||
</div>
|
||
<div class="flex space-x-4">
|
||
<button class="tech-button-primary" @click="createArticle">
|
||
<PlusIcon class="w-5 h-5 mr-2" />
|
||
新建文章
|
||
</button>
|
||
<button class="tech-button-secondary" @click="refreshData">
|
||
<RefreshIcon class="w-5 h-5 mr-2" />
|
||
刷新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选和搜索 -->
|
||
<div class="mb-8" data-aos="fade-up" data-aos-delay="100">
|
||
<div class="bg-glass rounded-3xl p-6 border border-white/10">
|
||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
<!-- 搜索框 -->
|
||
<div class="md:col-span-2">
|
||
<div class="relative">
|
||
<SearchIcon class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||
<input
|
||
v-model="searchKeyword"
|
||
type="text"
|
||
placeholder="搜索文章标题或内容..."
|
||
class="tech-input pl-10"
|
||
@keyup.enter="searchArticles"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 状态筛选 -->
|
||
<div>
|
||
<select v-model="statusFilter" class="tech-select">
|
||
<option value="">全部状态</option>
|
||
<option value="published">已发布</option>
|
||
<option value="draft">草稿</option>
|
||
<option value="archived">已归档</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 分类筛选 -->
|
||
<div>
|
||
<select v-model="categoryFilter" class="tech-select">
|
||
<option value="">全部分类</option>
|
||
<option value="tech">技术</option>
|
||
<option value="news">新闻</option>
|
||
<option value="tutorial">教程</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文章列表 -->
|
||
<div class="mb-8" data-aos="fade-up" data-aos-delay="200">
|
||
<div class="bg-glass rounded-3xl border border-white/10 overflow-hidden">
|
||
<!-- 表格头部 -->
|
||
<div class="px-6 py-4 border-b border-white/10">
|
||
<div class="flex items-center justify-between">
|
||
<h3 class="text-lg font-semibold text-white">文章列表</h3>
|
||
<div class="flex items-center space-x-4 text-sm text-gray-400">
|
||
<span>共 {{ totalArticles }} 篇文章</span>
|
||
<div class="flex items-center space-x-2">
|
||
<span>每页显示</span>
|
||
<select v-model="pageSize" class="bg-white/10 border border-white/20 rounded px-2 py-1 text-white">
|
||
<option value="10">10</option>
|
||
<option value="20">20</option>
|
||
<option value="50">50</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-tech-blue-500"></div>
|
||
<span class="ml-3 text-gray-400">加载中...</span>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-else-if="!articles.length" class="py-12">
|
||
<TechEmptyState
|
||
title="暂无文章"
|
||
description="还没有创建任何文章,点击上方按钮开始创建您的第一篇文章"
|
||
:show-action="true"
|
||
action-text="创建文章"
|
||
@action="createArticle"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 文章表格 -->
|
||
<div v-else class="overflow-x-auto">
|
||
<table class="w-full">
|
||
<thead class="bg-white/5">
|
||
<tr>
|
||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||
<input type="checkbox" v-model="selectAll" @change="toggleSelectAll" class="tech-checkbox" />
|
||
</th>
|
||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">标题</th>
|
||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">状态</th>
|
||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">分类</th>
|
||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">作者</th>
|
||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">创建时间</th>
|
||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-white/10">
|
||
<tr
|
||
v-for="(article, index) in articles"
|
||
:key="article.id"
|
||
class="hover:bg-white/5 transition-colors duration-200"
|
||
:data-aos="'fade-up'"
|
||
:data-aos-delay="index * 50"
|
||
>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<input
|
||
type="checkbox"
|
||
v-model="selectedArticles"
|
||
:value="article.id"
|
||
class="tech-checkbox"
|
||
/>
|
||
</td>
|
||
<td class="px-6 py-4">
|
||
<div class="flex items-center">
|
||
<div class="flex-shrink-0 h-12 w-12">
|
||
<img
|
||
:src="article.cover_image || '/placeholder-tech.jpg'"
|
||
:alt="article.title"
|
||
class="h-12 w-12 rounded-lg object-cover border border-white/20"
|
||
/>
|
||
</div>
|
||
<div class="ml-4">
|
||
<div class="text-sm font-medium text-white hover:text-tech-blue-300 cursor-pointer" @click="editArticle(article.id)">
|
||
{{ article.title }}
|
||
</div>
|
||
<div class="text-sm text-gray-400 line-clamp-1">
|
||
{{ article.summary || '暂无摘要' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<span :class="[getStatusClass(article.status), 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium']">
|
||
{{ getStatusText(article.status) }}
|
||
</span>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
|
||
{{ article.category || '未分类' }}
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
|
||
{{ article.author || '未知' }}
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||
{{ formatDate(article.created_at) }}
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||
<div class="flex items-center space-x-2">
|
||
<button @click="editArticle(article.id)" class="tech-button-ghost-sm">
|
||
<EditIcon class="w-4 h-4" />
|
||
</button>
|
||
<button @click="viewArticle(article.id)" class="tech-button-ghost-sm">
|
||
<EyeIcon class="w-4 h-4" />
|
||
</button>
|
||
<button @click="deleteArticle(article.id)" class="tech-button-ghost-sm text-red-400 hover:text-red-300">
|
||
<TrashIcon class="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 批量操作栏 -->
|
||
<div v-if="selectedArticles.length > 0" class="px-6 py-4 bg-white/5 border-t border-white/10">
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-sm text-gray-300">已选择 {{ selectedArticles.length }} 篇文章</span>
|
||
<div class="flex items-center space-x-4">
|
||
<button @click="batchPublish" class="tech-button-ghost-sm">
|
||
<CheckIcon class="w-4 h-4 mr-1" />
|
||
批量发布
|
||
</button>
|
||
<button @click="batchArchive" class="tech-button-ghost-sm">
|
||
<ArchiveIcon class="w-4 h-4 mr-1" />
|
||
批量归档
|
||
</button>
|
||
<button @click="batchDelete" class="tech-button-ghost-sm text-red-400 hover:text-red-300">
|
||
<TrashIcon class="w-4 h-4 mr-1" />
|
||
批量删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="totalPages > 1" class="px-6 py-4 border-t border-white/10">
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-sm text-gray-400">
|
||
显示第 {{ (currentPage - 1) * pageSize + 1 }} - {{ Math.min(currentPage * pageSize, totalArticles) }} 条,共 {{ totalArticles }} 条
|
||
</div>
|
||
<div class="flex items-center space-x-2">
|
||
<button
|
||
@click="changePage(currentPage - 1)"
|
||
:disabled="currentPage <= 1"
|
||
class="tech-button-ghost-sm"
|
||
:class="{ 'opacity-50 cursor-not-allowed': currentPage <= 1 }"
|
||
>
|
||
<ChevronLeftIcon class="w-4 h-4" />
|
||
</button>
|
||
|
||
<div class="flex items-center space-x-1">
|
||
<button
|
||
v-for="page in getPageNumbers()"
|
||
:key="page"
|
||
@click="changePage(page)"
|
||
:class="[
|
||
'px-3 py-1 rounded-lg text-sm transition-colors duration-200',
|
||
page === currentPage
|
||
? 'bg-tech-gradient text-white'
|
||
: 'text-gray-400 hover:text-white hover:bg-white/10'
|
||
]"
|
||
>
|
||
{{ page }}
|
||
</button>
|
||
</div>
|
||
|
||
<button
|
||
@click="changePage(currentPage + 1)"
|
||
:disabled="currentPage >= totalPages"
|
||
class="tech-button-ghost-sm"
|
||
:class="{ 'opacity-50 cursor-not-allowed': currentPage >= totalPages }"
|
||
>
|
||
<ChevronRightIcon class="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import TechEmptyState from '@/components/TechEmptyState.vue'
|
||
|
||
const router = useRouter()
|
||
|
||
// 图标组件
|
||
const PlusIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>`
|
||
}
|
||
|
||
const RefreshIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>`
|
||
}
|
||
|
||
const SearchIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>`
|
||
}
|
||
|
||
const EditIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>`
|
||
}
|
||
|
||
const EyeIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>`
|
||
}
|
||
|
||
const TrashIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>`
|
||
}
|
||
|
||
const CheckIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>`
|
||
}
|
||
|
||
const ArchiveIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path></svg>`
|
||
}
|
||
|
||
const ChevronLeftIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>`
|
||
}
|
||
|
||
const ChevronRightIcon = {
|
||
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>`
|
||
}
|
||
|
||
// 响应式数据
|
||
const loading = ref(false)
|
||
const articles = ref([])
|
||
const searchKeyword = ref('')
|
||
const statusFilter = ref('')
|
||
const categoryFilter = ref('')
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(10)
|
||
const totalArticles = ref(0)
|
||
const selectAll = ref(false)
|
||
const selectedArticles = ref([])
|
||
|
||
// 计算属性
|
||
const totalPages = computed(() => Math.ceil(totalArticles.value / pageSize.value))
|
||
|
||
// 模拟数据
|
||
const mockArticles = [
|
||
{
|
||
id: 1,
|
||
title: 'Vue3 Composition API 深度解析',
|
||
summary: '详细介绍Vue3 Composition API的使用方法和最佳实践',
|
||
status: 'published',
|
||
category: 'tech',
|
||
author: '张三',
|
||
created_at: '2024-01-15T10:30:00Z',
|
||
cover_image: 'https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=400'
|
||
},
|
||
{
|
||
id: 2,
|
||
title: 'React vs Vue 性能对比分析',
|
||
summary: '从多个维度对比React和Vue的性能表现',
|
||
status: 'draft',
|
||
category: 'tech',
|
||
author: '李四',
|
||
created_at: '2024-01-14T15:20:00Z',
|
||
cover_image: 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=400'
|
||
},
|
||
{
|
||
id: 3,
|
||
title: 'TypeScript 入门教程',
|
||
summary: '从零开始学习TypeScript的基础知识',
|
||
status: 'published',
|
||
category: 'tutorial',
|
||
author: '王五',
|
||
created_at: '2024-01-13T09:15:00Z',
|
||
cover_image: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400'
|
||
}
|
||
]
|
||
|
||
// 方法
|
||
const fetchArticles = async () => {
|
||
loading.value = true
|
||
try {
|
||
// 模拟API调用
|
||
await new Promise(resolve => setTimeout(resolve, 500))
|
||
|
||
let filteredArticles = [...mockArticles]
|
||
|
||
// 搜索过滤
|
||
if (searchKeyword.value) {
|
||
const keyword = searchKeyword.value.toLowerCase()
|
||
filteredArticles = filteredArticles.filter(article =>
|
||
article.title.toLowerCase().includes(keyword) ||
|
||
article.summary.toLowerCase().includes(keyword)
|
||
)
|
||
}
|
||
|
||
// 状态过滤
|
||
if (statusFilter.value) {
|
||
filteredArticles = filteredArticles.filter(article => article.status === statusFilter.value)
|
||
}
|
||
|
||
// 分类过滤
|
||
if (categoryFilter.value) {
|
||
filteredArticles = filteredArticles.filter(article => article.category === categoryFilter.value)
|
||
}
|
||
|
||
totalArticles.value = filteredArticles.length
|
||
|
||
// 分页
|
||
const start = (currentPage.value - 1) * pageSize.value
|
||
const end = start + pageSize.value
|
||
articles.value = filteredArticles.slice(start, end)
|
||
|
||
} catch (error) {
|
||
console.error('获取文章列表失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const searchArticles = () => {
|
||
currentPage.value = 1
|
||
fetchArticles()
|
||
}
|
||
|
||
const refreshData = () => {
|
||
fetchArticles()
|
||
}
|
||
|
||
const createArticle = () => {
|
||
router.push('/admin/articles/create')
|
||
}
|
||
|
||
const editArticle = (id) => {
|
||
router.push(`/admin/articles/edit/${id}`)
|
||
}
|
||
|
||
const viewArticle = (id) => {
|
||
router.push(`/articles/${id}`)
|
||
}
|
||
|
||
const deleteArticle = async (id) => {
|
||
if (confirm('确定要删除这篇文章吗?')) {
|
||
// 模拟删除操作
|
||
console.log('删除文章:', id)
|
||
await fetchArticles()
|
||
}
|
||
}
|
||
|
||
const toggleSelectAll = () => {
|
||
if (selectAll.value) {
|
||
selectedArticles.value = articles.value.map(article => article.id)
|
||
} else {
|
||
selectedArticles.value = []
|
||
}
|
||
}
|
||
|
||
const batchPublish = async () => {
|
||
if (selectedArticles.value.length === 0) return
|
||
console.log('批量发布:', selectedArticles.value)
|
||
selectedArticles.value = []
|
||
selectAll.value = false
|
||
await fetchArticles()
|
||
}
|
||
|
||
const batchArchive = async () => {
|
||
if (selectedArticles.value.length === 0) return
|
||
console.log('批量归档:', selectedArticles.value)
|
||
selectedArticles.value = []
|
||
selectAll.value = false
|
||
await fetchArticles()
|
||
}
|
||
|
||
const batchDelete = async () => {
|
||
if (selectedArticles.value.length === 0) return
|
||
if (confirm(`确定要删除选中的 ${selectedArticles.value.length} 篇文章吗?`)) {
|
||
console.log('批量删除:', selectedArticles.value)
|
||
selectedArticles.value = []
|
||
selectAll.value = false
|
||
await fetchArticles()
|
||
}
|
||
}
|
||
|
||
const changePage = (page) => {
|
||
if (page >= 1 && page <= totalPages.value) {
|
||
currentPage.value = page
|
||
fetchArticles()
|
||
}
|
||
}
|
||
|
||
const getPageNumbers = () => {
|
||
const pages = []
|
||
const total = totalPages.value
|
||
const current = currentPage.value
|
||
|
||
if (total <= 7) {
|
||
for (let i = 1; i <= total; i++) {
|
||
pages.push(i)
|
||
}
|
||
} else {
|
||
if (current <= 4) {
|
||
for (let i = 1; i <= 5; i++) {
|
||
pages.push(i)
|
||
}
|
||
pages.push('...')
|
||
pages.push(total)
|
||
} else if (current >= total - 3) {
|
||
pages.push(1)
|
||
pages.push('...')
|
||
for (let i = total - 4; i <= total; i++) {
|
||
pages.push(i)
|
||
}
|
||
} else {
|
||
pages.push(1)
|
||
pages.push('...')
|
||
for (let i = current - 1; i <= current + 1; i++) {
|
||
pages.push(i)
|
||
}
|
||
pages.push('...')
|
||
pages.push(total)
|
||
}
|
||
}
|
||
|
||
return pages
|
||
}
|
||
|
||
const getStatusClass = (status) => {
|
||
const classes = {
|
||
published: 'bg-green-500/20 text-green-400',
|
||
draft: 'bg-yellow-500/20 text-yellow-400',
|
||
archived: 'bg-gray-500/20 text-gray-400'
|
||
}
|
||
return classes[status] || 'bg-gray-500/20 text-gray-400'
|
||
}
|
||
|
||
const getStatusText = (status) => {
|
||
const texts = {
|
||
published: '已发布',
|
||
draft: '草稿',
|
||
archived: '已归档'
|
||
}
|
||
return texts[status] || '未知'
|
||
}
|
||
|
||
const formatDate = (dateString) => {
|
||
const date = new Date(dateString)
|
||
return date.toLocaleDateString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})
|
||
}
|
||
|
||
// 组件挂载时获取数据
|
||
onMounted(() => {
|
||
fetchArticles()
|
||
})
|
||
|
||
// 监听筛选条件变化
|
||
import { watch } from 'vue'
|
||
watch([statusFilter, categoryFilter, pageSize], () => {
|
||
currentPage.value = 1
|
||
fetchArticles()
|
||
})
|
||
|
||
// 监听选中状态
|
||
watch(selectedArticles, (newVal) => {
|
||
selectAll.value = newVal.length === articles.value.length && articles.value.length > 0
|
||
})
|
||
</script>
|
||
|
||
<style lang='scss' scoped>
|
||
/* 科技感按钮样式 */
|
||
.tech-button-primary {
|
||
@apply px-6 py-3 bg-tech-gradient text-white rounded-2xl font-medium transition-all duration-300 hover:shadow-lg hover:shadow-tech-blue-500/25 active:scale-95 flex items-center;
|
||
}
|
||
|
||
.tech-button-secondary {
|
||
@apply px-6 py-3 bg-white/10 border border-white/20 text-white rounded-2xl font-medium transition-all duration-300 hover:bg-white/20 active:scale-95 flex items-center;
|
||
}
|
||
|
||
.tech-button-ghost-sm {
|
||
@apply p-2 text-gray-400 hover:text-white hover:bg-white/10 rounded-lg transition-colors duration-200 flex items-center;
|
||
}
|
||
|
||
/* 科技感输入框样式 */
|
||
.tech-input {
|
||
@apply w-full px-4 py-3 bg-white/10 border border-white/20 rounded-2xl text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-tech-blue-500 focus:border-transparent transition-all duration-300;
|
||
}
|
||
|
||
.tech-select {
|
||
@apply w-full px-4 py-3 bg-white/10 border border-white/20 rounded-2xl text-white focus:outline-none focus:ring-2 focus:ring-tech-blue-500 focus:border-transparent transition-all duration-300;
|
||
}
|
||
|
||
/* 科技感复选框样式 */
|
||
.tech-checkbox {
|
||
@apply w-4 h-4 text-tech-blue-500 bg-white/10 border-white/20 rounded focus:ring-tech-blue-500 focus:ring-2;
|
||
}
|
||
|
||
/* 玻璃拟态背景 */
|
||
.bg-glass {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(20px);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
/* 渐变文字 */
|
||
.text-gradient {
|
||
background: linear-gradient(135deg, #60a5fa, #c084fc);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
/* 科技渐变背景 */
|
||
.bg-tech-gradient {
|
||
background: linear-gradient(135deg, #2563EB 0%, #A855F7 100%);
|
||
}
|
||
|
||
/* 文本截断 */
|
||
.line-clamp-1 {
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 1;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 响应式优化 */
|
||
@media (max-width: 768px) {
|
||
.grid-cols-1.md\:grid-cols-4 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.flex-col.md\:flex-row {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.md\:col-span-2 {
|
||
grid-column: span 1;
|
||
}
|
||
}
|
||
</style>
|