完成部分页面UI

This commit is contained in:
2025-04-24 16:59:40 +08:00
parent 6b9faa702b
commit f1de79863e
27 changed files with 2435 additions and 47 deletions

View File

@@ -1,5 +1,56 @@
# Vue 3 + Vite
# 项目展示平台
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## 技术栈
- 前端框架Vue3 + Composition API
- UI框架Ant Design Vue (后台) + Tailwind CSS (前台)
- 动画库AOS.js
- 路由Vue Router 4
- 状态管理Pinia
- 请求库Axios
- 轮播组件Swiper
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
## 目录结构
```
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── ProjectCard.vue # 项目卡片
│ └── Skeleton.vue # 加载骨架屏
├── layouts/ # 布局组件
│ ├── AdminLayout.vue # 后台布局
│ └── MainLayout.vue # 前台布局
├── router/
│ └── index.js # 路由配置
├── stores/
│ └── user.js # 用户状态管理
├── views/
│ ├── frontend/
│ │ ├── Home.vue # 首页
│ │ ├── Projects.vue # 项目列表
│ │ └── Detail.vue # 项目详情
│ └── admin/
│ ├── Login.vue # 后台登录
│ ├── Dashboard.vue # 数据看板
│ └── ProjectsCRUD.vue # 项目管理
├── App.vue
└── main.js
```
## 响应式方案
| 设备 | 断点 | 布局模式 | 导航模式 |
|---------|----------|-------------------|------------------|
| 手机 | <640px | 单列流式布局 | 底部导航栏 |
| 平板 | 640-1024 | 双栏布局 | 侧边折叠菜单 |
| PC | >1024px | 12栅格系统 | 顶部水平菜单 |
## 动效规范
1. 页面过渡fade 300ms缓动
2. 卡片入场aos fade-up + 交错延迟
3. 交互反馈hover缩放+投影提升
4. 加载动画:骨架屏脉冲效果
## 开发准备
```bash
npm install
npm run dev
```

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
<title>奶酪云-应用中心</title>
</head>
<body>
<div id="app"></div>

View File

@@ -9,12 +9,22 @@
"preview": "vite preview"
},
"dependencies": {
"@ant-design-vue/pro-layout": "^3.2.5",
"@ant-design/icons-vue": "^7.0.1",
"@tailwindcss/vite": "^4.1.4",
"@vueuse/head": "^2.0.0",
"ant-design-vue": "^4.1.1",
"aos": "^2.3.4",
"axios": "^1.6.2",
"pinia": "^2.1.7",
"swiper": "^11.0.5",
"tailwindcss": "^4.1.4",
"vue": "^3.5.13"
"vue": "^3.5.13",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
"sass": "^1.71.0",
"vite": "^6.3.1"
}
}

885
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,13 @@
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<div class="bg-gray-100 min-h-screen">
<header class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white py-4 px-6 flex items-center justify-between">
<div class="flex items-center space-x-4">
<!-- Logo -->
<img src="https://via.placeholder.com/40" alt="Logo" class="h-8 w-8 rounded-full">
<span class="font-bold">My App</span>
</div>
<!-- Menu -->
<nav class="hidden md:flex space-x-4">
<a href="#" class="hover:text-gray-300">Home</a>
<a href="#" class="hover:text-gray-300">About</a>
<a href="#" class="hover:text-gray-300">Services</a>
<a href="#" class="hover:text-gray-300">Contact</a>
</nav>
<!-- User Profile -->
<div class="flex items-center space-x-4">
<button class="text-sm bg-gray-800 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
Personal Center
</button>
<img src="https://via.placeholder.com/40" alt="User Avatar" class="h-8 w-8 rounded-full cursor-pointer">
</div>
</header>
<main class="p-6">
<slot></slot>
</main>
</div>
<router-view />
</template>
<style scoped>
<style lang='scss' scoped>
#app {
background-color: #333 !important;
}
</style>

BIN
src/assets/logo-admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
src/assets/nl-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

View File

@@ -0,0 +1,96 @@
<script setup>
import { ref } from 'vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/user'
const visible = ref(false)
const loading = ref(false)
const formState = ref({
username: '',
password: '',
remember: true
})
const show = () => visible.value = true
const close = () => visible.value = false
const handleSubmit = async () => {
loading.value = true
try {
await useUserStore().login(formState.value)
close()
} finally {
loading.value = false
}
}
defineExpose({
show,
close
})
</script>
<template>
<a-modal
:visible="visible"
title="欢迎登录"
:footer="null"
:width="400"
:maskClosable="false"
@cancel="close"
:wrapClassName="'auth-modal'"
>
<a-form
layout="vertical"
:model="formState"
@finish="handleSubmit"
class="px-8 pb-6"
>
<a-form-item name="username">
<a-input
v-model:value="formState.username"
placeholder="用户名"
size="large"
autocomplete="username"
class="rounded-lg hover:border-primary focus:border-primary"
>
<template #prefix><UserOutlined class="text-gray-400" /></template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="formState.password"
placeholder="密码"
size="large"
autocomplete="current-password"
class="rounded-lg hover:border-primary"
>
<template #prefix><LockOutlined class="text-gray-400" /></template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:loading="loading"
block
size="large"
class="rounded-lg transition-all hover:shadow-md"
>
立即登录
</a-button>
</a-form-item>
</a-form>
</a-modal>
</template>
<style scoped>
:deep(.ant-modal-body) {
padding: 40px;
}
:deep(.ant-input-affix-wrapper) {
padding: 12px 16px;
}
</style>

View File

@@ -0,0 +1,65 @@
<script setup>
import { ProLayout } from '@ant-design-vue/pro-layout'
import { useRouter } from 'vue-router'
import { computed } from 'vue'
const router = useRouter()
const selectedKeys = computed(() => [router.currentRoute.value.name])
const menuData = [
{
path: '/admin/dashboard',
name: 'Dashboard',
meta: { icon: 'PieChartOutlined', title: '数据看板' }
},
{
path: '/admin/projects',
name: 'ProjectManage',
meta: { icon: 'AppstoreOutlined', title: '项目管理' }
}
]
</script>
<template>
<ProLayout
:menu-data="menuData"
:selected-keys="selectedKeys"
layout="mix"
:content-width="1200"
:header-height="64"
fix-siderbar
:primary-color="'#1890ff'"
>
<template #logo>
<div class="flex items-center text-lg font-bold">
<img src="@/assets/logo-admin.png" class="h-8 mr-2" />
项目管理系统
</div>
</template>
<template #rightContent>
<a-dropdown :trigger="['click']">
<a-space class="cursor-pointer px-4">
<a-avatar :size="32" src="https://via.placeholder.com/32" />
<span class="hidden md:inline">管理员</span>
</a-space>
<template #overlay>
<a-menu>
<a-menu-item key="logout">退出登录</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<router-view />
</ProLayout>
</template>
<style lang="scss" scoped>
:deep(.ant-pro-sider-menu) {
@apply shadow-lg;
}
:deep(.ant-pro-page-container) {
@apply bg-gray-50;
}
</style>

160
src/layouts/MainLayout.vue Normal file
View File

@@ -0,0 +1,160 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { UserOutlined, MenuOutlined } from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/user'
import AuthModal from '@/components/common/AuthModal.vue'
// 合并后的状态管理
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const authModal = ref(null)
const isScrolled = ref(false)
const mobileMenuVisible = ref(false)
const menuItems = [
{ path: '/', title: '首页', icon: 'HomeOutlined' },
{ path: '/projects', title: '项目列表', icon: 'InfoCircleOutlined' },
{ path: '/about', title: '关于我们', icon: 'InfoCircleOutlined' },
{ path: '/services', title: '服务项目', icon: 'AppstoreOutlined' },
]
// 滚动监听逻辑
onMounted(() => {
window.addEventListener('scroll', () => {
isScrolled.value = window.scrollY > 0
})
})
</script>
<template>
<a-layout class="min-h-screen">
<!-- 顶部导航栏 -->
<header class="sticky top-0 z-50 bg-gradient-to-r from-blue-600 to-purple-700 transition-shadow duration-300" :class="{ 'shadow-lg': isScrolled }">
<div class="flex items-center justify-between px-6 py-4">
<div class="flex items-center space-x-4">
<img src="@/assets/nl-logo.png" alt="Logo" class="h-10 w-10 transition-transform hover:scale-110">
<span class="text-2xl font-extrabold tracking-wide bg-gradient-to-r from-white to-blue-100 bg-clip-text text-transparent">AppStop</span>
</div>
<nav class="hidden md:flex space-x-4">
<a
href="javascript:;"
v-for="item in menuItems"
:key="item.path"
type="text"
:class="`${route.path === item.path? 'router-link-active': 'router-link'} flex items-center p-3 rounded-lg hover:bg-opacity-10 hover:bg-white transition-colors text-white text-lg`"
@click="router.push(item.path)"
>
<component :is="item.icon" class="mr-3" />
{{ item.title }}
</a>
</nav>
<div class="flex items-center space-x-4">
<AuthModal ref="authModal" />
<a-button
v-if="!userStore.isAuthenticated"
type="primary"
class="text-sm"
@click="authModal?.show()"
>
登录/注册
</a-button>
<div v-else>
<a-dropdown>
<a class="ant-dropdown-link" @click.prevent>
<img src="https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280" alt="User Avatar" class="h-8 w-8 rounded-full cursor-pointer">
</a>
<template #overlay>
<a-menu>
<a-menu-item>
<a href="/personal">个人中心</a>
</a-menu-item>
<a-menu-item v-if="userStore.user?.role === 'admin'">
<a href="/admin">后台管理</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</div>
</header>
<!-- 移动端菜单 -->
<a-drawer
v-model:visible="mobileMenuVisible"
placement="left"
:width="280"
class="md:hidden"
>
<div class="p-4 space-y-4">
<a-button
v-for="item in menuItems"
:key="item.path"
type="link"
:class="`${route.path === item.path? 'router-link-active': 'router-link'} flex items-center p-3 rounded-lg hover:bg-opacity-10 hover:bg-white transition-colors text-white text-lg`"
@click="router.push(item.path)"
>
<component :is="item.icon" class="mr-4" />
{{ item.title }}
</a-button>
</div>
</a-drawer>
<!-- 移动端菜单开关 -->
<div class="fixed bottom-6 right-6 z-50 md:hidden">
<a-button
shape="circle"
type="primary"
size="large"
class="!w-14 !h-14 shadow-xl hover:scale-110 transition-transform"
@click="mobileMenuVisible = true"
>
<template #icon><MenuOutlined class="text-2xl" /></template>
</a-button>
</div>
<a-layout-content class="pt-16 body-box">
<router-view />
</a-layout-content>
</a-layout>
</template>
<style lang="scss" scoped>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.body-box {
background-color: #111 !important
}
:deep(.ant-menu-horizontal) {
border-bottom: none !important;
}
:deep(.ant-drawer-body) {
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
}
header {
backdrop-filter: blur(10px);
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
// 激活状态样式增强
.router-link {
color: #fff !important;
}
// 激活状态样式增强
.router-link-active {
color: #fff !important;
background-color: rgba(255, 255, 255, 0.1) !important;
}
}
</style>

View File

@@ -1,5 +1,24 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import AOS from 'aos'
import 'aos/dist/aos.css'
import './style.css'
import App from './App.vue'
// 注册路由
import router from './router'
createApp(App).mount('#app')
const app = createApp(App)
// 初始化AOS动画
AOS.init({
duration: 600,
offset: 120,
easing: 'ease-out-quad'
})
// 注册Ant Design组件
app.use(createPinia())
app.use(router)
app.use(Antd)
app.mount('#app')

70
src/router/index.js Normal file
View File

@@ -0,0 +1,70 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
import MainLayout from '@/layouts/MainLayout.vue'
import AdminLayout from '@/layouts/AdminLayout.vue'
// 在路由配置中添加个人中心路由
const routes = [
{
path: '/',
component: MainLayout,
children: [
{ path: '', name: 'Home', component: () => import('@/views/frontend/Home.vue') },
{ path: '/projects', name: 'Projects', component: () => import('@/views/frontend/Projects.vue') },
{ path: '/project/:id', name: 'ProjectDetail', component: () => import('@/views/frontend/Detail.vue') },
// 确保所有前台路由在此注册
{ path: '/about', name: 'About', component: () => import('@/views/frontend/About.vue') },
{ path: '/services', name: 'Services', component: () => import('@/views/frontend/Services.vue') },
{
path: '/about',
name: 'About',
component: () => import('@/views/frontend/About.vue')
},
{
path: '/services',
name: 'Services',
component: () => import('@/views/frontend/Services.vue')
},
{
path: '/personal',
name: 'Personal',
component: () => import('@/views/frontend/Personal.vue'),
meta: { requiresAuth: true }
},
]
},
{
path: '/admin',
component: AdminLayout,
meta: { requiresAuth: true },
children: [
{ path: '', redirect: '/admin/dashboard' },
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/admin/Dashboard.vue') },
{ path: 'projects', name: 'ProjectManage', component: () => import('@/views/admin/ProjectsCRUD.vue') }
]
},
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
return savedPosition || { top: 0 }
}
})
router.beforeEach((to, from) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.user) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
export default router
router.beforeEach((to, from) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.user) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})

54
src/stores/user.js Normal file
View File

@@ -0,0 +1,54 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const user = ref(JSON.parse(localStorage.getItem('user') || 'null'))
// 添加初始化状态校验
const initUser = () => {
const userData = localStorage.getItem('user')
if (!userData) return null
try {
return JSON.parse(userData)
} catch {
localStorage.removeItem('user')
localStorage.removeItem('token')
return null
}
}
const isAuthenticated = computed(() => !!user.value?.token)
function login(credentials) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (credentials.username === 'admin' && credentials.password === 'admin123') {
const userData = {
username: credentials.username,
role: 'admin',
token: 'mock_token_' + Math.random().toString(36).slice(2)
}
localStorage.setItem('user', JSON.stringify(userData))
user.value = userData // 添加响应式状态更新
localStorage.setItem('token', userData.token)
resolve()
} else {
reject(new Error('用户名或密码错误'))
}
}, 500)
})
}
function logout() {
user.value = null
localStorage.removeItem('user')
localStorage.removeItem('token')
}
return {
user,
isAuthenticated,
login,
logout
}
})

View File

@@ -1 +1 @@
@import "tailwindcss";
@import "tailwindcss";

View File

@@ -0,0 +1,58 @@
<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'
const [ALayout, ALayoutContent, APageHeader, ACard, ARow, ACol] = [
Layout,
Layout.Content,
PageHeader,
Card,
Row,
Col
]
</script>
<style lang="scss" scoped>
:deep(.ant-page-header) {
border-bottom: 1px solid #f0f0f0;
}
</style>

96
src/views/admin/Login.vue Normal file
View File

@@ -0,0 +1,96 @@
<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

@@ -0,0 +1,116 @@
<script setup>
import { ref } from 'vue'
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { Table, Modal } from 'ant-design-vue'
const dataSource = ref([
{
id: 1,
title: '电商平台',
category: 'Web应用',
tags: ['Vue3', 'Node.js'],
author: 'Admin',
createdAt: '2024-03-20'
}
])
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 visible = ref(false)
const currentItem = ref(null)
const showModal = (item = null) => {
currentItem.value = item || { tags: [] }
visible.value = true
}
const handleDelete = (id) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个项目吗?',
onOk: () => {
dataSource.value = dataSource.value.filter(item => item.id !== id)
}
})
}
</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>
新建项目
</a-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>
<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>
</div>
</template>
<style lang="scss" scoped>
:deep(.ant-table-thead > tr > th) {
@apply bg-gray-50;
}
:deep(.ant-table-tbody > tr:hover td) {
@apply bg-gray-100;
}
</style>

View File

@@ -0,0 +1,31 @@
<template>
<div class="max-w-4xl mx-auto p-6">
<h1 class="text-3xl font-bold mb-6 text-blue-600">关于我们</h1>
<section class="space-y-6">
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">公司简介</h2>
<p class="text-gray-600 leading-relaxed">
我们是一家专注数字化转型的科技企业致力于为客户提供创新的解决方案...
</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">核心团队</h2>
<div class="grid md:grid-cols-2 gap-6">
<!-- 团队成员卡片 -->
</div>
</div>
</section>
</div>
</template>
<script setup>
import { useHead } from '@vueuse/head'
useHead({
title: '关于我们 - 企业数字化解决方案专家',
meta: [
{ name: 'description', content: '了解我们的企业使命、核心团队和技术优势' }
]
})
</script>

View File

@@ -0,0 +1,106 @@
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { Swiper, SwiperSlide } from 'swiper/vue'
const route = useRoute()
const activeKey = ref('1')
const project = {
id: route.params.id,
title: '电商平台',
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
category: 'Web应用',
tags: ['Vue3', 'Node.js'],
author: '技术团队',
price: '¥ 29,999',
description: '基于微服务架构的电商平台,包含商品管理、订单系统、支付对接等模块',
technicalStack: ['前端Vue3 + TypeScript', '后端NestJS + MySQL', '部署Docker + Kubernetes'],
screenshots: [
'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280'
],
video: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
wechatQR: 'https://via.placeholder.com/200x200'
}
</script>
<template>
<div class="container mx-auto px-4 py-8">
<!-- 项目封面 -->
<div class="mb-8" data-aos="fade-up">
<img :src="project.cover" class="w-full h-64 md:h-96 object-cover rounded-xl" />
</div>
<!-- 基本信息卡片 -->
<a-card class="mb-8 shadow-lg" data-aos="fade-up">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<h1 class="text-2xl font-bold mb-2">{{ project.title }}</h1>
<a-tag color="blue" class="mb-2">{{ project.category }}</a-tag>
<div class="flex flex-wrap gap-2">
<a-tag v-for="tag in project.tags" :key="tag">{{ tag }}</a-tag>
</div>
</div>
<div class="space-y-2">
<p class="text-gray-600">作者{{ project.author }}</p>
<p class="text-xl text-red-500 font-bold">{{ project.price }}</p>
</div>
<div class="flex items-center justify-center">
<a-button type="primary" size="large" class="w-full">立即咨询</a-button>
</div>
</div>
</a-card>
<!-- 详情标签页 -->
<a-tabs v-model:activeKey="activeKey" class="mb-8" data-aos="fade-up">
<a-tab-pane key="1" tab="项目详情">
<div class="prose max-w-none">
<h3 class="text-xl font-bold mb-4">项目描述</h3>
<p class="text-gray-600 mb-6">{{ project.description }}</p>
<h3 class="text-xl font-bold mb-4">技术栈</h3>
<ul class="list-disc pl-6 mb-6">
<li v-for="tech in project.technicalStack" :key="tech">{{ tech }}</li>
</ul>
</div>
</a-tab-pane>
<a-tab-pane key="2" tab="项目截图">
<swiper
:slides-per-view="1"
:space-between="30"
class="mb-6"
>
<swiper-slide v-for="(img, index) in project.screenshots" :key="index">
<img :src="img" class="w-full h-96 object-contain" />
</swiper-slide>
</swiper>
</a-tab-pane>
<a-tab-pane key="3" tab="演示视频">
<div class="aspect-w-16 aspect-h-9">
<iframe
:src="project.video"
class="w-full h-96"
frameborder="0"
allowfullscreen
/>
</div>
</a-tab-pane>
<a-tab-pane key="4" tab="联系作者">
<div class="text-center">
<img :src="project.wechatQR" class="w-48 h-48 mx-auto mb-4" />
<p class="text-gray-600">扫描二维码添加微信咨询</p>
</div>
</a-tab-pane>
</a-tabs>
</div>
</template>
<style lang="scss" scoped>
.prose :deep(h3) {
@apply border-l-4 border-blue-500 pl-4 mb-4;
}
</style>

225
src/views/frontend/Home.vue Normal file
View File

@@ -0,0 +1,225 @@
<script setup>
import { Swiper, SwiperSlide } from 'swiper/vue'
import { Autoplay, Pagination, Navigation, EffectCreative } from 'swiper/modules'
import AOS from 'aos'
import 'swiper/css'
import 'swiper/css/pagination'
import 'swiper/css/navigation'
import {onMounted} from "vue";
const modules = [Autoplay, Pagination, Navigation, EffectCreative]
onMounted(() => {
AOS.init({
duration: 1000,
offset: 120,
easing: 'ease-out-quart'
})
})
const projects = [
{
id: 1,
title: '电商平台',
tags: ['Vue3', 'Node.js'],
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
route: '/project/1'
},
{
id: 2,
title: 'OA系统',
tags: ['React', 'Java'],
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
route: '/project/2'
}
]
const testimonials = [
{
id: 1,
author: '某科技公司',
role: '技术总监',
rating: 5,
content: '项目交付非常专业,技术方案极具创新性,团队响应速度超出预期',
avatar: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280'
},
{
id: 2,
author: '某金融机构',
role: '技术负责人',
rating: 4,
content: '系统稳定性表现优异,故障响应机制完善,合作体验良好',
avatar: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280'
}
]
const partners = [
{ name: '腾讯云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '阿里云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '华为云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: 'Microsoft', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '腾讯云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '阿里云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '华为云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: 'Microsoft', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '腾讯云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '阿里云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '华为云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: 'Microsoft', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
]
</script>
<template>
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<!-- 增强型轮播图 -->
<div class="mb-16 lg:mb-24" data-aos="fade-up">
<swiper
class="rounded-3xl lg:rounded-[2rem] overflow-hidden shadow-2xl hover:shadow-3xl transition-shadow duration-500"
:modules="modules"
:autoplay="{ delay: 5000 }"
:pagination="{ clickable: true }"
:navigation="true"
>
<swiper-slide
class="group relative cursor-pointer hover:z-10 transition-transform duration-500"
v-for="(item, index) in 3"
:key="item"
@click="$router.push(projects[index % 2].route)"
>
<div class="relative h-64 md:h-[480px] xl:h-[560px]">
<img
:src="`https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280?text=Banner${item}`"
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div class="absolute inset-0 bg-gradient-to-t from-gray-900/80 via-transparent to-transparent p-6 flex flex-col justify-end space-y-3">
<h3 class="text-2xl md:text-4xl font-bold text-white mb-2 drop-shadow-2xl">
{{ projects[index % 2].title }}
</h3>
<div class="flex gap-2">
<span
v-for="tag in projects[index % 2].tags"
class="px-3 py-1.5 bg-white/10 backdrop-blur-lg text-sm font-medium text-gray-100 rounded-lg transition-colors hover:bg-white/20"
>
{{ tag }}
</span>
</div>
</div>
</div>
</swiper-slide>
</swiper>
</div>
<!-- 精选项目 - 网格布局 -->
<section class="mb-20 lg:mb-28">
<h2 class="text-3xl md:text-4xl font-bold mb-8 md:mb-12 text-gray-900 dark:text-white" data-aos="fade-up">精选项目</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
<div
v-for="(project, index) in projects"
class="relative group bg-white dark:bg-gray-800 rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 overflow-hidden cursor-pointer"
data-aos="fade-up"
:data-aos-delay="index * 100"
>
<div class="relative h-56 md:h-64 overflow-hidden">
<img
:src="project.cover"
class="w-full h-full object-cover transform transition-transform duration-500 group-hover:scale-110"
/>
<div class="absolute inset-0 bg-gradient-to-t from-gray-900/40 via-transparent" />
</div>
<div class="p-6 md:p-7">
<h3 class="text-xl md:text-2xl font-semibold mb-3 text-gray-900 dark:text-white">
{{ project.title }}
</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="tag in project.tags"
class="px-3 py-1.5 bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-300 text-sm font-medium rounded-lg"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
</section>
<!-- 客户评价 - 卡片布局 -->
<section class="mb-20 lg:mb-28" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-bold mb-8 md:mb-12 text-gray-900 dark:text-white">客户评价</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
<div
v-for="testimonial in testimonials"
class="bg-white dark:bg-gray-800 p-6 md:p-8 rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500"
>
<div class="flex items-start gap-5 mb-6">
<img
:src="testimonial.avatar"
class="w-16 h-16 rounded-xl border-2 border-white/30 shadow-md"
/>
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<div>
<h4 class="text-lg font-bold text-gray-900 dark:text-white">{{ testimonial.author }}</h4>
<p class="text-sm text-blue-600 dark:text-blue-400">{{ testimonial.role }}</p>
</div>
<div class="flex gap-1 text-yellow-400">
<svg v-for="n in 5" :key="n" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</div>
</div>
<p class="text-gray-600 dark:text-gray-300 leading-relaxed">
{{ testimonial.content }}
</p>
</div>
</div>
</div>
</div>
</section>
<!-- 合作伙伴 - 品牌墙 -->
<section class="mb-20 lg:mb-28" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-bold mb-8 md:mb-12 text-gray-900 dark:text-white">合作伙伴</h2>
<div class="relative overflow-hidden py-4">
<swiper
class="!overflow-visible"
:modules="modules"
:slides-per-view="auto"
:space-between="40"
:breakpoints="{ 640: { slidesPerView: 4 }, 1024: { slidesPerView: 6 } }"
:autoplay="{ delay: 2000 }"
>
<swiper-slide v-for="(partner, index) in partners" :key="index">
<div class="flex items-center justify-center h-32 bg-white dark:bg-gray-800 rounded-xl p-4 shadow-lg hover:shadow-xl transition-all duration-500 hover:-translate-y-2">
<img
:src="partner.logo"
class="h-12 object-contain opacity-80 hover:opacity-100 transition-opacity grayscale hover:grayscale-0"
/>
</div>
</swiper-slide>
</swiper>
</div>
</section>
</div>
</template>
<style lang="scss" scoped>
/* 优化 Swiper 导航样式 */
.swiper-button-next,
.swiper-button-prev {
@apply w-12 h-12 bg-white/80 backdrop-blur-sm rounded-full shadow-lg hover:bg-white transition-colors text-gray-900 hover:text-blue-600;
}
.swiper-button-next::after,
.swiper-button-prev::after {
@apply text-xl font-bold;
}
.swiper-pagination-bullet {
@apply w-3 h-3 bg-white/50 hover:bg-white transition-colors;
}
.swiper-pagination-bullet-active {
@apply bg-blue-600;
}
</style>

View File

@@ -0,0 +1,221 @@
<script setup>
import { Swiper, SwiperSlide } from 'swiper/vue'
import { Autoplay, Pagination, Navigation, EffectCreative } from 'swiper/modules'
import AOS from 'aos'
import 'swiper/css'
import 'swiper/css/pagination'
import 'swiper/css/navigation'
import {onMounted} from "vue";
const modules = [Autoplay, Pagination, Navigation, EffectCreative]
onMounted(() => {
AOS.init({
duration: 1000,
offset: 120,
easing: 'ease-out-quart'
})
})
const projects = [
{
id: 1,
title: '电商平台',
tags: ['Vue3', 'Node.js'],
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
route: '/project/1'
},
{
id: 2,
title: 'OA系统',
tags: ['React', 'Java'],
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
route: '/project/2'
}
]
const testimonials = [
{
id: 1,
author: '某科技公司',
role: '技术总监',
rating: 5,
content: '项目交付非常专业,技术方案极具创新性,团队响应速度超出预期',
avatar: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280'
},
{
id: 2,
author: '某金融机构',
role: '技术负责人',
rating: 4,
content: '系统稳定性表现优异,故障响应机制完善,合作体验良好',
avatar: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280'
}
]
const partners = [
{ name: '腾讯云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '阿里云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '华为云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: 'Microsoft', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '腾讯云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '阿里云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '华为云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: 'Microsoft', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '腾讯云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '阿里云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: '华为云', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
{ name: 'Microsoft', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
]
</script>
<template>
<div class="container mx-auto px-4">
<!-- 轮播图 -->
<div class="mb-16" data-aos="fade-up">
<swiper
:modules="modules"
:autoplay="{ delay: 5000 }"
:pagination="{ clickable: true }"
:navigation="true"
class="rounded-xl overflow-hidden"
>
<swiper-slide
v-for="(item, index) in 3"
:key="item"
@click="$router.push(projects[index % 2].route)"
class="group relative cursor-pointer"
>
<div class="relative h-64 md:h-96">
<img
:src="`https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280?text=Banner${item}`"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent p-6 flex flex-col justify-end">
<h3 class="text-2xl font-bold text-white mb-2 drop-shadow-xl">
{{ projects[index % 2].title }}
</h3>
<div class="flex gap-2">
<span
v-for="tag in projects[index % 2].tags"
class="px-3 py-1 bg-white/20 backdrop-blur-sm text-white rounded-full text-sm"
>
{{ tag }}
</span>
</div>
</div>
</div>
</swiper-slide>
</swiper>
</div>
<!-- 精选项目 -->
<section class="mb-20">
<h2 class="text-3xl font-bold mb-8" data-aos="fade-up">精选项目</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="(project, index) in projects"
:key="project.id"
class="bg-card-gradient backdrop-blur-lg border border-white/20 rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl hover:border-blue-400/30 group text-gray-100"
data-aos="fade-up"
:data-aos-delay="index * 100"
@click="$router.push(project.route)"
>
<img :src="project.cover" class="w-full h-48 object-cover" />
<div class="p-4">
<h3 class="text-xl font-semibold mb-2">{{ project.title }}</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="tag in project.tags"
class="px-3 py-1 bg-blue-500/20 text-blue-300 rounded-full text-sm backdrop-blur-sm"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
</section>
<!-- 客户评价 -->
<section class="mb-20" data-aos="fade-up">
<h2 class="text-3xl font-bold mb-8">客户评价</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 客户评价模块修复 -->
<div
v-for="testimonial in testimonials"
:key="testimonial.id"
class="bg-card-gradient backdrop-blur-lg border border-white/20 p-8 rounded-2xl transition-all duration-300 hover:border-blue-400/50 hover:shadow-2xl text-gray-100"
>
<!-- 内容结构保持不变 -->
<div class="flex items-start gap-5 mb-6">
<img :src="testimonial.avatar" class="w-16 h-16 rounded-2xl border-2 border-white/30">
<div class="flex-1">
<h4 class="text-lg font-bold text-white mb-1">{{ testimonial.author }}</h4>
<p class="text-sm text-blue-200 mb-2">{{ testimonial.role }}</p>
<div class="flex gap-1 text-yellow-400">
<span v-for="n in 5" :key="n"
:class="n <= testimonial.rating ? 'opacity-100' : 'opacity-30'">
</span>
</div>
</div>
</div>
<p class="text-white/90 leading-relaxed text-left indent-8">{{ testimonial.content }}</p>
</div>
</div>
</section>
<!-- 合作伙伴横向滚动 -->
<section class="mb-20" data-aos="fade-up">
<h2 class="text-3xl font-bold mb-8">合作伙伴</h2>
<swiper
:modules="modules"
:slides-per-view="3"
:space-between="30"
:autoplay="{ delay: 2500 }"
class="py-4"
>
<swiper-slide v-for="(partner, index) in partners" :key="index">
<div class="flex items-center justify-center h-32 bg-partner-gradient border border-white/20 rounded-xl p-4 transition-all hover:scale-105 hover:border-blue-400/30">
<img :src="partner.logo" class="h-full w-full object-contain opacity-80 hover:opacity-100 transition-opacity"/>
</div>
</swiper-slide>
</swiper>
</section>
</div>
</template>
<style lang="scss" scoped>
.swiper-button-prev,
.swiper-button-next {
@apply text-white hover:text-blue-400;
}
.swiper-pagination-bullet {
@apply bg-white opacity-80;
}
.swiper-pagination-bullet-active {
@apply bg-blue-500;
}
.bg-glass {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.shadow-glass {
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
}
@keyframes gradient-flow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
</style>

View File

@@ -0,0 +1,39 @@
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>
<template>
<a-card class="personal-card">
<a-row :gutter="24">
<a-col :span="8">
<a-avatar
:size="120"
:src="userStore.user?.avatar || 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280'"
/>
</a-col>
<a-col :span="16">
<h2 class="username">{{ userStore.user?.username || '未命名用户' }}</h2>
<a-descriptions :column="1">
<a-descriptions-item label="注册时间">
{{ userStore.user?.createdAt }}
</a-descriptions-item>
<a-descriptions-item label="最后登录">
{{ userStore.user?.lastLogin }}
</a-descriptions-item>
</a-descriptions>
</a-col>
</a-row>
</a-card>
</template>
<style scoped>
.personal-card {
margin: 24px;
}
.username {
font-size: 24px;
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,98 @@
<script setup>
import { ref, computed } from 'vue'
import { Tag, Card, Select } from 'ant-design-vue'
// 模拟项目数据
const projects = ref(Array.from({ length: 12 }, (_, i) => ({
id: i + 1,
title: `项目 ${i + 1}`,
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
tags: ['Vue3', 'Node.js', 'TypeScript'],
category: ['Web应用', '移动端'][i % 2],
author: '开发者' + (i + 1),
price: (Math.random() * 10000).toFixed(2),
createdAt: new Date().toISOString()
})))
// 筛选排序逻辑
const selectedCategory = ref('all')
const sortBy = ref('date')
const filteredProjects = computed(() => {
return projects.value
.filter(p =>
selectedCategory.value === 'all' ||
p.category === selectedCategory.value
)
.sort((a, b) => {
if (sortBy.value === 'date') {
return new Date(b.createdAt) - new Date(a.createdAt)
}
return b.price - a.price
})
})
</script>
<template>
<div class="container mx-auto px-4 py-8">
<!-- 过滤排序栏 -->
<div class="mb-8 flex flex-col md:flex-row gap-4">
<Select
v-model:value="selectedCategory"
class="w-full md:w-48"
:options="[
{ value: 'all', label: '全部分类' },
{ value: 'Web应用', label: 'Web应用' },
{ value: '移动端', label: '移动端' },
]"
/>
<Select
v-model:value="sortBy"
class="w-full md:w-48"
:options="[
{ value: 'date', label: '最新发布' },
{ value: 'price', label: '价格排序' },
]"
/>
</div>
<!-- 项目卡片网格 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card
v-for="project in filteredProjects"
:key="project.id"
class="hover:shadow-xl transition-shadow duration-300"
>
<template #cover>
<img
:src="'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280'"
class="h-48 w-full object-cover"
alt="项目封面"
/>
</template>
<div class="p-4">
<h3 class="text-lg font-semibold mb-2">{{ project.title }}</h3>
<div class="flex flex-wrap gap-2 mb-4">
<Tag
v-for="(tag, index) in project.tags"
:key="index"
:color="['#87d068', '#108ee9', '#2db7f5'][index % 3]"
>
{{ tag }}
</Tag>
</div>
<div class="flex justify-between items-center text-gray-600">
<span>{{ project.category }}</span>
<span>¥{{ project.price }}</span>
</div>
<div class="mt-4 text-sm text-gray-500 flex justify-between">
<span>{{ project.author }}</span>
<span>{{ new Date(project.createdAt).toLocaleDateString() }}</span>
</div>
</div>
</Card>
</div>
</div>
</template>

View File

@@ -0,0 +1,12 @@
<script setup>
// 服务项目页面内容
</script>
<template>
<main class="p-6 max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-6">服务项目</h1>
<section class="prose">
<p>这里是服务项目页面内容...</p>
</section>
</main>
</template>

11
tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts}'],
theme: {
extend: {
backgroundImage: {
'card-gradient': 'linear-gradient(135deg, rgba(16,24,39,0.9) 0%, rgba(30,41,59,0.9) 100%)',
'partner-gradient': 'linear-gradient(145deg, rgba(15,23,42,0.9) 0%, rgba(30,41,59,0.7) 100%)'
}
}
}
}

View File

@@ -1,6 +1,7 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from "@tailwindcss/vite";
import tailwindcss from "@tailwindcss/vite"
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
@@ -8,4 +9,9 @@ export default defineConfig({
vue(),
tailwindcss()
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})