更新前台UI设计

This commit is contained in:
2025-07-31 10:00:19 +08:00
parent 4b2d0a6e94
commit c25051a9ef
35 changed files with 8123 additions and 2120 deletions

View File

@@ -9,18 +9,22 @@
"preview": "vite preview"
},
"dependencies": {
"tailwindcss": "^4.1.4",
"vue": "^3.5.13",
"@ant-design-vue/pro-layout": "^3.2.5",
"@ant-design/icons-vue": "^7.0.1",
"@fortawesome/fontawesome-free": "^6.7.2",
"@headlessui/vue": "^1.7.23",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/vite": "^4.1.4",
"@vueuse/core": "^13.6.0",
"@vueuse/head": "^2.0.0",
"ant-design-vue": "^4.1.1",
"aos": "^2.3.4",
"axios": "^1.6.2",
"lucide-vue-next": "^0.507.0",
"pinia": "^2.1.7",
"swiper": "^11.0.5",
"tailwindcss": "^4.1.4",
"vue": "^3.5.13",
"vue-router": "^4.2.5"
},
"devDependencies": {

102
pnpm-lock.yaml generated
View File

@@ -17,15 +17,27 @@ importers:
'@fortawesome/fontawesome-free':
specifier: ^6.7.2
version: 6.7.2
'@headlessui/vue':
specifier: ^1.7.23
version: 1.7.23(vue@3.5.13)
'@tailwindcss/line-clamp':
specifier: ^0.4.4
version: 0.4.4(tailwindcss@4.1.4)
'@tailwindcss/vite':
specifier: ^4.1.4
version: 4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))
'@vueuse/core':
specifier: ^13.6.0
version: 13.6.0(vue@3.5.13)
'@vueuse/head':
specifier: ^2.0.0
version: 2.0.0(vue@3.5.13)
ant-design-vue:
specifier: ^4.1.1
version: 4.2.6(vue@3.5.13)
aos:
specifier: ^2.3.4
version: 2.3.4
axios:
specifier: ^1.6.2
version: 1.9.0
@@ -267,6 +279,12 @@ packages:
resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==}
engines: {node: '>=6'}
'@headlessui/vue@1.7.23':
resolution: {integrity: sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==}
engines: {node: '>=10'}
peerDependencies:
vue: ^3.2.0
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
@@ -472,6 +490,11 @@ packages:
'@simonwep/pickr@1.8.2':
resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
'@tailwindcss/line-clamp@0.4.4':
resolution: {integrity: sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==}
peerDependencies:
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
'@tailwindcss/node@4.1.4':
resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==}
@@ -566,9 +589,20 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6
'@tanstack/virtual-core@3.13.12':
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
'@tanstack/vue-virtual@3.13.12':
resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==}
peerDependencies:
vue: ^2.7.0 || ^3.0.0
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@unhead/dom@1.11.20':
resolution: {integrity: sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA==}
@@ -625,17 +659,33 @@ packages:
'@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vueuse/core@13.6.0':
resolution: {integrity: sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A==}
peerDependencies:
vue: ^3.5.0
'@vueuse/head@2.0.0':
resolution: {integrity: sha512-ykdOxTGs95xjD4WXE4na/umxZea2Itl0GWBILas+O4oqS7eXIods38INvk3XkJKjqMdWPcpCyLX/DioLQxU1KA==}
peerDependencies:
vue: '>=2.7 || >=3'
'@vueuse/metadata@13.6.0':
resolution: {integrity: sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ==}
'@vueuse/shared@13.6.0':
resolution: {integrity: sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg==}
peerDependencies:
vue: ^3.5.0
ant-design-vue@4.2.6:
resolution: {integrity: sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==}
engines: {node: '>=12.22.0'}
peerDependencies:
vue: '>=3.2.0'
aos@2.3.4:
resolution: {integrity: sha512-zh/ahtR2yME4I51z8IttIt4lC1Nw0ktsFtmeDzID1m9naJnWXhCoARaCgNOGXb5CLy3zm+wqmRAEgMYB5E2HUw==}
array-tree-filter@2.1.0:
resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==}
@@ -660,6 +710,9 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
classlist-polyfill@1.2.0:
resolution: {integrity: sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -891,6 +944,12 @@ packages:
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -1223,6 +1282,11 @@ snapshots:
'@fortawesome/fontawesome-free@6.7.2': {}
'@headlessui/vue@1.7.23(vue@3.5.13)':
dependencies:
'@tanstack/vue-virtual': 3.13.12(vue@3.5.13)
vue: 3.5.13
'@jridgewell/sourcemap-codec@1.5.0': {}
'@parcel/watcher-android-arm64@2.5.1':
@@ -1351,6 +1415,10 @@ snapshots:
core-js: 3.42.0
nanopop: 2.4.2
'@tailwindcss/line-clamp@0.4.4(tailwindcss@4.1.4)':
dependencies:
tailwindcss: 4.1.4
'@tailwindcss/node@4.1.4':
dependencies:
enhanced-resolve: 5.18.1
@@ -1416,8 +1484,17 @@ snapshots:
tailwindcss: 4.1.4
vite: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0)
'@tanstack/virtual-core@3.13.12': {}
'@tanstack/vue-virtual@3.13.12(vue@3.5.13)':
dependencies:
'@tanstack/virtual-core': 3.13.12
vue: 3.5.13
'@types/estree@1.0.7': {}
'@types/web-bluetooth@0.0.21': {}
'@unhead/dom@1.11.20':
dependencies:
'@unhead/schema': 1.11.20
@@ -1507,6 +1584,13 @@ snapshots:
'@vue/shared@3.5.13': {}
'@vueuse/core@13.6.0(vue@3.5.13)':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 13.6.0
'@vueuse/shared': 13.6.0(vue@3.5.13)
vue: 3.5.13
'@vueuse/head@2.0.0(vue@3.5.13)':
dependencies:
'@unhead/dom': 1.11.20
@@ -1515,6 +1599,12 @@ snapshots:
'@unhead/vue': 1.11.20(vue@3.5.13)
vue: 3.5.13
'@vueuse/metadata@13.6.0': {}
'@vueuse/shared@13.6.0(vue@3.5.13)':
dependencies:
vue: 3.5.13
ant-design-vue@4.2.6(vue@3.5.13):
dependencies:
'@ant-design/colors': 6.0.0
@@ -1541,6 +1631,12 @@ snapshots:
vue-types: 3.0.2(vue@3.5.13)
warning: 4.0.3
aos@2.3.4:
dependencies:
classlist-polyfill: 1.2.0
lodash.debounce: 4.0.8
lodash.throttle: 4.1.1
array-tree-filter@2.1.0: {}
async-validator@4.2.5: {}
@@ -1569,6 +1665,8 @@ snapshots:
dependencies:
readdirp: 4.1.2
classlist-polyfill@1.2.0: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@@ -1773,6 +1871,10 @@ snapshots:
lodash-es@4.17.21: {}
lodash.debounce@4.0.8: {}
lodash.throttle@4.1.1: {}
lodash@4.17.21: {}
loose-envify@1.4.0:

View File

@@ -1,11 +1,51 @@
<script setup>
</script>
<template>
<div id="app" class="theme-bg">
<Transition name="page" mode="out-in">
<router-view />
</Transition>
<template>
<router-view />
<!-- 全局主题切换按钮 -->
<!-- <div class="fixed top-4 right-4 z-50">-->
<!-- <ThemeToggle />-->
<!-- </div>-->
</div>
</template>
<style lang='scss' scoped>
<script setup>
import { onMounted } from 'vue'
import { useThemeStore } from './stores/theme'
import ThemeToggle from './components/ThemeToggle.vue'
const themeStore = useThemeStore()
onMounted(() => {
// 确保主题正确初始化
themeStore.initTheme()
})
</script>
<style>
#app {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
transition: all var(--animation-duration-normal) var(--animation-easing);
}
/* 页面过渡动画 */
.page-enter-active,
.page-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.page-enter-from {
opacity: 0;
transform: translateX(20px);
}
.page-leave-to {
opacity: 0;
transform: translateX(-20px);
}
</style>

View File

@@ -1,5 +1,5 @@
// API 基础配置
const API_BASE_URL = 'http://localhost:3000/api'
const API_BASE_URL = 'http://localhost:8000/api/v1'
// 通用请求函数
const request = async (url, options = {}) => {
@@ -280,6 +280,113 @@ export const settingsAPI = {
},
}
// 招贤纳士相关API
export const careersAPI = {
// 获取职位分类列表
getCategories: () => {
return request('/public/jobs/categories')
},
// 获取职位列表
getJobs: (params = {}) => {
const queryString = new URLSearchParams(params).toString()
return request(`/public/jobs?${queryString}`)
},
// 获取职位详情
getJobDetail: (id) => {
return request(`/public/jobs/${id}`)
},
// 搜索职位
searchJobs: (params = {}) => {
const queryString = new URLSearchParams(params).toString()
return request(`/public/jobs/search?${queryString}`)
},
// 获取推荐职位
getFeaturedJobs: (limit = 5) => {
return request(`/public/jobs/featured?limit=${limit}`)
},
// 创建简历
createResume: (data) => {
return request('/public/resumes', {
method: 'POST',
body: JSON.stringify(data),
})
},
// 投递简历
applyJob: (data) => {
return request('/public/resumes/apply', {
method: 'POST',
body: JSON.stringify(data),
})
},
// 管理后台 - 获取职位列表
admin: {
// 职位管理
getJobs: (params = {}) => {
const queryString = new URLSearchParams(params).toString()
return request(`/admin/jobs?${queryString}`)
},
createJob: (data) => {
return request('/admin/jobs', {
method: 'POST',
body: JSON.stringify(data),
})
},
updateJob: (id, data) => {
return request(`/admin/jobs/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
},
deleteJob: (id) => {
return request(`/admin/jobs/${id}`, {
method: 'DELETE',
})
},
// 简历管理
getResumes: (params = {}) => {
const queryString = new URLSearchParams(params).toString()
return request(`/admin/resumes?${queryString}`)
},
getResumeDetail: (id) => {
return request(`/admin/resumes/${id}`)
},
// 投递记录管理
getApplications: (params = {}) => {
const queryString = new URLSearchParams(params).toString()
return request(`/admin/resumes/applications?${queryString}`)
},
updateApplicationStatus: (id, data) => {
return request(`/admin/resumes/applications/${id}/status`, {
method: 'PUT',
body: JSON.stringify(data),
})
},
// 统计信息
getJobStats: () => {
return request('/admin/jobs/stats')
},
getResumeStats: () => {
return request('/admin/resumes/stats')
}
}
}
// 默认导出
export default {
articlesAPI,
@@ -287,4 +394,5 @@ export default {
userAPI,
contactAPI,
settingsAPI,
}
careersAPI,
}

View File

@@ -1,32 +1,58 @@
<template>
<div class="flex flex-col items-center justify-center py-12 px-4">
<div class="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mb-6">
<svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<div class="empty-state-container flex flex-col items-center justify-center py-16 relative">
<!-- 动画图标 -->
<div class="relative mb-8">
<div class="w-24 h-24 rounded-full theme-gradient-primary flex items-center justify-center pulse-glow">
<component :is="iconComponent" class="w-12 h-12 text-white" />
</div>
<div class="absolute -inset-2 rounded-full border-2 border-blue-400/30 animate-ping"></div>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">{{ title }}</h3>
<p class="text-gray-500 text-center max-w-sm">{{ description }}</p>
<div v-if="showAction" class="mt-6">
<button
@click="$emit('action')"
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
{{ actionText }}
</button>
<!-- 文字内容 -->
<div class="text-center space-y-4 max-w-md">
<h3 class="text-2xl font-bold gradient-text">
{{ title }}
</h3>
<p class="theme-text-secondary">
{{ description }}
</p>
<!-- 操作按钮 -->
<div v-if="showAction" class="pt-4">
<button
class="btn btn-glass"
@click="$emit('action')"
>
{{ actionText }}
</button>
</div>
</div>
<!-- 装饰元素 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-blue-400 rounded-full twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-purple-400 rounded-full twinkle" style="animation-delay: 1s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-blue-300 rounded-full twinkle" style="animation-delay: 2s;"></div>
</div>
</div>
</template>
<script setup>
defineProps({
import { computed } from 'vue'
const props = defineProps({
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'article', 'news', 'image', 'video'].includes(value)
},
title: {
type: String,
default: '暂时没有数据'
default: '敬请期待'
},
description: {
type: String,
default: '敬请期待更多精彩内容'
default: '精彩内容正在准备中,我们将为您带来更多优质的内容'
},
showAction: {
type: Boolean,
@@ -34,9 +60,73 @@ defineProps({
},
actionText: {
type: String,
default: '刷新'
default: '刷新重试'
}
})
defineEmits(['action'])
</script>
// 图标组件
const ZapIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>`
}
const FileTextIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>`
}
const ImageIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>`
}
const VideoIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>`
}
const iconComponent = computed(() => {
const icons = {
default: ZapIcon,
article: FileTextIcon,
news: FileTextIcon,
image: ImageIcon,
video: VideoIcon
}
return icons[props.type] || icons.default
})
</script>
<style scoped>
/* 添加缺失的动画效果 */
.pulse-glow {
animation: pulse-glow 2s ease-in-out infinite alternate;
}
@keyframes pulse-glow {
0% {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
}
100% {
box-shadow: 0 0 40px rgba(59, 130, 246, 0.6);
}
}
.twinkle {
animation: twinkle 2s ease-in-out infinite alternate;
}
@keyframes twinkle {
0% { opacity: 0.3; transform: scale(1); }
100% { opacity: 1; transform: scale(1.2); }
}
.animate-ping {
animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
}
@keyframes ping {
75%, 100% {
transform: scale(1.5);
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,388 @@
<template>
<Teleport to="body">
<Transition name="modal" appear>
<div
v-if="visible"
class="modal-overlay"
@click="handleOverlayClick"
>
<div
class="modal-container"
:class="[
`modal-${size}`,
{ 'modal-fullscreen': fullscreen }
]"
@click.stop
>
<!-- 毛玻璃背景 -->
<div class="modal-glass-bg"></div>
<!-- 模态框内容 -->
<div class="modal-content">
<!-- 头部 -->
<div v-if="showHeader" class="modal-header">
<div class="modal-title">
<slot name="title">{{ title }}</slot>
</div>
<button
v-if="closable"
class="modal-close-btn"
@click="handleClose"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- 主体内容 -->
<div class="modal-body">
<slot></slot>
</div>
<!-- 底部 -->
<div v-if="showFooter" class="modal-footer">
<slot name="footer">
<button
v-if="showCancel"
class="btn-secondary"
@click="handleCancel"
>
{{ cancelText }}
</button>
<button
v-if="showConfirm"
class="btn-primary"
:disabled="confirmLoading"
@click="handleConfirm"
>
<svg v-if="confirmLoading" class="animate-spin w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ confirmText }}
</button>
</slot>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { computed, watch, nextTick } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
size: {
type: String,
default: 'md',
validator: (value) => ['sm', 'md', 'lg', 'xl'].includes(value)
},
fullscreen: {
type: Boolean,
default: false
},
closable: {
type: Boolean,
default: true
},
maskClosable: {
type: Boolean,
default: true
},
showHeader: {
type: Boolean,
default: true
},
showFooter: {
type: Boolean,
default: false
},
showCancel: {
type: Boolean,
default: true
},
showConfirm: {
type: Boolean,
default: true
},
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
},
confirmLoading: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible', 'close', 'cancel', 'confirm'])
const handleClose = () => {
emit('update:visible', false)
emit('close')
}
const handleCancel = () => {
emit('cancel')
handleClose()
}
const handleConfirm = () => {
emit('confirm')
}
const handleOverlayClick = () => {
if (props.maskClosable) {
handleClose()
}
}
// 监听visible变化控制body滚动
watch(() => props.visible, (newVal) => {
nextTick(() => {
if (newVal) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
})
})
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
padding: 1rem;
}
.modal-container {
position: relative;
max-height: 90vh;
overflow: hidden;
border-radius: 1.5rem;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px var(--theme-glass-border);
}
.modal-glass-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--theme-glass-bg);
backdrop-filter: blur(var(--theme-glass-blur));
border-radius: 1.5rem;
}
.modal-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
max-height: 90vh;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem 2rem 1rem;
border-bottom: 1px solid var(--theme-border);
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--theme-text-primary);
}
.modal-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 0.5rem;
color: var(--theme-text-muted);
transition: all 0.2s ease;
background: transparent;
border: none;
cursor: pointer;
}
.modal-close-btn:hover {
background: var(--theme-surface-hover);
color: var(--theme-text-primary);
transform: scale(1.1);
}
.modal-body {
flex: 1;
padding: 1.5rem 2rem;
overflow-y: auto;
color: var(--theme-text-primary);
}
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 2rem 1.5rem;
border-top: 1px solid var(--theme-border);
}
/* 尺寸变体 */
.modal-sm {
width: 100%;
max-width: 28rem;
}
.modal-md {
width: 100%;
max-width: 32rem;
}
.modal-lg {
width: 100%;
max-width: 48rem;
}
.modal-xl {
width: 100%;
max-width: 64rem;
}
.modal-fullscreen {
width: 100vw;
height: 100vh;
max-width: none;
max-height: none;
border-radius: 0;
}
.modal-fullscreen .modal-glass-bg {
border-radius: 0;
}
/* 按钮样式 */
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: white;
background: var(--theme-gradient-primary);
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--theme-text-secondary);
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background: var(--theme-surface-hover);
border-color: var(--theme-border-hover);
transform: translateY(-1px);
}
/* 动画 */
.modal-enter-active,
.modal-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
transform: scale(0.9) translateY(1rem);
}
.modal-enter-to .modal-container,
.modal-leave-from .modal-container {
transform: scale(1) translateY(0);
}
/* 响应式 */
@media (max-width: 640px) {
.modal-overlay {
padding: 0;
}
.modal-container {
width: 100vw;
height: 100vh;
max-width: none;
max-height: none;
border-radius: 0;
}
.modal-glass-bg {
border-radius: 0;
}
.modal-header,
.modal-body,
.modal-footer {
padding-left: 1rem;
padding-right: 1rem;
}
}
</style>

View File

@@ -0,0 +1,411 @@
<template>
<div
class="rich-text-content"
:class="themeStore.isDark ? 'dark-theme' : 'light-theme'"
v-html="formattedContent"
></div>
</template>
<script setup>
import { computed } from 'vue'
import { useThemeStore } from '@/stores/theme'
const props = defineProps({
content: {
type: String,
required: true
}
})
const themeStore = useThemeStore()
const formattedContent = computed(() => {
if (!props.content) return ''
let html = props.content
// 处理代码块 - 添加Mac样式的窗口装饰
html = html.replace(/```(\w+)?\n([\s\S]*?)```/gim, (match, language, code) => {
const lang = language || 'text'
return `
<div class="code-block-container">
<div class="code-block-header">
<div class="code-block-controls">
<div class="control-dot red"></div>
<div class="control-dot yellow"></div>
<div class="control-dot green"></div>
</div>
<div class="code-block-language">${lang}</div>
</div>
<pre class="code-block-content"><code class="language-${lang}">${code.trim()}</code></pre>
</div>
`
})
// 处理行内代码
html = html.replace(/`([^`]+)`/gim, '<code class="inline-code">$1</code>')
// 处理标题
html = html.replace(/^### (.*$)/gim, '<h3 class="rich-h3">$1</h3>')
html = html.replace(/^## (.*$)/gim, '<h2 class="rich-h2">$1</h2>')
html = html.replace(/^# (.*$)/gim, '<h1 class="rich-h1">$1</h1>')
// 处理粗体和斜体
html = html.replace(/\*\*(.*?)\*\*/gim, '<strong class="rich-strong">$1</strong>')
html = html.replace(/\*(.*?)\*/gim, '<em class="rich-em">$1</em>')
// 处理链接
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '<a href="$2" class="rich-link" target="_blank" rel="noopener noreferrer">$1</a>')
// 处理引用
html = html.replace(/^> (.*$)/gim, '<blockquote class="rich-blockquote">$1</blockquote>')
// 处理无序列表
html = html.replace(/^\* (.*$)/gim, '<li class="rich-li">$1</li>')
html = html.replace(/(<li class="rich-li">.*<\/li>)/gims, '<ul class="rich-ul">$1</ul>')
// 处理有序列表
html = html.replace(/^\d+\. (.*$)/gim, '<li class="rich-oli">$1</li>')
html = html.replace(/(<li class="rich-oli">.*<\/li>)/gims, '<ol class="rich-ol">$1</ol>')
// 处理分割线
html = html.replace(/^---$/gim, '<hr class="rich-hr">')
// 处理段落
html = html.replace(/\n\n/gim, '</p><p class="rich-p">')
html = `<p class="rich-p">${html}</p>`
// 清理空段落
html = html.replace(/<p class="rich-p"><\/p>/gim, '')
return html
})
</script>
<style scoped>
.rich-text-content {
@apply leading-relaxed;
}
/* 标题样式 */
.rich-text-content :deep(.rich-h1) {
@apply text-3xl font-bold mb-6 mt-8;
}
.rich-text-content.light-theme :deep(.rich-h1) {
@apply text-gray-900;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.rich-text-content.dark-theme :deep(.rich-h1) {
@apply text-white;
background: linear-gradient(135deg, #f97316, #ef4444);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.rich-text-content :deep(.rich-h2) {
@apply text-2xl font-semibold mb-4 mt-6;
}
.rich-text-content.light-theme :deep(.rich-h2) {
@apply text-gray-800;
}
.rich-text-content.dark-theme :deep(.rich-h2) {
@apply text-gray-100;
}
.rich-text-content :deep(.rich-h3) {
@apply text-xl font-medium mb-3 mt-4;
}
.rich-text-content.light-theme :deep(.rich-h3) {
@apply text-gray-700;
}
.rich-text-content.dark-theme :deep(.rich-h3) {
@apply text-gray-200;
}
/* 段落样式 */
.rich-text-content :deep(.rich-p) {
@apply mb-4 leading-7;
}
.rich-text-content.light-theme :deep(.rich-p) {
@apply text-gray-700;
}
.rich-text-content.dark-theme :deep(.rich-p) {
@apply text-gray-300;
}
/* 强调样式 */
.rich-text-content :deep(.rich-strong) {
@apply font-semibold;
}
.rich-text-content.light-theme :deep(.rich-strong) {
@apply text-gray-900;
}
.rich-text-content.dark-theme :deep(.rich-strong) {
@apply text-white;
}
.rich-text-content :deep(.rich-em) {
@apply italic;
}
.rich-text-content.light-theme :deep(.rich-em) {
@apply text-gray-600;
}
.rich-text-content.dark-theme :deep(.rich-em) {
@apply text-gray-400;
}
/* 链接样式 */
.rich-text-content :deep(.rich-link) {
@apply font-medium underline decoration-2 underline-offset-2 transition-all duration-300;
}
.rich-text-content.light-theme :deep(.rich-link) {
@apply text-blue-600 decoration-blue-300 hover:text-blue-700 hover:decoration-blue-500;
}
.rich-text-content.dark-theme :deep(.rich-link) {
@apply text-orange-400 decoration-orange-300 hover:text-orange-300 hover:decoration-orange-400;
}
/* 引用样式 */
.rich-text-content :deep(.rich-blockquote) {
@apply border-l-4 pl-6 py-2 my-4 italic relative;
}
.rich-text-content.light-theme :deep(.rich-blockquote) {
@apply border-blue-400 bg-blue-50/50 text-gray-700;
}
.rich-text-content.dark-theme :deep(.rich-blockquote) {
@apply border-orange-400 bg-orange-900/20 text-gray-300;
}
.rich-text-content :deep(.rich-blockquote::before) {
content: '"';
@apply absolute -left-2 -top-2 text-4xl opacity-30;
}
.rich-text-content.light-theme :deep(.rich-blockquote::before) {
@apply text-blue-400;
}
.rich-text-content.dark-theme :deep(.rich-blockquote::before) {
@apply text-orange-400;
}
/* 列表样式 */
.rich-text-content :deep(.rich-ul) {
@apply mb-4 pl-6 space-y-2;
}
.rich-text-content :deep(.rich-ol) {
@apply mb-4 pl-6 space-y-2;
}
.rich-text-content :deep(.rich-li) {
@apply relative;
}
.rich-text-content.light-theme :deep(.rich-li) {
@apply text-gray-700;
}
.rich-text-content.dark-theme :deep(.rich-li) {
@apply text-gray-300;
}
.rich-text-content :deep(.rich-ul .rich-li::before) {
content: '•';
@apply absolute -left-4 font-bold;
}
.rich-text-content.light-theme :deep(.rich-ul .rich-li::before) {
@apply text-blue-500;
}
.rich-text-content.dark-theme :deep(.rich-ul .rich-li::before) {
@apply text-orange-400;
}
/* 分割线样式 */
.rich-text-content :deep(.rich-hr) {
@apply my-8 border-0 h-px;
}
.rich-text-content.light-theme :deep(.rich-hr) {
background: linear-gradient(90deg, transparent, #3b82f6, transparent);
}
.rich-text-content.dark-theme :deep(.rich-hr) {
background: linear-gradient(90deg, transparent, #f97316, transparent);
}
/* Mac风格代码块样式 */
.rich-text-content :deep(.code-block-container) {
@apply mb-6 rounded-2xl overflow-hidden shadow-lg;
}
.rich-text-content.light-theme :deep(.code-block-container) {
@apply bg-gray-900 border border-gray-200;
}
.rich-text-content.dark-theme :deep(.code-block-container) {
@apply bg-gray-800 border border-gray-700;
}
.rich-text-content :deep(.code-block-header) {
@apply flex items-center justify-between px-4 py-3 border-b;
}
.rich-text-content.light-theme :deep(.code-block-header) {
@apply bg-gray-100 border-gray-200;
}
.rich-text-content.dark-theme :deep(.code-block-header) {
@apply bg-gray-700 border-gray-600;
}
.rich-text-content :deep(.code-block-controls) {
@apply flex items-center space-x-2;
}
.rich-text-content :deep(.control-dot) {
@apply w-3 h-3 rounded-full;
}
.rich-text-content :deep(.control-dot.red) {
@apply bg-red-500;
}
.rich-text-content :deep(.control-dot.yellow) {
@apply bg-yellow-500;
}
.rich-text-content :deep(.control-dot.green) {
@apply bg-green-500;
}
.rich-text-content :deep(.code-block-language) {
@apply text-sm font-medium px-3 py-1 rounded-full;
}
.rich-text-content.light-theme :deep(.code-block-language) {
@apply bg-blue-100 text-blue-700;
}
.rich-text-content.dark-theme :deep(.code-block-language) {
@apply bg-orange-900/30 text-orange-300;
}
.rich-text-content :deep(.code-block-content) {
@apply p-4 overflow-x-auto text-sm leading-6;
font-family: 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
}
.rich-text-content.light-theme :deep(.code-block-content) {
@apply bg-gray-900 text-gray-100;
}
.rich-text-content.dark-theme :deep(.code-block-content) {
@apply bg-gray-800 text-gray-200;
}
.rich-text-content :deep(.code-block-content code) {
@apply text-inherit;
}
/* 行内代码样式 */
.rich-text-content :deep(.inline-code) {
@apply px-2 py-1 rounded-md text-sm font-mono;
}
.rich-text-content.light-theme :deep(.inline-code) {
@apply bg-gray-100 text-gray-800 border border-gray-200;
}
.rich-text-content.dark-theme :deep(.inline-code) {
@apply bg-gray-700 text-gray-200 border border-gray-600;
}
/* 代码高亮样式 */
.rich-text-content :deep(.code-block-content .language-javascript),
.rich-text-content :deep(.code-block-content .language-js) {
color: #f7df1e;
}
.rich-text-content :deep(.code-block-content .language-python) {
color: #3776ab;
}
.rich-text-content :deep(.code-block-content .language-html) {
color: #e34c26;
}
.rich-text-content :deep(.code-block-content .language-css) {
color: #1572b6;
}
.rich-text-content :deep(.code-block-content .language-vue) {
color: #4fc08d;
}
.rich-text-content :deep(.code-block-content .language-json) {
color: #ff6b6b;
}
/* 响应式设计 */
@media (max-width: 768px) {
.rich-text-content :deep(.code-block-header) {
@apply px-3 py-2;
}
.rich-text-content :deep(.code-block-content) {
@apply p-3 text-xs;
}
.rich-text-content :deep(.control-dot) {
@apply w-2.5 h-2.5;
}
.rich-text-content :deep(.code-block-language) {
@apply text-xs px-2 py-0.5;
}
}
/* 滚动条样式 */
.rich-text-content :deep(.code-block-content::-webkit-scrollbar) {
height: 8px;
}
.rich-text-content :deep(.code-block-content::-webkit-scrollbar-track) {
@apply bg-gray-700 rounded;
}
.rich-text-content :deep(.code-block-content::-webkit-scrollbar-thumb) {
@apply bg-gray-500 rounded hover:bg-gray-400;
}
.rich-text-content.dark-theme :deep(.code-block-content::-webkit-scrollbar-track) {
@apply bg-gray-600;
}
.rich-text-content.dark-theme :deep(.code-block-content::-webkit-scrollbar-thumb) {
@apply bg-gray-400 hover:bg-gray-300;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div class="text-center py-12">
<div class="backdrop-blur-lg border rounded-3xl p-8 max-w-md mx-auto" :class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/60 border-white/20'">
<div class="w-24 h-24 rounded-full flex items-center justify-center mx-auto mb-6 bg-gradient-to-r"
:class="themeStore.isDark ? 'from-orange-600 to-red-500' : 'from-blue-600 to-purple-500'">
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<h3 class="text-xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">{{ title }}</h3>
<p :class="[themeStore.isDark ? 'text-gray-300' : 'text-gray-600', 'mb-6']">
{{ description }}
</p>
<button
v-if="showAction"
@click="$emit('action')"
class="px-8 py-3 backdrop-blur-10 border font-medium rounded-2xl transition-all duration-300"
:class="themeStore.isDark
? 'bg-white/10 border-white/20 hover:bg-white/20 text-white'
: 'bg-white/60 border-white/20 hover:bg-white/80 text-gray-900'"
>
{{ actionText }}
</button>
</div>
</div>
</template>
<script setup>
import { useThemeStore } from '@/stores/theme'
const themeStore = useThemeStore()
defineProps({
title: {
type: String,
default: '暂无内容'
},
description: {
type: String,
default: '没有找到相关内容'
},
showAction: {
type: Boolean,
default: false
},
actionText: {
type: String,
default: '重新加载'
}
})
defineEmits(['action'])
</script>

View File

@@ -0,0 +1,109 @@
<template>
<div class="theme-toggle-wrapper">
<button
class="theme-toggle"
:class="{ dark: themeStore.isDark }"
@click="themeStore.toggleTheme"
:title="themeStore.isDark ? '切换到亮色模式' : '切换到暗色模式'"
>
<div class="theme-toggle-thumb">
<Transition name="icon" mode="out-in">
<svg
v-if="themeStore.isDark"
key="moon"
class="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg
v-else
key="sun"
class="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clip-rule="evenodd"
></path>
</svg>
</Transition>
</div>
</button>
</div>
</template>
<script setup>
import { useThemeStore } from '@/stores/theme'
const themeStore = useThemeStore()
</script>
<style scoped>
.theme-toggle-wrapper {
display: flex;
align-items: center;
}
.theme-toggle {
position: relative;
width: 3rem;
height: 1.5rem;
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 0.75rem;
cursor: pointer;
transition: all var(--animation-duration-normal) var(--animation-easing);
outline: none;
}
.theme-toggle:hover {
background: var(--theme-surface-hover);
border-color: var(--theme-border-hover);
}
.theme-toggle:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.theme-toggle-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 1.25rem;
height: 1.25rem;
background: var(--theme-gradient-primary);
border-radius: 50%;
transition: all var(--animation-duration-normal) var(--animation-easing);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.theme-toggle.dark .theme-toggle-thumb {
transform: translateX(1.5rem);
background: var(--theme-gradient-secondary);
}
/* 图标过渡动画 */
.icon-enter-active,
.icon-leave-active {
transition: all 0.2s ease;
}
.icon-enter-from {
opacity: 0;
transform: rotate(-90deg) scale(0.8);
}
.icon-leave-to {
opacity: 0;
transform: rotate(90deg) scale(0.8);
}
</style>

View File

@@ -1,210 +1,299 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="min-h-screen theme-bg">
<!-- 侧边栏 -->
<div class="fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-lg transform transition-transform duration-300 ease-in-out lg:translate-x-0" :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'">
<!-- Logo -->
<div class="flex items-center justify-center h-16 px-4 bg-gradient-to-r from-blue-600 to-purple-600">
<h1 class="text-xl font-bold text-white">CMS管理后台</h1>
<div
:class="[
'fixed inset-y-0 left-0 z-50 w-64 theme-glass border-r theme-border transform transition-transform duration-300 ease-in-out',
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
]"
>
<!-- Logo区域 -->
<div class="flex items-center justify-between h-16 px-6 border-b theme-border">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 theme-gradient-primary rounded-lg flex items-center justify-center">
<LogoIcon class="w-5 h-5 text-white" />
</div>
<span class="text-lg font-bold theme-text-primary">CMS管理</span>
</div>
<button
@click="toggleSidebar"
class="lg:hidden p-2 theme-text-secondary hover:theme-text-primary transition-colors"
>
<XIcon class="w-5 h-5" />
</button>
</div>
<!-- 导航菜单 -->
<nav class="mt-8">
<div class="px-4 space-y-2">
<router-link
to="/admin/dashboard"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-blue-50 hover:text-blue-600 transition-colors"
:class="{ 'bg-blue-50 text-blue-600': $route.path === '/admin/dashboard' }"
<nav class="flex-1 px-4 py-6 space-y-2">
<div
v-for="item in menuItems"
:key="item.name"
class="space-y-1"
>
<!-- 一级菜单 -->
<div
v-if="!item.children"
@click="navigateTo(item.path)"
:class="[
'flex items-center px-3 py-2 rounded-lg cursor-pointer transition-all duration-200',
isActiveRoute(item.path)
? 'theme-gradient-primary text-white shadow-lg'
: 'theme-text-secondary hover:theme-card-hover hover:theme-text-primary'
]"
>
<DashboardIcon class="w-5 h-5 mr-3" />
仪表盘
</router-link>
<router-link
to="/admin/articles"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-blue-50 hover:text-blue-600 transition-colors"
:class="{ 'bg-blue-50 text-blue-600': $route.path.startsWith('/admin/articles') }"
>
<ArticleIcon class="w-5 h-5 mr-3" />
文章管理
</router-link>
<router-link
to="/admin/news"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-blue-50 hover:text-blue-600 transition-colors"
:class="{ 'bg-blue-50 text-blue-600': $route.path.startsWith('/admin/news') }"
>
<NewsIcon class="w-5 h-5 mr-3" />
新闻管理
</router-link>
<router-link
to="/admin/users"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-blue-50 hover:text-blue-600 transition-colors"
:class="{ 'bg-blue-50 text-blue-600': $route.path === '/admin/users' }"
>
<UsersIcon class="w-5 h-5 mr-3" />
用户管理
</router-link>
<router-link
to="/admin/settings"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-blue-50 hover:text-blue-600 transition-colors"
:class="{ 'bg-blue-50 text-blue-600': $route.path === '/admin/settings' }"
>
<SettingsIcon class="w-5 h-5 mr-3" />
系统设置
</router-link>
</div>
<!-- 底部操作 -->
<div class="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-200">
<button
@click="logout"
class="flex items-center w-full px-4 py-3 text-gray-700 rounded-lg hover:bg-red-50 hover:text-red-600 transition-colors"
>
<LogoutIcon class="w-5 h-5 mr-3" />
退出登录
</button>
</div>
</nav>
</div>
<!-- 主内容区域 -->
<div class="lg:pl-64">
<!-- 顶部导航栏 -->
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="flex items-center justify-between px-4 py-4">
<div class="flex items-center">
<!-- 移动端菜单按钮 -->
<button
@click="toggleSidebar"
class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100"
>
<MenuIcon class="w-6 h-6" />
</button>
<!-- 面包屑导航 -->
<nav class="ml-4 flex" aria-label="Breadcrumb">
<ol class="flex items-center space-x-4">
<li>
<div class="flex items-center">
<router-link to="/admin/dashboard" class="text-gray-400 hover:text-gray-500">
管理后台
</router-link>
</div>
</li>
<li v-if="breadcrumbs.length > 0">
<div class="flex items-center">
<ChevronRightIcon class="flex-shrink-0 h-5 w-5 text-gray-400" />
<span class="ml-4 text-sm font-medium text-gray-500">{{ breadcrumbs[breadcrumbs.length - 1] }}</span>
</div>
</li>
</ol>
</nav>
<component :is="item.icon" class="w-5 h-5 mr-3" />
<span class="font-medium">{{ item.name }}</span>
</div>
<!-- 右侧用户信息 -->
<div class="flex items-center space-x-4">
<!-- 通知 -->
<button class="p-2 text-gray-400 hover:text-gray-500 relative">
<BellIcon class="w-6 h-6" />
<span class="absolute top-0 right-0 block h-2 w-2 rounded-full bg-red-400 ring-2 ring-white"></span>
<!-- 有子菜单的项目 -->
<div v-else>
<button
@click="toggleSubmenu(item.name)"
:class="[
'w-full flex items-center justify-between px-3 py-2 rounded-lg transition-all duration-200',
hasActiveChild(item.children)
? 'theme-gradient-primary text-white shadow-lg'
: 'theme-text-secondary hover:theme-card-hover hover:theme-text-primary'
]"
>
<div class="flex items-center">
<component :is="item.icon" class="w-5 h-5 mr-3" />
<span class="font-medium">{{ item.name }}</span>
</div>
<ChevronDownIcon
:class="[
'w-4 h-4 transition-transform duration-200',
openSubmenus.includes(item.name) ? 'rotate-180' : ''
]"
/>
</button>
<!-- 用户菜单 -->
<div class="relative">
<button
@click="showUserMenu = !showUserMenu"
class="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-100 transition-colors"
<!-- 菜单 -->
<div
v-show="openSubmenus.includes(item.name)"
class="ml-8 mt-2 space-y-1"
>
<div
v-for="child in item.children"
:key="child.name"
@click="navigateTo(child.path)"
:class="[
'flex items-center px-3 py-2 rounded-lg cursor-pointer transition-all duration-200',
isActiveRoute(child.path)
? 'theme-gradient-secondary text-white shadow-md'
: 'theme-text-muted hover:theme-card-hover hover:theme-text-primary'
]"
>
<img
class="w-8 h-8 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=40&h=40&fit=crop&crop=face"
alt="用户头像"
/>
<div class="hidden md:block text-left">
<div class="text-sm font-medium">管理员</div>
<div class="text-xs text-gray-500">超级管理员</div>
</div>
<ChevronDownIcon class="w-4 h-4 text-gray-400" />
</button>
<component :is="child.icon" class="w-4 h-4 mr-3" />
<span class="text-sm">{{ child.name }}</span>
</div>
</div>
</div>
</div>
</nav>
<!-- 用户下拉菜单 -->
<div
v-show="showUserMenu"
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50"
@click.away="showUserMenu = false"
>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">个人资料</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">账户设置</a>
<div class="border-t border-gray-100"></div>
<button
@click="logout"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
<!-- 用户信息 -->
<div class="p-4 border-t theme-border">
<div class="relative">
<button
@click="showUserMenu = !showUserMenu"
class="w-full flex items-center space-x-3 p-3 theme-card hover:theme-card-hover rounded-lg transition-all duration-200"
>
<div class="w-10 h-10 theme-gradient-accent rounded-full flex items-center justify-center">
<UserIcon class="w-5 h-5 text-white" />
</div>
<div class="flex-1 min-w-0 text-left">
<p class="text-sm font-medium theme-text-primary truncate">{{ user.name }}</p>
<p class="text-xs theme-text-muted truncate">{{ user.role }}</p>
</div>
<ChevronDownIcon
:class="['w-4 h-4 theme-text-muted transition-transform duration-200', showUserMenu ? 'rotate-180' : '']"
/>
</button>
<!-- 用户下拉菜单 -->
<div
v-if="showUserMenu"
class="absolute bottom-full left-0 right-0 mb-2 theme-glass border theme-border rounded-lg shadow-xl z-50"
>
<!-- 主题切换 -->
<div class="p-3 border-b theme-border">
<p class="text-xs theme-text-muted mb-2">主题设置</p>
<div class="grid grid-cols-3 gap-2">
<button
v-for="theme in themes"
:key="theme.key"
@click="setTheme(theme.key)"
:class="[
'flex flex-col items-center p-2 rounded-lg transition-all duration-200',
themeStore.currentTheme === theme.key
? 'theme-gradient-primary text-white'
: 'theme-surface hover:theme-surface-hover theme-text-secondary'
]"
>
退出登录
<component :is="theme.icon" class="w-4 h-4 mb-1" />
<span class="text-xs">{{ theme.name }}</span>
</button>
</div>
</div>
<!-- 用户操作 -->
<div class="p-2">
<button
@click="logout"
class="w-full flex items-center px-3 py-2 theme-text-muted hover:theme-text-primary hover:theme-card-hover rounded-lg transition-all duration-200"
>
<LogoutIcon class="w-4 h-4 mr-3" />
<span class="text-sm">退出登录</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 主内容区域 -->
<div :class="['transition-all duration-300', sidebarOpen ? 'lg:ml-64' : '']">
<!-- 顶部导航栏 -->
<header class="theme-glass border-b theme-border sticky top-0 z-40">
<div class="flex items-center justify-between h-16 px-6">
<!-- 左侧 -->
<div class="flex items-center space-x-4">
<button
@click="toggleSidebar"
class="p-2 theme-text-secondary hover:theme-text-primary hover:theme-card-hover rounded-lg transition-all duration-200"
>
<MenuIcon class="w-5 h-5" />
</button>
<!-- 面包屑导航 -->
<nav class="flex items-center space-x-2 theme-text-muted">
<HomeIcon class="w-4 h-4" />
<ChevronRightIcon class="w-4 h-4" />
<span class="theme-text-primary font-medium">{{ currentPageTitle }}</span>
</nav>
</div>
<!-- 右侧 -->
<div class="flex items-center space-x-4">
<!-- 搜索框 -->
<div class="relative hidden md:block">
<SearchIcon class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 theme-text-muted" />
<input
v-model="searchQuery"
type="text"
placeholder="搜索..."
class="pl-10 pr-4 py-2 w-64 theme-glass border theme-border rounded-lg theme-text-primary placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
/>
</div>
<!-- 通知 -->
<button
@click="showNotifications = !showNotifications"
class="relative p-2 theme-text-secondary hover:theme-text-primary hover:theme-card-hover rounded-lg transition-all duration-200"
>
<BellIcon class="w-5 h-5" />
<span
v-if="unreadNotifications > 0"
class="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center"
>
{{ unreadNotifications > 9 ? '9+' : unreadNotifications }}
</span>
</button>
<!-- 全屏切换 -->
<button
@click="toggleFullscreen"
class="p-2 theme-text-secondary hover:theme-text-primary hover:theme-card-hover rounded-lg transition-all duration-200"
title="全屏切换"
>
<ExpandIcon v-if="!isFullscreen" class="w-5 h-5" />
<CompressIcon v-else class="w-5 h-5" />
</button>
</div>
</div>
<!-- 通知下拉菜单 -->
<div
v-if="showNotifications"
class="absolute right-6 top-16 w-80 theme-glass border theme-border rounded-lg shadow-xl z-50"
>
<div class="p-4 border-b theme-border">
<h3 class="font-semibold theme-text-primary">通知</h3>
</div>
<div class="max-h-64 overflow-y-auto">
<div
v-for="notification in notifications"
:key="notification.id"
class="p-4 border-b theme-border hover:theme-surface-hover transition-colors"
>
<div class="flex items-start space-x-3">
<div class="w-2 h-2 bg-blue-500 rounded-full mt-2 flex-shrink-0"></div>
<div class="flex-1 min-w-0">
<p class="text-sm theme-text-primary">{{ notification.title }}</p>
<p class="text-xs theme-text-muted mt-1">{{ notification.time }}</p>
</div>
</div>
</div>
</div>
<div class="p-4 text-center">
<button class="text-sm theme-text-secondary hover:theme-text-primary transition-colors">
查看全部通知
</button>
</div>
</div>
</header>
<!-- 页面内容 -->
<main class="p-6">
<main class="flex-1">
<router-view />
</main>
</div>
<!-- 移动端遮罩 -->
<div
v-show="sidebarOpen"
<div
v-if="sidebarOpen"
@click="closeSidebar"
class="fixed inset-0 z-40 bg-black bg-opacity-50 lg:hidden"
class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
></div>
<!-- 用户菜单遮罩 -->
<div
v-if="showUserMenu"
@click="showUserMenu = false"
class="fixed inset-0 z-30"
></div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { useThemeStore } from '@/stores/theme'
const route = useRoute()
const router = useRouter()
// 响应式数据
const sidebarOpen = ref(false)
const showUserMenu = ref(false)
const route = useRoute()
const userStore = useUserStore()
const themeStore = useThemeStore()
// 图标组件
const DashboardIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6a2 2 0 01-2 2H10a2 2 0 01-2-2V5z"></path></svg>`
const LogoIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-4m-5 0H3m2 0h3M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path></svg>`
}
const ArticleIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>`
}
const NewsIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path></svg>`
}
const UsersIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>`
}
const SettingsIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>`
}
const LogoutIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>`
const XIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>`
}
const MenuIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>`
}
const HomeIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>`
}
const ChevronRightIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>`
}
@@ -213,24 +302,346 @@ const ChevronDownIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>`
}
const BellIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>`
const SearchIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>`
}
// 计算属性
const breadcrumbs = computed(() => {
const BellIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-5 5v-5zM10.07 2.82l3.93 3.93-3.93 3.93-3.93-3.93 3.93-3.93z"></path></svg>`
}
const SunIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>`
}
const MoonIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>`
}
const ComputerIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>`
}
const ExpandIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path></svg>`
}
const CompressIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.5 3.5M15 9h4.5M15 9V4.5M15 9l5.5-5.5M9 15v4.5M9 15H4.5M9 15l-5.5 5.5M15 15h4.5M15 15v4.5m0-4.5l5.5 5.5"></path></svg>`
}
const UserIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>`
}
const LogoutIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>`
}
const DashboardIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>`
}
const DocumentIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>`
}
const NewspaperIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path></svg>`
}
const UsersIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>`
}
const CogIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>`
}
const PlusIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>`
}
const ListIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path></svg>`
}
const EditIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>`
}
const FolderIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>`
}
const TagIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path></svg>`
}
const ShieldIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>`
}
const KeyIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path></svg>`
}
const PhotoIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>`
}
const ChatIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>`
}
const ChartBarIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>`
}
const TrendingUpIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>`
}
const GlobeIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>`
}
const MailIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>`
}
const DatabaseIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>`
}
const ClipboardListIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path></svg>`
}
// 响应式数据
const sidebarOpen = ref(true)
const showNotifications = ref(false)
const showUserMenu = ref(false)
const searchQuery = ref('')
const openSubmenus = ref(['content'])
const isFullscreen = ref(false)
const unreadNotifications = ref(3)
// 用户信息
const user = computed(() => userStore.user || { name: '管理员', role: '超级管理员' })
// 当前页面标题
const currentPageTitle = computed(() => {
const pathMap = {
'/admin/dashboard': '仪表盘',
'/admin': '仪表盘',
'/admin/articles': '文章管理',
'/admin/articles/create': '创建文章',
'/admin/articles/edit': '编辑文章',
'/admin/news': '新闻管理',
'/admin/users': '用户管理',
'/admin/settings': '系统设置'
'/admin/news/create': '创建新闻',
'/admin/categories': '分类管理',
'/admin/tags': '标签管理',
'/admin/users': '用户列表',
'/admin/roles': '角色管理',
'/admin/permissions': '权限管理',
'/admin/media': '媒体管理',
'/admin/comments': '评论管理',
'/admin/analytics/visits': '访问统计',
'/admin/analytics/content': '内容统计',
'/admin/analytics/users': '用户统计',
'/admin/settings': '系统设置',
'/admin/config': '网站配置',
'/admin/mail': '邮件设置',
'/admin/backup': '备份管理',
'/admin/logs': '日志管理'
}
return pathMap[route.path] ? [pathMap[route.path]] : []
// 处理动态路由,如编辑页面
const path = route.path
if (path.startsWith('/admin/articles/edit/')) {
return '编辑文章'
}
if (path.startsWith('/admin/news/edit/')) {
return '编辑新闻'
}
if (path.startsWith('/admin/users/edit/')) {
return '编辑用户'
}
return pathMap[path] || '管理后台'
})
// 菜单项
const menuItems = ref([
{
name: '仪表盘',
path: '/admin',
icon: DashboardIcon
},
{
name: '内容管理',
icon: DocumentIcon,
children: [
{
name: '文章管理',
path: '/admin/articles',
icon: ListIcon
},
{
name: '创建文章',
path: '/admin/articles/create',
icon: PlusIcon
},
{
name: '编辑文章',
path: '/admin/articles/edit',
icon: EditIcon
},
{
name: '新闻管理',
path: '/admin/news',
icon: NewspaperIcon
},
{
name: '创建新闻',
path: '/admin/news/create',
icon: PlusIcon
},
{
name: '分类管理',
path: '/admin/categories',
icon: FolderIcon
},
{
name: '标签管理',
path: '/admin/tags',
icon: TagIcon
}
]
},
{
name: '用户管理',
icon: UsersIcon,
children: [
{
name: '用户列表',
path: '/admin/users',
icon: ListIcon
},
{
name: '角色管理',
path: '/admin/roles',
icon: ShieldIcon
},
{
name: '权限管理',
path: '/admin/permissions',
icon: KeyIcon
}
]
},
{
name: '媒体管理',
path: '/admin/media',
icon: PhotoIcon
},
{
name: '评论管理',
path: '/admin/comments',
icon: ChatIcon
},
{
name: '统计分析',
icon: ChartBarIcon,
children: [
{
name: '访问统计',
path: '/admin/analytics/visits',
icon: TrendingUpIcon
},
{
name: '内容统计',
path: '/admin/analytics/content',
icon: DocumentIcon
},
{
name: '用户统计',
path: '/admin/analytics/users',
icon: UsersIcon
}
]
},
{
name: '系统管理',
icon: CogIcon,
children: [
{
name: '系统设置',
path: '/admin/settings',
icon: CogIcon
},
{
name: '网站配置',
path: '/admin/config',
icon: GlobeIcon
},
{
name: '邮件设置',
path: '/admin/mail',
icon: MailIcon
},
{
name: '备份管理',
path: '/admin/backup',
icon: DatabaseIcon
},
{
name: '日志管理',
path: '/admin/logs',
icon: ClipboardListIcon
}
]
}
])
// 通知数据
const notifications = ref([
{
id: 1,
title: '新用户注册',
time: '5分钟前'
},
{
id: 2,
title: '文章审核通过',
time: '1小时前'
},
{
id: 3,
title: '系统更新完成',
time: '2小时前'
}
])
// 主题选项
const themes = ref([
{
key: 'light',
name: '浅色',
icon: SunIcon
},
{
key: 'dark',
name: '深色',
icon: MoonIcon
},
{
key: 'auto',
name: '自动',
icon: ComputerIcon
}
])
// 方法
const toggleSidebar = () => {
sidebarOpen.value = !sidebarOpen.value
@@ -240,12 +651,113 @@ const closeSidebar = () => {
sidebarOpen.value = false
}
const logout = () => {
// 清除本地存储的用户信息
localStorage.removeItem('token')
localStorage.removeItem('user')
// 跳转到登录页
router.push('/login')
const toggleSubmenu = (menuName) => {
const index = openSubmenus.value.indexOf(menuName)
if (index > -1) {
openSubmenus.value.splice(index, 1)
} else {
openSubmenus.value.push(menuName)
}
}
</script>
const navigateTo = (path) => {
router.push(path)
if (window.innerWidth < 1024) {
closeSidebar()
}
}
const isActiveRoute = (path) => {
if (path === '/admin') {
return route.path === '/admin'
}
return route.path === path || (route.path.startsWith(path + '/') && path !== '/admin')
}
const hasActiveChild = (children) => {
return children.some(child => {
if (child.path === '/admin') {
return route.path === '/admin'
}
return route.path === child.path || (route.path.startsWith(child.path + '/') && child.path !== '/admin')
})
}
const logout = async () => {
if (confirm('确定要退出登录吗?')) {
await userStore.logout()
router.push('/auth/login')
}
}
const setTheme = (theme) => {
themeStore.setTheme(theme)
showUserMenu.value = false
}
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen()
isFullscreen.value = true
} else {
document.exitFullscreen()
isFullscreen.value = false
}
}
// 监听全屏状态变化
const handleFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement
}
// 响应式处理
const handleResize = () => {
if (window.innerWidth >= 1024) {
sidebarOpen.value = true
} else {
sidebarOpen.value = false
}
}
// 生命周期
onMounted(() => {
handleResize()
window.addEventListener('resize', handleResize)
document.addEventListener('fullscreenchange', handleFullscreenChange)
// 初始化主题
themeStore.initTheme()
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
document.removeEventListener('fullscreenchange', handleFullscreenChange)
})
</script>
<style scoped>
/* 确保侧边栏在移动端正确显示 */
@media (max-width: 1023px) {
.lg\:ml-64 {
margin-left: 0 !important;
}
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>

View File

@@ -1,5 +1,10 @@
<template>
<footer class="bg-gray-900 text-white">
<footer :class="[
'transition-colors duration-300',
themeStore.isDark
? 'bg-gray-900 text-white'
: 'bg-gray-100 text-gray-800'
]">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- 公司信息 -->
@@ -10,17 +15,17 @@
</div>
<span class="text-xl font-bold">CMS系统</span>
</div>
<p class="text-gray-400 mb-4 max-w-md">
<p :class="[themeStore.isDark ? 'text-gray-400' : 'text-gray-600', 'mb-4 max-w-md']">
专业的内容管理解决方案为您提供高效安全易用的内容管理体验
</p>
<div class="flex space-x-4">
<a href="#" class="text-gray-400 hover:text-white transition-colors">
<a href="#" :class="[themeStore.isDark ? 'text-gray-400 hover:text-white' : 'text-gray-500 hover:text-gray-900', 'transition-colors']">
<span class="sr-only">微信</span>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 4.882-1.900 7.852.194.002-.063.002-.125.002-.188C17.288 5.476 13.397 2.188 8.691 2.188z"/>
</svg>
</a>
<a href="#" class="text-gray-400 hover:text-white transition-colors">
<a href="#" :class="[themeStore.isDark ? 'text-gray-400 hover:text-white' : 'text-gray-500 hover:text-gray-900', 'transition-colors']">
<span class="sr-only">微博</span>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.194 14.197c0 3.362-3.53 6.086-7.885 6.086-4.354 0-7.885-2.724-7.885-6.086 0-3.362 3.531-6.086 7.885-6.086 4.355 0 7.885 2.724 7.885 6.086z"/>
@@ -31,25 +36,25 @@
<!-- 快速链接 -->
<div>
<h3 class="text-sm font-semibold text-white uppercase tracking-wider mb-4">快速链接</h3>
<h3 class="text-sm font-semibold uppercase tracking-wider mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">快速链接</h3>
<ul class="space-y-2">
<li>
<router-link to="/articles" class="text-gray-400 hover:text-white transition-colors">
<router-link to="/articles" :class="themeStore.isDark ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'" class="transition-colors">
技术文章
</router-link>
</li>
<li>
<router-link to="/news" class="text-gray-400 hover:text-white transition-colors">
<router-link to="/news" :class="themeStore.isDark ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'" class="transition-colors">
新闻资讯
</router-link>
</li>
<li>
<router-link to="/about" class="text-gray-400 hover:text-white transition-colors">
<router-link to="/about" :class="themeStore.isDark ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'" class="transition-colors">
关于我们
</router-link>
</li>
<li>
<router-link to="/contact" class="text-gray-400 hover:text-white transition-colors">
<router-link to="/contact" :class="themeStore.isDark ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'" class="transition-colors">
联系我们
</router-link>
</li>
@@ -58,8 +63,8 @@
<!-- 联系信息 -->
<div>
<h3 class="text-sm font-semibold text-white uppercase tracking-wider mb-4">联系我们</h3>
<ul class="space-y-2 text-gray-400">
<h3 class="text-sm font-semibold uppercase tracking-wider mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">联系我们</h3>
<ul class="space-y-2" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-600'">
<li class="flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
@@ -84,19 +89,19 @@
</div>
<!-- 底部版权信息 -->
<div class="mt-8 pt-8 border-t border-gray-800">
<div class="mt-8 pt-8 border-t" :class="themeStore.isDark ? 'border-gray-800' : 'border-gray-300'">
<div class="flex flex-col md:flex-row justify-between items-center">
<p class="text-gray-400 text-sm">
<p :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-600'" class="text-sm">
© 2024 CMS系统. 保留所有权利.
</p>
<div class="flex space-x-6 mt-4 md:mt-0">
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors">
<a href="#" :class="themeStore.isDark ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'" class="text-sm transition-colors">
隐私政策
</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors">
<a href="#" :class="themeStore.isDark ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'" class="text-sm transition-colors">
服务条款
</a>
<a href="#" class="text-gray-400 hover:text-white text-sm transition-colors">
<a href="#" :class="themeStore.isDark ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'" class="text-sm transition-colors">
网站地图
</a>
</div>
@@ -107,7 +112,9 @@
</template>
<script setup>
// Footer组件逻辑
import { useThemeStore } from '@/stores/theme'
const themeStore = useThemeStore()
</script>
<style scoped>

View File

@@ -1,266 +1,262 @@
<template>
<div>
<!-- 顶部导航栏 -->
<header class="bg-white shadow-sm sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Logo和品牌 -->
<div class="flex items-center">
<router-link to="/" class="flex items-center">
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center mr-3">
<span class="text-white font-bold text-lg">C</span>
</div>
<span class="text-xl font-bold text-gray-900">CMS系统</span>
</router-link>
</div>
<!-- 桌面端导航菜单 -->
<nav class="hidden md:flex space-x-8">
<router-link
to="/"
class="text-gray-700 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors"
:class="{ 'text-blue-600': $route.path === '/' }"
>
首页
</router-link>
<router-link
to="/articles"
class="text-gray-700 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors"
:class="{ 'text-blue-600': $route.path.startsWith('/articles') }"
>
技术文章
</router-link>
<router-link
to="/news"
class="text-gray-700 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors"
:class="{ 'text-blue-600': $route.path.startsWith('/news') }"
>
新闻资讯
</router-link>
<router-link
to="/about"
class="text-gray-700 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors"
:class="{ 'text-blue-600': $route.path === '/about' }"
>
关于我们
</router-link>
<router-link
to="/contact"
class="text-gray-700 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors"
:class="{ 'text-blue-600': $route.path === '/contact' }"
>
联系我们
</router-link>
</nav>
<!-- 右侧操作区 -->
<div class="flex items-center space-x-4">
<!-- 搜索按钮 -->
<button
@click="searchOpen = !searchOpen"
class="text-gray-500 hover:text-gray-700 p-2 rounded-lg hover:bg-gray-100"
>
<SearchIcon class="w-5 h-5" />
</button>
<!-- 管理员入口 -->
<router-link
to="/admin"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
管理后台
</router-link>
<!-- 移动端菜单按钮 -->
<button
@click="mobileMenuOpen = !mobileMenuOpen"
class="md:hidden text-gray-500 hover:text-gray-700 p-2 rounded-lg hover:bg-gray-100"
>
<MenuIcon v-if="!mobileMenuOpen" class="w-6 h-6" />
<XIcon v-else class="w-6 h-6" />
</button>
</div>
</div>
<!-- 搜索栏 -->
<div v-if="searchOpen" class="py-4 border-t border-gray-200">
<div class="relative">
<SearchIcon class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
v-model="searchKeyword"
type="text"
placeholder="搜索文章、新闻..."
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
@keyup.enter="handleSearch"
/>
</div>
</div>
</div>
<!-- 移动端菜单 -->
<div v-if="mobileMenuOpen" class="md:hidden bg-white border-t border-gray-200">
<div class="px-2 pt-2 pb-3 space-y-1">
<header
:class="[
'sticky top-0 z-50 transition-all duration-300 backdrop-blur-md border-b',
themeStore.isDark
? 'bg-gray-900/80 border-gray-700/50'
: 'bg-white/80 border-gray-200/50'
]"
>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Logo -->
<div class="flex items-center">
<router-link
to="/"
class="block px-3 py-2 text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 rounded-md"
@click="mobileMenuOpen = false"
class="flex items-center space-x-2 group"
>
首页
</router-link>
<router-link
to="/articles"
class="block px-3 py-2 text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 rounded-md"
@click="mobileMenuOpen = false"
>
技术文章
</router-link>
<router-link
to="/news"
class="block px-3 py-2 text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 rounded-md"
@click="mobileMenuOpen = false"
>
新闻资讯
</router-link>
<router-link
to="/about"
class="block px-3 py-2 text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 rounded-md"
@click="mobileMenuOpen = false"
>
关于我们
</router-link>
<router-link
to="/contact"
class="block px-3 py-2 text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-gray-50 rounded-md"
@click="mobileMenuOpen = false"
>
联系我们
<div class="w-8 h-8 rounded-lg bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<span class="text-white font-bold text-sm">C</span>
</div>
<span class="text-xl font-bold bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
CMS系统
</span>
</router-link>
</div>
<!-- 桌面端导航 -->
<nav class="hidden md:flex items-center space-x-8">
<router-link
v-for="item in navigation"
:key="item.name"
:to="item.path"
:class="[
'px-3 py-2 rounded-lg text-sm font-medium transition-all duration-300 relative',
isActive(item.path)
? 'text-blue-500'
: themeStore.isDark
? 'text-gray-300 hover:text-white hover:bg-white/10'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
]"
>
{{ item.name }}
<!-- 活跃状态指示器 -->
<div
v-if="isActive(item.path)"
class="absolute -bottom-1 left-1/2 transform -translate-x-1/2 w-1 h-1 bg-blue-500 rounded-full"
></div>
</router-link>
</nav>
<!-- 右侧操作区 -->
<div class="flex items-center space-x-4">
<!-- 主题切换下拉菜单 -->
<div class="relative" ref="themeDropdown">
<button
@click="toggleThemeDropdown"
:class="[
'flex items-center space-x-2 px-3 py-2 rounded-lg transition-all duration-300',
themeStore.isDark
? 'bg-white/10 hover:bg-white/20 text-gray-300 hover:text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-800'
]"
>
<component :is="currentThemeIcon" class="w-4 h-4" />
<span class="text-sm hidden sm:block">{{ currentThemeText }}</span>
<ChevronDown :class="['w-4 h-4 transition-transform duration-200', showThemeDropdown ? 'rotate-180' : '']" />
</button>
<!-- 下拉菜单 -->
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition ease-in duration-150"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div
v-show="showThemeDropdown"
:class="[
'absolute right-0 mt-2 w-48 rounded-xl shadow-lg border backdrop-blur-md z-50',
themeStore.isDark
? 'bg-gray-800/90 border-gray-700/50'
: 'bg-white/90 border-gray-200/50'
]"
>
<div class="py-2">
<button
v-for="option in themeOptions"
:key="option.value"
@click="setTheme(option.value)"
:class="[
'w-full flex items-center space-x-3 px-4 py-2 text-sm transition-colors duration-200',
(option.value === 'light' && !themeStore.isDark && !themeStore.isSystemTheme) ||
(option.value === 'dark' && themeStore.isDark && !themeStore.isSystemTheme) ||
(option.value === 'system' && themeStore.isSystemTheme)
? 'text-blue-500 bg-blue-500/10'
: themeStore.isDark
? 'text-gray-300 hover:text-white hover:bg-white/10'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
]"
>
<component :is="option.icon" class="w-4 h-4" />
<span>{{ option.label }}</span>
<Check v-if="themeStore.theme === option.value" class="w-4 h-4 ml-auto" />
</button>
</div>
</div>
</transition>
</div>
<!-- 移动端菜单按钮 -->
<button
@click="toggleMobileMenu"
:class="[
'md:hidden p-2 rounded-lg transition-all duration-300',
themeStore.isDark
? 'bg-white/10 hover:bg-white/20 text-gray-300 hover:text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-800'
]"
>
<Menu v-if="!showMobileMenu" class="w-5 h-5" />
<X v-else class="w-5 h-5" />
</button>
</div>
</div>
</header>
</div>
<!-- 移动端导航菜单 -->
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="opacity-0 -translate-y-1"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition ease-in duration-150"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-1"
>
<div
v-show="showMobileMenu"
:class="[
'md:hidden mt-4 pb-4 border-t',
themeStore.isDark ? 'border-gray-700/50' : 'border-gray-200/50'
]"
>
<nav class="flex flex-col space-y-2 mt-4">
<router-link
v-for="item in navigation"
:key="item.name"
:to="item.path"
@click="showMobileMenu = false"
:class="[
'px-3 py-2 rounded-lg text-sm font-medium transition-all duration-300',
isActive(item.path)
? 'text-blue-500 bg-blue-500/10'
: themeStore.isDark
? 'text-gray-300 hover:text-white hover:bg-white/10'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
]"
>
{{ item.name }}
</router-link>
</nav>
</div>
</transition>
</div>
</header>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useThemeStore } from '@/stores/theme'
import { Menu, X, Sun, Moon, Monitor, ChevronDown, Check } from 'lucide-vue-next'
const router = useRouter()
// 图标组件
const SearchIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>`
}
const MenuIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>`
}
const XIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>`
}
const route = useRoute()
const themeStore = useThemeStore()
// 响应式数据
const searchOpen = ref(false)
const mobileMenuOpen = ref(false)
const searchKeyword = ref('')
const showMobileMenu = ref(false)
const showThemeDropdown = ref(false)
const themeDropdown = ref(null)
// 处理搜索
const handleSearch = () => {
if (searchKeyword.value.trim()) {
router.push({
path: '/search',
query: { q: searchKeyword.value }
})
searchOpen.value = false
searchKeyword.value = ''
// 导航菜单
const navigation = [
{ name: '首页', path: '/' },
{ name: '技术文章', path: '/articles' },
{ name: '新闻资讯', path: '/news' },
{ name: '关于我们', path: '/about' },
{ name: '联系我们', path: '/contact' },
{ name: '招贤纳士', path: '/careers' }
]
// 主题选项
const themeOptions = [
{ value: 'light', label: '亮色模式', icon: Sun },
{ value: 'dark', label: '暗色模式', icon: Moon },
{ value: 'system', label: '跟随系统', icon: Monitor }
]
// 当前主题图标和文本
const currentThemeIcon = computed(() => {
// 使用themeStore中的isSystemTheme计算属性
if (themeStore.isSystemTheme) {
return Monitor
} else {
return themeStore.isDark ? Moon : Sun
}
})
const currentThemeText = computed(() => {
// 使用themeStore中的isSystemTheme计算属性
if (themeStore.isSystemTheme) {
return '跟随系统'
} else {
return themeStore.isDark ? '暗色模式' : '亮色模式'
}
})
// 判断路由是否激活
const isActive = (path) => {
if (path === '/') {
return route.path === '/'
}
return route.path.startsWith(path)
}
// 方法
const toggleMobileMenu = () => {
showMobileMenu.value = !showMobileMenu.value
}
const toggleThemeDropdown = () => {
showThemeDropdown.value = !showThemeDropdown.value
}
const setTheme = (theme) => {
themeStore.setTheme(theme)
showThemeDropdown.value = false
}
// 点击外部关闭下拉菜单
const handleClickOutside = (event) => {
if (themeDropdown.value && !themeDropdown.value.contains(event.target)) {
showThemeDropdown.value = false
}
}
// 监听路由变化,关闭移动端菜单
import { watch } from 'vue'
import { useRoute } from 'vue-router'
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
const route = useRoute()
watch(() => route.path, () => {
mobileMenuOpen.value = false
searchOpen.value = false
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
/* 导航链接激活状态 */
.router-link-active {
color: #2563eb;
/* 确保玻璃拟态效果 */
.backdrop-blur-md {
backdrop-filter: blur(12px);
}
/* 移动端菜单动画 */
.mobile-menu-enter-active,
.mobile-menu-leave-active {
transition: all 0.3s ease;
}
.mobile-menu-enter-from,
.mobile-menu-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* 搜索栏动画 */
.search-enter-active,
.search-leave-active {
transition: all 0.3s ease;
}
.search-enter-from,
.search-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* 响应式优化 */
@media (max-width: 768px) {
.hidden.md\:flex {
display: none;
}
.md\:hidden {
display: block;
}
}
/* 悬停效果 */
.hover\:bg-gray-100:hover {
background-color: #f3f4f6;
}
.hover\:text-blue-600:hover {
color: #2563eb;
}
.hover\:bg-blue-700:hover {
background-color: #1d4ed8;
}
/* 过渡动画 */
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
/* 粘性定位 */
.sticky {
position: sticky;
}
/* 阴影效果 */
.shadow-sm {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
/* Z-index层级 */
.z-50 {
z-index: 50;
}
</style>
</style>

View File

@@ -1,14 +1,31 @@
<template>
<div class="min-h-screen bg-gray-50">
<div :class="['min-h-screen transition-colors duration-300', themeClasses]">
<!-- 头部导航 -->
<Header />
<main>
<!-- 主要内容区域 -->
<main class="pt-16">
<router-view />
</main>
<!-- 底部 -->
<Footer />
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useThemeStore } from '@/stores/theme'
import Header from './Header.vue'
import Footer from './Footer.vue'
const themeStore = useThemeStore()
const themeClasses = computed(() => {
if (themeStore.isDark) {
return 'bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 text-white'
} else {
return 'bg-gradient-to-br from-gray-50 via-blue-50 to-purple-50 text-gray-900'
}
})
</script>

View File

@@ -1,16 +1,37 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import './style.css'
import App from './App.vue'
// 注册路由
import router from './router'
import { createPinia } from 'pinia'
import AOS from 'aos'
import 'aos/dist/aos.css'
const app = createApp(App)
const pinia = createPinia()
// 注册Ant Design组件
app.use(createPinia())
app.use(pinia)
app.use(router)
app.use(Antd)
app.mount('#app')
// 挂载应用
const mountedApp = app.mount('#app')
// 初始化主题系统
import { useThemeStore } from './stores/theme'
const themeStore = useThemeStore()
themeStore.initTheme()
// 初始化AOS动画
AOS.init({
duration: 800,
easing: 'ease-out-cubic',
once: true,
offset: 100,
delay: 0,
})
// 监听主题变化重新初始化AOS
themeStore.$subscribe(() => {
setTimeout(() => {
AOS.refresh()
}, 100)
})

View File

@@ -43,6 +43,11 @@ const routes = [
path: 'contact',
name: 'Contact',
component: () => import('@/views/contact/index.vue')
},
{
path: 'careers',
name: 'Careers',
component: () => import('@/views/careers/index.vue')
}
]
},

293
src/stores/theme.js Normal file
View File

@@ -0,0 +1,293 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 内存中存储主题设置
let memoryTheme = null
// 安全地检测系统颜色偏好
const getSystemPreference = () => {
try {
if (typeof window === 'undefined' || !window.matchMedia) {
return false
}
return window.matchMedia('(prefers-color-scheme: dark)').matches
} catch (e) {
console.error('无法检测系统颜色偏好:', e)
return false
}
}
// 安全地访问localStorage
const safeLocalStorage = {
get: (key) => {
try {
if (typeof localStorage === 'undefined') {
return null
}
return localStorage.getItem(key)
} catch (e) {
console.error('无法读取localStorage:', e)
return null
}
},
set: (key, value) => {
try {
if (typeof localStorage === 'undefined') {
return false
}
localStorage.setItem(key, value)
return true
} catch (e) {
console.error('无法写入localStorage:', e)
return false
}
},
remove: (key) => {
try {
if (typeof localStorage === 'undefined') {
return false
}
localStorage.removeItem(key)
return true
} catch (e) {
console.error('无法删除localStorage:', e)
return false
}
}
}
export const useThemeStore = defineStore('theme', () => {
// 检查系统偏好
const prefersDark = getSystemPreference()
// 尝试从localStorage读取主题设置
try {
if (memoryTheme === null) {
const storedTheme = safeLocalStorage.get('theme')
if (storedTheme) {
memoryTheme = storedTheme
}
}
} catch (e) {
console.error('初始化主题时出错:', e)
}
// 主题状态 - 优先使用内存中的设置,如果没有则跟随系统
const isDark = ref(
memoryTheme === 'dark' ||
(memoryTheme === null && prefersDark)
)
// 计算属性
const theme = computed(() => isDark.value ? 'dark' : 'light')
// 主题配置
const themeConfig = computed(() => ({
dark: {
// 深色渐变背景 - 更加丰富的色彩过渡
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 20%, #312e81 50%, #4c1d95 70%, #1e1b4b 100%)',
surface: 'rgba(30, 41, 59, 0.8)',
surfaceHover: 'rgba(51, 65, 85, 0.9)',
card: 'rgba(255, 255, 255, 0.05)',
cardHover: 'rgba(255, 255, 255, 0.1)',
border: 'rgba(255, 255, 255, 0.1)',
borderHover: 'rgba(99, 102, 241, 0.4)',
text: {
primary: '#f8fafc',
secondary: '#cbd5e1',
muted: '#94a3b8'
},
glass: {
background: 'rgba(15, 23, 42, 0.7)',
border: 'rgba(255, 255, 255, 0.1)',
blur: '20px'
},
gradient: {
primary: 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)',
secondary: 'linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%)',
accent: 'linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%)',
success: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
warning: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
error: 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)',
info: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
purple: 'linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%)',
pink: 'linear-gradient(135deg, #ec4899 0%, #be185d 100%)',
orange: 'linear-gradient(135deg, #f97316 0%, #c2410c 100%)',
teal: 'linear-gradient(135deg, #14b8a6 0%, #0d9488 100%)'
},
// 强调色
accent: {
blue: 'rgba(59, 130, 246, 0.15)',
purple: 'rgba(139, 92, 246, 0.15)',
pink: 'rgba(236, 72, 153, 0.15)',
green: 'rgba(16, 185, 129, 0.15)',
orange: 'rgba(249, 115, 22, 0.15)',
teal: 'rgba(20, 184, 166, 0.15)'
}
},
light: {
// 亮色渐变背景 - 更加丰富的色彩过渡
background: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 20%, #ddd6fe 50%, #fbcfe8 70%, #e2e8f0 100%)',
surface: 'rgba(248, 250, 252, 0.9)',
surfaceHover: 'rgba(241, 245, 249, 0.95)',
card: 'rgba(255, 255, 255, 0.8)',
cardHover: 'rgba(255, 255, 255, 0.95)',
border: 'rgba(203, 213, 225, 0.3)',
borderHover: 'rgba(99, 102, 241, 0.4)',
text: {
primary: '#1e293b',
secondary: '#475569',
muted: '#64748b'
},
glass: {
background: 'rgba(255, 255, 255, 0.7)',
border: 'rgba(203, 213, 225, 0.3)',
blur: '20px'
},
gradient: {
primary: 'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)',
secondary: 'linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%)',
accent: 'linear-gradient(135deg, #f59e0b 0%, #ef4444 100%)',
success: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
warning: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
error: 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)',
info: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
purple: 'linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%)',
pink: 'linear-gradient(135deg, #ec4899 0%, #be185d 100%)',
orange: 'linear-gradient(135deg, #f97316 0%, #c2410c 100%)',
teal: 'linear-gradient(135deg, #14b8a6 0%, #0d9488 100%)'
},
// 强调色
accent: {
blue: 'rgba(59, 130, 246, 0.15)',
purple: 'rgba(139, 92, 246, 0.15)',
pink: 'rgba(236, 72, 153, 0.15)',
green: 'rgba(16, 185, 129, 0.15)',
orange: 'rgba(249, 115, 22, 0.15)',
teal: 'rgba(20, 184, 166, 0.15)'
}
}
}))
// 当前主题配置
const currentTheme = computed(() => themeConfig.value[theme.value])
// 切换主题
const toggleTheme = () => {
isDark.value = !isDark.value
const newTheme = theme.value
memoryTheme = newTheme
safeLocalStorage.set('theme', newTheme)
updateDocumentTheme()
}
// 设置主题
const setTheme = (newTheme) => {
if (newTheme === 'system') {
// 跟随系统
const prefersDark = getSystemPreference()
isDark.value = prefersDark
memoryTheme = null
safeLocalStorage.remove('theme') // 删除本地存储,表示跟随系统
} else {
// 明确设置为亮色或暗色
isDark.value = newTheme === 'dark'
memoryTheme = newTheme
safeLocalStorage.set('theme', newTheme)
}
updateDocumentTheme()
}
// 更新文档主题
const updateDocumentTheme = () => {
// 确保在浏览器环境中运行
if (typeof document === 'undefined') {
return
}
try {
const root = document.documentElement
const config = currentTheme.value
// 设置CSS变量
root.style.setProperty('--theme-background', config.background)
root.style.setProperty('--theme-surface', config.surface)
root.style.setProperty('--theme-surface-hover', config.surfaceHover)
root.style.setProperty('--theme-card', config.card)
root.style.setProperty('--theme-card-hover', config.cardHover)
root.style.setProperty('--theme-border', config.border)
root.style.setProperty('--theme-border-hover', config.borderHover)
root.style.setProperty('--theme-text-primary', config.text.primary)
root.style.setProperty('--theme-text-secondary', config.text.secondary)
root.style.setProperty('--theme-text-muted', config.text.muted)
root.style.setProperty('--theme-glass-bg', config.glass.background)
root.style.setProperty('--theme-glass-border', config.glass.border)
root.style.setProperty('--theme-glass-blur', config.glass.blur)
root.style.setProperty('--theme-gradient-primary', config.gradient.primary)
root.style.setProperty('--theme-gradient-secondary', config.gradient.secondary)
root.style.setProperty('--theme-gradient-accent', config.gradient.accent)
// 设置主题类
if (isDark.value) {
root.classList.add('dark')
root.classList.remove('light')
} else {
root.classList.add('light')
root.classList.remove('dark')
}
} catch (e) {
console.error('更新文档主题时出错:', e)
}
}
// 初始化主题
const initTheme = () => {
updateDocumentTheme()
// 确保在浏览器环境中运行
if (typeof window === 'undefined') {
return
}
// 监听系统主题变化
try {
if (window.matchMedia) {
const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)')
// 添加主题变化监听器
const handleThemeChange = (e) => {
// 只有当用户没有明确设置主题时才跟随系统
if (memoryTheme === null) {
isDark.value = e.matches
updateDocumentTheme()
}
}
// 添加监听器
if (colorSchemeQuery.addEventListener) {
colorSchemeQuery.addEventListener('change', handleThemeChange)
} else if (colorSchemeQuery.addListener) {
// Safari 和 IE 支持
colorSchemeQuery.addListener(handleThemeChange)
}
}
} catch (e) {
console.error('监听系统主题变化时出错:', e)
}
}
// 检查当前是否为系统主题
const isSystemTheme = computed(() => {
return memoryTheme === null
})
return {
isDark,
theme,
currentTheme,
toggleTheme,
setTheme,
initTheme,
isSystemTheme
}
})

View File

@@ -1,87 +1,83 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import {loginApi, registerApi} from "@/api/request/user.js";
import {message, notification} from "ant-design-vue";
export const useUserStore = defineStore('user', () => {
const user = ref(JSON.parse(localStorage.getItem('user') || 'null'))
const modal = ref(localStorage.getItem('authModal') || 'false')
// 添加初始化状态校验
const initUser = () => {
const userData = localStorage.getItem('user')
if (!userData) return null
try {
return JSON.parse(userData)
} catch {
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: null,
isLoggedIn: false
}),
getters: {
userInfo: (state) => state.user,
isAuthenticated: (state) => state.isLoggedIn && !!state.token,
userRole: (state) => state.user?.role || 'guest'
},
actions: {
// 设置用户信息
setUser(userData) {
this.user = userData
this.token = userData.token
this.isLoggedIn = true
// 保存到localStorage
if (userData.token) {
localStorage.setItem('token', userData.token)
}
localStorage.setItem('user', JSON.stringify(userData))
},
// 登出
logout() {
this.user = null
this.token = null
this.isLoggedIn = false
// 清除localStorage
localStorage.removeItem('user')
localStorage.removeItem('token')
return null
localStorage.removeItem('remember_user')
},
// 从localStorage恢复用户状态
restoreUser() {
try {
const token = localStorage.getItem('token')
const userStr = localStorage.getItem('user')
if (token && userStr) {
const user = JSON.parse(userStr)
this.user = user
this.token = token
this.isLoggedIn = true
return true
}
} catch (error) {
console.error('恢复用户状态失败:', error)
this.logout()
}
return false
},
// 更新用户信息
updateUser(userData) {
if (this.user) {
this.user = { ...this.user, ...userData }
localStorage.setItem('user', JSON.stringify(this.user))
}
},
// 检查是否有权限
hasPermission(permission) {
if (!this.user) return false
// 管理员拥有所有权限
if (this.user.role === 'admin' || this.user.role === 'super_admin') {
return true
}
// 其他权限检查逻辑
return this.user.permissions?.includes(permission) || false
}
}
const isAuthenticated = computed(() => !!user.value?.nick_name)
function login(credentials, functionName) {
console.log(functionName, 'sssssssssssss')
loginApi({
account: credentials.username,
password: credentials.password,
remember: credentials.remember,
}).then((res) => {
const userData = {
nick_name: res.nick_name,
role_name: res.role_name,
role_value: res.role_value,
}
user.value = userData;
localStorage.setItem('user', JSON.stringify(userData))
localStorage.setItem('token', res.token)
notification.success({
message: '登录成功',
description: `欢迎回来${res.nick_name}`,
})
functionName()
})
}
function register(credentials, functionName) {
registerApi({
account: credentials.username,
password: credentials.password,
email: credentials.email,
code: credentials.code,
}).then((res) => {
const userData = {
nick_name: res.nick_name,
role_name: res.role_name,
role_value: res.role_value,
}
user.value = userData;
localStorage.setItem('user', JSON.stringify(userData))
localStorage.setItem('token', res.token)
message.success('注册成功')
functionName()
})
}
function logout() {
user.value = null
localStorage.removeItem('user')
localStorage.removeItem('token')
}
function setModal(value) {
modal.value = modal.value === 'false' ? 'true' : 'false';
localStorage.setItem('authModal', value)
}
return {
modal,
user,
isAuthenticated,
setModal,
login,
register,
logout
}
})

View File

@@ -1 +1,548 @@
@import "tailwindcss";
@import 'tailwindcss';
/* 全局CSS变量 - 将由主题系统动态更新 */
:root {
/* 主题颜色 */
--theme-background: linear-gradient(135deg, #0f172a 0%, #1e293b 25%, #334155 50%, #1e293b 75%, #0f172a 100%);
--theme-surface: rgba(30, 41, 59, 0.8);
--theme-surface-hover: rgba(51, 65, 85, 0.9);
--theme-card: rgba(255, 255, 255, 0.05);
--theme-card-hover: rgba(255, 255, 255, 0.1);
--theme-border: rgba(255, 255, 255, 0.1);
--theme-border-hover: rgba(59, 130, 246, 0.3);
--theme-text-primary: #f8fafc;
--theme-text-secondary: #cbd5e1;
--theme-text-muted: #94a3b8;
--theme-glass-bg: rgba(15, 23, 42, 0.7);
--theme-glass-border: rgba(255, 255, 255, 0.1);
--theme-glass-blur: 20px;
--theme-gradient-primary: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
--theme-gradient-secondary: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
--theme-gradient-accent: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%);
/* 动画变量 */
--animation-duration-fast: 200ms;
--animation-duration-normal: 300ms;
--animation-duration-slow: 500ms;
--animation-easing: cubic-bezier(0.4, 0, 0.2, 1);
--animation-easing-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
/* 基础样式重置 */
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--theme-background);
color: var(--theme-text-primary);
line-height: 1.6;
overflow-x: hidden;
transition: all var(--animation-duration-normal) var(--animation-easing);
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--theme-surface);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--theme-gradient-primary);
border-radius: 4px;
transition: all var(--animation-duration-normal) var(--animation-easing);
}
::-webkit-scrollbar-thumb:hover {
background: var(--theme-gradient-secondary);
}
/* 选择文本样式 */
::selection {
background: rgba(59, 130, 246, 0.3);
color: var(--theme-text-primary);
}
/* 主题工具类 */
.theme-bg {
background: var(--theme-background);
}
.theme-surface {
background: var(--theme-surface);
}
.theme-card {
background: var(--theme-card);
border: 1px solid var(--theme-border);
transition: all var(--animation-duration-normal) var(--animation-easing);
}
.theme-card:hover {
background: var(--theme-card-hover);
border-color: var(--theme-border-hover);
transform: translateY(-2px);
}
.theme-glass {
background: var(--theme-glass-bg);
backdrop-filter: blur(var(--theme-glass-blur));
border: 1px solid var(--theme-glass-border);
}
.theme-text-primary {
color: var(--theme-text-primary);
}
.theme-text-secondary {
color: var(--theme-text-secondary);
}
.theme-text-muted {
color: var(--theme-text-muted);
}
.theme-gradient-primary {
background: var(--theme-gradient-primary);
}
.theme-gradient-secondary {
background: var(--theme-gradient-secondary);
}
.theme-gradient-accent {
background: var(--theme-gradient-accent);
}
/* 按钮组件 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.75rem;
border: none;
cursor: pointer;
transition: all var(--animation-duration-normal) var(--animation-easing);
text-decoration: none;
position: relative;
overflow: hidden;
}
.btn:before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.btn:hover:before {
left: 100%;
}
.btn-primary {
color: white;
background: var(--theme-gradient-primary);
box-shadow: 0 4px 14px 0 rgba(59, 130, 246, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px 0 rgba(59, 130, 246, 0.4);
}
.btn-secondary {
color: var(--theme-text-primary);
background: var(--theme-surface);
border: 1px solid var(--theme-border);
}
.btn-secondary:hover {
background: var(--theme-surface-hover);
border-color: var(--theme-border-hover);
transform: translateY(-2px);
}
.btn-glass {
color: var(--theme-text-primary);
background: var(--theme-glass-bg);
backdrop-filter: blur(var(--theme-glass-blur));
border: 1px solid var(--theme-glass-border);
}
.btn-glass:hover {
background: var(--theme-card-hover);
transform: translateY(-2px);
}
/* 卡片组件 */
.card {
border-radius: 1rem;
padding: 1.5rem;
transition: all var(--animation-duration-normal) var(--animation-easing);
position: relative;
overflow: hidden;
}
.card-glass {
background: var(--theme-glass-bg);
backdrop-filter: blur(var(--theme-glass-blur));
border: 1px solid var(--theme-glass-border);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
/* 输入框组件 */
.input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 0.875rem;
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 0.5rem;
color: var(--theme-text-primary);
transition: all var(--animation-duration-normal) var(--animation-easing);
}
.input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.input::placeholder {
color: var(--theme-text-muted);
}
/* 动画类 */
.fade-enter-active,
.fade-leave-active {
transition: all var(--animation-duration-normal) var(--animation-easing);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: all var(--animation-duration-normal) var(--animation-easing);
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
transform: translateY(20px);
}
.scale-enter-active,
.scale-leave-active {
transition: all var(--animation-duration-normal) var(--animation-easing);
}
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.9);
}
/* 加载动画 */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid var(--theme-border);
border-radius: 50%;
border-top-color: #3b82f6;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 脉冲动画 */
.pulse-glow {
animation: pulse-glow 2s ease-in-out infinite alternate;
}
@keyframes pulse-glow {
0% {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
}
100% {
box-shadow: 0 0 40px rgba(59, 130, 246, 0.6);
}
}
/* 浮动动画 */
.float {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
/* 闪烁动画 */
.twinkle {
animation: twinkle 2s ease-in-out infinite alternate;
}
@keyframes twinkle {
0% { opacity: 0.3; transform: scale(1); }
100% { opacity: 1; transform: scale(1.2); }
}
/* 响应式工具类 */
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 640px) {
.container { max-width: 640px; }
}
@media (min-width: 768px) {
.container { max-width: 768px; }
}
@media (min-width: 1024px) {
.container { max-width: 1024px; }
}
@media (min-width: 1280px) {
.container { max-width: 1280px; }
}
@media (min-width: 1536px) {
.container { max-width: 1536px; }
}
/* 主题切换按钮 */
.theme-toggle {
position: relative;
width: 3rem;
height: 1.5rem;
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 0.75rem;
cursor: pointer;
transition: all var(--animation-duration-normal) var(--animation-easing);
}
.theme-toggle:hover {
background: var(--theme-surface-hover);
}
.theme-toggle-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 1.25rem;
height: 1.25rem;
background: var(--theme-gradient-primary);
border-radius: 50%;
transition: all var(--animation-duration-normal) var(--animation-easing);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
}
.theme-toggle.dark .theme-toggle-thumb {
transform: translateX(1.5rem);
}
/* 错误和成功状态 */
.error-state {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 0.5rem;
padding: 0.75rem;
}
.success-state {
color: #10b981;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: 0.5rem;
padding: 0.75rem;
}
.warning-state {
color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.2);
border-radius: 0.5rem;
padding: 0.75rem;
}
/* 工具提示 */
.tooltip {
position: relative;
}
.tooltip::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--theme-surface);
color: var(--theme-text-primary);
padding: 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity var(--animation-duration-normal) var(--animation-easing);
border: 1px solid var(--theme-border);
z-index: 1000;
}
.tooltip:hover::after {
opacity: 1;
}
/* 响应式文本 */
.text-responsive {
font-size: clamp(1rem, 2.5vw, 1.5rem);
}
.text-responsive-lg {
font-size: clamp(1.5rem, 4vw, 3rem);
}
.text-responsive-xl {
font-size: clamp(2rem, 6vw, 5rem);
}
/* 渐变文本 */
.gradient-text {
background: var(--theme-gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* 页面过渡 */
.page-enter-active,
.page-leave-active {
transition: all var(--animation-duration-slow) var(--animation-easing);
}
.page-enter-from {
opacity: 0;
transform: translateX(30px);
}
.page-leave-to {
opacity: 0;
transform: translateX(-30px);
}
/* 列表过渡 */
.list-enter-active,
.list-leave-active {
transition: all var(--animation-duration-normal) var(--animation-easing);
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(20px);
}
.list-move {
transition: transform var(--animation-duration-normal) var(--animation-easing);
}
/* 亮色主题特定样式 */
.light {
--shadow-color: rgba(0, 0, 0, 0.1);
}
.light .card-glass {
box-shadow: 0 8px 32px var(--shadow-color);
}
.light .btn-primary {
box-shadow: 0 4px 14px 0 rgba(59, 130, 246, 0.25);
}
/* 暗色主题特定样式 */
.dark {
--shadow-color: rgba(0, 0, 0, 0.3);
}
.dark .card-glass {
box-shadow: 0 8px 32px var(--shadow-color);
}
.dark .btn-primary {
box-shadow: 0 4px 14px 0 rgba(59, 130, 246, 0.4);
}
/* 无障碍支持 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 高对比度模式 */
@media (prefers-contrast: high) {
:root {
--theme-border: rgba(255, 255, 255, 0.3);
--theme-text-muted: rgba(255, 255, 255, 0.8);
}
}
/* 打印样式 */
@media print {
body {
background: white !important;
color: black !important;
}
.btn,
.theme-toggle,
.modal-overlay {
display: none !important;
}
}

View File

@@ -1,302 +1,354 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 页面头部 -->
<div class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="text-center">
<h1 class="text-4xl font-bold text-gray-900 mb-4">关于我们</h1>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
我们致力于为客户提供最优质的产品和服务以创新驱动发展以质量赢得信任
</p>
<div class="min-h-screen relative" :class="themeStore.isDark
? 'bg-gradient-to-br from-gray-900 via-gray-800 to-purple-900'
: 'bg-gradient-to-br from-gray-50 via-blue-50 to-purple-50'">
<!-- 装饰元素 - 全局 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<!-- 亮色模式装饰 -->
<template v-if="!themeStore.isDark">
<div class="absolute top-20 left-10 w-72 h-72 bg-blue-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-cyan-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-blue-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-purple-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-cyan-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
<!-- 暗色模式装饰 -->
<template v-else>
<div class="absolute top-20 left-10 w-72 h-72 bg-orange-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-red-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-amber-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-orange-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-red-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-amber-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
</div>
<!-- 页面头部 - 英雄区域 -->
<div class="relative overflow-hidden h-screen flex items-center justify-center">
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center" data-aos="fade-up">
<h1 class="text-5xl md:text-6xl font-bold mb-6">
<span :class="themeStore.isDark
? 'text-orange-400'
: 'text-blue-400'">
关于我们
</span>
</h1>
<p class="text-xl mb-8 max-w-3xl mx-auto" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
致力于打造最前沿的科技资讯平台为用户提供高质量的技术内容和深度洞察
</p>
<div class="flex items-center justify-center space-x-4 mb-12">
<div class="w-2 h-2 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-orange-400' : 'bg-blue-400'"></div>
<div class="w-1 h-1 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-red-400' : 'bg-purple-400'" style="animation-delay: 0.5s;"></div>
<div class="w-1.5 h-1.5 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-amber-300' : 'bg-blue-300'" style="animation-delay: 1s;"></div>
</div>
<!-- 滚动到内容按钮 -->
<button
@click="scrollToContent"
class="animate-bounce mt-8 p-2 rounded-full bg-gradient-to-r shadow-lg transition-all duration-300 hover:shadow-xl"
:class="themeStore.isDark
? 'from-orange-500 to-red-500 hover:shadow-orange-500/30'
: 'from-blue-500 to-purple-500 hover:shadow-blue-500/30'"
>
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</button>
</div>
</div>
<!-- 公司简介 -->
<section class="py-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<h2 class="text-3xl font-bold text-gray-900 mb-6">公司简介</h2>
<div class="space-y-4 text-gray-600">
<p>
我们公司成立于2010年是一家专注于技术创新和产品研发的现代化企业
经过十多年的发展我们已经成为行业内的领军企业之一
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<!-- 公司介绍 -->
<section class="mb-20" data-aos="fade-up">
<div class="backdrop-blur-lg border rounded-3xl p-8 lg:p-12" :class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/60 border-white/20'">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<h2 class="text-3xl font-bold mb-6" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">我们的使命</h2>
<p class="text-lg leading-relaxed mb-6" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
在这个快速发展的数字时代我们致力于成为技术创新与知识分享的桥梁通过提供最新的科技资讯深度的技术分析和实用的开发指南我们帮助开发者和技术爱好者保持在技术前沿
</p>
<p>
公司秉承"创新、品质、服务、共赢"的经营理念始终坚持以客户需求为导向
以技术创新为驱动为客户提供高品质的产品和专业的服务
</p>
<p>
我们拥有一支专业的技术团队和完善的服务体系能够为客户提供从产品设计
开发到售后服务的全方位解决方案
<p class="text-lg leading-relaxed" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
我们相信优质的内容能够启发创新推动技术进步让每个人都能在数字化浪潮中找到自己的位置
</p>
</div>
<div class="mt-8 grid grid-cols-3 gap-6">
<div class="text-center">
<div class="text-3xl font-bold text-blue-600">10+</div>
<div class="text-sm text-gray-500">年发展历程</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-blue-600">500+</div>
<div class="text-sm text-gray-500">合作客户</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-blue-600">100+</div>
<div class="text-sm text-gray-500">专业团队</div>
</div>
<div class="relative">
<img
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80"
alt="团队协作"
class="rounded-2xl shadow-2xl"
/>
<div class="absolute -inset-4 bg-gradient-to-r from-blue-600/20 to-purple-600/20 rounded-3xl blur-xl -z-10"></div>
</div>
</div>
<div>
<img
src="https://images.unsplash.com/photo-1497366216548-37526070297c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80"
alt="公司办公环境"
class="rounded-lg shadow-lg w-full h-96 object-cover"
/>
</div>
</div>
</div>
</section>
</section>
<!-- 企业文化 -->
<section class="py-16 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- 核心价值 -->
<section class="mb-20" data-aos="fade-up" data-aos-delay="200">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-gray-900 mb-4">企业文化</h2>
<p class="text-gray-600 max-w-2xl mx-auto">
我们的企业文化体现在每一个细节中指导着我们的行为和决策
</p>
<h2 class="text-3xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">核心价值</h2>
<p class="text-lg" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">我们坚持的原则和价值观</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div class="text-center">
<div class="bg-blue-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<InnovationIcon class="w-8 h-8 text-blue-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">创新</h3>
<p class="text-gray-600">持续创新引领行业发展</p>
</div>
<div class="text-center">
<div class="bg-green-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<QualityIcon class="w-8 h-8 text-green-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">品质</h3>
<p class="text-gray-600">严格把控追求卓越品质</p>
</div>
<div class="text-center">
<div class="bg-purple-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<ServiceIcon class="w-8 h-8 text-purple-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">服务</h3>
<p class="text-gray-600">用心服务超越客户期望</p>
</div>
<div class="text-center">
<div class="bg-orange-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<TeamIcon class="w-8 h-8 text-orange-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">共赢</h3>
<p class="text-gray-600">合作共赢共创美好未来</p>
</div>
</div>
</div>
</section>
<!-- 团队介绍 -->
<section class="py-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-gray-900 mb-4">核心团队</h2>
<p class="text-gray-600 max-w-2xl mx-auto">
我们拥有一支经验丰富专业素质过硬的核心团队
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div v-for="member in teamMembers" :key="member.id" class="bg-white rounded-lg shadow-lg overflow-hidden">
<img
:src="member.avatar"
:alt="member.name"
class="w-full h-64 object-cover"
/>
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-900 mb-1">{{ member.name }}</h3>
<p class="text-blue-600 mb-3">{{ member.position }}</p>
<p class="text-gray-600 text-sm">{{ member.description }}</p>
<div
v-for="(value, index) in coreValues"
:key="index"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
class="backdrop-blur-lg border rounded-3xl p-8 text-center hover:-translate-y-2 transition-all duration-500 hover:shadow-2xl"
:class="themeStore.isDark
? 'bg-white/5 border-white/10 hover:shadow-orange-500/20'
: 'bg-white/60 border-white/20 hover:shadow-blue-500/20'"
>
<div class="w-16 h-16 bg-gradient-to-r rounded-2xl flex items-center justify-center mx-auto mb-6"
:class="themeStore.isDark ? 'from-orange-500 to-red-500' : 'from-blue-500 to-purple-500'">
<component :is="value.icon" class="w-8 h-8 text-white" />
</div>
<h3 class="text-xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">{{ value.title }}</h3>
<p :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">{{ value.description }}</p>
</div>
</div>
</div>
</section>
</section>
<!-- 发展历程 -->
<section class="py-16 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- 团队介绍 -->
<section class="mb-20" data-aos="fade-up" data-aos-delay="400">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-gray-900 mb-4">发展历程</h2>
<p class="text-gray-600 max-w-2xl mx-auto">
回顾我们的发展历程每一步都见证着我们的成长与进步
</p>
<h2 class="text-3xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">核心团队</h2>
<p class="text-lg" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">经验丰富的专业团队</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div
v-for="(member, index) in teamMembers"
:key="index"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
class="backdrop-blur-lg border rounded-3xl p-8 text-center hover:-translate-y-2 transition-all duration-500 hover:shadow-2xl"
:class="themeStore.isDark
? 'bg-white/5 border-white/10 hover:shadow-orange-500/20'
: 'bg-white/60 border-white/20 hover:shadow-purple-500/20'"
>
<img
:src="member.avatar"
:alt="member.name"
class="w-24 h-24 rounded-full mx-auto mb-6 object-cover"
/>
<h3 class="text-xl font-bold mb-2" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">{{ member.name }}</h3>
<p class="font-medium mb-4" :class="themeStore.isDark ? 'text-orange-400' : 'text-blue-500'">{{ member.position }}</p>
<p class="text-sm" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">{{ member.description }}</p>
</div>
</div>
</section>
<!-- 发展历程 -->
<section class="mb-20" data-aos="fade-up" data-aos-delay="600">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">发展历程</h2>
<p class="text-lg" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">我们的成长足迹</p>
</div>
<div class="relative">
<div class="absolute left-1/2 transform -translate-x-1/2 w-1 h-full bg-blue-200"></div>
<!-- 时间线 -->
<div class="absolute left-1/2 transform -translate-x-1/2 w-1 h-full bg-gradient-to-b rounded-full"
:class="themeStore.isDark ? 'from-orange-500 to-red-500' : 'from-blue-500 to-purple-500'"></div>
<div class="space-y-12">
<div v-for="(milestone, index) in milestones" :key="milestone.id"
:class="['relative flex items-center', index % 2 === 0 ? 'justify-start' : 'justify-end']">
<div :class="['w-5/12', index % 2 === 0 ? 'pr-8' : 'pl-8']">
<div class="bg-white rounded-lg shadow-lg p-6">
<div class="text-blue-600 font-semibold mb-2">{{ milestone.year }}</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ milestone.title }}</h3>
<p class="text-gray-600">{{ milestone.description }}</p>
<div
v-for="(milestone, index) in milestones"
:key="index"
:data-aos="index % 2 === 0 ? 'fade-right' : 'fade-left'"
:class="[
'flex items-center',
index % 2 === 0 ? 'flex-row' : 'flex-row-reverse'
]"
>
<div class="flex-1 px-8">
<div class="backdrop-blur-lg border rounded-3xl p-6"
:class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/60 border-white/20'">
<div class="flex items-center mb-4">
<div class="w-12 h-12 bg-gradient-to-r rounded-2xl flex items-center justify-center mr-4"
:class="themeStore.isDark ? 'from-orange-500 to-red-500' : 'from-blue-500 to-purple-500'">
<span class="text-white font-bold">{{ milestone.year }}</span>
</div>
<h3 class="text-xl font-bold" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">{{ milestone.title }}</h3>
</div>
<p :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">{{ milestone.description }}</p>
</div>
</div>
<div class="absolute left-1/2 transform -translate-x-1/2 w-4 h-4 bg-blue-600 rounded-full border-4 border-white"></div>
<!-- 时间线节点 -->
<div class="w-6 h-6 bg-gradient-to-r rounded-full border-4 z-10"
:class="themeStore.isDark
? 'from-orange-500 to-red-500 border-gray-900'
: 'from-blue-500 to-purple-500 border-white'"></div>
<div class="flex-1 px-8"></div>
</div>
</div>
</div>
</div>
</section>
</section>
<!-- 联系我们 -->
<section class="py-16 bg-blue-600">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-3xl font-bold text-white mb-4">加入我们</h2>
<p class="text-blue-100 mb-8 max-w-2xl mx-auto">
如果您对我们的事业感兴趣欢迎加入我们的团队一起创造美好的未来
</p>
<router-link
to="/contact"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-gray-50 transition-colors"
>
联系我们
<ArrowRightIcon class="ml-2 w-5 h-5" />
</router-link>
</div>
</section>
<!-- 联系我们 -->
<section data-aos="fade-up" data-aos-delay="800">
<div class="bg-gradient-to-r rounded-3xl p-12 text-center text-white"
:class="themeStore.isDark ? 'from-orange-600 to-red-500' : 'from-blue-600 to-purple-500'">
<h2 class="text-3xl font-bold mb-4">加入我们的旅程</h2>
<p class="text-xl mb-8 opacity-90">
如果您对我们的使命感兴趣或者想要与我们合作我们很乐意听到您的声音
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<router-link
to="/contact"
class="px-8 py-4 bg-white font-medium rounded-2xl hover:bg-gray-100 transition-all duration-300 hover:shadow-lg"
:class="themeStore.isDark ? 'text-orange-600' : 'text-blue-600'"
>
联系我们
</router-link>
<a
href="mailto:careers@techcms.com"
class="px-8 py-4 bg-white/20 backdrop-blur-10 border border-white/30 hover:bg-white/30 text-white font-medium rounded-2xl transition-all duration-300"
>
加入团队
</a>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, markRaw } from 'vue'
import { useThemeStore } from '@/stores/theme'
// 图标组件
const InnovationIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path></svg>`
const themeStore = useThemeStore()
// 滚动到内容区域
const scrollToContent = () => {
const contentElement = document.querySelector('.max-w-7xl.mx-auto.px-4')
if (contentElement) {
contentElement.scrollIntoView({ behavior: 'smooth' })
}
}
const QualityIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"></path></svg>`
}
const ServiceIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path></svg>`
}
const TeamIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>`
}
const ArrowRightIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>`
}
// 核心价值数据
const coreValues = ref([
{
title: '创新驱动',
description: '持续探索新技术,推动行业创新发展',
icon: markRaw({
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path></svg>`
})
},
{
title: '质量至上',
description: '严格把控内容质量,为用户提供最有价值的信息',
icon: markRaw({
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"></path></svg>`
})
},
{
title: '开放共享',
description: '促进知识共享,构建开放的技术社区',
icon: markRaw({
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>`
})
},
{
title: '用户为本',
description: '以用户需求为中心,持续优化产品体验',
icon: markRaw({
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path></svg>`
})
},
{
title: '专业精神',
description: '保持专业态度,提供准确可靠的技术内容',
icon: markRaw({
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>`
})
},
{
title: '持续学习',
description: '保持学习热情,与技术发展同步前进',
icon: markRaw({
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>`
})
}
])
// 团队成员数据
const teamMembers = ref([
{
id: 1,
name: '张总',
name: '张伟',
position: '创始人 & CEO',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80',
description: '拥有15年行业经验致力于推动公司技术创新和战略发展。'
description: '10年互联网行业经验专注于产品战略和团队管理',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80'
},
{
id: 2,
name: '李总',
name: '李小明',
position: '技术总监',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80',
description: '资深技术专家,负责公司技术架构设计和团队管理。'
description: '资深全栈工程师,在大型系统架构设计方面有丰富经验',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80'
},
{
id: 3,
name: '王总',
position: '市场总监',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80',
description: '市场营销专家,负责公司品牌建设和市场拓展工作。'
name: '王丽华',
position: '内容总监',
description: '资深技术编辑,擅长将复杂技术概念转化为易懂的内容',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80'
}
])
// 发展历程数据
const milestones = ref([
{
id: 1,
year: '2010',
year: '2020',
title: '公司成立',
description: '公司正式成立,开始专注于技术研发和产品创新。'
description: '在北京成立,专注于科技内容创作和技术社区建设'
},
{
id: 2,
year: '2013',
title: '首个重大项目',
description: '成功完成首个重大项目,获得客户高度认可。'
year: '2021',
title: '平台上线',
description: '正式推出科技资讯平台,开始为用户提供优质技术内容'
},
{
id: 3,
year: '2016',
title: '团队扩张',
description: '团队规模扩大到50人建立完善的研发体系。'
},
{
id: 4,
year: '2019',
title: '技术突破',
description: '在核心技术领域取得重大突破,获得多项专利。'
},
{
id: 5,
year: '2022',
title: '市场领先',
description: '成为行业领军企业服务客户超过500家。'
title: '用户突破',
description: '注册用户突破10万月活跃用户达到5万成为知名技术社区'
},
{
year: '2023',
title: '产品升级',
description: '推出全新的内容管理系统,提升用户体验和内容质量'
},
{
id: 6,
year: '2024',
title: '持续发展',
description: '继续深耕技术创新,为客户提供更优质的服务。'
description: '继续扩大团队规模,探索更多技术领域,服务更多用户'
}
])
</script>
<style scoped>
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.lg\:grid-cols-2 {
grid-template-columns: 1fr;
}
.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-3 {
grid-template-columns: 1fr;
}
/* 时间轴在移动端的优化 */
.absolute.left-1\/2 {
left: 2rem;
}
.justify-start,
.justify-end {
justify-content: flex-start;
}
.w-5\/12 {
width: calc(100% - 4rem);
margin-left: 2rem;
}
.pr-8,
.pl-8 {
padding-left: 0;
padding-right: 0;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.animate-twinkle {
animation: twinkle 2s ease-in-out infinite;
}
</style>

View File

@@ -27,7 +27,7 @@
<!-- 基本信息 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">基本信息</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
@@ -60,7 +60,7 @@
<!-- 文章内容 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">文章内容</h3>
<div class="border border-gray-300 rounded-md">
<!-- 工具栏 -->
<div class="border-b border-gray-200 p-3 flex items-center space-x-2 bg-gray-50">
@@ -75,7 +75,7 @@
<component :is="tool.icon" class="w-4 h-4" />
</button>
</div>
<!-- 编辑器 -->
<textarea
v-model="form.content"
@@ -96,7 +96,7 @@
<!-- 发布设置 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">发布设置</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">状态</label>
@@ -165,7 +165,7 @@
<!-- 特色图片 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">特色图片</h3>
<div class="space-y-4">
<div v-if="form.cover" class="relative">
<img
@@ -181,7 +181,7 @@
×
</button>
</div>
<div v-else class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
<ImageIcon class="mx-auto h-12 w-12 text-gray-400" />
<div class="mt-2">
@@ -203,7 +203,7 @@
<!-- SEO设置 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">SEO设置</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">SEO标题</label>
@@ -247,7 +247,7 @@
>
{{ loading ? '保存中...' : (isEdit ? '更新文章' : '发布文章') }}
</button>
<button
type="button"
@click="saveDraft"
@@ -256,7 +256,7 @@
>
保存草稿
</button>
<button
type="button"
@click="previewArticle"
@@ -337,24 +337,24 @@ const isEdit = computed(() => {
// 表单验证
const validateForm = () => {
errors.value = {}
if (!form.value.title.trim()) {
errors.value.title = '请输入文章标题'
}
if (!form.value.content.trim()) {
errors.value.content = '请输入文章内容'
}
return Object.keys(errors.value).length === 0
}
// 提交表单
const handleSubmit = async () => {
if (!validateForm()) return
loading.value = true
try {
if (isEdit.value) {
// 更新文章
@@ -363,7 +363,7 @@ const handleSubmit = async () => {
// 创建文章
await articlesAPI.create(form.value)
}
// 保存成功后跳转
router.push('/admin/articles')
} catch (error) {
@@ -387,7 +387,7 @@ const previewArticle = () => {
...form.value,
id: 'preview'
}
// 这里可以实现预览功能
console.log('预览文章:', previewData)
alert('预览功能开发中...')
@@ -428,7 +428,7 @@ const applyFormat = (action) => {
const start = textarea.selectionStart
const end = textarea.selectionEnd
const selectedText = textarea.value.substring(start, end)
let formattedText = selectedText
switch (action) {
case 'bold':
@@ -444,7 +444,7 @@ const applyFormat = (action) => {
formattedText = `\n- ${selectedText}`
break
}
const newContent = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end)
form.value.content = newContent
}
@@ -457,7 +457,7 @@ const initData = async () => {
// 获取文章数据
// const response = await articlesAPI.getDetail(route.params.id)
// form.value = response.data
// 模拟数据
form.value = {
title: 'Vue3 开发指南',
@@ -472,7 +472,7 @@ const initData = async () => {
seo_description: '详细介绍Vue3的新特性和开发技巧包含实战案例',
seo_keywords: 'Vue3,前端开发,JavaScript,教程'
}
tagsInput.value = form.value.tags.join(', ')
} catch (error) {
console.error('获取文章数据失败:', error)
@@ -491,7 +491,7 @@ onMounted(() => {
})
</script>
<style scoped>
<style lang='scss' scoped>
/* 编辑器样式 */
.editor-toolbar {
background-color: #f9fafb;
@@ -503,7 +503,7 @@ onMounted(() => {
.grid-cols-1.lg\:grid-cols-3 {
grid-template-columns: 1fr;
}
.lg\:col-span-2 {
grid-column: span 1;
}
@@ -530,4 +530,4 @@ onMounted(() => {
.tag-remove:hover {
color: #1e3a8a;
}
</style>
</style>

View File

@@ -524,7 +524,7 @@ const getPageNumbers = () => {
const pages = []
const total = totalPages.value
const current = currentPage.value
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push(i)
@@ -552,7 +552,7 @@ const getPageNumbers = () => {
pages.push(total)
}
}
return pages
}
@@ -581,7 +581,7 @@ onMounted(() => {
})
</script>
<style scoped>
<style lang='scss' scoped>
/* 文本截断 */
.line-clamp-2 {
display: -webkit-box;
@@ -595,11 +595,11 @@ onMounted(() => {
.grid-cols-1.md\:grid-cols-4 {
grid-template-columns: 1fr;
}
.hidden.sm\:flex-1.sm\:flex.sm\:items-center.sm\:justify-between {
display: none;
}
.flex-1.flex.justify-between.sm\:hidden {
display: flex;
}
@@ -615,4 +615,4 @@ onMounted(() => {
.animate-spin {
animation: spin 1s linear infinite;
}
</style>
</style>

View File

@@ -1,91 +1,126 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-gray-900">仪表板</h1>
<div class="text-sm text-gray-500">
<span>今天是 {{ currentTime }}</span>
<span class="ml-4">欢迎回来</span>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div v-for="stat in stats" :key="stat.title" class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<div :class="['w-8 h-8 rounded-md flex items-center justify-center', stat.iconBg]">
<component :is="stat.icon" class="w-5 h-5 text-white" />
<div class="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 p-6">
<!-- 页面标题 - 科技感设计 -->
<div class="mb-8" data-aos="fade-down">
<div class="bg-glass rounded-3xl p-6 border border-white/10">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gradient mb-2">控制台</h1>
<div class="text-sm text-gray-300">
<span>{{ currentTime }}</span>
<span class="ml-4 text-tech-blue-400">欢迎回来</span>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">{{ stat.title }}</dt>
<dd class="flex items-baseline">
<div class="text-2xl font-semibold text-gray-900">{{ stat.value }}</div>
<div :class="['ml-2 flex items-baseline text-sm font-semibold', stat.changeType === 'increase' ? 'text-green-600' : 'text-red-600']">
<ArrowUpIcon v-if="stat.changeType === 'increase'" class="self-center flex-shrink-0 h-4 w-4" />
<ArrowDownIcon v-else class="self-center flex-shrink-0 h-4 w-4" />
<span class="sr-only">{{ stat.changeType === 'increase' ? '增加' : '减少' }}</span>
{{ stat.change }}
</div>
</dd>
</dl>
<p class="mt-1 text-xs text-gray-500">
<span>{{ stat.changeText }}</span>
<span class="ml-1">较上月</span>
</p>
<div class="hidden md:block">
<div class="w-16 h-16 rounded-full bg-tech-gradient flex items-center justify-center animate-pulse-glow">
<DashboardIcon class="w-8 h-8 text-white" />
</div>
</div>
</div>
</div>
</div>
<!-- 图表和数据 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 访问量趋势 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<!-- 统计卡片 - 科技感设计 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div
v-for="(stat, index) in stats"
:key="stat.title"
class="tech-card group cursor-pointer"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">访问量趋势</h3>
<select class="text-sm border border-gray-300 rounded-md px-3 py-1">
<option>最近7天</option>
<option>最近30天</option>
<option>最近90天</option>
<div :class="['w-12 h-12 rounded-2xl flex items-center justify-center', stat.iconBg, 'group-hover:scale-110 transition-transform duration-300']">
<component :is="stat.icon" class="w-6 h-6 text-white" />
</div>
<div :class="['text-xs px-2 py-1 rounded-full', stat.changeType === 'increase' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400']">
<div class="flex items-center">
<ArrowUpIcon v-if="stat.changeType === 'increase'" class="w-3 h-3 mr-1" />
<ArrowDownIcon v-else class="w-3 h-3 mr-1" />
{{ stat.change }}
</div>
</div>
</div>
<div class="space-y-2">
<h3 class="text-sm font-medium text-gray-400">{{ stat.title }}</h3>
<div class="text-3xl font-bold text-white group-hover:text-tech-blue-300 transition-colors duration-300">
{{ stat.value }}
</div>
<p class="text-xs text-gray-500">
{{ stat.changeText }} 较上月
</p>
</div>
<!-- 悬浮发光效果 -->
<div class="absolute -inset-0.5 bg-gradient-to-r from-tech-blue-600 to-neon-purple-500 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 -z-10 blur-sm"></div>
</div>
</div>
<!-- 图表和数据区域 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- 访问量趋势 - 科技感图表 -->
<div class="tech-card" data-aos="fade-right">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold text-white">访问量趋势</h3>
<select class="bg-white/10 border border-white/20 rounded-xl px-4 py-2 text-white text-sm focus:outline-none focus:ring-2 focus:ring-tech-blue-500">
<option value="7">最近7天</option>
<option value="30">最近30天</option>
<option value="90">最近90天</option>
</select>
</div>
<div class="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
<p class="text-gray-500">图表区域 (可集成Chart.js 或其他图表库)</p>
<!-- 模拟图表区域 -->
<div class="h-64 bg-black/20 rounded-2xl border border-white/10 flex items-center justify-center relative overflow-hidden">
<div class="text-center">
<ChartIcon class="w-16 h-16 text-tech-blue-400 mx-auto mb-4 animate-float" />
<p class="text-gray-400">图表区域</p>
<p class="text-xs text-gray-500 mt-1">可集成 Chart.js ECharts</p>
</div>
<!-- 装饰性动画元素 -->
<div class="absolute top-4 left-4 w-2 h-2 bg-tech-blue-400 rounded-full animate-twinkle"></div>
<div class="absolute bottom-6 right-8 w-1 h-1 bg-neon-purple-400 rounded-full animate-twinkle" style="animation-delay: 1s;"></div>
</div>
</div>
<!-- 最新活动 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">最新活动</h3>
<div class="space-y-4">
<div v-for="activity in activities" :key="activity.id" class="flex items-start space-x-3">
<div :class="['w-2 h-2 rounded-full mt-2', activity.color]"></div>
<!-- 最新活动 - 科技感时间线 -->
<div class="tech-card" data-aos="fade-left">
<h3 class="text-xl font-semibold text-white mb-6">最新活动</h3>
<div class="space-y-4 max-h-64 overflow-y-auto custom-scrollbar">
<div
v-for="(activity, index) in activities"
:key="activity.id"
class="flex items-start space-x-4 p-3 rounded-xl hover:bg-white/5 transition-colors duration-300"
:data-aos="'fade-up'"
:data-aos-delay="index * 50"
>
<div :class="['w-3 h-3 rounded-full mt-2 flex-shrink-0', activity.color, 'animate-pulse']"></div>
<div class="flex-1 min-w-0">
<p class="text-sm text-gray-900">{{ activity.title }}</p>
<p class="text-xs text-gray-500">{{ activity.time }}</p>
<p class="text-sm text-gray-200 leading-relaxed">{{ activity.title }}</p>
<p class="text-xs text-gray-500 mt-1">{{ activity.time }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- 快速操作 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">快速操作</h3>
<!-- 快速操作 - 科技感网格 -->
<div class="tech-card" data-aos="fade-up">
<h3 class="text-xl font-semibold text-white mb-6">快速操作</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<router-link
v-for="action in quickActions"
v-for="(action, index) in quickActions"
:key="action.name"
:to="action.href"
class="flex flex-col items-center p-4 border border-gray-200 rounded-lg hover:border-blue-300 hover:shadow-md transition-all"
class="group flex flex-col items-center p-6 bg-white/5 border border-white/10 rounded-2xl hover:bg-white/10 hover:border-tech-blue-400/50 transition-all duration-300 hover:-translate-y-1"
:data-aos="'zoom-in'"
:data-aos-delay="index * 100"
>
<component :is="action.icon" class="w-8 h-8 text-blue-600 mb-2" />
<span class="text-sm font-medium text-gray-900">{{ action.name }}</span>
<div class="w-12 h-12 rounded-xl bg-tech-gradient flex items-center justify-center mb-3 group-hover:scale-110 transition-transform duration-300">
<component :is="action.icon" class="w-6 h-6 text-white" />
</div>
<span class="text-sm font-medium text-gray-300 group-hover:text-white transition-colors duration-300">{{ action.name }}</span>
</router-link>
</div>
</div>
@@ -96,6 +131,10 @@
import { ref, onMounted } from 'vue'
// 图标组件
const DashboardIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>`
}
const UsersIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path></svg>`
}
@@ -132,6 +171,10 @@ const CogIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>`
}
const ChartIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>`
}
// 响应式数据
const currentTime = ref('')
@@ -144,7 +187,7 @@ const stats = ref([
changeType: 'increase',
changeText: '新增 156 人',
icon: 'UsersIcon',
iconBg: 'bg-blue-500'
iconBg: 'bg-gradient-to-r from-blue-500 to-blue-600'
},
{
title: '文章总数',
@@ -153,7 +196,7 @@ const stats = ref([
changeType: 'increase',
changeText: '新增 32 篇',
icon: 'DocumentTextIcon',
iconBg: 'bg-green-500'
iconBg: 'bg-gradient-to-r from-green-500 to-green-600'
},
{
title: '总浏览量',
@@ -162,7 +205,7 @@ const stats = ref([
changeType: 'increase',
changeText: '增长 5,832',
icon: 'EyeIcon',
iconBg: 'bg-yellow-500'
iconBg: 'bg-gradient-to-r from-yellow-500 to-orange-500'
},
{
title: '评论总数',
@@ -171,7 +214,7 @@ const stats = ref([
changeType: 'decrease',
changeText: '减少 28 条',
icon: 'ChatIcon',
iconBg: 'bg-purple-500'
iconBg: 'bg-gradient-to-r from-purple-500 to-purple-600'
}
])
@@ -181,7 +224,7 @@ const activities = ref([
id: 1,
title: '用户 张三 发表了新文章《Vue3 开发指南》',
time: '2分钟前',
color: 'bg-blue-500'
color: 'bg-tech-blue-500'
},
{
id: 2,
@@ -199,13 +242,19 @@ const activities = ref([
id: 4,
title: '文章《React vs Vue》获得100个赞',
time: '2小时前',
color: 'bg-purple-500'
color: 'bg-neon-purple-500'
},
{
id: 5,
title: '系统备份完成',
time: '3小时前',
color: 'bg-gray-500'
},
{
id: 6,
title: '新闻《AI技术发展趋势》被推荐',
time: '4小时前',
color: 'bg-indigo-500'
}
])
@@ -241,6 +290,9 @@ const updateTime = () => {
month: 'long',
day: 'numeric',
weekday: 'long'
}) + ' ' + now.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
@@ -252,35 +304,79 @@ onMounted(() => {
})
</script>
<style scoped>
<style lang='scss' scoped>
/* 科技感卡片样式 */
.tech-card {
@apply relative rounded-3xl p-6 transition-all duration-500;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.tech-card:hover {
@apply -translate-y-2 shadow-2xl;
box-shadow: 0 20px 40px rgba(37, 99, 235, 0.2);
border-color: rgba(37, 99, 235, 0.3);
}
/* 渐变文字 */
.text-gradient {
background: linear-gradient(135deg, #60a5fa, #c084fc);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* 玻璃拟态背景 */
.bg-glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* 科技渐变背景 */
.bg-tech-gradient {
background: linear-gradient(135deg, #2563EB 0%, #A855F7 100%);
}
/* 自定义滚动条 */
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #2563EB, #A855F7);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #1D4ED8, #9333EA);
}
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
.grid-cols-1.lg\:grid-cols-2 {
grid-template-columns: 1fr;
}
.grid-cols-2.md\:grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
}
/* 卡片悬停效果 */
.hover\:border-blue-300:hover {
border-color: #93c5fd;
}
.hover\:shadow-md:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
/* 过渡动画 */
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
</style>
/* 动画延迟类 */
.animation-delay-100 { animation-delay: 100ms; }
.animation-delay-200 { animation-delay: 200ms; }
.animation-delay-300 { animation-delay: 300ms; }
.animation-delay-500 { animation-delay: 500ms; }
</style>

View File

@@ -480,7 +480,7 @@ const getPageNumbers = () => {
const total = Math.ceil(pagination.value.total / pagination.value.pageSize)
const current = pagination.value.current
const pages = []
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push(i)
@@ -508,7 +508,7 @@ const getPageNumbers = () => {
pages.push(total)
}
}
return pages
}
@@ -517,13 +517,13 @@ onMounted(() => {
})
</script>
<style scoped>
<style lang='scss' scoped>
/* 响应式优化 */
@media (max-width: 768px) {
.overflow-x-auto {
-webkit-overflow-scrolling: touch;
}
table {
min-width: 600px;
}
@@ -536,4 +536,4 @@ onMounted(() => {
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</style>

View File

@@ -342,12 +342,12 @@ const settings = ref({
siteDescription: '这是一个基于Vue3和TailwindCSS构建的现代化内容管理系统提供完整的文章管理、用户管理和系统配置功能。',
contactEmail: 'admin@example.com',
icp: '京ICP备12345678号',
// 安全设置
twoFactorAuth: false,
loginCaptcha: true,
minPasswordLength: '8',
// 邮件设置
smtpHost: '',
smtpPort: 587,
@@ -355,7 +355,7 @@ const settings = ref({
fromName: '系统管理员',
smtpUsername: '',
smtpPassword: '',
// 存储设置
storageType: 'local',
accessKey: '',
@@ -403,7 +403,7 @@ const clearLogs = () => {
}
</script>
<style scoped>
<style lang='scss' scoped>
/* 开关样式 */
.peer:checked ~ .peer-checked\:bg-blue-600 {
background-color: #2563eb;
@@ -414,7 +414,7 @@ const clearLogs = () => {
.grid-cols-1.md\:grid-cols-2 {
grid-template-columns: 1fr;
}
.grid-cols-1.md\:grid-cols-3 {
grid-template-columns: 1fr;
}
@@ -424,4 +424,4 @@ const clearLogs = () => {
nav ul li button {
transition: all 0.2s ease-in-out;
}
</style>
</style>

View File

@@ -477,7 +477,7 @@ const getPageNumbers = () => {
const total = Math.ceil(pagination.value.total / pagination.value.pageSize)
const current = pagination.value.current
const pages = []
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push(i)
@@ -505,7 +505,7 @@ const getPageNumbers = () => {
pages.push(total)
}
}
return pages
}
@@ -514,15 +514,15 @@ onMounted(() => {
})
</script>
<style scoped>
<style lang='scss' scoped>
/* 响应式优化 */
@media (max-width: 768px) {
.overflow-x-auto {
-webkit-overflow-scrolling: touch;
}
table {
min-width: 600px;
}
}
</style>
</style>

View File

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

490
src/views/admin/index.vue Normal file
View File

@@ -0,0 +1,490 @@
<template>
<div class="min-h-screen theme-bg">
<!-- 欢迎区域 -->
<div class="theme-glass border-b theme-border p-6 mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold theme-text-primary mb-2">
欢迎回来{{ user.name }}
</h1>
<p class="theme-text-secondary">今天是 {{ currentDate }}祝您工作愉快</p>
</div>
<div class="flex items-center space-x-4">
<div class="text-right">
<p class="text-sm theme-text-muted">上次登录</p>
<p class="font-medium theme-text-primary">{{ lastLoginTime }}</p>
</div>
<div class="w-12 h-12 theme-gradient-primary rounded-full flex items-center justify-center">
<UserIcon class="w-6 h-6 text-white" />
</div>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 px-6">
<div
v-for="(stat, index) in stats"
:key="index"
class="theme-card card-hover p-6 text-center relative overflow-hidden"
data-aos="fade-up"
:data-aos-delay="index * 100"
>
<!-- 背景装饰 -->
<div class="absolute top-0 right-0 w-20 h-20 opacity-10 transform translate-x-6 -translate-y-6">
<component :is="stat.icon" class="w-full h-full" />
</div>
<div class="relative z-10">
<div :class="['w-12 h-12 rounded-lg flex items-center justify-center mx-auto mb-4', stat.bgClass]">
<component :is="stat.icon" class="h-6 w-6 text-white" />
</div>
<h3 class="text-3xl font-bold theme-text-primary mb-2">{{ stat.value }}</h3>
<p class="theme-text-secondary text-sm">{{ stat.label }}</p>
<div class="mt-3 flex items-center justify-center text-xs">
<component :is="stat.trend === 'up' ? TrendingUpIcon : TrendingDownIcon"
:class="['w-4 h-4 mr-1', stat.trend === 'up' ? 'text-green-500' : 'text-red-500']" />
<span :class="stat.trend === 'up' ? 'text-green-500' : 'text-red-500'">
{{ stat.change }}
</span>
<span class="theme-text-muted ml-1">vs 上月</span>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 px-6">
<!-- 左侧内容 -->
<div class="lg:col-span-2 space-y-8">
<!-- 数据图表 -->
<div class="theme-card p-6" data-aos="fade-up">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold theme-text-primary">访问统计</h3>
<div class="flex items-center space-x-2">
<button
v-for="period in chartPeriods"
:key="period.key"
@click="activeChartPeriod = period.key"
:class="[
'px-3 py-1 rounded-lg text-sm transition-all duration-200',
activeChartPeriod === period.key
? 'theme-gradient-primary text-white'
: 'theme-text-muted hover:theme-text-primary hover:theme-card-hover'
]"
>
{{ period.label }}
</button>
</div>
</div>
<!-- 模拟图表区域 -->
<div class="h-64 theme-surface rounded-lg flex items-center justify-center">
<div class="text-center">
<ChartIcon class="w-16 h-16 theme-text-muted mx-auto mb-4" />
<p class="theme-text-muted">图表数据加载中...</p>
</div>
</div>
</div>
<!-- 最新文章 -->
<div class="theme-card p-6" data-aos="fade-up" data-aos-delay="100">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold theme-text-primary">最新文章</h3>
<router-link
to="/admin/articles"
class="text-sm theme-text-secondary hover:theme-text-primary transition-colors flex items-center"
>
查看全部
<ArrowRightIcon class="w-4 h-4 ml-1" />
</router-link>
</div>
<div class="space-y-4">
<div
v-for="article in recentArticles"
:key="article.id"
class="flex items-center space-x-4 p-4 theme-surface hover:theme-surface-hover rounded-lg transition-colors cursor-pointer"
@click="viewArticle(article.id)"
>
<div class="w-16 h-16 theme-gradient-secondary rounded-lg flex items-center justify-center flex-shrink-0">
<DocumentIcon class="w-8 h-8 text-white" />
</div>
<div class="flex-1 min-w-0">
<h4 class="font-medium theme-text-primary truncate">{{ article.title }}</h4>
<p class="text-sm theme-text-muted mt-1">{{ article.summary }}</p>
<div class="flex items-center mt-2 text-xs theme-text-muted">
<CalendarIcon class="w-4 h-4 mr-1" />
<span>{{ formatDate(article.created_at) }}</span>
<span class="mx-2"></span>
<EyeIcon class="w-4 h-4 mr-1" />
<span>{{ article.views }} 浏览</span>
</div>
</div>
<div class="flex-shrink-0">
<span
:class="[
'px-2 py-1 rounded-full text-xs font-medium',
article.status === 'published' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
]"
>
{{ article.status === 'published' ? '已发布' : '草稿' }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧内容 -->
<div class="space-y-8">
<!-- 快速操作 -->
<div class="theme-card p-6" data-aos="fade-up" data-aos-delay="200">
<h3 class="text-xl font-semibold theme-text-primary mb-6">快速操作</h3>
<div class="space-y-3">
<button
v-for="action in quickActions"
:key="action.name"
@click="handleQuickAction(action.action)"
class="w-full flex items-center p-3 theme-surface hover:theme-surface-hover rounded-lg transition-all duration-200 text-left"
>
<div :class="['w-10 h-10 rounded-lg flex items-center justify-center mr-3', action.bgClass]">
<component :is="action.icon" class="w-5 h-5 text-white" />
</div>
<div>
<p class="font-medium theme-text-primary">{{ action.name }}</p>
<p class="text-sm theme-text-muted">{{ action.description }}</p>
</div>
</button>
</div>
</div>
<!-- 系统状态 -->
<div class="theme-card p-6" data-aos="fade-up" data-aos-delay="300">
<h3 class="text-xl font-semibold theme-text-primary mb-6">系统状态</h3>
<div class="space-y-4">
<div
v-for="status in systemStatus"
:key="status.name"
class="flex items-center justify-between"
>
<div class="flex items-center">
<div :class="['w-3 h-3 rounded-full mr-3', status.color]"></div>
<span class="theme-text-primary">{{ status.name }}</span>
</div>
<span class="text-sm theme-text-muted">{{ status.value }}</span>
</div>
</div>
</div>
<!-- 最新通知 -->
<div class="theme-card p-6" data-aos="fade-up" data-aos-delay="400">
<h3 class="text-xl font-semibold theme-text-primary mb-6">最新通知</h3>
<div class="space-y-4">
<div
v-for="notification in notifications"
:key="notification.id"
class="flex items-start space-x-3 p-3 theme-surface rounded-lg"
>
<div class="w-2 h-2 bg-blue-500 rounded-full mt-2 flex-shrink-0"></div>
<div class="flex-1 min-w-0">
<p class="text-sm theme-text-primary">{{ notification.title }}</p>
<p class="text-xs theme-text-muted mt-1">{{ notification.time }}</p>
</div>
</div>
</div>
<div class="mt-4 text-center">
<button class="text-sm theme-text-secondary hover:theme-text-primary transition-colors">
查看全部通知
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const userStore = useUserStore()
// 图标组件
const UserIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>`
}
const DocumentIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>`
}
const NewspaperIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path></svg>`
}
const UsersIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>`
}
const EyeIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>`
}
const TrendingUpIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>`
}
const TrendingDownIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"></path></svg>`
}
const ChartIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>`
}
const ArrowRightIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>`
}
const CalendarIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>`
}
const PlusIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>`
}
const CogIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>`
}
// 响应式数据
const activeChartPeriod = ref('week')
// 计算属性
const user = computed(() => userStore.user || { name: '管理员' })
const currentDate = computed(() => {
return new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
})
})
const lastLoginTime = computed(() => {
return '2024-01-16 09:30'
})
// 统计数据
const stats = ref([
{
label: '总文章数',
value: '128',
icon: DocumentIcon,
bgClass: 'theme-gradient-primary',
trend: 'up',
change: '+12%'
},
{
label: '总用户数',
value: '1,024',
icon: UsersIcon,
bgClass: 'theme-gradient-secondary',
trend: 'up',
change: '+8%'
},
{
label: '新闻资讯',
value: '86',
icon: NewspaperIcon,
bgClass: 'theme-gradient-accent',
trend: 'up',
change: '+15%'
},
{
label: '总浏览量',
value: '50,000',
icon: EyeIcon,
bgClass: 'bg-gradient-to-r from-green-500 to-emerald-600',
trend: 'up',
change: '+25%'
}
])
// 图表周期选项
const chartPeriods = ref([
{ key: 'week', label: '本周' },
{ key: 'month', label: '本月' },
{ key: 'year', label: '本年' }
])
// 最新文章
const recentArticles = ref([
{
id: 1,
title: 'Vue 3 Composition API 深度解析',
summary: '详细介绍Vue 3中Composition API的使用方法和最佳实践',
status: 'published',
views: 1250,
created_at: '2024-01-15T10:30:00Z'
},
{
id: 2,
title: 'React vs Vue 性能对比分析',
summary: '从多个维度对比React和Vue的性能表现',
status: 'draft',
views: 890,
created_at: '2024-01-14T15:20:00Z'
},
{
id: 3,
title: 'TypeScript 入门教程',
summary: '从零开始学习TypeScript的基础知识',
status: 'published',
views: 756,
created_at: '2024-01-13T09:15:00Z'
}
])
// 快速操作
const quickActions = ref([
{
name: '创建文章',
description: '撰写新的文章内容',
icon: PlusIcon,
bgClass: 'theme-gradient-primary',
action: 'createArticle'
},
{
name: '发布新闻',
description: '发布最新的新闻资讯',
icon: NewspaperIcon,
bgClass: 'theme-gradient-secondary',
action: 'createNews'
},
{
name: '用户管理',
description: '管理系统用户',
icon: UsersIcon,
bgClass: 'theme-gradient-accent',
action: 'manageUsers'
},
{
name: '系统设置',
description: '配置系统参数',
icon: CogIcon,
bgClass: 'bg-gradient-to-r from-gray-500 to-gray-600',
action: 'systemSettings'
}
])
// 系统状态
const systemStatus = ref([
{
name: 'CPU 使用率',
value: '45%',
color: 'bg-green-500'
},
{
name: '内存使用率',
value: '68%',
color: 'bg-yellow-500'
},
{
name: '磁盘使用率',
value: '32%',
color: 'bg-green-500'
},
{
name: '网络状态',
value: '正常',
color: 'bg-green-500'
}
])
// 通知数据
const notifications = ref([
{
id: 1,
title: '新用户注册',
time: '5分钟前'
},
{
id: 2,
title: '文章审核通过',
time: '1小时前'
},
{
id: 3,
title: '系统更新完成',
time: '2小时前'
},
{
id: 4,
title: '数据备份完成',
time: '3小时前'
}
])
// 方法
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
const viewArticle = (id) => {
router.push(`/admin/articles/edit/${id}`)
}
const handleQuickAction = (action) => {
const actions = {
createArticle: () => router.push('/admin/articles/create'),
createNews: () => router.push('/admin/news/create'),
manageUsers: () => router.push('/admin/users'),
systemSettings: () => router.push('/admin/settings')
}
if (actions[action]) {
actions[action]()
}
}
// 生命周期
onMounted(() => {
console.log('管理后台仪表盘已加载')
})
</script>
<style scoped>
/* 确保卡片悬停效果 */
.card-hover:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
.lg\:col-span-2 {
grid-column: span 1;
}
}
@media (max-width: 640px) {
.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-4 {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -1,169 +1,202 @@
<template>
<div class="min-h-screen bg-gray-50">
<div :class="['min-h-screen transition-all duration-500', themeStore.isDark ? 'bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900' : 'bg-gradient-to-br from-gray-50 via-blue-50 to-purple-50']">
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center min-h-screen">
<LoadingIcon class="w-8 h-8 animate-spin text-blue-600" />
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-r from-tech-blue-500 to-neon-purple-500 animate-pulse-glow">
<div class="w-8 h-8 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
</div>
</div>
<!-- 文章不存在 -->
<div v-else-if="!article" class="flex items-center justify-center min-h-screen">
<div class="text-center">
<h2 class="text-2xl font-bold text-gray-900 mb-4">文章不存在</h2>
<p class="text-gray-600 mb-8">抱歉您访问的文章不存在或已被删除</p>
<router-link
to="/articles"
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
<div class="text-center" data-aos="fade-up">
<div class="w-24 h-24 rounded-full bg-gradient-to-r from-tech-blue-600 to-neon-purple-500 flex items-center justify-center mx-auto mb-6 animate-pulse-glow">
<FileText class="w-12 h-12 text-white" />
</div>
<h2 :class="['text-2xl font-bold mb-4', themeStore.isDark ? 'text-white' : 'text-gray-900']">文章不存在</h2>
<p :class="['mb-8', themeStore.isDark ? 'text-gray-400' : 'text-gray-600']">抱歉您访问的文章不存在或已被删除</p>
<button
@click="goBack"
class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-tech-blue-600 to-neon-purple-500 hover:from-tech-blue-500 hover:to-neon-purple-400 text-white font-medium rounded-2xl transition-all duration-300 hover:shadow-lg hover:shadow-tech-blue-500/25 active:scale-95"
>
<ArrowLeft class="w-5 h-5 mr-2" />
返回文章列表
</router-link>
</button>
</div>
</div>
<article v-else class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 面包屑导航 -->
<nav class="flex mb-8" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3">
<li class="inline-flex items-center">
<router-link to="/" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600">
<HomeIcon class="w-4 h-4 mr-2" />
首页
</router-link>
</li>
<li>
<div class="flex items-center">
<ChevronRightIcon class="w-4 h-4 text-gray-400" />
<router-link to="/articles" class="ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2">
文章
</router-link>
</div>
</li>
<li aria-current="page">
<div class="flex items-center">
<ChevronRightIcon class="w-4 h-4 text-gray-400" />
<span class="ml-1 text-sm font-medium text-gray-500 md:ml-2 truncate">
{{ article.title }}
</span>
</div>
</li>
</ol>
</nav>
<!-- 文章内容 -->
<div v-else>
<!-- 文章头部 -->
<header class="bg-white rounded-lg shadow-sm p-8 mb-8">
<div class="mb-4">
<span
:class="[
'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium',
getCategoryClass(article.category)
]"
>
{{ getCategoryName(article.category) }}
</span>
</div>
<div class="relative overflow-hidden">
<!-- 背景装饰 -->
<div :class="['absolute inset-0', themeStore.isDark ? 'bg-gradient-to-r from-tech-blue-600/20 to-neon-purple-600/20' : 'bg-gradient-to-r from-tech-blue-600/10 to-neon-purple-600/10']"></div>
<div class="absolute top-20 left-10 w-72 h-72 bg-tech-blue-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-neon-purple-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<h1 class="text-3xl md:text-4xl font-bold text-gray-900 mb-6 leading-tight">
{{ article.title }}
</h1>
<div class="flex flex-wrap items-center gap-6 text-sm text-gray-500 mb-6">
<div class="flex items-center">
<UserIcon class="w-4 h-4 mr-2" />
<span>{{ article.author || '管理员' }}</span>
<div class="relative z-10 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<!-- 面包屑导航 -->
<nav class="mb-8" data-aos="fade-up">
<ol class="flex items-center space-x-2 text-sm">
<li>
<router-link to="/" :class="['transition-colors', themeStore.isDark ? 'text-gray-400 hover:text-tech-blue-400' : 'text-gray-500 hover:text-tech-blue-500']">
首页
</router-link>
</li>
<li :class="themeStore.isDark ? 'text-gray-500' : 'text-gray-400'">/</li>
<li>
<router-link to="/articles" :class="['transition-colors', themeStore.isDark ? 'text-gray-400 hover:text-tech-blue-400' : 'text-gray-500 hover:text-tech-blue-500']">
文章
</router-link>
</li>
<li :class="themeStore.isDark ? 'text-gray-500' : 'text-gray-400'">/</li>
<li :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">{{ article.title || '文章详情' }}</li>
</ol>
</nav>
<!-- 文章分类 -->
<div class="mb-4" data-aos="fade-up" data-aos-delay="100">
<span
:class="[
'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium',
getCategoryClass(article.category)
]"
>
{{ getCategoryName(article.category) }}
</span>
</div>
<div class="flex items-center">
<CalendarIcon class="w-4 h-4 mr-2" />
<span>{{ formatDate(article.publishedAt) }}</span>
</div>
<div class="flex items-center">
<EyeIcon class="w-4 h-4 mr-2" />
<span>{{ article.views || 0 }} 次浏览</span>
</div>
</div>
<!-- 摘要 -->
<div v-if="article.summary" class="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-r-lg">
<p class="text-blue-800 font-medium">{{ article.summary }}</p>
</div>
</header>
<!-- 文章内容 -->
<div class="bg-white rounded-lg shadow-sm p-8 mb-8">
<!-- 特色图片 -->
<div v-if="article.image" class="mb-8">
<img
:src="article.image"
:alt="article.title"
class="w-full h-64 md:h-96 object-cover rounded-lg shadow-lg"
/>
</div>
<!-- 正文内容 -->
<div class="prose prose-lg max-w-none">
<div v-html="article.content"></div>
</div>
</div>
<!-- 相关文章 -->
<div v-if="relatedArticles.length > 0" class="bg-white rounded-lg shadow-sm p-8">
<h3 class="text-2xl font-bold text-gray-900 mb-6">相关文章</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<article
v-for="item in relatedArticles"
:key="item.id"
class="flex gap-4 p-4 rounded-lg border border-gray-200 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer"
@click="goToArticle(item.id)"
>
<img
:src="item.image || '/placeholder.svg?height=80&width=120'"
:alt="item.title"
class="w-20 h-20 object-cover rounded-lg flex-shrink-0"
/>
<div class="flex-1 min-w-0">
<h4 class="text-lg font-semibold text-gray-900 mb-2 line-clamp-2">
{{ item.title }}
</h4>
<div class="flex items-center text-sm text-gray-500">
<CalendarIcon class="w-4 h-4 mr-1" />
<span>{{ formatDate(item.publishedAt) }}</span>
<!-- 文章标题和元信息 -->
<div class="mb-12" data-aos="fade-up" data-aos-delay="200">
<h1 :class="['text-4xl md:text-5xl font-bold mb-6 leading-tight', themeStore.isDark ? 'text-white' : 'text-gray-900']">
{{ article.title }}
</h1>
<div :class="['flex flex-wrap items-center gap-6 mb-6', themeStore.isDark ? 'text-gray-300' : 'text-gray-600']">
<div class="flex items-center">
<User class="w-5 h-5 mr-2 text-tech-blue-400" />
<span>{{ article.author || '管理员' }}</span>
</div>
<div class="flex items-center">
<Calendar class="w-5 h-5 mr-2 text-tech-blue-400" />
<span>{{ formatDate(article.publishedAt) }}</span>
</div>
<div class="flex items-center">
<Eye class="w-5 h-5 mr-2 text-tech-blue-400" />
<span>{{ article.views || 0 }} 次浏览</span>
</div>
</div>
</article>
<!-- 文章摘要 -->
<div v-if="article.summary" :class="['p-6 border rounded-2xl backdrop-blur-10', themeStore.isDark ? 'bg-tech-blue-500/10 border-tech-blue-500/20' : 'bg-tech-blue-50 border-tech-blue-200']">
<h3 class="text-lg font-semibold text-tech-blue-400 mb-3">文章摘要</h3>
<p :class="['leading-relaxed', themeStore.isDark ? 'text-gray-300' : 'text-gray-700']">{{ article.summary }}</p>
</div>
</div>
<!-- 特色图片 -->
<div v-if="article.image" class="mb-12" data-aos="fade-up" data-aos-delay="400">
<div :class="['relative rounded-2xl overflow-hidden backdrop-blur-20 border p-2', themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/80 border-gray-200/50']">
<img
:src="article.image"
:alt="article.title"
class="w-full h-64 md:h-96 object-cover rounded-xl"
/>
</div>
</div>
</div>
</div>
</article>
<!-- 文章正文 -->
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div :class="['backdrop-blur-20 border rounded-3xl p-8 md:p-12', themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/80 border-gray-200/50 shadow-lg']" data-aos="fade-up">
<div v-if="article.content">
<RichTextRenderer :content="article.content" />
</div>
<div v-else :class="['text-center py-12', themeStore.isDark ? 'text-gray-400' : 'text-gray-500']">
<FileText class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>文章内容加载中...</p>
</div>
<!-- 标签 -->
<div v-if="article.tags && article.tags.length" :class="['mt-12 pt-8 border-t', themeStore.isDark ? 'border-white/10' : 'border-gray-200']">
<h3 :class="['text-lg font-semibold mb-4', themeStore.isDark ? 'text-white' : 'text-gray-900']">相关标签</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="tag in article.tags"
:key="tag"
:class="[
'px-3 py-1 border rounded-full text-sm transition-all duration-300 cursor-pointer',
themeStore.isDark
? 'bg-white/10 hover:bg-tech-blue-500/20 border-white/20 hover:border-tech-blue-500/30 text-gray-300 hover:text-tech-blue-300'
: 'bg-gray-100 hover:bg-tech-blue-50 border-gray-200 hover:border-tech-blue-200 text-gray-600 hover:text-tech-blue-600'
]"
>
{{ tag }}
</span>
</div>
</div>
</div>
<!-- 相关文章推荐 -->
<div v-if="relatedArticles.length > 0" class="mt-16" data-aos="fade-up">
<h2 :class="['text-2xl font-bold mb-8 text-center', themeStore.isDark ? 'text-white' : 'text-gray-900']">相关文章推荐</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<article
v-for="relatedArticle in relatedArticles"
:key="relatedArticle.id"
class="group cursor-pointer"
@click="goToArticle(relatedArticle.id)"
>
<div :class="['backdrop-blur-20 border rounded-2xl p-6 transition-all duration-500 hover:-translate-y-2', themeStore.isDark ? 'bg-white/5 border-white/10 hover:bg-white/10 hover:border-tech-blue-500/30' : 'bg-white/80 border-gray-200/50 hover:bg-white hover:border-tech-blue-500/30 hover:shadow-lg']">
<img
:src="relatedArticle.image || 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80'"
:alt="relatedArticle.title"
class="w-full h-32 object-cover rounded-xl mb-4"
/>
<h3 :class="['text-lg font-semibold group-hover:text-tech-blue-400 transition-colors duration-300 line-clamp-2 mb-2', themeStore.isDark ? 'text-white' : 'text-gray-900']">
{{ relatedArticle.title }}
</h3>
<p :class="['text-sm line-clamp-2 mb-3', themeStore.isDark ? 'text-gray-400' : 'text-gray-600']">
{{ relatedArticle.summary }}
</p>
<div :class="['flex items-center justify-between text-xs', themeStore.isDark ? 'text-gray-500' : 'text-gray-500']">
<span>{{ formatDate(relatedArticle.publishedAt) }}</span>
<div class="flex items-center">
<Eye class="w-3 h-3 mr-1" />
<span>{{ relatedArticle.views || 0 }}</span>
</div>
</div>
</div>
</article>
</div>
</div>
<!-- 返回按钮 -->
<div class="mt-12 text-center" data-aos="fade-up">
<button
@click="goBack"
class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-tech-blue-600 to-neon-purple-500 hover:from-tech-blue-500 hover:to-neon-purple-400 text-white font-medium rounded-2xl transition-all duration-300 hover:shadow-lg hover:shadow-tech-blue-500/25 active:scale-95"
>
<ArrowLeft class="w-5 h-5 mr-2" />
返回文章列表
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { articlesAPI } from '@/api'
import { useThemeStore } from '@/stores/theme'
import { User, Calendar, Eye, FileText, ArrowLeft } from 'lucide-vue-next'
import RichTextRenderer from '@/components/RichTextRenderer.vue'
const route = useRoute()
const router = useRouter()
// 图标组件
const LoadingIcon = {
template: `<svg fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>`
}
const HomeIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>`
}
const ChevronRightIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>`
}
const UserIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>`
}
const CalendarIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>`
}
const EyeIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>`
}
const themeStore = useThemeStore()
// 响应式数据
const loading = ref(true)
@@ -174,46 +207,359 @@ const relatedArticles = ref([])
const mockArticleData = {
1: {
id: 1,
title: '如何构建现代化的内容管理系统',
summary: '本文将详细介绍如何使用Vue3和现代化技术栈构建一个功能完整的内容管理系统。',
title: 'Vue 3 Composition API 深度解析与最佳实践',
summary: '本文将详细介绍Vue 3 Composition API的设计理念、核心特性以及在实际项目中的应用技巧帮助开发者更好地理解和使用这一强大的特性。',
content: `
<p>在当今数字化时代内容管理系统CMS已成为企业和个人管理数字内容的重要工具。本文将详细介绍如何使用Vue3和现代化技术栈构建一个功能完整的内容管理系统。</p>
<h3>技术选型</h3>
<p>我们选择了以下技术栈来构建这个CMS系统</p>
<ul>
<li>前端Vue3 + Vite + TailwindCSS</li>
<li>状态管理Pinia</li>
<li>路由Vue Router</li>
<li>UI组件自定义组件库</li>
</ul>
<h3>系统架构</h3>
<p>系统采用前后端分离的架构设计前端负责用户界面和交互后端提供API接口和数据管理。这种架构具有以下优势</p>
<ul>
<li>开发效率高,前后端可并行开发</li>
<li>维护性好,职责分离清晰</li>
<li>扩展性强,易于横向扩展</li>
<li>用户体验佳支持SPA应用</li>
</ul>
<h3>核心功能</h3>
<p>系统包含以下核心功能模块:</p>
<ul>
<li>用户管理:支持用户注册、登录、权限控制</li>
<li>内容管理:文章、新闻的创建、编辑、发布</li>
<li>媒体管理:图片、文件的上传和管理</li>
<li>系统设置:网站配置、主题设置等</li>
</ul>
<h3>总结</h3>
<p>通过合理的技术选型和架构设计,我们成功构建了一个现代化的内容管理系统。该系统不仅功能完整,而且具有良好的用户体验和可维护性。</p>
# Vue 3 Composition API 深度解析与最佳实践
在当今数字化时代,**Vue 3 Composition API**已成为现代前端开发的重要工具。本文将详细介绍如何使用Vue 3 Composition API构建高质量的前端应用。
> "Composition API是Vue 3最重要的特性之一它为开发者提供了更灵活的代码组织方式。" —— Vue.js 官方团队
## 什么是Composition API
Composition API是Vue 3引入的一套新的API它提供了一种更灵活的方式来组织组件逻辑
* **setup函数**组合式API的入口点
* **响应式系统**ref、reactive、computed等
* **生命周期钩子**onMounted、onUpdated等
* **依赖注入**provide/inject机制
## 核心优势
Composition API相比Options API具有以下优势
1. 更好的逻辑复用和组织
2. 更好的TypeScript支持
3. 更灵活的代码组织方式
4. 更容易进行单元测试
---
## 实际应用示例
以下是一个使用Composition API的实际示例
\`\`\`javascript
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
onMounted(() => {
console.log('组件已挂载')
})
return {
count,
doubleCount,
increment
}
}
}
\`\`\`
### 自定义组合函数示例
创建可复用的逻辑非常简单:
\`\`\`javascript
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
\`\`\`
在组件中使用:
\`\`\`vue
<template>
<div>
<p>计数: {{ count }}</p>
<p>双倍: {{ doubleCount }}</p>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="reset">重置</button>
</div>
</template>
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, doubleCount, increment, decrement, reset } = useCounter(10)
</script>
\`\`\`
## 响应式系统深入
Vue 3的响应式系统基于**Proxy**实现,提供了更好的性能和功能:
### ref vs reactive
\`\`\`javascript
import { ref, reactive } from 'vue'
// ref - 用于基本类型
const count = ref(0)
const message = ref('Hello')
// reactive - 用于对象类型
const state = reactive({
user: {
name: 'John',
age: 30
},
todos: []
})
// 访问ref需要.value
console.log(count.value) // 0
count.value++
// reactive对象可以直接访问
console.log(state.user.name) // 'John'
state.user.age++
\`\`\`
### 计算属性和监听器
\`\`\`javascript
import { ref, computed, watch, watchEffect } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 计算属性
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value
})
// 监听器
watch(firstName, (newValue, oldValue) => {
console.log('名字从 ' + oldValue + ' 改为 ' + newValue)
})
// 立即执行的监听器
watchEffect(() => {
console.log('全名: ' + fullName.value)
})
\`\`\`
## 最佳实践建议
在使用Composition API时建议遵循以下最佳实践
### 1. 代码组织
按功能而不是选项类型组织代码:
\`\`\`javascript
// ❌ 不推荐 - 按选项类型组织
export default {
setup() {
// 所有的ref
const loading = ref(false)
const error = ref(null)
const data = ref([])
// 所有的computed
const hasData = computed(() => data.value.length > 0)
const errorMessage = computed(() => error.value?.message)
// 所有的methods
const fetchData = async () => { /* ... */ }
const handleError = (err) => { /* ... */ }
return { /* ... */ }
}
}
// ✅ 推荐 - 按功能组织
export default {
setup() {
// 数据获取相关
const { data, loading, error, fetchData } = useDataFetching()
// 用户交互相关
const { selectedItems, selectItem, clearSelection } = useSelection()
// 表单处理相关
const { form, validate, submit } = useForm()
return {
data, loading, error, fetchData,
selectedItems, selectItem, clearSelection,
form, validate, submit
}
}
}
\`\`\`
### 2. 类型安全
充分利用TypeScript的类型推导
\`\`\`typescript
import { ref } from 'vue'
interface User {
id: number
name: string
email: string
}
// 明确指定类型
const user = ref(null)
const users = ref([])
// 或者使用泛型
const currentUser = ref({
id: 1,
name: 'John',
email: 'john@example.com'
})
\`\`\`
### 3. 生命周期管理
合理使用生命周期钩子:
\`\`\`javascript
import { onMounted, onUnmounted, onBeforeUnmount } from 'vue'
export default {
setup() {
let timer = null
onMounted(() => {
// 组件挂载后执行
timer = setInterval(() => {
console.log('定时任务')
}, 1000)
})
onBeforeUnmount(() => {
// 组件卸载前清理
if (timer) {
clearInterval(timer)
timer = null
}
})
}
}
\`\`\`
## 性能优化技巧
### 1. 使用shallowRef和shallowReactive
对于大型对象,可以使用浅层响应式:
\`\`\`javascript
import { shallowRef, shallowReactive } from 'vue'
// 只有根级别的属性是响应式的
const state = shallowReactive({
user: { name: 'John' }, // 这个对象内部不是响应式的
count: 0 // 这个是响应式的
})
const largeObject = shallowRef({
// 大型对象,只有整个替换时才触发更新
})
\`\`\`
### 2. 使用readonly
保护数据不被意外修改:
\`\`\`javascript
import { reactive, readonly } from 'vue'
const state = reactive({
count: 0
})
// 创建只读版本
const readonlyState = readonly(state)
// 可以在子组件中传递只读版本
provide('state', readonlyState)
\`\`\`
## 总结
Vue 3 Composition API为我们提供了更强大、更灵活的开发方式。通过合理使用这些特性我们可以构建出更加健壮和可维护的前端应用。
关键要点:
* 按功能组织代码,而不是按选项类型
* 善用自定义组合函数实现逻辑复用
* 充分利用TypeScript的类型系统
* 注意性能优化和内存管理
希望本文能够帮助您更好地掌握这一重要技术。更多信息请参考[Vue.js官方文档](https://vuejs.org/guide/composition-api-introduction.html)。
`,
category: 'technology',
category: 'frontend',
author: '技术团队',
publishedAt: '2024-01-15',
views: 1580,
image: 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80'
views: 2580,
tags: ['Vue3', 'Composition API', '前端开发', '最佳实践'],
image: 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&auto=format&fit=crop&w=1200&q=80'
},
2: {
id: 2,
title: 'Node.js 微服务架构设计与实践',
summary: '深入探讨基于Node.js的微服务架构设计原则和实践经验帮助开发者构建可扩展的后端系统。',
content: `
<p>微服务架构是现代后端开发的重要模式本文将深入探讨如何使用Node.js构建微服务系统。</p>
<h3>微服务架构概述</h3>
<p>微服务架构是一种将单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中。</p>
<h3>核心设计原则</h3>
<ul>
<li><strong>单一职责</strong>:每个服务只负责一个业务功能</li>
<li><strong>去中心化</strong>服务之间通过API通信</li>
<li><strong>容错性</strong>:系统能够处理部分服务的故障</li>
<li><strong>可扩展性</strong>:可以独立扩展各个服务</li>
</ul>
<h3>技术实现</h3>
<p>在Node.js中实现微服务架构需要考虑以下技术栈</p>
<ul>
<li>Express.js或Fastify作为Web框架</li>
<li>Docker进行容器化部署</li>
<li>Redis或RabbitMQ进行消息队列</li>
<li>MongoDB或PostgreSQL作为数据存储</li>
</ul>
`,
category: 'backend',
author: '后端专家',
publishedAt: '2024-01-14',
views: 1890,
tags: ['Node.js', '微服务', '架构设计'],
image: 'https://images.unsplash.com/photo-1627398242454-45a1465c2479?ixlib=rb-4.0.3&auto=format&fit=crop&w=1200&q=80'
}
}
@@ -221,25 +567,40 @@ const mockArticleData = {
const mockRelatedArticles = [
{
id: 2,
title: 'Vue3组合式API最佳实践',
title: 'Node.js 微服务架构设计与实践',
summary: '深入探讨基于Node.js的微服务架构设计原则和实践经验',
publishedAt: '2024-01-14',
image: 'https://images.unsplash.com/photo-1627398242454-45a1465c2479?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2074&q=80'
views: 1890,
image: 'https://images.unsplash.com/photo-1627398242454-45a1465c2479?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80'
},
{
id: 3,
title: 'TailwindCSS实用技巧分享',
title: 'React Native 跨平台开发实战指南',
summary: '从零开始学习React Native开发掌握跨平台移动应用开发的核心技术',
publishedAt: '2024-01-13',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80'
views: 1456,
image: 'https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80'
},
{
id: 4,
title: '深度学习在计算机视觉中的应用',
summary: '探索深度学习技术在图像识别、目标检测等领域的应用',
publishedAt: '2024-01-12',
views: 2134,
image: 'https://images.unsplash.com/photo-1555949963-aa79dcee981c?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80'
}
]
// 获取分类名称
const getCategoryName = (category) => {
const categoryMap = {
technology: '技术分享',
tutorial: '教程指南',
news: '新闻资讯',
product: '产品介绍'
frontend: '前端开发',
backend: '后端开发',
mobile: '移动开发',
ai: '人工智能',
devops: 'DevOps',
database: '数据库',
architecture: '系统架构'
}
return categoryMap[category] || '其他'
}
@@ -247,16 +608,20 @@ const getCategoryName = (category) => {
// 获取分类样式
const getCategoryClass = (category) => {
const classes = {
technology: 'bg-blue-100 text-blue-800',
tutorial: 'bg-green-100 text-green-800',
news: 'bg-purple-100 text-purple-800',
product: 'bg-orange-100 text-orange-800'
frontend: 'bg-tech-blue-500/20 text-tech-blue-400 border border-tech-blue-500/30',
backend: 'bg-green-500/20 text-green-400 border border-green-500/30',
mobile: 'bg-neon-purple-500/20 text-neon-purple-400 border border-neon-purple-500/30',
ai: 'bg-orange-500/20 text-orange-400 border border-orange-500/30',
devops: 'bg-blue-500/20 text-blue-400 border border-blue-500/30',
database: 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30',
architecture: 'bg-red-500/20 text-red-400 border border-red-500/30'
}
return classes[category] || 'bg-gray-100 text-gray-800'
return classes[category] || 'bg-gray-500/20 text-gray-400 border border-gray-500/30'
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return '未知时间'
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
@@ -281,8 +646,8 @@ const fetchArticleDetail = async () => {
// 增加浏览量
article.value.views = (article.value.views || 0) + 1
// 获取相关文章
relatedArticles.value = mockRelatedArticles
// 获取相关文章(排除当前文章)
relatedArticles.value = mockRelatedArticles.filter(item => item.id != articleId)
}
} catch (error) {
@@ -298,13 +663,17 @@ const goToArticle = (id) => {
router.push(`/articles/${id}`)
}
// 返回文章列表
const goBack = () => {
router.push('/articles')
}
// 组件挂载时获取数据
onMounted(() => {
fetchArticleDetail()
})
// 监听路由变化
import { watch } from 'vue'
watch(() => route.params.id, () => {
if (route.params.id) {
fetchArticleDetail()
@@ -321,61 +690,119 @@ watch(() => route.params.id, () => {
overflow: hidden;
}
/* 文章内容样式 */
.prose {
color: #374151;
line-height: 1.75;
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Prose样式优化 */
.prose h3 {
font-size: 1.5rem;
font-size: 1.25rem;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 1rem;
color: #1f2937;
}
.prose p {
margin-bottom: 1.25rem;
line-height: 1.75;
margin-bottom: 1.5rem;
}
.prose ul {
margin: 1.25rem 0;
padding-left: 1.625rem;
.prose ul, .prose ol {
margin-bottom: 1.5rem;
}
.prose li {
margin-bottom: 0.5rem;
}
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.md\:grid-cols-2 {
grid-template-columns: 1fr;
}
.text-3xl.md\:text-4xl {
font-size: 1.875rem;
}
.flex-wrap {
flex-direction: column;
align-items: flex-start;
}
.gap-6 {
gap: 1rem;
}
.prose strong {
font-weight: 600;
}
/* 加载动画 */
@keyframes spin {
to {
transform: rotate(360deg);
}
.prose code {
background-color: rgba(255, 255, 255, 0.1);
color: #a78bfa;
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-size: 0.875em;
}
.animate-spin {
animation: spin 1s linear infinite;
.prose pre {
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
padding: 1rem;
overflow-x: auto;
margin: 1.5rem 0;
}
</style>
.prose pre code {
background: none;
padding: 0;
color: #e5e7eb;
}
.prose blockquote {
border-left: 4px solid #2563EB;
padding-left: 1rem;
margin: 1.5rem 0;
font-style: italic;
}
/* 亮色主题下的prose样式 */
.prose-gray h3 {
color: #1f2937;
}
.prose-gray p {
color: #374151;
}
.prose-gray ul, .prose-gray ol {
color: #374151;
}
.prose-gray strong {
color: #111827;
}
.prose-gray code {
background-color: rgba(0, 0, 0, 0.05);
color: #7c3aed;
}
.prose-gray pre {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
}
.prose-gray pre code {
color: #374151;
}
.prose-gray blockquote {
color: #6b7280;
}
/* 动画效果 */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
@keyframes pulse-glow {
0% { box-shadow: 0 0 20px rgba(37, 99, 235, 0.3); }
100% { box-shadow: 0 0 40px rgba(37, 99, 235, 0.6); }
}
.animate-pulse-glow {
animation: pulse-glow 2s ease-in-out infinite alternate;
}
</style>

View File

@@ -1,30 +1,83 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 页面头部 -->
<div class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="text-center">
<h1 class="text-4xl font-bold text-gray-900 mb-4">技术文章</h1>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
分享最新的技术知识和开发经验
</p>
<div class="min-h-screen relative" :class="themeStore.isDark
? 'bg-gradient-to-br from-gray-900 via-gray-800 to-purple-900'
: 'bg-gradient-to-br from-gray-50 via-blue-50 to-purple-50'">
<!-- 装饰元素 - 全局 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<!-- 亮色模式装饰 -->
<template v-if="!themeStore.isDark">
<div class="absolute top-20 left-10 w-72 h-72 bg-blue-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-cyan-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-blue-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-purple-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-cyan-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
<!-- 暗色模式装饰 -->
<template v-else>
<div class="absolute top-20 left-10 w-72 h-72 bg-orange-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-red-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-amber-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-orange-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-red-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-amber-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
</div>
<!-- 页面头部 - 英雄区域 -->
<div class="relative overflow-hidden h-screen flex items-center justify-center">
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center" data-aos="fade-up">
<h1 class="text-5xl md:text-6xl font-bold mb-6">
<span class="bg-gradient-to-r bg-clip-text text-transparent" :class="themeStore.isDark
? 'from-orange-400 to-red-400'
: 'from-blue-400 to-purple-400'"
>
技术文章
</span>
</h1>
<p class="text-xl mb-8 max-w-3xl mx-auto" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
深度技术分享前沿开发实践与您一起探索技术的无限可能
</p>
<div class="flex items-center justify-center space-x-4 mb-12">
<div class="w-2 h-2 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-orange-400' : 'bg-blue-400'"></div>
<div class="w-1 h-1 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-red-400' : 'bg-purple-400'" style="animation-delay: 0.5s;"></div>
<div class="w-1.5 h-1.5 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-amber-300' : 'bg-blue-300'" style="animation-delay: 1s;"></div>
</div>
<!-- 滚动到内容按钮 -->
<button
@click="scrollToContent"
class="animate-bounce mt-8 p-2 rounded-full bg-gradient-to-r shadow-lg transition-all duration-300 hover:shadow-xl"
:class="themeStore.isDark
? 'from-orange-500 to-red-500 hover:shadow-orange-500/30'
: 'from-blue-500 to-purple-500 hover:shadow-blue-500/30'"
>
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</button>
</div>
</div>
<!-- 文章分类导航 -->
<div class="bg-white border-b border-gray-200">
<div class="border-b border-opacity-20" :class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-black/5 border-black/10'" data-aos="fade-up" data-aos-delay="100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<nav class="flex space-x-8 overflow-x-auto py-4">
<nav class="flex space-x-4 overflow-x-auto py-6">
<button
v-for="category in categories"
:key="category.key"
@click="activeCategory = category.key"
:class="[
'whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors',
'whitespace-nowrap py-3 px-6 rounded-full font-medium text-sm transition-all duration-300',
activeCategory === category.key
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
? themeStore.isDark
? 'bg-gradient-to-r from-orange-500 to-red-500 text-white shadow-lg transform scale-105'
: 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg transform scale-105'
: themeStore.isDark
? 'bg-white/10 text-gray-300 hover:bg-white/20 hover:text-white'
: 'bg-white/60 text-gray-700 hover:bg-white/80 hover:text-gray-900'
]"
>
{{ category.name }}
@@ -34,29 +87,44 @@
</div>
<!-- 搜索和筛选 -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" data-aos="fade-up" data-aos-delay="200">
<div class="rounded-2xl p-6 mb-8 backdrop-blur-lg" :class="themeStore.isDark ? 'bg-white/5 border border-white/10' : 'bg-white/60 border border-white/20'">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<input
v-model="searchKeyword"
type="text"
placeholder="搜索文章标题或内容..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full px-4 py-3 rounded-lg transition-all duration-300 outline-none"
:class="themeStore.isDark
? 'bg-white/10 text-white placeholder-gray-400 border border-white/10 focus:border-orange-500/50'
: 'bg-white/80 text-gray-900 placeholder-gray-500 border border-gray-200 focus:border-blue-500/50'"
@keyup.enter="searchArticles"
/>
</div>
<div class="flex gap-2">
<div class="flex gap-3">
<button
@click="searchArticles"
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition-colors"
class="px-6 py-3 rounded-lg font-medium transition-all duration-300 flex items-center"
:class="themeStore.isDark
? 'bg-gradient-to-r from-orange-500 to-red-500 text-white hover:shadow-lg hover:shadow-orange-500/20'
: 'bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:shadow-lg hover:shadow-blue-500/20'"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
搜索
</button>
<button
@click="resetSearch"
class="bg-gray-500 hover:bg-gray-600 text-white px-6 py-2 rounded-lg transition-colors"
class="px-6 py-3 rounded-lg font-medium transition-all duration-300 flex items-center"
:class="themeStore.isDark
? 'bg-white/10 text-white hover:bg-white/20'
: 'bg-white/60 text-gray-700 hover:bg-white/80'"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
重置
</button>
</div>
@@ -65,31 +133,55 @@
<!-- 文章列表 -->
<div v-if="loading" class="text-center py-12">
<LoadingIcon class="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
<p class="text-gray-600">加载中...</p>
<div class="w-12 h-12 mx-auto mb-4 rounded-full border-4 border-t-transparent animate-spin" :class="themeStore.isDark ? 'border-orange-500' : 'border-blue-500'"></div>
<p :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-600'">加载中...</p>
</div>
<div v-else-if="articlesList.length === 0" class="text-center py-12">
<EmptyState />
<TechEmptyState
title="暂无相关文章"
description="没有找到符合条件的文章,请尝试其他搜索条件"
:show-action="true"
action-text="查看所有文章"
@action="resetSearch"
/>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<article
v-for="article in articlesList"
v-for="(article, index) in articlesList"
:key="article.id"
class="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow cursor-pointer"
class="rounded-xl overflow-hidden cursor-pointer group transition-all duration-300"
:class="themeStore.isDark
? 'bg-white/5 backdrop-blur-lg border border-white/10 hover:bg-white/10 hover:border-orange-500/30 hover:shadow-xl hover:shadow-orange-500/10'
: 'bg-white/60 backdrop-blur-lg border border-white/20 hover:bg-white/80 hover:border-blue-500/30 hover:shadow-xl hover:shadow-blue-500/10'"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
@click="goToDetail(article.id)"
>
<div class="relative">
<div class="relative overflow-hidden mb-4">
<img
:src="article.image || 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80'"
:src="article.cover_image || 'https://images.unsplash.com/photo-1504711434969-e33886168f5c?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80'"
:alt="article.title"
class="w-full h-48 object-cover"
class="w-full h-48 object-cover transition-transform duration-500 group-hover:scale-110"
@error="handleImageError"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<!-- 推荐标签 -->
<div
v-if="article.is_featured"
class="absolute top-4 right-4 px-3 py-1 rounded-full text-xs text-white font-medium bg-gradient-to-r"
:class="themeStore.isDark ? 'from-orange-500 to-red-500' : 'from-blue-500 to-purple-500'"
>
推荐
</div>
<!-- 分类标签 -->
<div class="absolute top-4 left-4">
<span
:class="[
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
'inline-flex items-center px-3 py-1 rounded-full text-xs font-medium',
getCategoryClass(article.category)
]"
>
@@ -98,33 +190,51 @@
</div>
</div>
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-900 mb-3 line-clamp-2">
<div class="p-6 space-y-3">
<h3 class="text-xl font-semibold mb-3 line-clamp-2 transition-all duration-300"
:class="themeStore.isDark
? 'text-white group-hover:text-orange-300'
: 'text-gray-900 group-hover:text-blue-600'">
{{ article.title }}
</h3>
<p class="text-gray-600 mb-4 line-clamp-3">
<p class="mb-4 line-clamp-3" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
{{ article.summary || article.content }}
</p>
<div class="flex items-center justify-between text-sm text-gray-500">
<!-- 标签 -->
<div v-if="article.tags && article.tags.length" class="flex flex-wrap gap-2 mb-4">
<span
v-for="tag in article.tags.slice(0, 3)"
:key="tag"
class="px-2 py-1 rounded-full text-xs"
:class="themeStore.isDark ? 'bg-white/10 text-gray-300' : 'bg-white/40 text-gray-700'"
>
{{ tag }}
</span>
</div>
<div class="flex items-center justify-between text-sm" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<div class="flex items-center">
<UserIcon class="w-4 h-4 mr-2" />
<span>{{ article.author || '管理员' }}</span>
</div>
<div class="flex items-center">
<CalendarIcon class="w-4 h-4 mr-2" />
<span>{{ formatDate(article.publishedAt) }}</span>
<span>{{ formatDate(article.published_at) }}</span>
</div>
</div>
<div class="flex items-center justify-between mt-4">
<div class="flex items-center text-sm text-gray-500">
<div class="flex items-center justify-between mt-4 pt-4 border-t" :class="themeStore.isDark ? 'border-white/10' : 'border-gray-200'">
<div class="flex items-center text-sm" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<EyeIcon class="w-4 h-4 mr-1" />
<span>{{ article.views || 0 }} 次浏览</span>
<span>{{ formatNumber(article.view_count) }} 次浏览</span>
</div>
<button
@click.stop="goToDetail(article.id)"
class="text-blue-600 hover:text-blue-800 font-medium text-sm"
class="text-sm font-medium hover:scale-105 transition-transform duration-200"
:class="themeStore.isDark
? 'text-orange-400 hover:text-red-400'
: 'text-blue-400 hover:text-purple-400'"
>
阅读更多
</button>
@@ -134,36 +244,54 @@
</div>
<!-- 分页 -->
<div v-if="articlesList.length > 0" class="mt-12 flex justify-center">
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<div v-if="articlesList.length > 0" class="mt-12 flex justify-center" data-aos="fade-up">
<nav class="flex items-center space-x-2">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage <= 1"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
class="px-4 py-2 rounded-lg font-medium transition-all duration-300 flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
:class="themeStore.isDark
? 'bg-white/10 text-white hover:bg-white/20'
: 'bg-white/60 text-gray-700 hover:bg-white/80'"
>
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
上一页
</button>
<button
v-for="page in getPageNumbers()"
:key="page"
@click="changePage(page)"
:class="[
'relative inline-flex items-center px-4 py-2 border text-sm font-medium',
page === currentPage
? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
]"
>
{{ page }}
</button>
<div class="flex space-x-1">
<button
v-for="page in getPageNumbers()"
:key="page"
@click="changePage(page)"
:class="[
'px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',
page === currentPage
? themeStore.isDark
? 'bg-gradient-to-r from-orange-500 to-red-500 text-white shadow-lg'
: 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: themeStore.isDark
? 'bg-white/10 text-gray-300 hover:bg-white/20 hover:text-white'
: 'bg-white/60 text-gray-700 hover:bg-white/80 hover:text-gray-900'
]"
>
{{ page }}
</button>
</div>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage >= totalPages"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
class="px-4 py-2 rounded-lg font-medium transition-all duration-300 flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
:class="themeStore.isDark
? 'bg-white/10 text-white hover:bg-white/20'
: 'bg-white/60 text-gray-700 hover:bg-white/80'"
>
下一页
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</nav>
</div>
@@ -172,18 +300,15 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { articlesAPI } from '@/api'
import EmptyState from '@/components/EmptyState.vue'
import { useThemeStore } from '@/stores/theme'
import TechEmptyState from '@/components/TechEmptyState.vue'
const themeStore = useThemeStore()
const router = useRouter()
// 图标组件
const LoadingIcon = {
template: `<svg fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>`
}
const UserIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>`
}
@@ -208,10 +333,12 @@ const total = ref(0)
// 文章分类
const categories = ref([
{ key: 'all', name: '全部' },
{ key: 'technology', name: '技术分享' },
{ key: 'tutorial', name: '教程指南' },
{ key: 'news', name: '新闻资讯' },
{ key: 'product', name: '产品介绍' }
{ key: 'frontend', name: '前端开发' },
{ key: 'backend', name: '后端开发' },
{ key: 'mobile', name: '移动开发' },
{ key: 'ai', name: '人工智能' },
{ key: 'devops', name: 'DevOps' },
{ key: 'design', name: '设计' }
])
// 计算属性
@@ -221,63 +348,81 @@ const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
const mockArticles = [
{
id: 1,
title: '如何构建现代化的内容管理系统',
summary: '本文将详细介绍如何使用Vue3和现代化技术栈构建一个功能完整的内容管理系统。',
category: 'technology',
author: '技术团队',
publishedAt: '2024-01-15',
views: 1580,
image: 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80'
title: 'Vue 3.4 正式发布:性能提升与新特性详解',
summary: 'Vue 3.4 带来了显著的性能提升和多项新特性包括更好的TypeScript支持、优化的响应式系统等。本文将详细介绍这些新特性的使用方法和最佳实践。',
content: 'Vue 3.4 正式发布了,这个版本带来了许多令人兴奋的新特性和性能改进...',
category: 'frontend',
author: '张三',
published_at: Date.now() / 1000,
view_count: 1250,
is_featured: true,
tags: ['Vue.js', '前端', '框架'],
cover_image: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80'
},
{
id: 2,
title: 'Vue3组合式API最佳实践',
summary: '深入探讨Vue3组合式API的使用技巧和最佳实践帮助开发者更好地掌握这一新特性。',
category: 'tutorial',
author: '前端专家',
publishedAt: '2024-01-14',
views: 1234,
image: 'https://images.unsplash.com/photo-1627398242454-45a1465c2479?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2074&q=80'
title: 'Node.js 微服务架构最佳实践',
summary: '深入探讨Node.js微服务架构的设计原则、技术选型和实施策略帮助开发者构建可扩展的后端系统。',
content: '微服务架构已经成为现代应用开发的主流模式...',
category: 'backend',
author: '李四',
published_at: Date.now() / 1000 - 86400,
view_count: 890,
is_featured: true,
tags: ['Node.js', '微服务', '架构'],
cover_image: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80'
},
{
id: 3,
title: 'TailwindCSS实用技巧分享',
summary: '分享一些TailwindCSS的实用技巧和高级用法让你的样式开发更加高效。',
category: 'tutorial',
author: 'UI设计师',
publishedAt: '2024-01-13',
views: 987,
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80'
title: 'React Native 跨平台开发实战指南',
summary: '从零开始学习React Native开发掌握跨平台移动应用开发的核心技术和实践经验。',
content: 'React Native作为跨平台移动开发的热门选择...',
category: 'mobile',
author: '王五',
published_at: Date.now() / 1000 - 172800,
view_count: 654,
is_featured: false,
tags: ['React Native', '移动开发', '跨平台'],
cover_image: 'https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80'
},
{
id: 4,
title: '前端性能优化实战指南',
summary: '从多个维度分析前端性能优化的方法和技巧,提升用户体验和页面加载速度。',
category: 'technology',
author: '性能专家',
publishedAt: '2024-01-12',
views: 1456,
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2015&q=80'
title: '深度学习在前端的应用与实践',
summary: '探索人工智能技术在前端开发中的应用场景,包括图像识别、自然语言处理等前沿技术。',
content: '随着AI技术的快速发展前端开发也迎来了新的机遇...',
category: 'ai',
author: '赵六',
published_at: Date.now() / 1000 - 259200,
view_count: 432,
is_featured: false,
tags: ['AI', '深度学习', '前端'],
cover_image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80'
},
{
id: 5,
title: '微服务架构设计原则',
summary: '探讨微服务架构的设计原则和实施策略,帮助团队构建可扩展的系统架构。',
category: 'technology',
author: '架构师',
publishedAt: '2024-01-11',
views: 2103,
image: 'https://images.unsplash.com/photo-1518186285589-2f7649de83e0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2074&q=80'
title: 'Docker容器化部署完全指南',
summary: '全面介绍Docker容器技术的原理、使用方法和最佳实践助力开发者实现高效的应用部署。',
content: 'Docker已经成为现代应用部署的标准工具...',
category: 'devops',
author: '孙七',
published_at: Date.now() / 1000 - 345600,
view_count: 789,
is_featured: true,
tags: ['Docker', 'DevOps', '容器'],
cover_image: 'https://images.unsplash.com/photo-1605745341112-85968b19335b?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80'
},
{
id: 6,
title: 'React vs Vue框架选择指南',
summary: '客观比较React和Vue两大前端框架的优缺点为项目选择提供参考依据。',
category: 'news',
author: '技术评论员',
publishedAt: '2024-01-10',
views: 1789,
image: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80'
title: '现代UI设计系统构建指南',
summary: '学习如何构建一套完整的设计系统,提升团队协作效率和产品一致性。',
content: '设计系统是现代产品开发的重要组成部分...',
category: 'design',
author: '周八',
published_at: Date.now() / 1000 - 432000,
view_count: 567,
is_featured: false,
tags: ['设计系统', 'UI', 'UX'],
cover_image: 'https://images.unsplash.com/photo-1561070791-2526d30994b5?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80'
}
]
@@ -289,18 +434,33 @@ const getCategoryName = (category) => {
// 获取分类样式
const getCategoryClass = (category) => {
const classes = {
technology: 'bg-blue-100 text-blue-800',
tutorial: 'bg-green-100 text-green-800',
news: 'bg-purple-100 text-purple-800',
product: 'bg-orange-100 text-orange-800'
if (themeStore.isDark) {
const classes = {
frontend: 'bg-blue-500/20 text-blue-300 border border-blue-500/30',
backend: 'bg-green-500/20 text-green-300 border border-green-500/30',
mobile: 'bg-purple-500/20 text-purple-300 border border-purple-500/30',
ai: 'bg-red-500/20 text-red-300 border border-red-500/30',
devops: 'bg-orange-500/20 text-orange-300 border border-orange-500/30',
design: 'bg-pink-500/20 text-pink-300 border border-pink-500/30'
}
return classes[category] || 'bg-gray-500/20 text-gray-300 border border-gray-500/30'
} else {
const classes = {
frontend: 'bg-blue-500/10 text-blue-700 border border-blue-500/30',
backend: 'bg-green-500/10 text-green-700 border border-green-500/30',
mobile: 'bg-purple-500/10 text-purple-700 border border-purple-500/30',
ai: 'bg-red-500/10 text-red-700 border border-red-500/30',
devops: 'bg-orange-500/10 text-orange-700 border border-orange-500/30',
design: 'bg-pink-500/10 text-pink-700 border border-pink-500/30'
}
return classes[category] || 'bg-gray-500/10 text-gray-700 border border-gray-500/30'
}
return classes[category] || 'bg-gray-100 text-gray-800'
}
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
const formatDate = (timestamp) => {
if (!timestamp) return '未知时间'
const date = new Date(timestamp * 1000)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
@@ -308,6 +468,18 @@ const formatDate = (dateString) => {
})
}
// 格式化数字
const formatNumber = (num) => {
if (!num) return '0'
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w'
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k'
}
return num.toString()
}
// 获取文章列表
const fetchArticles = async () => {
loading.value = true
@@ -328,7 +500,8 @@ const fetchArticles = async () => {
const keyword = searchKeyword.value.toLowerCase()
filteredArticles = filteredArticles.filter(article =>
article.title.toLowerCase().includes(keyword) ||
article.summary.toLowerCase().includes(keyword)
article.summary.toLowerCase().includes(keyword) ||
article.tags.some(tag => tag.toLowerCase().includes(keyword))
)
}
@@ -413,19 +586,31 @@ const goToDetail = (id) => {
router.push(`/articles/${id}`)
}
// 处理图片错误
const handleImageError = (event) => {
event.target.src = 'https://images.unsplash.com/photo-1504711434969-e33886168f5c?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80'
}
// 监听分类变化
const handleCategoryChange = () => {
currentPage.value = 1
fetchArticles()
}
// 滚动到内容区域
const scrollToContent = () => {
const contentElement = document.querySelector('.border-b.border-opacity-20')
if (contentElement) {
contentElement.scrollIntoView({ behavior: 'smooth' })
}
}
// 组件挂载时获取数据
onMounted(() => {
fetchArticles()
})
// 监听分类变化
import { watch } from 'vue'
watch(activeCategory, handleCategoryChange)
</script>
@@ -445,6 +630,27 @@ watch(activeCategory, handleCategoryChange)
overflow: hidden;
}
/* 动画效果 */
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
100% { transform: translateY(0px); }
}
@keyframes twinkle {
0% { opacity: 0.2; }
50% { opacity: 1; }
100% { opacity: 0.2; }
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-twinkle {
animation: twinkle 3s ease-in-out infinite;
}
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-3 {
@@ -455,25 +661,11 @@ watch(activeCategory, handleCategoryChange)
flex-direction: column;
}
.space-x-8 {
.space-x-4 {
display: flex;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.space-x-8 > * + * {
margin-left: 2rem;
gap: 1rem;
}
}
/* 加载动画 */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>
</style>

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="min-h-screen flex">
<!-- 左侧装饰区域 -->
<div class="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-blue-600 via-purple-600 to-indigo-800 relative overflow-hidden">
@@ -81,9 +81,9 @@
<!-- 错误提示 -->
<div v-if="errors.general" class="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
<div class="flex">
<AlertIcon class="h-5 w-5 text-red-400 mt-0.5" />
<AlertIcon class="h-5 w-5 text-red-500 mt-0.5" />
<div class="ml-3">
<p class="text-sm text-red-800">{{ errors.general }}</p>
<p class="text-sm text-red-800 font-medium">{{ errors.general }}</p>
</div>
</div>
</div>
@@ -91,12 +91,12 @@
<form @submit.prevent="handleLogin" class="space-y-6">
<!-- 用户名输入 -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-2">
<label for="username" class="block text-sm font-medium text-gray-800 mb-2">
用户名
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<UserIcon class="h-5 w-5 text-gray-400" />
<UserIcon class="h-5 w-5 text-gray-500" />
</div>
<input
id="username"
@@ -104,23 +104,23 @@
type="text"
autocomplete="username"
:class="[
'block w-full pl-10 pr-3 py-3 border rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors',
'block w-full pl-10 pr-3 py-3 border rounded-lg shadow-sm placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors',
errors.username ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : 'border-gray-300'
]"
placeholder="请输入用户名"
/>
</div>
<p v-if="errors.username" class="mt-2 text-sm text-red-600">{{ errors.username }}</p>
<p v-if="errors.username" class="mt-2 text-sm text-red-600 font-medium">{{ errors.username }}</p>
</div>
<!-- 密码输入 -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
<label for="password" class="block text-sm font-medium text-gray-800 mb-2">
密码
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<LockIcon class="h-5 w-5 text-gray-400" />
<LockIcon class="h-5 w-5 text-gray-500" />
</div>
<input
id="password"
@@ -128,7 +128,7 @@
:type="showPassword ? 'text' : 'password'"
autocomplete="current-password"
:class="[
'block w-full pl-10 pr-10 py-3 border rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors',
'block w-full pl-10 pr-10 py-3 border rounded-lg shadow-sm placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors',
errors.password ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : 'border-gray-300'
]"
placeholder="请输入密码"
@@ -138,11 +138,11 @@
@click="showPassword = !showPassword"
class="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<EyeIcon v-if="showPassword" class="h-5 w-5 text-gray-400 hover:text-gray-600" />
<EyeOffIcon v-else class="h-5 w-5 text-gray-400 hover:text-gray-600" />
<EyeIcon v-if="showPassword" class="h-5 w-5 text-gray-500 hover:text-gray-700" />
<EyeOffIcon v-else class="h-5 w-5 text-gray-500 hover:text-gray-700" />
</button>
</div>
<p v-if="errors.password" class="mt-2 text-sm text-red-600">{{ errors.password }}</p>
<p v-if="errors.password" class="mt-2 text-sm text-red-600 font-medium">{{ errors.password }}</p>
</div>
<!-- 记住我和忘记密码 -->
@@ -154,7 +154,7 @@
type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label for="remember" class="ml-2 block text-sm text-gray-700">
<label for="remember" class="ml-2 block text-sm text-gray-800">
记住我
</label>
</div>
@@ -183,15 +183,15 @@
<!-- 演示账号信息 -->
<div class="mt-8 pt-6 border-t border-gray-200">
<div class="text-center">
<p class="text-sm text-gray-500 mb-3">演示账号信息</p>
<p class="text-sm text-gray-600 font-medium mb-3">演示账号信息</p>
<div class="bg-gray-50 rounded-lg p-4 space-y-2">
<div class="flex justify-between items-center text-sm">
<span class="text-gray-600">用户名</span>
<span class="font-mono bg-white px-2 py-1 rounded border">admin</span>
<span class="text-gray-700">用户名</span>
<span class="font-mono bg-white px-2 py-1 rounded border border-gray-300">admin</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-gray-600">密码</span>
<span class="font-mono bg-white px-2 py-1 rounded border">123456</span>
<span class="text-gray-700">密码</span>
<span class="font-mono bg-white px-2 py-1 rounded border border-gray-300">123456</span>
</div>
</div>
</div>
@@ -338,7 +338,7 @@ const handleLogin = async () => {
}
</script>
<style scoped>
<style lang="scss" scoped>
/* 渐变背景动画 */
@keyframes gradient {
0% {
@@ -402,4 +402,54 @@ input:focus {
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
/* 确保文字在各种背景下都清晰可见 */
.text-white {
color: #ffffff !important;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.text-blue-100 {
color: #dbeafe !important;
}
/* 确保右侧登录区域文字颜色正确 */
.bg-white, .bg-gray-50 {
color: #1e293b;
}
.text-gray-900 {
color: #111827 !important;
}
.text-gray-800 {
color: #1e293b !important;
}
.text-gray-700 {
color: #334155 !important;
}
.text-gray-600 {
color: #475569 !important;
}
.text-gray-500 {
color: #64748b !important;
}
/* 确保输入框文字可见 */
input {
color: #1e293b !important;
}
/* 确保错误信息清晰可见 */
.text-red-600, .text-red-800 {
font-weight: 500;
}
/* 确保按钮文字清晰可见 */
button {
font-weight: 500;
}
</style>

709
src/views/careers/index.vue Normal file
View File

@@ -0,0 +1,709 @@
<template>
<div class="min-h-screen relative" :class="themeStore.isDark
? 'bg-gradient-to-br from-gray-900 via-gray-800 to-purple-900'
: 'bg-gradient-to-br from-gray-50 via-blue-50 to-purple-50'">
<!-- 装饰元素 - 全局 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<!-- 亮色模式装饰 -->
<template v-if="!themeStore.isDark">
<div class="absolute top-20 left-10 w-72 h-72 bg-blue-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-cyan-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-blue-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-purple-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-cyan-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
<!-- 暗色模式装饰 -->
<template v-else>
<div class="absolute top-20 left-10 w-72 h-72 bg-orange-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-red-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-amber-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-orange-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-red-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-amber-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
</div>
<!-- 页面头部 - 英雄区域 -->
<div class="relative overflow-hidden h-screen flex items-center justify-center">
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center" data-aos="fade-up">
<h1 class="text-5xl md:text-6xl font-bold mb-6">
<span :class="themeStore.isDark
? 'text-orange-400'
: 'text-blue-400'">
加入我们
</span>
</h1>
<p class="text-xl mb-8 max-w-3xl mx-auto" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
与优秀的团队一起打造改变世界的科技产品
</p>
<div class="flex items-center justify-center space-x-4 mb-12">
<div class="w-2 h-2 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-orange-400' : 'bg-blue-400'"></div>
<div class="w-1 h-1 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-red-400' : 'bg-purple-400'" style="animation-delay: 0.5s;"></div>
<div class="w-1.5 h-1.5 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-amber-300' : 'bg-blue-300'" style="animation-delay: 1s;"></div>
</div>
<!-- 滚动到内容按钮 -->
<button
@click="scrollToContent"
class="animate-bounce mt-8 p-2 rounded-full bg-gradient-to-r shadow-lg transition-all duration-300 hover:shadow-xl"
:class="themeStore.isDark
? 'from-orange-500 to-red-500 hover:shadow-orange-500/30'
: 'from-blue-500 to-purple-500 hover:shadow-blue-500/30'"
>
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</button>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<!-- 为什么选择我们 -->
<section class="mb-20" data-aos="fade-up">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">为什么选择我们</h2>
<p :class="[themeStore.isDark ? 'text-gray-300' : 'text-gray-600', 'text-lg']">在这里您将获得的不仅仅是一份工作</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div
v-for="(benefit, index) in benefits"
:key="index"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
class="backdrop-blur-lg border rounded-3xl p-8 text-center hover:-translate-y-2 transition-all duration-500 hover:shadow-2xl"
:class="themeStore.isDark
? 'bg-white/5 border-white/10 hover:shadow-orange-500/20'
: 'bg-white/60 border-white/20 hover:shadow-blue-500/20'"
>
<div class="w-16 h-16 bg-gradient-to-r rounded-2xl flex items-center justify-center mx-auto mb-6"
:class="themeStore.isDark ? 'from-orange-500 to-red-500' : 'from-blue-500 to-purple-500'">
<component :is="benefit.icon" class="w-8 h-8 text-white" />
</div>
<h3 class="text-xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">{{ benefit.title }}</h3>
<p :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">{{ benefit.description }}</p>
</div>
</div>
</section>
<!-- 开放职位 -->
<section class="mb-20" data-aos="fade-up" data-aos-delay="200">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">开放职位</h2>
<p :class="[themeStore.isDark ? 'text-gray-300' : 'text-gray-600', 'text-lg']">找到适合您的机会</p>
</div>
<!-- 搜索和筛选 -->
<div class="backdrop-blur-lg border rounded-3xl p-6 mb-8" :class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/60 border-white/20'">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<div class="relative">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<input
v-model="searchKeyword"
type="text"
placeholder="搜索职位..."
class="w-full pl-10 pr-4 py-3 border rounded-2xl focus:outline-none focus:ring-2 transition-all duration-300"
:class="themeStore.isDark
? 'bg-white/5 border-white/10 text-white placeholder-gray-400 focus:ring-orange-500/50'
: 'bg-white/60 border-white/20 text-gray-900 placeholder-gray-500 focus:ring-blue-500/50'"
/>
</div>
</div>
<div class="flex gap-2 flex-wrap">
<button
v-for="dept in departments"
:key="dept.key"
@click="activeDepartment = dept.key"
:class="[
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300',
activeDepartment === dept.key
? themeStore.isDark
? 'bg-gradient-to-r from-orange-500 to-red-500 text-white shadow-lg'
: 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: themeStore.isDark
? 'bg-white/10 text-gray-300 hover:bg-white/20'
: 'bg-white/60 text-gray-700 hover:bg-white/80'
]"
>
{{ dept.name }}
</button>
</div>
</div>
</div>
<!-- API错误提示 -->
<div v-if="apiError" class="mb-6 p-4 rounded-lg bg-yellow-100 border border-yellow-300 text-yellow-800">
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 15.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
{{ apiError }}
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="text-center py-12">
<div class="inline-flex items-center px-4 py-2 font-semibold leading-6 text-sm shadow rounded-md transition ease-in-out duration-150"
:class="themeStore.isDark ? 'text-white bg-gray-800' : 'text-gray-900 bg-white'">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5" :class="themeStore.isDark ? 'text-orange-500' : 'text-blue-500'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
正在加载职位信息...
</div>
</div>
<div v-else class="space-y-6">
<div
v-for="(job, index) in filteredJobs"
:key="job.id"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
class="backdrop-blur-lg border rounded-3xl p-8 hover:-translate-y-1 transition-all duration-300 hover:shadow-xl group cursor-pointer relative"
:class="themeStore.isDark
? 'bg-white/5 border-white/10 hover:border-orange-500/30 hover:shadow-orange-500/10'
: 'bg-white/60 border-white/20 hover:border-blue-500/30 hover:shadow-blue-500/10'"
@click="showJobDetail(job)"
>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div class="flex-1">
<div class="flex items-center mb-4">
<h3 class="text-2xl font-bold mr-4 transition-colors duration-300"
:class="themeStore.isDark
? 'text-white group-hover:text-orange-400'
: 'text-gray-900 group-hover:text-blue-600'">
{{ job.title }}
</h3>
<span class="px-3 py-1 text-white text-sm font-medium rounded-full bg-gradient-to-r"
:class="themeStore.isDark
? 'from-orange-500 to-red-500'
: 'from-blue-500 to-purple-500'">
{{ job.type }}
</span>
</div>
<p :class="[themeStore.isDark ? 'text-gray-300' : 'text-gray-600', 'mb-4']">{{ job.description }}</p>
<div class="flex flex-wrap gap-4 mb-4">
<div class="flex items-center" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
{{ job.location }}
</div>
<div class="flex items-center" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
</svg>
{{ job.salary }}
</div>
<div class="flex items-center" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ job.experience }}
</div>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="skill in job.skills"
:key="skill"
class="px-3 py-1 text-sm rounded-full"
:class="themeStore.isDark
? 'bg-white/10 text-gray-300'
: 'bg-white/40 text-gray-700'"
>
{{ skill }}
</span>
</div>
</div>
<div class="mt-6 lg:mt-0 lg:ml-8">
<button
@click.stop="applyForJob(job)"
class="w-full lg:w-auto px-8 py-3 text-white font-medium rounded-2xl transition-all duration-300 hover:shadow-lg bg-gradient-to-r"
:class="themeStore.isDark
? 'from-orange-600 to-red-500 hover:from-orange-500 hover:to-red-400 hover:shadow-orange-500/25'
: 'from-blue-600 to-purple-500 hover:from-blue-500 hover:to-purple-400 hover:shadow-blue-500/25'"
>
立即申请
</button>
</div>
</div>
</div>
</div>
<!-- 没有合适职位 -->
<div v-if="filteredJobs.length === 0" class="text-center mt-12" data-aos="fade-up" data-aos-delay="600">
<div class="backdrop-blur-lg border rounded-3xl p-8" :class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/60 border-white/20'">
<div class="w-24 h-24 rounded-full flex items-center justify-center mx-auto mb-6 bg-gradient-to-r"
:class="themeStore.isDark ? 'from-orange-600 to-red-500' : 'from-blue-600 to-purple-500'">
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<h3 class="text-xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">没有找到合适的职位</h3>
<p :class="[themeStore.isDark ? 'text-gray-300' : 'text-gray-600', 'mb-6']">
我们一直在寻找优秀的人才即使当前没有完全匹配的职位我们也欢迎您发送简历
</p>
<button
@click="sendResume"
class="px-8 py-3 backdrop-blur-10 border font-medium rounded-2xl transition-all duration-300"
:class="themeStore.isDark
? 'bg-white/10 border-white/20 hover:bg-white/20 text-white'
: 'bg-white/60 border-white/20 hover:bg-white/80 text-gray-900'"
>
发送简历
</button>
</div>
</div>
</section>
<!-- 工作环境 -->
<section class="mb-20" data-aos="fade-up" data-aos-delay="400">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">工作环境</h2>
<p :class="[themeStore.isDark ? 'text-gray-300' : 'text-gray-600', 'text-lg']">现代化的办公环境激发创造力</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="(image, index) in officeImages"
:key="index"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
class="relative overflow-hidden rounded-3xl group cursor-pointer"
>
<img
:src="image.url"
:alt="image.title"
class="w-full h-64 object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="absolute bottom-4 left-4 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<h4 class="text-lg font-semibold">{{ image.title }}</h4>
<p class="text-sm">{{ image.description }}</p>
</div>
</div>
</div>
</section>
<!-- 联系我们 -->
<section data-aos="fade-up" data-aos-delay="600">
<div class="bg-gradient-to-r rounded-3xl p-12 text-center text-white"
:class="themeStore.isDark ? 'from-orange-600 to-red-500' : 'from-blue-600 to-purple-500'">
<h2 class="text-3xl font-bold mb-4">准备好加入我们了吗</h2>
<p class="text-xl mb-8 opacity-90">
如果您对我们的职位感兴趣或者想了解更多信息请随时联系我们
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<router-link
to="/contact"
class="px-8 py-4 bg-white font-medium rounded-2xl hover:bg-gray-100 transition-all duration-300 hover:shadow-lg"
:class="themeStore.isDark ? 'text-orange-600' : 'text-blue-600'"
>
联系我们
</router-link>
<a
href="mailto:hr@techcms.com"
class="px-8 py-4 bg-white/20 backdrop-blur-10 border border-white/30 hover:bg-white/30 text-white font-medium rounded-2xl transition-all duration-300"
>
发送简历
</a>
</div>
</div>
</section>
</div>
<!-- 职位详情模态框 -->
<div
v-if="selectedJob"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
@click="closeJobDetail"
>
<div
class="max-w-4xl w-full max-h-[90vh] overflow-y-auto backdrop-blur-lg border rounded-3xl"
:class="themeStore.isDark ? 'bg-gray-900/95 border-white/10' : 'bg-white/95 border-white/20'"
@click.stop
>
<div class="p-8">
<!-- 职位头部 -->
<div class="flex items-start justify-between mb-6">
<div>
<h2 class="text-3xl font-bold mb-2" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">{{ selectedJob.title }}</h2>
<div class="flex flex-wrap gap-2 mb-4">
<span class="px-3 py-1 bg-gradient-to-r text-white text-sm font-medium rounded-full"
:class="themeStore.isDark ? 'from-orange-500 to-red-500' : 'from-blue-500 to-purple-500'">
{{ selectedJob.type }}
</span>
<span class="px-3 py-1 text-sm font-medium rounded-full"
:class="themeStore.isDark ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'">
{{ selectedJob.level }}
</span>
</div>
</div>
<button
@click="closeJobDetail"
class="p-2 rounded-lg transition-colors duration-300"
:class="themeStore.isDark
? 'text-gray-400 hover:bg-gray-700 hover:text-gray-200'
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-700'"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- 职位信息 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div>
<h3 class="text-lg font-semibold mb-3" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">基本信息</h3>
<div class="space-y-2">
<div class="flex items-center">
<svg class="w-4 h-4 mr-2" :class="themeStore.isDark ? 'text-orange-500' : 'text-blue-500'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">{{ selectedJob.location }}</span>
</div>
<div class="flex items-center">
<svg class="w-4 h-4 mr-2" :class="themeStore.isDark ? 'text-green-400' : 'text-green-500'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
</svg>
<span :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">{{ selectedJob.salary }}</span>
</div>
<div class="flex items-center">
<svg class="w-4 h-4 mr-2" :class="themeStore.isDark ? 'text-amber-400' : 'text-orange-500'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">{{ selectedJob.experience }}</span>
</div>
</div>
</div>
<div>
<h3 class="text-lg font-semibold mb-3" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">技能要求</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="skill in selectedJob.skills"
:key="skill"
class="px-2 py-1 text-xs font-medium rounded-lg"
:class="themeStore.isDark ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'"
>
{{ skill }}
</span>
</div>
</div>
</div>
<!-- 职位描述 -->
<div class="mb-8">
<h3 class="text-lg font-semibold mb-3" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">职位描述</h3>
<p :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">{{ selectedJob.description }}</p>
</div>
<!-- 申请按钮 -->
<div class="flex justify-end space-x-4">
<button
@click="closeJobDetail"
class="px-6 py-3 font-medium rounded-2xl transition-all duration-300"
:class="themeStore.isDark
? 'bg-gray-700 text-gray-300 hover:bg-gray-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'"
>
关闭
</button>
<button
@click="applyForJob(selectedJob)"
class="px-8 py-3 text-white font-medium rounded-2xl transition-all duration-300 shadow-lg bg-gradient-to-r"
:class="themeStore.isDark
? 'from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600'
: 'from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600'"
>
立即申请
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useThemeStore } from '@/stores/theme'
import { careersAPI } from '@/api/index.js'
const themeStore = useThemeStore()
// 添加加载状态
const loading = ref(false)
const apiError = ref(null)
// 滚动到内容区域
const scrollToContent = () => {
const contentElement = document.querySelector('.max-w-7xl.mx-auto.px-4')
if (contentElement) {
contentElement.scrollIntoView({ behavior: 'smooth' })
}
}
// 响应式数据
const searchKeyword = ref('')
const activeDepartment = ref('all')
const selectedJob = ref(null)
// 公司福利
const benefits = ref([
{
title: '技术成长',
description: '前沿技术栈,持续学习机会,技术大牛指导',
icon: {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>`
}
},
{
title: '工作生活平衡',
description: '弹性工作时间,远程办公支持,带薪年假',
icon: {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path></svg>`
}
},
{
title: '丰厚回报',
description: '有竞争力的薪资,股权激励,绩效奖金',
icon: {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"></path></svg>`
}
},
{
title: '舒适环境',
description: '现代化办公环境,免费咖啡茶点,团建活动',
icon: {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path></svg>`
}
}
])
// 部门分类
const departments = ref([
{ key: 'all', name: '全部' },
{ key: 'frontend', name: '前端开发' },
{ key: 'backend', name: '后端开发' },
{ key: 'mobile', name: '移动开发' },
{ key: 'ai', name: '人工智能' },
{ key: 'product', name: '产品设计' },
{ key: 'operation', name: '运营推广' }
])
// 职位数据 - 从API获取
const jobs = ref([])
// 从API加载职位数据
const loadJobs = async () => {
try {
loading.value = true
apiError.value = null
const response = await careersAPI.getJobs({
page: 1,
page_size: 20,
is_published: 1
})
if (response && response.data) {
// 转换后端数据格式为前端需要的格式
jobs.value = response.data.list.map(job => ({
id: job.id,
title: job.title,
department: mapDepartment(job.department),
type: mapEmploymentType(job.employment_type),
level: mapExperienceLevel(job.experience_level),
location: job.location,
salary: formatSalary(job.salary_min, job.salary_max, job.salary_type, job.is_salary_negotiable),
experience: mapExperienceText(job.experience_level),
description: job.description,
skills: Array.isArray(job.skills) ? job.skills : []
}))
}
} catch (error) {
console.error('加载职位数据失败:', error)
apiError.value = '加载职位数据失败,显示模拟数据'
// 如果API失败使用模拟数据
jobs.value = [
{
id: 1,
title: '高级前端工程师',
department: 'frontend',
type: '全职',
level: '高级',
location: '北京',
salary: '25K-40K',
experience: '3-5年',
description: '负责前端架构设计和核心功能开发使用Vue3、React等现代前端技术栈。',
skills: ['Vue3', 'React', 'TypeScript', 'Webpack', 'Sass', 'Git']
},
{
id: 2,
title: 'Node.js后端工程师',
department: 'backend',
type: '全职',
level: '中级',
location: '上海',
salary: '20K-35K',
experience: '2-4年',
description: '负责后端API开发和数据库设计构建高性能、高可用的服务端应用。',
skills: ['Node.js', 'Express', 'MongoDB', 'Redis', 'Docker', 'AWS']
}
]
} finally {
loading.value = false
}
}
// 数据转换辅助函数
const mapDepartment = (dept) => {
const deptMap = {
'技术部': 'frontend',
'后端部': 'backend',
'移动部': 'mobile',
'产品部': 'product',
'运营部': 'operation'
}
return deptMap[dept] || 'frontend'
}
const mapEmploymentType = (type) => {
const typeMap = {
'full_time': '全职',
'part_time': '兼职',
'contract': '合同工',
'intern': '实习生'
}
return typeMap[type] || '全职'
}
const mapExperienceLevel = (level) => {
const levelMap = {
'entry': '初级',
'junior': '初级',
'mid': '中级',
'senior': '高级',
'expert': '专家'
}
return levelMap[level] || '中级'
}
const mapExperienceText = (level) => {
const textMap = {
'entry': '应届生',
'junior': '1-3年',
'mid': '3-5年',
'senior': '5-10年',
'expert': '10年以上'
}
return textMap[level] || '1-3年'
}
const formatSalary = (min, max, type, negotiable) => {
if (negotiable) return '薪资面议'
const typeText = type === 'yearly' ? '年薪' : type === 'hourly' ? '时薪' : ''
const minK = Math.floor(min / 1000)
const maxK = Math.floor(max / 1000)
return `${minK}K-${maxK}K${typeText}`
}
// 组件挂载时加载数据
onMounted(() => {
loadJobs()
})
// 办公环境图片
const officeImages = ref([
{
url: 'https://images.unsplash.com/photo-1497366216548-37526070297c?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
title: '开放式办公区',
description: '宽敞明亮的工作空间'
},
{
url: 'https://images.unsplash.com/photo-1497366811353-6870744d04b2?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
title: '会议室',
description: '现代化的会议设施'
},
{
url: 'https://images.unsplash.com/photo-1556761175-4b46a572b786?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
title: '休息区',
description: '舒适的休息和交流空间'
}
])
// 计算属性
const filteredJobs = computed(() => {
let filtered = jobs.value
// 按部门筛选
if (activeDepartment.value !== 'all') {
filtered = filtered.filter(job => job.department === activeDepartment.value)
}
// 按关键词搜索
if (searchKeyword.value.trim()) {
const keyword = searchKeyword.value.toLowerCase()
filtered = filtered.filter(job =>
job.title.toLowerCase().includes(keyword) ||
job.description.toLowerCase().includes(keyword) ||
job.skills.some(skill => skill.toLowerCase().includes(keyword))
)
}
return filtered
})
// 方法
const showJobDetail = (job) => {
selectedJob.value = job
}
const closeJobDetail = () => {
selectedJob.value = null
}
const applyForJob = (job) => {
alert(`感谢您对"${job.title}"职位的关注!请发送简历到 hr@techcms.com`)
closeJobDetail()
}
const sendResume = () => {
alert('请发送简历到 hr@techcms.com我们会尽快与您联系')
}
</script>
<style scoped>
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.animate-twinkle {
animation: twinkle 2s ease-in-out infinite;
}
</style>

View File

@@ -1,14 +1,62 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 页面头部 -->
<div class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="text-center">
<h1 class="text-4xl font-bold text-gray-900 mb-4">联系我们</h1>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
我们随时准备为您提供帮助欢迎通过以下方式与我们取得联系
</p>
<template>
<div class="min-h-screen relative" :class="themeStore.isDark
? 'bg-gradient-to-br from-gray-900 via-gray-800 to-purple-900'
: 'bg-gradient-to-br from-gray-50 via-blue-50 to-purple-50'">
<!-- 装饰元素 - 全局 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<!-- 亮色模式装饰 -->
<template v-if="!themeStore.isDark">
<div class="absolute top-20 left-10 w-72 h-72 bg-blue-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-cyan-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-blue-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-purple-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-cyan-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
<!-- 暗色模式装饰 -->
<template v-else>
<div class="absolute top-20 left-10 w-72 h-72 bg-orange-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-red-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-amber-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-orange-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-red-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-amber-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
</div>
<!-- 页面头部 - 英雄区域 -->
<div class="relative overflow-hidden h-screen flex items-center justify-center">
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center" data-aos="fade-up">
<h1 class="text-5xl md:text-6xl font-bold mb-6">
<span :class="themeStore.isDark
? 'text-orange-400'
: 'text-blue-400'">
联系我们
</span>
</h1>
<p class="text-xl mb-8 max-w-3xl mx-auto" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
我们随时准备为您提供专业的技术支持和解决方案
</p>
<div class="flex items-center justify-center space-x-4 mb-12">
<div class="w-2 h-2 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-orange-400' : 'bg-blue-400'"></div>
<div class="w-1 h-1 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-red-400' : 'bg-purple-400'" style="animation-delay: 0.5s;"></div>
<div class="w-1.5 h-1.5 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-amber-300' : 'bg-blue-300'" style="animation-delay: 1s;"></div>
</div>
<!-- 滚动到内容按钮 -->
<button
@click="scrollToContent"
class="animate-bounce mt-8 p-2 rounded-full bg-gradient-to-r shadow-lg transition-all duration-300 hover:shadow-xl"
:class="themeStore.isDark
? 'from-orange-500 to-red-500 hover:shadow-orange-500/30'
: 'from-blue-500 to-purple-500 hover:shadow-blue-500/30'"
>
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</button>
</div>
</div>
@@ -17,197 +65,321 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<!-- 联系信息 -->
<div class="space-y-8">
<div>
<h2 class="text-2xl font-bold text-gray-900 mb-6">联系信息</h2>
<div class="space-y-6">
<div class="flex items-start">
<div class="flex-shrink-0">
<LocationIcon class="w-6 h-6 text-blue-600 mt-1" />
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 mb-1">公司地址</h3>
<p class="text-gray-600">深圳市南山区科技园南区深圳湾科技生态园</p>
<p class="text-gray-600">10栋A座5层501室</p>
</div>
<div data-aos="fade-right">
<h2 :class="['text-3xl font-bold mb-8', themeStore.isDark ? 'text-white' : 'text-gray-900']">
联系信息
</h2>
<div class="space-y-6">
<div
v-for="contact in contactInfo"
:key="contact.type"
:class="[
'flex items-start space-x-4 p-6 rounded-2xl backdrop-blur-sm border transition-all duration-300 hover:-translate-y-1',
themeStore.isDark
? 'bg-white/5 border-white/10 hover:bg-white/10'
: 'bg-white/80 border-gray-200/50 hover:bg-white'
]"
>
<div class="w-12 h-12 rounded-full bg-gradient-to-r from-tech-blue-500 to-neon-purple-500 flex items-center justify-center flex-shrink-0">
<component :is="contact.icon" class="w-6 h-6 text-white" />
</div>
<div class="flex items-start">
<div class="flex-shrink-0">
<PhoneIcon class="w-6 h-6 text-blue-600 mt-1" />
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 mb-1">联系电话</h3>
<p class="text-gray-600">客服热线400-123-4567</p>
<p class="text-gray-600">技术支持0755-8888-9999</p>
<p class="text-gray-600">周一至周五9:00 - 18:00</p>
<p class="text-gray-600">周六10:00 - 16:00</p>
</div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0">
<EmailIcon class="w-6 h-6 text-blue-600 mt-1" />
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 mb-1">电子邮箱</h3>
<p class="text-gray-600">商务合作business@example.com</p>
<p class="text-gray-600">技术支持support@example.com</p>
<p class="text-gray-600">人事招聘hr@example.com</p>
</div>
<div>
<h3 :class="['text-lg font-semibold mb-1', themeStore.isDark ? 'text-white' : 'text-gray-900']">
{{ contact.type }}
</h3>
<p :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
{{ contact.value }}
</p>
<p v-if="contact.description" :class="['text-sm mt-1', themeStore.isDark ? 'text-gray-400' : 'text-gray-500']">
{{ contact.description }}
</p>
</div>
</div>
</div>
<!-- 工作时间 -->
<div class="bg-blue-50 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">工作时间</h3>
<div class="space-y-2 text-gray-600">
<div class="flex justify-between">
<span>周一至周五</span>
<span>9:00 - 18:00</span>
</div>
<div class="flex justify-between">
<span>周六</span>
<span>10:00 - 16:00</span>
</div>
<div class="flex justify-between">
<span>周日</span>
<span>休息</span>
</div>
<!-- 社交媒体 -->
<div class="mt-8">
<h3 :class="['text-xl font-semibold mb-4', themeStore.isDark ? 'text-white' : 'text-gray-900']">
关注我们
</h3>
<div class="flex space-x-4">
<a
v-for="social in socialLinks"
:key="social.name"
:href="social.url"
target="_blank"
:class="[
'w-12 h-12 rounded-full flex items-center justify-center transition-all duration-300 hover:scale-110',
themeStore.isDark
? 'bg-white/10 hover:bg-white/20 text-gray-300 hover:text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-800'
]"
>
<component :is="social.icon" class="w-5 h-5" />
</a>
</div>
</div>
</div>
<!-- 联系表单 -->
<div class="bg-white rounded-lg shadow-lg p-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6">发送消息</h2>
<form @submit.prevent="submitForm" class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
姓名 <span class="text-red-500">*</span>
</label>
<input
id="name"
v-model="form.name"
type="text"
:class="[
'w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors',
errors.name ? 'border-red-500' : 'border-gray-300'
]"
placeholder="请输入您的姓名"
/>
<p v-if="errors.name" class="mt-1 text-sm text-red-600">{{ errors.name }}</p>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
邮箱 <span class="text-red-500">*</span>
</label>
<input
id="email"
v-model="form.email"
type="email"
:class="[
'w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors',
errors.email ? 'border-red-500' : 'border-gray-300'
]"
placeholder="请输入您的邮箱"
/>
<p v-if="errors.email" class="mt-1 text-sm text-red-600">{{ errors.email }}</p>
</div>
<div>
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">
联系电话
</label>
<input
id="phone"
v-model="form.phone"
type="tel"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
placeholder="请输入您的联系电话"
/>
</div>
<div>
<label for="subject" class="block text-sm font-medium text-gray-700 mb-2">
主题 <span class="text-red-500">*</span>
</label>
<select
id="subject"
v-model="form.subject"
:class="[
'w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors',
errors.subject ? 'border-red-500' : 'border-gray-300'
]"
<div data-aos="fade-left">
<div
:class="[
'rounded-2xl p-8 backdrop-blur-sm border',
themeStore.isDark
? 'bg-white/5 border-white/10'
: 'bg-white/80 border-gray-200/50'
]"
>
<h2 :class="['text-3xl font-bold mb-8', themeStore.isDark ? 'text-white' : 'text-gray-900']">
发送消息
</h2>
<form @submit.prevent="submitForm" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label :class="['block text-sm font-medium mb-2', themeStore.isDark ? 'text-gray-300' : 'text-gray-700']">
姓名 *
</label>
<input
v-model="form.name"
type="text"
required
:class="[
'w-full px-4 py-3 rounded-xl transition-all duration-300 backdrop-blur-sm border',
themeStore.isDark
? 'bg-white/5 border-white/10 text-white placeholder-gray-400 focus:bg-white/10 focus:border-tech-blue-500/50'
: 'bg-white/80 border-gray-200/50 text-gray-900 placeholder-gray-500 focus:bg-white focus:border-tech-blue-500/50'
]"
placeholder="请输入您的姓名"
/>
</div>
<div>
<label :class="['block text-sm font-medium mb-2', themeStore.isDark ? 'text-gray-300' : 'text-gray-700']">
邮箱 *
</label>
<input
v-model="form.email"
type="email"
required
:class="[
'w-full px-4 py-3 rounded-xl transition-all duration-300 backdrop-blur-sm border',
themeStore.isDark
? 'bg-white/5 border-white/10 text-white placeholder-gray-400 focus:bg-white/10 focus:border-tech-blue-500/50'
: 'bg-white/80 border-gray-200/50 text-gray-900 placeholder-gray-500 focus:bg-white focus:border-tech-blue-500/50'
]"
placeholder="请输入您的邮箱"
/>
</div>
</div>
<div>
<label :class="['block text-sm font-medium mb-2', themeStore.isDark ? 'text-gray-300' : 'text-gray-700']">
公司/组织
</label>
<input
v-model="form.company"
type="text"
:class="[
'w-full px-4 py-3 rounded-xl transition-all duration-300 backdrop-blur-sm border',
themeStore.isDark
? 'bg-white/5 border-white/10 text-white placeholder-gray-400 focus:bg-white/10 focus:border-tech-blue-500/50'
: 'bg-white/80 border-gray-200/50 text-gray-900 placeholder-gray-500 focus:bg-white focus:border-tech-blue-500/50'
]"
placeholder="请输入您的公司或组织名称"
/>
</div>
<div>
<label :class="['block text-sm font-medium mb-2', themeStore.isDark ? 'text-gray-300' : 'text-gray-700']">
联系类型 *
</label>
<select
v-model="form.type"
required
:class="[
'w-full px-4 py-3 rounded-xl transition-all duration-300 backdrop-blur-sm border',
themeStore.isDark
? 'bg-white/5 border-white/10 text-white focus:bg-white/10 focus:border-tech-blue-500/50'
: 'bg-white/80 border-gray-200/50 text-gray-900 focus:bg-white focus:border-tech-blue-500/50'
]"
>
<option value="">请选择联系类型</option>
<option value="consultation">产品咨询</option>
<option value="support">技术支持</option>
<option value="cooperation">商务合作</option>
<option value="feedback">意见反馈</option>
<option value="other">其他</option>
</select>
</div>
<div>
<label :class="['block text-sm font-medium mb-2', themeStore.isDark ? 'text-gray-300' : 'text-gray-700']">
消息内容 *
</label>
<textarea
v-model="form.message"
required
rows="6"
:class="[
'w-full px-4 py-3 rounded-xl transition-all duration-300 backdrop-blur-sm border resize-none',
themeStore.isDark
? 'bg-white/5 border-white/10 text-white placeholder-gray-400 focus:bg-white/10 focus:border-tech-blue-500/50'
: 'bg-white/80 border-gray-200/50 text-gray-900 placeholder-gray-500 focus:bg-white focus:border-tech-blue-500/50'
]"
placeholder="请详细描述您的需求或问题..."
></textarea>
</div>
<button
type="submit"
:disabled="submitting"
class="w-full bg-gradient-to-r from-tech-blue-500 to-neon-purple-500 hover:from-tech-blue-600 hover:to-neon-purple-600 disabled:opacity-50 disabled:cursor-not-allowed text-white px-8 py-3 rounded-xl transition-all duration-300 shadow-lg hover:shadow-tech-glow"
>
<option value="">请选择主题</option>
<option value="business">商务合作</option>
<option value="technical">技术支持</option>
<option value="feedback">意见反馈</option>
<option value="other">其他</option>
</select>
<p v-if="errors.subject" class="mt-1 text-sm text-red-600">{{ errors.subject }}</p>
</div>
<div>
<label for="message" class="block text-sm font-medium text-gray-700 mb-2">
消息内容 <span class="text-red-500">*</span>
</label>
<textarea
id="message"
v-model="form.message"
rows="5"
:class="[
'w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors resize-none',
errors.message ? 'border-red-500' : 'border-gray-300'
]"
placeholder="请详细描述您的问题或需求..."
></textarea>
<p v-if="errors.message" class="mt-1 text-sm text-red-600">{{ errors.message }}</p>
</div>
<div class="flex items-center">
<input
id="agree"
v-model="form.agree"
type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label for="agree" class="ml-2 text-sm text-gray-600">
我同意<a href="#" class="text-blue-600 hover:text-blue-800">隐私政策</a> <a href="#" class="text-blue-600 hover:text-blue-800">服务条款</a>
</label>
</div>
<p v-if="errors.agree" class="text-sm text-red-600">{{ errors.agree }}</p>
<button
type="submit"
:disabled="loading"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
<LoadingIcon v-if="loading" class="animate-spin -ml-1 mr-3 h-5 w-5" />
<span v-if="loading">发送中...</span>
<span v-else>发送消息</span>
</button>
</form>
<span v-if="submitting" class="flex items-center justify-center">
<div class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>
发送中...
</span>
<span v-else>发送消息</span>
</button>
</form>
</div>
</div>
</div>
</div>
</section>
<!-- 地图区域 -->
<section class="py-16 bg-white">
<!-- 地图和办公地址 -->
<section class="py-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-8">
<h2 class="text-3xl font-bold text-gray-900 mb-4">找到我们</h2>
<p class="text-gray-600">我们位于深圳市南山区交通便利欢迎您的到访</p>
<div class="text-center mb-12" data-aos="fade-up">
<h2 :class="['text-3xl font-bold mb-4', themeStore.isDark ? 'text-white' : 'text-gray-900']">
办公地址
</h2>
<p :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-600'">
欢迎您到访我们的办公室
</p>
</div>
<div class="bg-gray-200 rounded-lg h-96 flex items-center justify-center">
<div class="text-center">
<MapIcon class="w-16 h-16 text-gray-400 mx-auto mb-4" />
<p class="text-gray-600">地图加载中...</p>
<p class="text-sm text-gray-500 mt-2">深圳市南山区科技园南区深圳湾科技生态园</p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<!-- 地图 -->
<div data-aos="fade-right">
<div
:class="[
'rounded-2xl overflow-hidden backdrop-blur-sm border h-96',
themeStore.isDark
? 'bg-white/5 border-white/10'
: 'bg-white/80 border-gray-200/50'
]"
>
<!-- 这里可以集成真实的地图API如高德地图或百度地图 -->
<div class="w-full h-full bg-gradient-to-br from-tech-blue-100 to-neon-purple-100 flex items-center justify-center">
<div class="text-center">
<MapPin class="w-16 h-16 text-tech-blue-500 mx-auto mb-4" />
<p :class="['text-lg font-medium', themeStore.isDark ? 'text-white' : 'text-gray-900']">
地图加载中...
</p>
<p :class="['text-sm mt-2', themeStore.isDark ? 'text-gray-400' : 'text-gray-600']">
北京市朝阳区科技园区
</p>
</div>
</div>
</div>
</div>
<!-- 地址详情 -->
<div data-aos="fade-left">
<div class="space-y-6">
<div
v-for="office in offices"
:key="office.city"
:class="[
'p-6 rounded-2xl backdrop-blur-sm border transition-all duration-300 hover:-translate-y-1',
themeStore.isDark
? 'bg-white/5 border-white/10 hover:bg-white/10'
: 'bg-white/80 border-gray-200/50 hover:bg-white'
]"
>
<div class="flex items-start space-x-4">
<div class="w-12 h-12 rounded-full bg-gradient-to-r from-tech-blue-500 to-neon-purple-500 flex items-center justify-center flex-shrink-0">
<Building class="w-6 h-6 text-white" />
</div>
<div>
<h3 :class="['text-lg font-semibold mb-2', themeStore.isDark ? 'text-white' : 'text-gray-900']">
{{ office.city }}办公室
</h3>
<p :class="['mb-2', themeStore.isDark ? 'text-gray-300' : 'text-gray-600']">
{{ office.address }}
</p>
<p :class="['text-sm', themeStore.isDark ? 'text-gray-400' : 'text-gray-500']">
{{ office.hours }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- FAQ 常见问题 -->
<section class="py-16">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12" data-aos="fade-up">
<h2 :class="['text-3xl font-bold mb-4', themeStore.isDark ? 'text-white' : 'text-gray-900']">
常见问题
</h2>
<p :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-600'">
快速找到您需要的答案
</p>
</div>
<div class="space-y-4">
<div
v-for="(faq, index) in faqs"
:key="index"
:class="[
'rounded-2xl backdrop-blur-sm border overflow-hidden transition-all duration-300',
themeStore.isDark
? 'bg-white/5 border-white/10'
: 'bg-white/80 border-gray-200/50'
]"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
>
<button
@click="toggleFaq(index)"
:class="[
'w-full px-6 py-4 text-left flex items-center justify-between transition-colors duration-300',
themeStore.isDark
? 'hover:bg-white/5'
: 'hover:bg-gray-50/80'
]"
>
<span :class="['font-medium', themeStore.isDark ? 'text-white' : 'text-gray-900']">
{{ faq.question }}
</span>
<ChevronDown
:class="[
'w-5 h-5 transition-transform duration-300',
activeFaq === index ? 'rotate-180' : '',
themeStore.isDark ? 'text-gray-400' : 'text-gray-500'
]"
/>
</button>
<div
v-if="activeFaq === index"
:class="[
'px-6 pb-4 border-t transition-colors duration-300',
themeStore.isDark
? 'text-gray-300 border-white/10'
: 'text-gray-600 border-gray-200/50'
]"
>
<p class="pt-4">{{ faq.answer }}</p>
</div>
</div>
</div>
</div>
@@ -217,139 +389,182 @@
<script setup>
import { ref } from 'vue'
import { useThemeStore } from '@/stores/theme'
import {
Phone,
Mail,
MapPin,
Clock,
Building,
Github,
Twitter,
Linkedin,
MessageCircle,
ChevronDown
} from 'lucide-vue-next'
// 图标组件
const LocationIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>`
const themeStore = useThemeStore()
// 滚动到内容区域
const scrollToContent = () => {
const contentElement = document.querySelector('.py-16')
if (contentElement) {
contentElement.scrollIntoView({ behavior: 'smooth' })
}
}
const PhoneIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path></svg>`
}
const EmailIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>`
}
const MapIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path></svg>`
}
const LoadingIcon = {
template: `<svg fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>`
}
// 响应式数据
const loading = ref(false)
// 表单数据
const form = ref({
name: '',
email: '',
phone: '',
subject: '',
message: '',
agree: false
company: '',
type: '',
message: ''
})
const errors = ref({})
const submitting = ref(false)
const activeFaq = ref(null)
// 表单验证
const validateForm = () => {
errors.value = {}
if (!form.value.name.trim()) {
errors.value.name = '请输入您的姓名'
// 联系信息
const contactInfo = [
{
type: '客服热线',
value: '400-123-4567',
description: '工作日 9:00-18:00',
icon: Phone
},
{
type: '邮箱地址',
value: 'contact@cms-system.com',
description: '我们会在24小时内回复',
icon: Mail
},
{
type: '办公地址',
value: '北京市朝阳区科技园区A座1001室',
description: '欢迎预约到访',
icon: MapPin
},
{
type: '工作时间',
value: '周一至周五 9:00-18:00',
description: '节假日正常休息',
icon: Clock
}
]
if (!form.value.email.trim()) {
errors.value.email = '请输入邮箱地址'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.value.email)) {
errors.value.email = '请输入有效的邮箱地址'
// 社交媒体链接
const socialLinks = [
{ name: 'GitHub', icon: Github, url: 'https://github.com' },
{ name: 'Twitter', icon: Twitter, url: 'https://twitter.com' },
{ name: 'LinkedIn', icon: Linkedin, url: 'https://linkedin.com' },
{ name: '微信', icon: MessageCircle, url: '#' }
]
// 办公室地址
const offices = [
{
city: '北京',
address: '朝阳区科技园区A座1001室',
hours: '周一至周五 9:00-18:00'
},
{
city: '上海',
address: '浦东新区张江高科技园区B座2001室',
hours: '周一至周五 9:00-18:00'
}
]
if (!form.value.subject) {
errors.value.subject = '请选择主题'
// 常见问题
const faqs = [
{
question: '如何开始使用CMS系统',
answer: '您可以通过注册账号开始免费试用,我们提供详细的使用指南和在线客服支持。'
},
{
question: '系统支持哪些功能?',
answer: '我们的CMS系统支持内容管理、用户管理、权限控制、多媒体管理、SEO优化等全面功能。'
},
{
question: '是否提供技术支持?',
answer: '是的我们提供7x24小时技术支持包括在线客服、邮件支持和电话支持。'
},
{
question: '如何进行数据迁移?',
answer: '我们提供专业的数据迁移服务支持从各种主流CMS系统无缝迁移数据。'
},
{
question: '系统的安全性如何?',
answer: '我们采用企业级安全标准,包括数据加密、访问控制、定期备份等多重安全保障。'
}
if (!form.value.message.trim()) {
errors.value.message = '请输入消息内容'
} else if (form.value.message.trim().length < 10) {
errors.value.message = '消息内容至少需要10个字符'
}
if (!form.value.agree) {
errors.value.agree = '请同意隐私政策和服务条款'
}
return Object.keys(errors.value).length === 0
}
]
// 提交表单
const submitForm = async () => {
if (!validateForm()) {
return
}
loading.value = true
submitting.value = true
try {
// 这里应该调用API发送消息
console.log('发送消息:', form.value)
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 2000))
alert('消息发送成功!我们会尽快回复您。')
// 这里应该调用真实的API
console.log('提交表单:', form.value)
// 重置表单
form.value = {
name: '',
email: '',
phone: '',
subject: '',
message: '',
agree: false
company: '',
type: '',
message: ''
}
alert('消息发送成功!我们会尽快与您联系。')
} catch (error) {
console.error('发送失败', error)
console.error('提交失败:', error)
alert('发送失败,请稍后重试。')
} finally {
loading.value = false
submitting.value = false
}
}
// 切换FAQ
const toggleFaq = (index) => {
activeFaq.value = activeFaq.value === index ? null : index
}
</script>
<style scoped>
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.lg\:grid-cols-2 {
grid-template-columns: 1fr;
}
.space-y-8 > * + * {
margin-top: 2rem;
}
.space-y-6 > * + * {
margin-top: 1.5rem;
}
<style scoped lang="scss">
/* 动画效果 */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.shadow-tech-glow {
box-shadow: 0 0 20px rgba(37, 99, 235, 0.3);
}
/* 表单样式优化 */
input:focus,
select:focus,
textarea:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
textarea:focus,
select:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* 加载动画 */
@keyframes spin {
to {
transform: rotate(360deg);
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.md\:grid-cols-2 {
grid-template-columns: 1fr;
}
.grid-cols-1.lg\:grid-cols-2 {
grid-template-columns: 1fr;
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>
</style>

View File

@@ -1,171 +1,237 @@
<template>
<div>
<!-- 英雄区域 -->
<section class="bg-gradient-to-br from-blue-600 via-purple-600 to-indigo-800 text-white py-20 md:py-32 relative overflow-hidden">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
现代化的
<span class="bg-gradient-to-r from-yellow-400 to-orange-500 bg-clip-text text-transparent">
内容管理系统
</span>
</h1>
<p class="text-xl md:text-2xl mb-8 opacity-90 leading-relaxed">
高效安全易用的CMS解决方案助力您的内容创作与管理
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<router-link
to="/articles"
class="bg-white text-blue-600 px-8 py-4 rounded-lg font-semibold hover:bg-gray-100 transition-all duration-300 transform hover:scale-105 shadow-lg"
>
浏览文章
</router-link>
<router-link
to="/admin"
class="bg-transparent border-2 border-white text-white px-8 py-4 rounded-lg font-semibold hover:bg-white hover:text-blue-600 transition-all duration-300 transform hover:scale-105"
>
管理后台
</router-link>
<div class="min-h-screen" :class="themeStore.isDark ? 'bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900' : 'bg-gradient-to-br from-blue-50 via-purple-50 to-indigo-50'">
<!-- 轮播英雄区域 -->
<section class="relative overflow-hidden">
<div class="hero-carousel relative h-screen">
<!-- 轮播项 -->
<div
v-for="(slide, index) in heroSlides"
:key="index"
class="absolute inset-0 transition-all duration-1000 ease-in-out"
:class="currentSlide === index ? 'opacity-100 scale-100' : 'opacity-0 scale-105'"
>
<div
class="h-full flex items-center justify-center relative"
:class="slide.bgClass"
>
<!-- 背景装饰 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div
v-for="(decoration, i) in slide.decorations"
:key="i"
:class="decoration.class"
:style="decoration.style"
></div>
</div>
<!-- 内容 -->
<div class="container mx-auto px-4 relative z-10" data-aos="fade-up" :data-aos-delay="index * 200">
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
<span v-html="slide.title"></span>
</h1>
<p class="text-xl md:text-2xl mb-8 opacity-90 leading-relaxed">
{{ slide.subtitle }}
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<router-link
:to="slide.primaryAction.link"
class="bg-white/20 backdrop-blur-10 border border-white/30 text-white px-8 py-4 rounded-2xl font-semibold hover:bg-white/30 transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-white/20"
>
{{ slide.primaryAction.text }}
</router-link>
<button
@click="themeStore.toggleTheme"
class="bg-transparent border-2 border-white/50 text-white px-8 py-4 rounded-2xl font-semibold hover:bg-white/20 hover:border-white/70 transition-all duration-300 transform hover:scale-105 flex items-center justify-center gap-2"
>
<component :is="themeStore.isDark ? SunIcon : MoonIcon" class="w-5 h-5" />
{{ themeStore.isDark ? '亮色模式' : '暗色模式' }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 装饰性元素 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-40 -right-40 w-80 h-80 bg-white opacity-10 rounded-full"></div>
<div class="absolute -bottom-40 -left-40 w-96 h-96 bg-white opacity-5 rounded-full"></div>
<!-- 轮播指示器 -->
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 flex space-x-3 z-20">
<button
v-for="(slide, index) in heroSlides"
:key="index"
@click="currentSlide = index"
class="w-3 h-3 rounded-full transition-all duration-300"
:class="currentSlide === index ? 'bg-white scale-125' : 'bg-white/50 hover:bg-white/70'"
></button>
</div>
<!-- 自动播放进度条 -->
<div class="absolute bottom-0 left-0 h-1 bg-white/30 w-full">
<div
class="h-full bg-gradient-to-r from-blue-400 to-purple-400 transition-all duration-100 ease-linear"
:style="{ width: `${(slideProgress / slideDuration) * 100}%` }"
></div>
</div>
</section>
<!-- 特性介绍 -->
<section class="py-20 bg-white">
<!-- 科技感特性介绍 -->
<section class="py-20" :class="themeStore.isDark ? 'bg-gray-800/50' : 'bg-white/80'">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
为什么选择我们的CMS
<div class="text-center mb-16" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">
<span :class="themeStore.isDark ? 'text-blue-400' : 'text-blue-600'">
为什么选择我们的CMS
</span>
</h2>
<p class="text-xl text-gray-600 max-w-2xl mx-auto">
<p class="text-xl max-w-2xl mx-auto" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
我们提供完整的内容管理解决方案让您专注于创作优质内容
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="text-center group">
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:bg-blue-200 transition-colors">
<RocketIcon class="w-8 h-8 text-blue-600" />
<div
v-for="(feature, index) in features"
:key="index"
class="group cursor-pointer"
data-aos="fade-up"
:data-aos-delay="index * 100"
>
<div
class="text-center p-8 rounded-3xl transition-all duration-500 hover:-translate-y-2"
:class="themeStore.isDark
? 'bg-white/5 backdrop-blur-20 border border-white/10 hover:bg-white/10 hover:border-blue-400/30 hover:shadow-2xl hover:shadow-blue-500/20'
: 'bg-white/70 backdrop-blur-20 border border-white/20 hover:bg-white/90 hover:border-blue-400/30 hover:shadow-2xl hover:shadow-blue-500/20'"
>
<div
class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6 transition-all duration-300"
:class="feature.iconBg + ' group-hover:scale-110 group-hover:animate-pulse-glow'"
>
<component :is="feature.icon" class="w-8 h-8" :class="feature.iconColor" />
</div>
<h3 class="text-xl font-semibold mb-4" :class="themeStore.isDark ? 'text-white group-hover:text-blue-300' : 'text-gray-900 group-hover:text-blue-600'">
{{ feature.title }}
</h3>
<p class="leading-relaxed" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
{{ feature.description }}
</p>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-4">高效管理</h3>
<p class="text-gray-600 leading-relaxed">
直观的管理界面让内容创作和管理变得简单高效提升工作效率
</p>
</div>
<div class="text-center group">
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:bg-green-200 transition-colors">
<ShieldIcon class="w-8 h-8 text-green-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-4">安全可靠</h3>
<p class="text-gray-600 leading-relaxed">
企业级安全保障多层权限控制确保您的内容和数据安全
</p>
</div>
<div class="text-center group">
<div class="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:bg-purple-200 transition-colors">
<DeviceIcon class="w-8 h-8 text-purple-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-4">响应式设计</h3>
<p class="text-gray-600 leading-relaxed">
完美适配各种设备无论是桌面端还是移动端都有出色的体验
</p>
</div>
</div>
</div>
</div>
</section>
<!-- 统计数据 -->
<section class="py-20 bg-gray-50">
<div class="container mx-auto px-4">
<!-- 科技感统计数据 -->
<section class="py-20 relative overflow-hidden" :class="themeStore.isDark ? 'bg-gray-900/50' : 'bg-gray-50/80'">
<!-- 背景装饰 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-blue-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-purple-400 rounded-full animate-twinkle" style="animation-delay: 1s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-blue-300 rounded-full animate-twinkle" style="animation-delay: 2s;"></div>
</div>
<div class="container mx-auto px-4 relative z-10">
<div class="max-w-6xl mx-auto">
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
<div class="text-center">
<div class="text-3xl md:text-4xl font-bold text-blue-600 mb-2">{{ stats.articles }}+</div>
<div class="text-gray-600">技术文章</div>
</div>
<div class="text-center">
<div class="text-3xl md:text-4xl font-bold text-green-600 mb-2">{{ stats.users }}+</div>
<div class="text-gray-600">注册用户</div>
</div>
<div class="text-center">
<div class="text-3xl md:text-4xl font-bold text-purple-600 mb-2">{{ stats.views }}+</div>
<div class="text-gray-600">总浏览量</div>
</div>
<div class="text-center">
<div class="text-3xl md:text-4xl font-bold text-orange-600 mb-2">{{ stats.news }}+</div>
<div class="text-gray-600">新闻资讯</div>
<div
v-for="(stat, index) in stats"
:key="index"
class="text-center group"
data-aos="fade-up"
:data-aos-delay="index * 100"
>
<div class="text-3xl md:text-4xl font-bold mb-2 text-blue-500 group-hover:scale-110 transition-transform duration-300">
{{ stat.value }}+
</div>
<div :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
{{ stat.label }}
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 最新文章 -->
<section class="py-20 bg-white">
<!-- 最新文章 - 科技感卡片 -->
<section class="py-20" :class="themeStore.isDark ? 'bg-gray-800/30' : 'bg-white/60'">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
<div class="flex items-center justify-between mb-12">
<div class="flex items-center justify-between mb-12" data-aos="fade-up">
<div>
<h2 class="text-3xl font-bold text-gray-900 mb-2">最新文章</h2>
<p class="text-gray-600">探索最新的技术趋势和开发经验</p>
<h2 class="text-3xl font-bold mb-2" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">
<span :class="themeStore.isDark ? 'text-blue-400' : 'text-blue-600'">
最新文章
</span>
</h2>
<p :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
探索最新的技术趋势和开发经验
</p>
</div>
<router-link
to="/articles"
class="text-blue-600 hover:text-blue-800 font-medium flex items-center group"
class="font-medium flex items-center group transition-colors duration-300"
:class="themeStore.isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-800'"
>
查看全部
<ArrowRightIcon class="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform" />
</router-link>
</div>
<!-- 文章列表 -->
<!-- 科技感文章卡片 -->
<div v-if="featuredArticles.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<article
v-for="article in featuredArticles"
v-for="(article, index) in featuredArticles"
:key="article.id"
class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-all duration-300 group cursor-pointer"
class="group cursor-pointer"
@click="goToArticle(article.id)"
data-aos="fade-up"
:data-aos-delay="index * 100"
>
<div class="relative overflow-hidden h-48">
<img
:src="article.cover || 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=400&h=300&fit=crop'"
:alt="article.title"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
<div class="absolute top-4 left-4">
<span class="bg-blue-600 text-white px-2 py-1 rounded text-xs font-medium">
{{ article.category }}
</span>
<div
class="rounded-3xl overflow-hidden transition-all duration-500 hover:-translate-y-2 relative"
:class="themeStore.isDark
? 'bg-white/5 backdrop-blur-20 border border-white/10 hover:bg-white/10 hover:border-blue-400/30 hover:shadow-2xl hover:shadow-blue-500/20'
: 'bg-white/70 backdrop-blur-20 border border-white/20 hover:bg-white/90 hover:border-blue-400/30 hover:shadow-2xl hover:shadow-blue-500/20'"
>
<!-- 悬浮发光边框 -->
<div class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-purple-500 rounded-3xl opacity-0 group-hover:opacity-75 transition-opacity duration-300 -z-10 blur-sm"></div>
<div class="relative overflow-hidden h-48">
<img
:src="article.cover || 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=400&h=300&fit=crop'"
:alt="article.title"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="absolute top-4 left-4">
<span class="bg-gradient-to-r from-blue-500 to-purple-500 text-white px-3 py-1 rounded-full text-xs font-medium">
{{ article.category }}
</span>
</div>
</div>
</div>
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-2 line-clamp-2 group-hover:text-blue-600 transition-colors">
{{ article.title }}
</h3>
<p class="text-gray-600 text-sm mb-4 line-clamp-3">
{{ article.summary }}
</p>
<div class="flex items-center justify-between text-xs text-gray-500">
<span>{{ formatDate(article.created_at) }}</span>
<span>{{ article.views }} 阅读</span>
<div class="p-6">
<h3 class="text-lg font-semibold mb-2 line-clamp-2 transition-colors duration-300"
:class="themeStore.isDark ? 'text-white group-hover:text-blue-300' : 'text-gray-900 group-hover:text-blue-600'">
{{ article.title }}
</h3>
<p class="text-sm mb-4 line-clamp-3" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
{{ article.summary }}
</p>
<div class="flex items-center justify-between text-xs" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<span>{{ formatDate(article.created_at) }}</span>
<span class="flex items-center">
<EyeIcon class="w-4 h-4 mr-1" />
{{ article.views }} 阅读
</span>
</div>
</div>
</div>
</article>
</div>
<!-- 空状态 -->
<EmptyState
<TechEmptyState
v-else
title="暂时没有文章"
description="敬请期待更多精彩内容"
@@ -174,18 +240,25 @@
</div>
</section>
<!-- 最新新闻 -->
<section class="py-20 bg-gray-50">
<!-- 最新新闻 - 科技感设计 -->
<section class="py-20" :class="themeStore.isDark ? 'bg-gray-900/30' : 'bg-gray-50/60'">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
<div class="flex items-center justify-between mb-12">
<div class="flex items-center justify-between mb-12" data-aos="fade-up">
<div>
<h2 class="text-3xl font-bold text-gray-900 mb-2">新闻资讯</h2>
<p class="text-gray-600">关注行业动态掌握最新资讯</p>
<h2 class="text-3xl font-bold mb-2" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">
<span :class="themeStore.isDark ? 'text-blue-400' : 'text-blue-600'">
新闻资讯
</span>
</h2>
<p :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
关注行业动态掌握最新资讯
</p>
</div>
<router-link
to="/news"
class="text-blue-600 hover:text-blue-800 font-medium flex items-center group"
class="font-medium flex items-center group transition-colors duration-300"
:class="themeStore.isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-800'"
>
查看全部
<ArrowRightIcon class="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform" />
@@ -197,16 +270,25 @@
<div
class="relative group cursor-pointer"
@click="goToNews(featuredNews[0].id)"
data-aos="fade-right"
>
<div class="relative overflow-hidden rounded-lg h-64">
<div
class="relative overflow-hidden rounded-3xl h-64 transition-all duration-500 hover:-translate-y-2"
:class="themeStore.isDark
? 'bg-white/5 backdrop-blur-20 border border-white/10 hover:border-blue-400/30 hover:shadow-2xl hover:shadow-blue-500/20'
: 'bg-white/70 backdrop-blur-20 border border-white/20 hover:border-blue-400/30 hover:shadow-2xl hover:shadow-blue-500/20'"
>
<!-- 悬浮发光边框 -->
<div class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-purple-500 rounded-3xl opacity-0 group-hover:opacity-75 transition-opacity duration-300 -z-10 blur-sm"></div>
<img
:src="featuredNews[0].cover || 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=400&h=300&fit=crop'"
:src="featuredNews[0].cover || 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=500&h=300&fit=crop'"
:alt="featuredNews[0].title"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent"></div>
<div class="absolute bottom-6 left-6 right-6">
<span class="bg-red-500 text-white px-2 py-1 rounded text-xs font-medium mb-3 inline-block">
<span class="bg-gradient-to-r from-red-500 to-pink-500 text-white px-3 py-1 rounded-full text-xs font-medium mb-3 inline-block">
热门
</span>
<h3 class="text-white text-xl font-bold mb-2 line-clamp-2">
@@ -220,23 +302,27 @@
</div>
<!-- 其他新闻 -->
<div class="space-y-4">
<div class="space-y-4" data-aos="fade-left">
<div
v-for="news in featuredNews.slice(1, 4)"
v-for="(news, index) in featuredNews.slice(1, 4)"
:key="news.id"
class="flex space-x-4 group cursor-pointer"
class="flex space-x-4 group cursor-pointer p-4 rounded-2xl transition-all duration-300"
:class="themeStore.isDark
? 'hover:bg-white/5 hover:backdrop-blur-20'
: 'hover:bg-white/50 hover:backdrop-blur-20'"
@click="goToNews(news.id)"
>
<img
:src="news.cover || 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=400&h=300&fit=crop'"
:alt="news.title"
class="w-20 h-16 object-cover rounded-lg flex-shrink-0"
class="w-20 h-16 object-cover rounded-xl flex-shrink-0 group-hover:scale-105 transition-transform duration-300"
/>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 line-clamp-2 group-hover:text-blue-600 transition-colors">
<h4 class="text-sm font-medium line-clamp-2 transition-colors duration-300"
:class="themeStore.isDark ? 'text-white group-hover:text-blue-300' : 'text-gray-900 group-hover:text-blue-600'">
{{ news.title }}
</h4>
<p class="text-xs text-gray-500 mt-1">
<p class="text-xs mt-1" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
{{ formatDate(news.created_at) }}
</p>
</div>
@@ -245,7 +331,7 @@
</div>
<!-- 空状态 -->
<EmptyState
<TechEmptyState
v-else
title="暂时没有新闻"
description="敬请期待更多精彩内容"
@@ -257,11 +343,13 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
import { useThemeStore } from '@/stores/theme'
import TechEmptyState from '@/components/TechEmptyState.vue'
const router = useRouter()
const themeStore = useThemeStore()
// 图标组件
const RocketIcon = {
@@ -280,13 +368,90 @@ const ArrowRightIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>`
}
// 响应式数据
const stats = ref({
articles: 128,
users: 1024,
views: 50000,
news: 86
})
const EyeIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>`
}
const SunIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>`
}
const MoonIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>`
}
// 轮播数据
const currentSlide = ref(0)
const slideProgress = ref(0)
const slideDuration = 5000 // 5秒
let slideInterval = null
let progressInterval = null
const heroSlides = ref([
{
title: '现代化的<span class="text-yellow-400">内容管理系统</span>',
subtitle: '高效、安全、易用的CMS解决方案助力您的内容创作与管理',
bgClass: 'bg-gradient-to-br from-blue-600 via-purple-600 to-indigo-800 text-white',
primaryAction: { text: '浏览文章', link: '/articles' },
decorations: [
{ class: 'absolute -top-40 -right-40 w-80 h-80 bg-white opacity-10 rounded-full animate-float', style: '' },
{ class: 'absolute -bottom-40 -left-40 w-96 h-96 bg-white opacity-5 rounded-full animate-float', style: 'animation-delay: 1s;' }
]
},
{
title: '<span class="text-green-400">科技感</span>设计体验',
subtitle: '采用最新的设计语言,玻璃拟态效果与流畅动画,带来极致的视觉体验',
bgClass: 'bg-gradient-to-br from-green-600 via-teal-600 to-blue-800 text-white',
primaryAction: { text: '体验设计', link: '/news' },
decorations: [
{ class: 'absolute top-1/4 left-1/4 w-32 h-32 bg-white opacity-10 rounded-full animate-pulse-glow', style: '' },
{ class: 'absolute bottom-1/3 right-1/4 w-24 h-24 bg-white opacity-15 rounded-full animate-twinkle', style: 'animation-delay: 2s;' }
]
},
{
title: '<span class="text-pink-400">智能化</span>内容管理',
subtitle: 'AI驱动的内容推荐智能标签分类让内容管理变得更加高效便捷',
bgClass: 'bg-gradient-to-br from-pink-600 via-purple-600 to-indigo-800 text-white',
primaryAction: { text: '了解更多', link: '/about' },
decorations: [
{ class: 'absolute top-1/2 left-10 w-16 h-16 bg-white opacity-20 rounded-full animate-float', style: 'animation-delay: 0.5s;' },
{ class: 'absolute top-1/4 right-20 w-20 h-20 bg-white opacity-15 rounded-full animate-twinkle', style: 'animation-delay: 1.5s;' }
]
}
])
// 特性数据
const features = ref([
{
title: '高效管理',
description: '直观的管理界面,让内容创作和管理变得简单高效,提升工作效率',
icon: RocketIcon,
iconBg: 'bg-gradient-to-r from-blue-500 to-blue-600',
iconColor: 'text-white'
},
{
title: '安全可靠',
description: '企业级安全保障,多层权限控制,确保您的内容和数据安全',
icon: ShieldIcon,
iconBg: 'bg-gradient-to-r from-green-500 to-green-600',
iconColor: 'text-white'
},
{
title: '响应式设计',
description: '完美适配各种设备,无论是桌面端还是移动端都有出色的体验',
icon: DeviceIcon,
iconBg: 'bg-gradient-to-r from-purple-500 to-purple-600',
iconColor: 'text-white'
}
])
// 统计数据
const stats = ref([
{ value: 128, label: '技术文章' },
{ value: 1024, label: '注册用户' },
{ value: 50000, label: '总浏览量' },
{ value: 86, label: '新闻资讯' }
])
const featuredArticles = ref([
{
@@ -345,6 +510,29 @@ const featuredNews = ref([
}
])
// 轮播控制
const startSlideshow = () => {
slideInterval = setInterval(() => {
currentSlide.value = (currentSlide.value + 1) % heroSlides.value.length
slideProgress.value = 0
}, slideDuration)
progressInterval = setInterval(() => {
slideProgress.value += 100
}, 100)
}
const stopSlideshow = () => {
if (slideInterval) {
clearInterval(slideInterval)
slideInterval = null
}
if (progressInterval) {
clearInterval(progressInterval)
progressInterval = null
}
}
// 方法
const formatDate = (dateString) => {
const date = new Date(dateString)
@@ -365,12 +553,16 @@ const goToNews = (id) => {
// 生命周期
onMounted(() => {
// 这里可以调用API获取真实数据
startSlideshow()
console.log('首页已加载')
})
onUnmounted(() => {
stopSlideshow()
})
</script>
<style scoped>
<style scoped lang="scss">
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
@@ -384,4 +576,32 @@ onMounted(() => {
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
/* 自定义动画 */
@keyframes twinkle {
0% { opacity: 0.3; transform: scale(1); }
100% { opacity: 1; transform: scale(1.2); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
@keyframes pulse-glow {
0% { box-shadow: 0 0 20px rgba(37, 99, 235, 0.3); }
100% { box-shadow: 0 0 40px rgba(37, 99, 235, 0.6); }
}
.animate-twinkle {
animation: twinkle 2s ease-in-out infinite alternate;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-pulse-glow {
animation: pulse-glow 2s ease-in-out infinite alternate;
}
</style>

View File

@@ -1,271 +1,300 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="min-h-screen relative" :class="themeStore.isDark
? 'bg-gradient-to-br from-gray-900 via-gray-800 to-purple-900'
: 'bg-gradient-to-br from-gray-50 via-blue-50 to-purple-50'">
<!-- 装饰元素 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<template v-if="!themeStore.isDark">
<div class="absolute top-20 left-10 w-72 h-72 bg-blue-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
</template>
<template v-else>
<div class="absolute top-20 left-10 w-72 h-72 bg-orange-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-red-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
</template>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center min-h-screen">
<LoadingIcon class="w-8 h-8 animate-spin text-blue-600" />
<div class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 mx-auto mb-4" :class="themeStore.isDark ? 'border-orange-500' : 'border-blue-500'"></div>
<p :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-600'">加载中...</p>
</div>
</div>
<div v-else-if="!news" class="flex items-center justify-center min-h-screen">
<!-- 错误状态 -->
<div v-else-if="error" class="flex items-center justify-center min-h-screen">
<div class="text-center">
<h2 class="text-2xl font-bold text-gray-900 mb-4">新闻不存在</h2>
<p class="text-gray-600 mb-8">抱歉您访问的新闻不存在或已被删除</p>
<router-link
to="/news"
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
<h2 class="text-2xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">新闻加载失败</h2>
<p :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-600'" class="mb-6">{{ error }}</p>
<button
@click="fetchNews"
class="px-6 py-3 text-white font-medium rounded-2xl transition-all duration-300"
:class="themeStore.isDark
? 'bg-gradient-to-r from-orange-600 to-red-500'
: 'bg-gradient-to-r from-blue-600 to-purple-500'"
>
重新加载
</button>
</div>
</div>
<!-- 新闻内容 -->
<div v-else-if="news" class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 返回按钮 -->
<div class="mb-8">
<button
@click="goBack"
class="flex items-center transition-colors duration-300"
:class="themeStore.isDark ? 'text-gray-400 hover:text-orange-400' : 'text-gray-600 hover:text-blue-500'"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
返回新闻列表
</button>
</div>
<!-- 新闻头部 -->
<header class="mb-12">
<div class="backdrop-blur-lg border rounded-3xl p-8 lg:p-12" :class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/60 border-white/20'">
<!-- 分类标签 -->
<div class="mb-4">
<span
:class="getCategoryClass(news.category)"
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
>
{{ getCategoryName(news.category) }}
</span>
</div>
<!-- 标题 -->
<h1 class="text-3xl lg:text-4xl font-bold mb-6 leading-tight" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">
{{ news.title }}
</h1>
<!-- 摘要 -->
<p v-if="news.summary" class="text-xl mb-8 leading-relaxed" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
{{ news.summary }}
</p>
<!-- 元信息 -->
<div class="flex flex-wrap items-center gap-6 text-sm" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
<span>{{ news.author || '编辑部' }}</span>
</div>
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<span>{{ formatDate(news.publishedAt) }}</span>
</div>
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<span>{{ news.views || 0 }} 次浏览</span>
</div>
</div>
</div>
</header>
<!-- 封面图片 -->
<div v-if="news.image" class="mb-12">
<div class="relative overflow-hidden rounded-3xl">
<img
:src="news.image"
:alt="news.title"
class="w-full h-64 lg:h-96 object-cover"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
</div>
<!-- 正文 -->
<main class="mb-12">
<div class="backdrop-blur-lg border rounded-3xl p-8 lg:p-12" :class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/60 border-white/20'">
<div class="prose prose-lg max-w-none" :class="themeStore.isDark ? 'prose-invert' : ''">
<div v-html="news.content"></div>
</div>
</div>
</main>
<!-- 操作按钮 -->
<div class="flex items-center justify-between mb-12">
<div class="flex items-center space-x-4">
<button
@click="shareNews"
class="flex items-center px-6 py-3 border rounded-2xl font-medium transition-all duration-300"
:class="themeStore.isDark
? 'bg-white/10 border-white/20 text-gray-300 hover:bg-white/20'
: 'bg-white/60 border-white/20 text-gray-600 hover:bg-white/80'"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
</svg>
分享新闻
</button>
<button
@click="collectNews"
:class="[
'flex items-center px-6 py-3 rounded-2xl font-medium transition-all duration-300',
isCollected
? 'bg-gradient-to-r from-yellow-500 to-orange-500 text-white'
: themeStore.isDark
? 'bg-white/10 border border-white/20 text-gray-300 hover:bg-white/20'
: 'bg-white/60 border border-white/20 text-gray-600 hover:bg-white/80'
]"
>
<svg class="w-5 h-5 mr-2" :fill="isCollected ? 'currentColor' : 'none'" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
</svg>
{{ isCollected ? '已收藏' : '收藏' }}
</button>
</div>
<button
@click="scrollToTop"
class="flex items-center px-6 py-3 text-white rounded-2xl font-medium transition-all duration-300"
:class="themeStore.isDark
? 'bg-gradient-to-r from-orange-600 to-red-500'
: 'bg-gradient-to-r from-blue-600 to-purple-500'"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"></path>
</svg>
回到顶部
</button>
</div>
<!-- 相关新闻 -->
<section v-if="relatedNews.length" class="mb-12">
<div class="backdrop-blur-lg border rounded-3xl p-8" :class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-white/60 border-white/20'">
<h2 class="text-2xl font-bold mb-6" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">相关新闻</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<article
v-for="relatedItem in relatedNews"
:key="relatedItem.id"
class="group cursor-pointer"
@click="navigateToNews(relatedItem.id)"
>
<div class="backdrop-blur-lg border rounded-2xl p-6 transition-all duration-300 hover:-translate-y-2"
:class="themeStore.isDark
? 'bg-white/5 border-white/10 hover:border-orange-500/30'
: 'bg-white/60 border-white/20 hover:border-blue-500/30'">
<img
v-if="relatedItem.image"
:src="relatedItem.image"
:alt="relatedItem.title"
class="w-full h-32 object-cover rounded-xl mb-4"
/>
<div class="mb-3">
<span
:class="getCategoryClass(relatedItem.category)"
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
>
{{ getCategoryName(relatedItem.category) }}
</span>
</div>
<h3 class="text-lg font-semibold mb-2 line-clamp-2 transition-colors duration-300"
:class="themeStore.isDark
? 'text-white group-hover:text-orange-400'
: 'text-gray-900 group-hover:text-blue-600'">
{{ relatedItem.title }}
</h3>
<p class="text-sm line-clamp-2 mb-3" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
{{ relatedItem.summary }}
</p>
<div class="flex items-center text-xs" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
{{ formatDate(relatedItem.publishedAt) }}
</div>
</div>
</article>
</div>
</div>
</section>
</div>
<!-- 新闻不存在 -->
<div v-else class="flex items-center justify-center min-h-screen">
<div class="text-center">
<h2 class="text-2xl font-bold mb-4" :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">新闻不存在</h2>
<p :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-600'" class="mb-6">您访问的新闻可能已被删除或不存在</p>
<router-link
to="/news"
class="inline-flex items-center px-6 py-3 text-white font-medium rounded-2xl transition-all duration-300"
:class="themeStore.isDark
? 'bg-gradient-to-r from-orange-600 to-red-500'
: 'bg-gradient-to-r from-blue-600 to-purple-500'"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
返回新闻列表
</router-link>
</div>
</div>
<article v-else class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 面包屑导航 -->
<nav class="flex mb-8" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3">
<li class="inline-flex items-center">
<router-link to="/" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600">
<HomeIcon class="w-4 h-4 mr-2" />
首页
</router-link>
</li>
<li>
<div class="flex items-center">
<ChevronRightIcon class="w-4 h-4 text-gray-400" />
<router-link to="/news" class="ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2">
新闻资讯
</router-link>
</div>
</li>
<li aria-current="page">
<div class="flex items-center">
<ChevronRightIcon class="w-4 h-4 text-gray-400" />
<span class="ml-1 text-sm font-medium text-gray-500 md:ml-2 truncate">
{{ news.title }}
</span>
</div>
</li>
</ol>
</nav>
<!-- 新闻头部 -->
<header class="bg-white rounded-lg shadow-sm p-8 mb-8">
<div class="mb-4">
<span
:class="[
'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium',
getCategoryClass(news.category)
]"
>
{{ getCategoryName(news.category) }}
</span>
</div>
<h1 class="text-3xl md:text-4xl font-bold text-gray-900 mb-6 leading-tight">
{{ news.title }}
</h1>
<div class="flex flex-wrap items-center gap-6 text-sm text-gray-500 mb-6">
<div class="flex items-center">
<UserIcon class="w-4 h-4 mr-2" />
<span>{{ news.author || '管理员' }}</span>
</div>
<div class="flex items-center">
<CalendarIcon class="w-4 h-4 mr-2" />
<span>{{ formatDate(news.publishedAt) }}</span>
</div>
<div class="flex items-center">
<EyeIcon class="w-4 h-4 mr-2" />
<span>{{ news.views || 0 }} 次浏览</span>
</div>
</div>
<!-- 摘要 -->
<div v-if="news.summary" class="bg-blue-50 border-l-4 border-blue-400 p-4 rounded-r-lg">
<p class="text-blue-800 font-medium">{{ news.summary }}</p>
</div>
</header>
<!-- 新闻内容 -->
<div class="bg-white rounded-lg shadow-sm p-8 mb-8">
<!-- 特色图片 -->
<div v-if="news.image" class="mb-8">
<img
:src="news.image"
:alt="news.title"
class="w-full h-64 md:h-96 object-cover rounded-lg shadow-lg"
/>
</div>
<!-- 正文内容 -->
<div class="prose prose-lg max-w-none">
<div v-html="news.content"></div>
</div>
</div>
<!-- 相关新闻 -->
<div v-if="relatedNews.length > 0" class="bg-white rounded-lg shadow-sm p-8">
<h3 class="text-2xl font-bold text-gray-900 mb-6">相关新闻</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<article
v-for="item in relatedNews"
:key="item.id"
class="flex gap-4 p-4 rounded-lg border border-gray-200 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer"
@click="goToNews(item.id)"
>
<img
:src="item.image || '/placeholder.svg?height=80&width=120'"
:alt="item.title"
class="w-20 h-20 object-cover rounded-lg flex-shrink-0"
/>
<div class="flex-1 min-w-0">
<h4 class="text-lg font-semibold text-gray-900 mb-2 line-clamp-2">
{{ item.title }}
</h4>
<div class="flex items-center text-sm text-gray-500">
<CalendarIcon class="w-4 h-4 mr-1" />
<span>{{ formatDate(item.publishedAt) }}</span>
</div>
</div>
</article>
</div>
</div>
</article>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { newsAPI } from '@/api'
import { useThemeStore } from '@/stores/theme'
const route = useRoute()
const router = useRouter()
// 图标组件
const LoadingIcon = {
template: `<svg fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>`
}
const HomeIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>`
}
const ChevronRightIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>`
}
const UserIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>`
}
const CalendarIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>`
}
const EyeIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>`
}
const themeStore = useThemeStore()
// 响应式数据
const loading = ref(true)
const error = ref(null)
const news = ref(null)
const relatedNews = ref([])
const isCollected = ref(false)
// 模拟新闻数据
const mockNewsData = {
1: {
id: 1,
title: '公司成功获得ISO9001质量管理体系认证',
summary: '经过严格的审核流程我们公司正式获得ISO9001质量管理体系认证这标志着我们在质量管理方面达到了国际先进水平。',
content: `
<p>经过为期三个月的严格审核流程我们公司正式获得了ISO9001质量管理体系认证。这一重要里程碑标志着我们在质量管理方面达到了国际先进水平为公司的可持续发展奠定了坚实基础。</p>
<h3>认证过程</h3>
<p>ISO9001认证过程包括了文件审核、现场审核等多个环节。审核专家对我们的质量管理体系进行了全面评估涵盖了从产品设计、生产制造到售后服务的全过程。</p>
<h3>认证意义</h3>
<p>获得ISO9001认证不仅是对我们质量管理水平的权威认可更是我们持续改进、追求卓越的重要动力。这将有助于</p>
<ul>
<li>提升产品和服务质量</li>
<li>增强客户信任和满意度</li>
<li>提高运营效率</li>
<li>增强市场竞争力</li>
</ul>
<h3>未来展望</h3>
<p>我们将以此次认证为新起点,继续完善质量管理体系,不断提升产品和服务质量,为客户创造更大价值。</p>
`,
category: 'company',
author: '管理员',
publishedAt: '2024-01-15',
views: 1250,
image: 'https://images.unsplash.com/photo-1560472354-b33ff0c44a43?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2126&q=80'
},
2: {
id: 2,
title: '行业数字化转型趋势分析报告发布',
summary: '我们发布了最新的行业数字化转型趋势分析报告,深入分析了当前市场的发展趋势和未来机遇。',
content: `
<p>随着数字技术的快速发展,各行各业都在经历着深刻的数字化转型。我们的研究团队经过深入调研和分析,发布了这份行业数字化转型趋势分析报告。</p>
<h3>主要发现</h3>
<p>报告显示,数字化转型已成为企业发展的必然趋势,主要体现在以下几个方面:</p>
<ul>
<li>云计算技术的广泛应用</li>
<li>人工智能在业务流程中的深度融合</li>
<li>数据驱动决策成为主流</li>
<li>移动办公模式的普及</li>
</ul>
<h3>发展机遇</h3>
<p>数字化转型为企业带来了前所未有的发展机遇,包括提高运营效率、优化客户体验、创新商业模式等。</p>
<h3>挑战与应对</h3>
<p>同时,数字化转型也面临着技术复杂性、人才短缺、安全风险等挑战。企业需要制定合适的策略来应对这些挑战。</p>
`,
category: 'industry',
author: '研究部',
publishedAt: '2024-01-14',
views: 890,
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80'
}
}
// 相关新闻数据
const mockRelatedNews = [
{
id: 3,
title: '新产品发布会将于下月举行',
publishedAt: '2024-01-13',
image: 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80'
},
{
id: 4,
title: '技术团队分享最新开发经验',
publishedAt: '2024-01-12',
image: 'https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80'
}
]
// 新闻分类
const categories = ref([
{ key: 'company', name: '公司新闻' },
{ key: 'industry', name: '行业动态' },
{ key: 'product', name: '产品资讯' },
{ key: 'technology', name: '技术分享' }
])
// 获取分类名称
const getCategoryName = (category) => {
const categoryMap = {
company: '公司新闻',
industry: '行业动态',
product: '产品资讯',
technology: '技术分享'
}
return categoryMap[category] || '其他'
const categoryItem = categories.value.find(cat => cat.key === category)
return categoryItem ? categoryItem.name : '其他'
}
// 获取分类样式
const getCategoryClass = (category) => {
const classes = {
company: 'bg-blue-100 text-blue-800',
industry: 'bg-green-100 text-green-800',
product: 'bg-purple-100 text-purple-800',
technology: 'bg-orange-100 text-orange-800'
if (themeStore.isDark) {
const classes = {
company: 'bg-blue-900/20 text-blue-400 border border-blue-500/30',
industry: 'bg-green-900/20 text-green-400 border border-green-500/30',
product: 'bg-purple-900/20 text-purple-400 border border-purple-500/30',
technology: 'bg-orange-900/20 text-orange-400 border border-orange-500/30'
}
return classes[category] || 'bg-gray-900/20 text-gray-400 border border-gray-500/30'
} else {
const classes = {
company: 'bg-blue-100 text-blue-700 border border-blue-300',
industry: 'bg-green-100 text-green-700 border border-green-300',
product: 'bg-purple-100 text-purple-700 border border-purple-300',
technology: 'bg-orange-100 text-orange-700 border border-orange-300'
}
return classes[category] || 'bg-gray-100 text-gray-700 border border-gray-300'
}
return classes[category] || 'bg-gray-100 text-gray-800'
}
// 格式化日期
@@ -279,116 +308,187 @@ const formatDate = (dateString) => {
}
// 获取新闻详情
const fetchNewsDetail = async () => {
const fetchNews = async () => {
loading.value = true
error.value = null
try {
const newsId = route.params.id
const newsId = parseInt(route.params.id)
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500))
news.value = mockNewsData[newsId] || null
if (news.value) {
// 增加浏览量
news.value.views = (news.value.views || 0) + 1
// 获取相关新闻
relatedNews.value = mockRelatedNews
// 模拟新闻数据
const mockNews = {
id: newsId,
title: '公司成功获得ISO9001质量管理体系认证',
summary: '经过严格的审核流程我们公司正式获得ISO9001质量管理体系认证这标志着我们在质量管理方面达到了国际先进水平。',
content: `
<h2>认证意义</h2>
<p>ISO9001是国际标准化组织ISO制定的质量管理体系标准是全球公认的质量管理体系认证。获得此认证意味着我们的产品和服务质量得到了国际权威机构的认可有助于提升公司形象和客户信任度。</p>
<h2>认证过程</h2>
<p>认证过程历时3个月包括以下几个阶段</p>
<ol>
<li><strong>内部审核准备</strong>:组建专项团队,对公司各部门进行全面质量管理培训</li>
<li><strong>文件审核</strong>:编制和完善质量管理手册、程序文件和作业指导书</li>
<li><strong>现场审核</strong>:认证机构专家对公司进行为期一周的现场审核</li>
<li><strong>整改完善</strong>:针对审核中发现的问题进行整改和完善</li>
<li><strong>获得认证</strong>通过最终评审获得ISO9001质量管理体系认证证书</li>
</ol>
<h2>关键指标</h2>
<p>我们在认证过程中建立了以下关键绩效指标KPI</p>
<ul>
<li>客户满意度≥95%当前值97.2%</li>
<li>缺陷率≤0.1%当前值0.08%</li>
<li>按时交付率≥98%当前值99.1%</li>
<li>员工培训完成率100%</li>
</ul>
<h2>未来规划</h2>
<p>获得认证只是起点,我们将持续改进质量管理体系,不断提升产品和服务质量,为客户创造更大价值。</p>
<p>下一步,我们计划:</p>
<ul>
<li>深化质量管理培训,提升全员质量意识</li>
<li>优化生产流程,减少不必要的浪费</li>
<li>加强供应商管理,确保原材料质量</li>
<li>完善客户反馈机制,及时响应客户需求</li>
</ul>
<p>我们相信,通过持续的质量改进和技术创新,公司将在激烈的市场竞争中赢得更多客户的信赖和支持。</p>
`,
category: 'company',
author: '管理员',
publishedAt: '2024-01-15',
views: 1250,
image: 'https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=800&h=400&fit=crop',
source: '公司官网'
}
} catch (error) {
console.error('获取新闻详情失败:', error)
news.value = null
news.value = mockNews
// 模拟相关新闻
relatedNews.value = [
{
id: 2,
title: '行业数字化转型趋势分析报告发布',
summary: '我们发布了最新的行业数字化转型趋势分析报告,深入分析了当前市场的发展趋势和未来机遇。',
category: 'industry',
publishedAt: '2024-01-14',
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400&h=200&fit=crop'
},
{
id: 3,
title: '新产品发布会将于下月举行',
summary: '我们将在下个月举办新产品发布会,届时将展示我们最新的技术成果和产品创新。',
category: 'product',
publishedAt: '2024-01-13',
image: 'https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=400&h=200&fit=crop'
},
{
id: 4,
title: '技术团队分享最新开发经验',
summary: '我们的技术团队在最新的技术分享会上,分享了在项目开发过程中积累的宝贵经验和最佳实践。',
category: 'technology',
publishedAt: '2024-01-12',
image: 'https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=400&h=200&fit=crop'
}
]
} catch (err) {
console.error('获取新闻详情失败:', err)
error.value = '获取新闻详情失败,请稍后再试'
} finally {
loading.value = false
}
}
// 跳转到其他新闻
const goToNews = (id) => {
router.push(`/news/${id}`)
// 返回上一页
const goBack = () => {
router.back()
}
// 组件挂载时获取数据
onMounted(() => {
fetchNewsDetail()
})
// 分享新闻
const shareNews = () => {
alert('分享功能即将上线')
}
// 监听路由变化
import { watch } from 'vue'
watch(() => route.params.id, () => {
if (route.params.id) {
fetchNewsDetail()
// 收藏新闻
const collectNews = () => {
isCollected.value = !isCollected.value
if (isCollected.value) {
console.log('收藏成功')
} else {
console.log('取消收藏')
}
}
// 滚动到顶部
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
})
}
// 导航到其他新闻
const navigateToNews = (newsId) => {
router.push({ name: 'news-detail', params: { id: newsId } })
}
// 生命周期钩子
onMounted(() => {
fetchNews()
})
</script>
<style scoped>
/* 文本截断 */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 文章内容样式 */
.prose {
color: #374151;
line-height: 1.75;
color: inherit;
}
.prose h3 {
font-size: 1.5rem;
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
color: inherit;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 1rem;
color: #1f2937;
}
.prose h2 {
font-size: 1.5rem;
border-bottom: 2px solid;
padding-bottom: 0.5rem;
}
.prose p {
margin-bottom: 1.25rem;
margin-bottom: 1rem;
line-height: 1.7;
}
.prose ul {
margin: 1.25rem 0;
padding-left: 1.625rem;
.prose ul,
.prose ol {
margin: 1rem 0;
padding-left: 2rem;
}
.prose li {
margin-bottom: 0.5rem;
}
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.md\:grid-cols-2 {
grid-template-columns: 1fr;
}
.text-3xl.md\:text-4xl {
font-size: 1.875rem;
}
.flex-wrap {
flex-direction: column;
align-items: flex-start;
}
.gap-6 {
gap: 1rem;
}
.prose strong {
font-weight: 600;
}
/* 加载动画 */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -1,30 +1,83 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 页面头部 -->
<div class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="text-center">
<h1 class="text-4xl font-bold text-gray-900 mb-4">新闻资讯</h1>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
了解最新的行业动态和公司资讯
</p>
<div class="min-h-screen relative" :class="themeStore.isDark
? 'bg-gradient-to-br from-gray-900 via-gray-800 to-purple-900'
: 'bg-gradient-to-br from-gray-50 via-blue-50 to-purple-50'">
<!-- 装饰元素 - 全局 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<!-- 亮色模式装饰 -->
<template v-if="!themeStore.isDark">
<div class="absolute top-20 left-10 w-72 h-72 bg-blue-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-cyan-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-blue-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-purple-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-cyan-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
<!-- 暗色模式装饰 -->
<template v-else>
<div class="absolute top-20 left-10 w-72 h-72 bg-orange-500/10 rounded-full blur-3xl animate-float"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-red-500/10 rounded-full blur-3xl animate-float" style="animation-delay: 1s;"></div>
<div class="absolute top-1/3 right-1/4 w-48 h-48 bg-amber-500/10 rounded-full blur-2xl animate-float" style="animation-delay: 2s;"></div>
<div class="absolute top-1/4 left-1/4 w-2 h-2 bg-orange-400 rounded-full animate-twinkle"></div>
<div class="absolute top-3/4 right-1/3 w-1 h-1 bg-red-400 rounded-full animate-twinkle" style="animation-delay: 0.5s;"></div>
<div class="absolute bottom-1/4 left-1/2 w-1.5 h-1.5 bg-amber-300 rounded-full animate-twinkle" style="animation-delay: 1.5s;"></div>
</template>
</div>
<!-- 页面头部 - 英雄区域 -->
<div class="relative overflow-hidden h-screen flex items-center justify-center">
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center" data-aos="fade-up">
<h1 class="text-5xl md:text-6xl font-bold mb-6">
<span class="bg-gradient-to-r bg-clip-text text-transparent" :class="themeStore.isDark
? 'from-orange-400 to-red-400'
: 'from-blue-400 to-purple-400'"
>
新闻资讯
</span>
</h1>
<p class="text-xl mb-8 max-w-3xl mx-auto" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
探索最前沿的科技动态分享最有价值的行业洞察
</p>
<div class="flex items-center justify-center space-x-4 mb-12">
<div class="w-2 h-2 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-orange-400' : 'bg-blue-400'"></div>
<div class="w-1 h-1 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-red-400' : 'bg-purple-400'" style="animation-delay: 0.5s;"></div>
<div class="w-1.5 h-1.5 rounded-full animate-twinkle" :class="themeStore.isDark ? 'bg-amber-300' : 'bg-blue-300'" style="animation-delay: 1s;"></div>
</div>
<!-- 滚动到内容按钮 -->
<button
@click="scrollToContent"
class="animate-bounce mt-8 p-2 rounded-full bg-gradient-to-r shadow-lg transition-all duration-300 hover:shadow-xl"
:class="themeStore.isDark
? 'from-orange-500 to-red-500 hover:shadow-orange-500/30'
: 'from-blue-500 to-purple-500 hover:shadow-blue-500/30'"
>
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</button>
</div>
</div>
<!-- 新闻分类导航 -->
<div class="bg-white border-b border-gray-200">
<div class="border-b border-opacity-20" :class="themeStore.isDark ? 'bg-white/5 border-white/10' : 'bg-black/5 border-black/10'" data-aos="fade-up" data-aos-delay="100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<nav class="flex space-x-8 overflow-x-auto py-4">
<nav class="flex space-x-4 overflow-x-auto py-6">
<button
v-for="category in categories"
:key="category.key"
@click="activeCategory = category.key"
:class="[
'whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors',
'whitespace-nowrap py-3 px-6 rounded-full font-medium text-sm transition-all duration-300',
activeCategory === category.key
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
? themeStore.isDark
? 'bg-gradient-to-r from-orange-500 to-red-500 text-white shadow-lg transform scale-105'
: 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg transform scale-105'
: themeStore.isDark
? 'bg-white/10 text-gray-300 hover:bg-white/20 hover:text-white'
: 'bg-white/60 text-gray-700 hover:bg-white/80 hover:text-gray-900'
]"
>
{{ category.name }}
@@ -34,29 +87,44 @@
</div>
<!-- 搜索和筛选 -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" data-aos="fade-up" data-aos-delay="200">
<div class="rounded-2xl p-6 mb-8 backdrop-blur-lg" :class="themeStore.isDark ? 'bg-white/5 border border-white/10' : 'bg-white/60 border border-white/20'">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<input
v-model="searchKeyword"
type="text"
placeholder="搜索新闻标题或内容..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full px-4 py-3 rounded-lg transition-all duration-300 outline-none"
:class="themeStore.isDark
? 'bg-white/10 text-white placeholder-gray-400 border border-white/10 focus:border-orange-500/50'
: 'bg-white/80 text-gray-900 placeholder-gray-500 border border-gray-200 focus:border-blue-500/50'"
@keyup.enter="searchNews"
/>
</div>
<div class="flex gap-2">
<div class="flex gap-3">
<button
@click="searchNews"
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition-colors"
class="px-6 py-3 rounded-lg font-medium transition-all duration-300 flex items-center"
:class="themeStore.isDark
? 'bg-gradient-to-r from-orange-500 to-red-500 text-white hover:shadow-lg hover:shadow-orange-500/20'
: 'bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:shadow-lg hover:shadow-blue-500/20'"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
搜索
</button>
<button
@click="resetSearch"
class="bg-gray-500 hover:bg-gray-600 text-white px-6 py-2 rounded-lg transition-colors"
class="px-6 py-3 rounded-lg font-medium transition-all duration-300 flex items-center"
:class="themeStore.isDark
? 'bg-white/10 text-white hover:bg-white/20'
: 'bg-white/60 text-gray-700 hover:bg-white/80'"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
重置
</button>
</div>
@@ -65,8 +133,8 @@
<!-- 新闻列表 -->
<div v-if="loading" class="text-center py-12">
<LoadingIcon class="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
<p class="text-gray-600">加载中...</p>
<div class="w-12 h-12 mx-auto mb-4 rounded-full border-4 border-t-transparent animate-spin" :class="themeStore.isDark ? 'border-orange-500' : 'border-blue-500'"></div>
<p :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-600'">加载中...</p>
</div>
<div v-else-if="newsList.length === 0" class="text-center py-12">
@@ -75,21 +143,27 @@
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<article
v-for="news in newsList"
v-for="(news, index) in newsList"
:key="news.id"
class="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow cursor-pointer"
class="rounded-xl overflow-hidden cursor-pointer group transition-all duration-300"
:class="themeStore.isDark
? 'bg-white/5 backdrop-blur-lg border border-white/10 hover:bg-white/10 hover:border-orange-500/30 hover:shadow-xl hover:shadow-orange-500/10'
: 'bg-white/60 backdrop-blur-lg border border-white/20 hover:bg-white/80 hover:border-blue-500/30 hover:shadow-xl hover:shadow-blue-500/10'"
:data-aos="'fade-up'"
:data-aos-delay="index * 100"
@click="goToDetail(news.id)"
>
<div class="relative">
<div class="relative overflow-hidden mb-4">
<img
:src="news.image || 'https://images.unsplash.com/photo-1504711434969-e33886168f5c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80'"
:alt="news.title"
class="w-full h-48 object-cover"
class="w-full h-48 object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="absolute top-4 left-4">
<span
:class="[
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
'inline-flex items-center px-3 py-1 rounded-full text-xs font-medium',
getCategoryClass(news.category)
]"
>
@@ -98,15 +172,18 @@
</div>
</div>
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-900 mb-3 line-clamp-2">
<div class="p-6 space-y-3">
<h3 class="text-xl font-semibold mb-3 line-clamp-2 transition-all duration-300"
:class="themeStore.isDark
? 'text-white group-hover:text-orange-300'
: 'text-gray-900 group-hover:text-blue-600'">
{{ news.title }}
</h3>
<p class="text-gray-600 mb-4 line-clamp-3">
<p class="mb-4 line-clamp-3" :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">
{{ news.summary || news.content }}
</p>
<div class="flex items-center justify-between text-sm text-gray-500">
<div class="flex items-center justify-between text-sm" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<div class="flex items-center">
<UserIcon class="w-4 h-4 mr-2" />
<span>{{ news.author || '管理员' }}</span>
@@ -117,14 +194,17 @@
</div>
</div>
<div class="flex items-center justify-between mt-4">
<div class="flex items-center text-sm text-gray-500">
<div class="flex items-center justify-between mt-4 pt-4 border-t" :class="themeStore.isDark ? 'border-white/10' : 'border-gray-200'">
<div class="flex items-center text-sm" :class="themeStore.isDark ? 'text-gray-400' : 'text-gray-500'">
<EyeIcon class="w-4 h-4 mr-1" />
<span>{{ news.views || 0 }} 次浏览</span>
</div>
<button
@click.stop="goToDetail(news.id)"
class="text-blue-600 hover:text-blue-800 font-medium text-sm"
class="text-sm font-medium hover:scale-105 transition-transform duration-200"
:class="themeStore.isDark
? 'text-orange-400 hover:text-red-400'
: 'text-blue-400 hover:text-purple-400'"
>
阅读更多
</button>
@@ -134,56 +214,88 @@
</div>
<!-- 分页 -->
<div v-if="newsList.length > 0" class="mt-12 flex justify-center">
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<div v-if="newsList.length > 0" class="mt-12 flex justify-center" data-aos="fade-up">
<nav class="flex items-center space-x-2">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage <= 1"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
class="px-4 py-2 rounded-lg font-medium transition-all duration-300 flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
:class="themeStore.isDark
? 'bg-white/10 text-white hover:bg-white/20'
: 'bg-white/60 text-gray-700 hover:bg-white/80'"
>
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
上一页
</button>
<button
v-for="page in getPageNumbers()"
:key="page"
@click="changePage(page)"
:class="[
'relative inline-flex items-center px-4 py-2 border text-sm font-medium',
page === currentPage
? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
]"
>
{{ page }}
</button>
<div class="flex space-x-1">
<button
v-for="page in getPageNumbers()"
:key="page"
@click="changePage(page)"
:class="[
'px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',
page === currentPage
? themeStore.isDark
? 'bg-gradient-to-r from-orange-500 to-red-500 text-white shadow-lg'
: 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: themeStore.isDark
? 'bg-white/10 text-gray-300 hover:bg-white/20 hover:text-white'
: 'bg-white/60 text-gray-700 hover:bg-white/80 hover:text-gray-900'
]"
>
{{ page }}
</button>
</div>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage >= totalPages"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
class="px-4 py-2 rounded-lg font-medium transition-all duration-300 flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
:class="themeStore.isDark
? 'bg-white/10 text-white hover:bg-white/20'
: 'bg-white/60 text-gray-700 hover:bg-white/80'"
>
下一页
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</nav>
</div>
</div>
<!-- 毛玻璃模态框示例 -->
<GlassModal
v-model:visible="showModal"
title="新闻详情预览"
size="lg"
:show-footer="true"
@confirm="handleModalConfirm"
@cancel="showModal = false"
>
<div class="space-y-4">
<p :class="themeStore.isDark ? 'text-white' : 'text-gray-900'">这是一个毛玻璃效果的模态框示例</p>
<p :class="themeStore.isDark ? 'text-gray-300' : 'text-gray-600'">支持多种尺寸和自定义内容</p>
</div>
</GlassModal>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { newsAPI } from '@/api'
import EmptyState from '@/components/EmptyState.vue'
import GlassModal from '@/components/GlassModal.vue'
import { useThemeStore } from '@/stores/theme'
const themeStore = useThemeStore()
const router = useRouter()
// 图标组件
const LoadingIcon = {
template: `<svg fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>`
}
const UserIcon = {
template: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>`
}
@@ -204,6 +316,7 @@ const activeCategory = ref('all')
const currentPage = ref(1)
const pageSize = ref(9)
const total = ref(0)
const showModal = ref(false)
// 新闻分类
const categories = ref([
@@ -295,13 +408,23 @@ const getCategoryName = (category) => {
// 获取分类样式
const getCategoryClass = (category) => {
const classes = {
company: 'bg-blue-100 text-blue-800',
industry: 'bg-green-100 text-green-800',
product: 'bg-purple-100 text-purple-800',
technology: 'bg-orange-100 text-orange-800'
if (themeStore.isDark) {
const classes = {
company: 'bg-blue-500/20 text-blue-300 border border-blue-500/30',
industry: 'bg-green-500/20 text-green-300 border border-green-500/30',
product: 'bg-purple-500/20 text-purple-300 border border-purple-500/30',
technology: 'bg-orange-500/20 text-orange-300 border border-orange-500/30'
}
return classes[category] || 'bg-gray-500/20 text-gray-300 border border-gray-500/30'
} else {
const classes = {
company: 'bg-blue-500/10 text-blue-700 border border-blue-500/30',
industry: 'bg-green-500/10 text-green-700 border border-green-500/30',
product: 'bg-purple-500/10 text-purple-700 border border-purple-500/30',
technology: 'bg-orange-500/10 text-orange-700 border border-orange-500/30'
}
return classes[category] || 'bg-gray-500/10 text-gray-700 border border-gray-500/30'
}
return classes[category] || 'bg-gray-100 text-gray-800'
}
// 格式化日期
@@ -319,14 +442,6 @@ const fetchNews = async () => {
loading.value = true
try {
// 这里应该调用真实的API
// const response = await newsAPI.getList({
// page: currentPage.value,
// pageSize: pageSize.value,
// category: activeCategory.value === 'all' ? '' : activeCategory.value,
// keyword: searchKeyword.value
// })
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500))
@@ -427,6 +542,12 @@ const goToDetail = (id) => {
router.push(`/news/${id}`)
}
// 模态框确认
const handleModalConfirm = () => {
console.log('模态框确认')
showModal.value = false
}
// 监听分类变化
const handleCategoryChange = () => {
currentPage.value = 1
@@ -439,8 +560,15 @@ onMounted(() => {
})
// 监听分类变化
import { watch } from 'vue'
watch(activeCategory, handleCategoryChange)
// 滚动到内容区域
const scrollToContent = () => {
const contentElement = document.querySelector('.border-b.border-opacity-20')
if (contentElement) {
contentElement.scrollIntoView({ behavior: 'smooth' })
}
}
</script>
<style scoped>
@@ -459,6 +587,27 @@ watch(activeCategory, handleCategoryChange)
overflow: hidden;
}
/* 动画效果 */
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
100% { transform: translateY(0px); }
}
@keyframes twinkle {
0% { opacity: 0.2; }
50% { opacity: 1; }
100% { opacity: 0.2; }
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-twinkle {
animation: twinkle 3s ease-in-out infinite;
}
/* 响应式优化 */
@media (max-width: 768px) {
.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-3 {
@@ -469,25 +618,11 @@ watch(activeCategory, handleCategoryChange)
flex-direction: column;
}
.space-x-8 {
.space-x-4 {
display: flex;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.space-x-8 > * + * {
margin-left: 2rem;
gap: 1rem;
}
}
/* 加载动画 */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>
</style>