完成部分页面UI
This commit is contained in:
57
README.md
57
README.md
@@ -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
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
12
package.json
12
package.json
@@ -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
885
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
37
src/App.vue
37
src/App.vue
@@ -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
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
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
BIN
src/assets/nl-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
96
src/components/common/AuthModal.vue
Normal file
96
src/components/common/AuthModal.vue
Normal 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>
|
||||
65
src/layouts/AdminLayout.vue
Normal file
65
src/layouts/AdminLayout.vue
Normal 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
160
src/layouts/MainLayout.vue
Normal 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>
|
||||
21
src/main.js
21
src/main.js
@@ -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
70
src/router/index.js
Normal 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
54
src/stores/user.js
Normal 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
|
||||
}
|
||||
})
|
||||
@@ -1 +1 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
|
||||
58
src/views/admin/Dashboard.vue
Normal file
58
src/views/admin/Dashboard.vue
Normal 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
96
src/views/admin/Login.vue
Normal 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>
|
||||
116
src/views/admin/ProjectsCRUD.vue
Normal file
116
src/views/admin/ProjectsCRUD.vue
Normal 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>
|
||||
31
src/views/frontend/About.vue
Normal file
31
src/views/frontend/About.vue
Normal 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>
|
||||
106
src/views/frontend/Detail.vue
Normal file
106
src/views/frontend/Detail.vue
Normal 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
225
src/views/frontend/Home.vue
Normal 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>
|
||||
221
src/views/frontend/Home2.vue
Normal file
221
src/views/frontend/Home2.vue
Normal 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>
|
||||
39
src/views/frontend/Personal.vue
Normal file
39
src/views/frontend/Personal.vue
Normal 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>
|
||||
98
src/views/frontend/Projects.vue
Normal file
98
src/views/frontend/Projects.vue
Normal 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>
|
||||
12
src/views/frontend/Services.vue
Normal file
12
src/views/frontend/Services.vue
Normal 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
11
tailwind.config.js
Normal 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%)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user