页面优化

This commit is contained in:
2025-04-24 20:47:00 +08:00
parent 34e97fb6f7
commit 96b7141bba
10 changed files with 1497 additions and 447 deletions

View File

@@ -1,58 +1,312 @@
<template>
<a-layout>
<a-page-header title="数据看板" class="p-4 bg-white"/>
<a-layout-content class="p-4">
<a-row :gutter="[16, 16]">
<!-- 统计卡片 -->
<a-col :span="6">
<a-card>
<div class="flex items-center">
<div class="text-2xl font-bold mr-4">12</div>
<div class="text-gray-500">进行中项目</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<div class="flex items-center">
<div class="text-2xl font-bold mr-4">86%</div>
<div class="text-gray-500">完成率</div>
</div>
</a-card>
</a-col>
</a-row>
<!-- 图表区域 -->
<a-card class="mt-4">
<div id="chart-container" style="height: 400px">
<!-- 图表占位 -->
<div class="flex items-center justify-center h-full text-gray-400">
图表加载中...
</div>
</div>
</a-card>
</a-layout-content>
</a-layout>
</template>
<script setup>
import { onMounted } from 'vue'
import { Layout, PageHeader, Card, Row, Col } from 'ant-design-vue'
import { ref, onMounted } from 'vue'
import AOS from 'aos'
import 'aos/dist/aos.css'
const [ALayout, ALayoutContent, APageHeader, ACard, ARow, ACol] = [
Layout,
Layout.Content,
PageHeader,
Card,
Row,
Col
onMounted(() => {
AOS.init({
duration: 1200,
offset: 120,
easing: 'ease-out-cubic',
once: false
})
// 这里可以添加图表初始化代码
// 例如使用 ECharts 或其他图表库
})
// 统计数据
const stats = [
{
title: '项目总数',
value: 24,
change: '+12%',
trend: 'up',
icon: 'folder',
color: 'from-rose-400 to-rose-600'
},
{
title: '活跃用户',
value: 1842,
change: '+22%',
trend: 'up',
icon: 'users',
color: 'from-blue-400 to-blue-600'
},
{
title: '本月收入',
value: '¥ 128,400',
change: '+18%',
trend: 'up',
icon: 'dollar',
color: 'from-emerald-400 to-emerald-600'
},
{
title: '完成率',
value: '94%',
change: '+2%',
trend: 'up',
icon: 'check-circle',
color: 'from-amber-400 to-amber-600'
}
]
// 最近项目
const recentProjects = [
{ id: 1, title: '电商平台开发', status: '进行中', progress: 75, date: '2024-03-20' },
{ id: 2, title: 'CRM系统定制', status: '已完成', progress: 100, date: '2024-03-15' },
{ id: 3, title: '数据分析平台', status: '进行中', progress: 45, date: '2024-03-10' },
{ id: 4, title: '企业官网重构', status: '已完成', progress: 100, date: '2024-03-05' },
{ id: 5, title: '移动端APP开发', status: '进行中', progress: 30, date: '2024-03-01' }
]
// 最近活动
const recentActivities = [
{
id: 1,
type: 'project',
action: '创建了新项目',
target: '电商平台开发',
user: '管理员',
time: '2小时前',
icon: 'plus-circle',
color: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400'
},
{
id: 2,
type: 'user',
action: '新用户注册',
target: '张三',
user: '系统',
time: '4小时前',
icon: 'user-plus',
color: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
},
{
id: 3,
type: 'project',
action: '更新了项目',
target: 'CRM系统定制',
user: '管理员',
time: '昨天',
icon: 'edit',
color: 'bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400'
},
{
id: 4,
type: 'payment',
action: '收到新付款',
target: '¥ 19,999',
user: '李四',
time: '2天前',
icon: 'credit-card',
color: 'bg-rose-100 text-rose-600 dark:bg-rose-900/30 dark:text-rose-400'
}
]
</script>
<template>
<div>
<!-- 页面标题 -->
<div class="mb-8" data-aos="fade-up">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">数据看板</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">欢迎回来这是您的系统概览</p>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8" data-aos="fade-up">
<div
v-for="(stat, index) in stats"
:key="index"
class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 transition-transform hover:-translate-y-1 hover:shadow-xl"
data-aos="fade-up"
:data-aos-delay="index * 100"
>
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">{{ stat.title }}</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stat.value }}</p>
</div>
<div :class="`w-12 h-12 rounded-xl bg-gradient-to-br ${stat.color} flex items-center justify-center text-white shadow-lg`">
<svg v-if="stat.icon === 'folder'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<svg v-else-if="stat.icon === 'users'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg v-else-if="stat.icon === 'dollar'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<svg v-else-if="stat.icon === 'check-circle'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" viewBox="0 0 24 24" stroke="currentColor" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<div class="mt-4 flex items-center">
<span
:class="`text-sm font-medium ${stat.trend === 'up' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`"
>
{{ stat.change }}
</span>
<svg
v-if="stat.trend === 'up'"
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 text-green-600 dark:text-green-400 ml-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 text-red-600 dark:text-red-400 ml-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0v-8m0 8l-8-8-4 4-6-6" />
</svg>
<span class="text-sm text-gray-500 dark:text-gray-400 ml-2">较上月</span>
</div>
</div>
</div>
<!-- 图表和项目列表 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
<!-- 图表区域 -->
<div class="lg:col-span-2" data-aos="fade-up">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-bold text-gray-900 dark:text-white">收入趋势</h2>
<div class="flex space-x-2">
<button class="px-3 py-1 text-sm bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-400 rounded-lg">月度</button>
<button class="px-3 py-1 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">季度</button>
<button class="px-3 py-1 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">年度</button>
</div>
</div>
<div class="h-80 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 rounded-xl">
<!-- 图表占位 -->
<div class="text-gray-400 dark:text-gray-500 flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<p>图表加载中...</p>
</div>
</div>
</div>
<!-- 最近活动 -->
<div data-aos="fade-up">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 mt-5" data-aos="fade-up">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-bold text-gray-900 dark:text-white">最近活动</h2>
<button class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 text-sm font-medium">
查看全部
</button>
</div>
<div class="space-y-6">
<div
v-for="activity in recentActivities"
:key="activity.id"
class="flex"
>
<div :class="`w-10 h-10 rounded-full ${activity.color} flex items-center justify-center mr-4 flex-shrink-0`">
<svg v-if="activity.icon === 'plus-circle'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<svg v-else-if="activity.icon === 'user-plus'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
</svg>
<svg v-else-if="activity.icon === 'edit'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
<svg v-else-if="activity.icon === 'credit-card'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
</div>
<div>
<div class="flex items-center mb-1">
<span class="font-medium text-gray-900 dark:text-white">{{ activity.user }}</span>
<span class="mx-2 text-gray-500 dark:text-gray-400">{{ activity.action }}</span>
<span class="font-medium text-gray-900 dark:text-white">{{ activity.target }}</span>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ activity.time }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 最近项目 -->
<div data-aos="fade-up">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-bold text-gray-900 dark:text-white">最近项目</h2>
<a href="/admin/projects" class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 text-sm font-medium">
查看全部
</a>
</div>
<div class="space-y-4">
<div
v-for="project in recentProjects"
:key="project.id"
class="p-4 border border-gray-100 dark:border-gray-700 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div class="flex justify-between items-start mb-2">
<h3 class="font-medium text-gray-900 dark:text-white">{{ project.title }}</h3>
<span
class="px-2 py-1 text-xs font-medium rounded-full"
:class="project.status === '进行中' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' : 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'"
>
{{ project.status }}
</span>
</div>
<div class="mb-2">
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mb-1">
<span>进度</span>
<span>{{ project.progress }}%</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
class="h-2 rounded-full bg-gradient-to-r from-rose-400 to-rose-600"
:style="`width: ${project.progress}%`"
></div>
</div>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ project.date }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.ant-page-header) {
border-bottom: 1px solid #f0f0f0;
/* 添加轻奢风格的渐变背景和阴影效果 */
.bg-gradient-luxury {
background: linear-gradient(135deg, #f6f8fb 0%, #f0f4f8 100%);
}
</style>
/* 暗色模式下的轻奢风格 */
@media (prefers-color-scheme: dark) {
.bg-gradient-luxury {
background: linear-gradient(135deg, #1a1d23 0%, #111827 100%);
}
}
</style>

View File

@@ -1,96 +0,0 @@
<script setup>
import { reactive } from 'vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
const router = useRouter()
const userStore = useUserStore()
const formState = reactive({
username: '',
password: '',
remember: true
})
const rules = {
username: [{
required: true,
message: '请输入用户名',
trigger: 'blur'
}],
password: [{
required: true,
message: '请输入密码',
trigger: 'blur'
}]
}
const handleLogin = async () => {
try {
await userStore.login({
username: formState.username,
password: formState.password
})
message.success('登录成功')
router.push('/admin/dashboard')
} catch (e) {
message.error('登录失败:' + e.message)
}
}
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-100">
<a-card class="w-full max-w-md shadow-lg" :bordered="false">
<h2 class="text-2xl font-bold mb-6 text-center">后台管理系统</h2>
<a-form
:model="formState"
:rules="rules"
@finish="handleLogin"
layout="vertical"
>
<a-form-item name="username">
<a-input
v-model:value="formState.username"
placeholder="用户名"
size="large"
>
<template #prefix><UserOutlined /></template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="formState.password"
placeholder="密码"
size="large"
>
<template #prefix><LockOutlined /></template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
size="large"
class="w-full"
>
立即登录
</a-button>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<style lang="scss" scoped>
:deep(.ant-input-affix-wrapper) {
@apply rounded-lg px-3 py-2;
}
:deep(.ant-btn-primary) {
@apply rounded-lg;
}
</style>

View File

@@ -1,7 +1,16 @@
<script setup>
import { ref } from 'vue'
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { Table, Modal } from 'ant-design-vue'
import { ref, onMounted, computed } from 'vue'
import AOS from 'aos'
import 'aos/dist/aos.css'
onMounted(() => {
AOS.init({
duration: 1200,
offset: 120,
easing: 'ease-out-cubic',
once: false
})
})
const dataSource = ref([
{
@@ -10,107 +19,533 @@ const dataSource = ref([
category: 'Web应用',
tags: ['Vue3', 'Node.js'],
author: 'Admin',
price: '29,999',
status: '已发布',
createdAt: '2024-03-20'
},
{
id: 2,
title: 'CRM系统',
category: '企业系统',
tags: ['React', 'Express'],
author: 'Admin',
price: '19,999',
status: '已发布',
createdAt: '2024-03-15'
},
{
id: 3,
title: '数据分析平台',
category: 'Web应用',
tags: ['Vue3', 'Python'],
author: 'Admin',
price: '24,999',
status: '草稿',
createdAt: '2024-03-10'
}
])
const columns = [
{
title: '项目名称',
dataIndex: 'title',
key: 'title',
width: 200,
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 120,
},
{
title: '标签',
key: 'tags',
customRender: ({ text }) => text.join(', ')
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 120,
slots: { customRender: 'action' }
}
const categoryOptions = [
{ value: 'Web应用', label: 'Web应用' },
{ value: '移动端', label: '移动端' },
{ value: '小程序', label: '小程序' },
{ value: '企业系统', label: '企业系统' }
]
const visible = ref(false)
const currentItem = ref(null)
const statusOptions = [
{ value: '草稿', label: '草稿' },
{ value: '已发布', label: '已发布' },
{ value: '已下架', label: '已下架' }
]
const showModal = (item = null) => {
currentItem.value = item || { tags: [] }
visible.value = true
// 分页
const currentPage = ref(1)
const pageSize = ref(10)
const totalItems = ref(dataSource.value.length)
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return dataSource.value.slice(start, end)
})
const changePage = (page) => {
currentPage.value = page
}
const handleDelete = (id) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个项目吗?',
onOk: () => {
dataSource.value = dataSource.value.filter(item => item.id !== id)
// 模态框
const showModal = ref(false)
const currentItem = ref(null)
const formLoading = ref(false)
const openModal = (item = null) => {
currentItem.value = item ? { ...item } : {
title: '',
category: 'Web应用',
tags: [],
price: '',
status: '草稿',
description: ''
}
showModal.value = true
}
const closeModal = () => {
showModal.value = false
}
// 确认删除模态框
const showDeleteConfirm = ref(false)
const itemToDelete = ref(null)
const openDeleteConfirm = (id) => {
itemToDelete.value = id
showDeleteConfirm.value = true
}
const closeDeleteConfirm = () => {
showDeleteConfirm.value = false
itemToDelete.value = null
}
const handleDelete = () => {
if (itemToDelete.value) {
dataSource.value = dataSource.value.filter(item => item.id !== itemToDelete.value)
totalItems.value = dataSource.value.length
closeDeleteConfirm()
}
}
const handleSubmit = () => {
formLoading.value = true
// 模拟API请求
setTimeout(() => {
if (currentItem.value.id) {
// 更新现有项目
const index = dataSource.value.findIndex(item => item.id === currentItem.value.id)
if (index !== -1) {
dataSource.value[index] = { ...currentItem.value }
}
} else {
// 添加新项目
const newId = Math.max(...dataSource.value.map(item => item.id), 0) + 1
dataSource.value.push({
...currentItem.value,
id: newId,
author: 'Admin',
createdAt: new Date().toISOString().split('T')[0]
})
totalItems.value = dataSource.value.length
}
})
formLoading.value = false
closeModal()
}, 800)
}
// 标签管理
const tagInputValue = ref('')
const handleAddTag = () => {
if (tagInputValue.value && !currentItem.value.tags.includes(tagInputValue.value)) {
currentItem.value.tags.push(tagInputValue.value)
tagInputValue.value = ''
}
}
const removeTag = (index) => {
currentItem.value.tags.splice(index, 1)
}
</script>
<template>
<div class="bg-white p-6 rounded-lg">
<div class="mb-4 flex justify-between items-center">
<h2 class="text-xl font-semibold">项目管理</h2>
<a-button type="primary" @click="showModal">
<template #icon><PlusOutlined /></template>
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
<!-- 页面标题和新建按钮 -->
<div class="mb-6 flex justify-between items-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">项目管理</h2>
<button
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
@click="openModal()"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
新建项目
</a-button>
</button>
</div>
<a-table
:columns="columns"
:data-source="dataSource"
:scroll="{ x: 800 }"
bordered
>
<template #action="{ record }">
<a-space>
<a-tooltip title="编辑">
<a-button type="link" @click="showModal(record)">
<EditOutlined />
</a-button>
</a-tooltip>
<a-tooltip title="删除">
<a-button type="link" danger @click="handleDelete(record.id)">
<DeleteOutlined />
</a-button>
</a-tooltip>
</a-space>
</template>
</a-table>
<!-- 表格 -->
<div class="rounded-2xl border border-gray-200 dark:border-gray-700">
<div class="overflow-x-auto" style="height: 75vh">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<!-- 表头 -->
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
项目名称
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
分类
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
标签
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
价格
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
状态
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
创建时间
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<a-modal
v-model:visible="visible"
:title="currentItem.id ? '编辑项目' : '新建项目'"
width="800px"
>
<!-- 项目表单内容 -->
<template #footer>
<a-button @click="visible = false">取消</a-button>
<a-button type="primary">确认</a-button>
</template>
</a-modal>
<!-- 表格内容 -->
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="item in paginatedData" :key="item.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{{ item.title }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ item.category }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex flex-wrap gap-1">
<span
v-for="(tag, index) in item.tags"
:key="index"
class="px-2 py-1 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-xs font-medium rounded-full"
>
{{ tag }}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
¥ {{ item.price }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
:class="`px-2 py-1 text-xs font-medium rounded-full ${
item.status === '已发布'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
}`"
>
{{ item.status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ item.createdAt }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end space-x-2">
<button
class="p-1.5 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors cursor-pointer"
title="查看"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<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" />
</svg>
</button>
<button
class="p-1.5 text-amber-600 hover:text-amber-800 dark:text-amber-400 dark:hover:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded-lg transition-colors cursor-pointer"
title="编辑"
@click="openModal(item)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
</button>
<button
class="p-1.5 text-rose-600 hover:text-rose-800 dark:text-rose-400 dark:hover:text-rose-300 hover:bg-rose-50 dark:hover:bg-rose-900/20 rounded-lg transition-colors cursor-pointer"
title="删除"
@click="openDeleteConfirm(item.id)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="bg-white dark:bg-gray-800 px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div class="text-sm text-gray-700 dark:text-gray-300">
{{ totalItems }} 条记录
</div>
<div class="flex space-x-1">
<button
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
:disabled="currentPage === 1"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }"
@click="currentPage > 1 && changePage(currentPage - 1)"
>
上一页
</button>
<button
v-for="page in Math.ceil(totalItems / pageSize)"
:key="page"
class="px-3 py-1 border rounded-md text-sm font-medium cursor-pointer"
:class="page === currentPage
? 'bg-rose-600 text-white border-rose-600 dark:bg-rose-600 dark:border-rose-600'
: 'border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'"
@click="changePage(page)"
>
{{ page }}
</button>
<button
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
:disabled="currentPage === Math.ceil(totalItems / pageSize)"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === Math.ceil(totalItems / pageSize) }"
@click="currentPage < Math.ceil(totalItems / pageSize) && changePage(currentPage + 1)"
>
下一页
</button>
</div>
</div>
</div>
</div>
<!-- 项目表单模态框 -->
<div v-if="showModal" class="fixed inset-0 z-50 " aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeModal"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-6 pt-5 pb-4 sm:p-6">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white" id="modal-title">
{{ currentItem?.id ? '编辑项目' : '新建项目' }}
</h3>
<button
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
@click="closeModal"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form @submit.prevent="handleSubmit" class="space-y-6">
<!-- 项目名称 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目名称</label>
<input
v-model="currentItem.title"
type="text"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入项目名称"
required
/>
</div>
<!-- 分类和状态 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目分类</label>
<select
v-model="currentItem.category"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option v-for="option in categoryOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目状态</label>
<select
v-model="currentItem.status"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
>
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
<!-- 价格 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目价格</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400">¥</span>
<input
v-model="currentItem.price"
type="text"
class="w-full pl-8 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入项目价格"
/>
</div>
</div>
<!-- 标签 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目标签</label>
<div class="flex flex-wrap gap-2 mb-2">
<span
v-for="(tag, index) in currentItem.tags"
:key="index"
class="px-3 py-1.5 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-sm font-medium rounded-lg flex items-center"
>
{{ tag }}
<button
type="button"
class="ml-1.5 text-rose-500 hover:text-rose-700 dark:text-rose-400 dark:hover:text-rose-200 cursor-pointer"
@click="removeTag(index)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
</div>
<div class="flex">
<input
v-model="tagInputValue"
type="text"
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-l-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="输入标签"
@keyup.enter="handleAddTag"
/>
<button
type="button"
class="px-4 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-r-xl transition-colors cursor-pointer"
@click="handleAddTag"
>
添加
</button>
</div>
</div>
<!-- 项目描述 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目描述</label>
<textarea
v-model="currentItem.description"
rows="4"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入项目描述"
></textarea>
</div>
<!-- 表单按钮 -->
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
class="px-5 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="closeModal"
>
取消
</button>
<button
type="submit"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
:disabled="formLoading"
>
<span v-if="formLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
{{ currentItem?.id ? '保存修改' : '创建项目' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeDeleteConfirm"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/30 sm:mx-0 sm:h-10 sm:w-10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white" id="modal-title">
确认删除
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
确定要删除这个项目吗此操作无法撤销
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<a-button
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm cursor-pointer"
@click="handleDelete"
>
确认
</a-button>
<a-button
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm cursor-pointer"
@click="closeDeleteConfirm"
>
取消
</a-button>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.ant-table-thead > tr > th) {
@apply bg-gray-50;
/* 自定义样式 */
/* 毛玻璃效果增强 */
.backdrop-blur-xl {
backdrop-filter: blur(16px);
}
:deep(.ant-table-tbody > tr:hover td) {
@apply bg-gray-100;
.backdrop-blur-md {
backdrop-filter: blur(12px);
}
</style>
.backdrop-blur-sm {
backdrop-filter: blur(4px);
}
</style>