This commit is contained in:
2025-11-28 20:04:58 +08:00
parent c88cdd787a
commit f5a3f045a8
15 changed files with 1426 additions and 1000 deletions

BIN
public/wx.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -0,0 +1,50 @@
<script setup>
const props = defineProps({
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
}
})
</script>
<template>
<section
class="mb-6 rounded-2xl border border-gray-100/80 dark:border-gray-700/80 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md shadow-sm shadow-slate-900/5"
>
<header class="px-5 pt-4 pb-3 border-b border-gray-100/80 dark:border-gray-800/80 flex items-start justify-between">
<div>
<h3 class="text-sm font-semibold tracking-tight text-slate-900 dark:text-slate-50">
{{ title }}
</h3>
<p v-if="description" class="mt-1 text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
{{ description }}
</p>
</div>
<slot name="extra"></slot>
</header>
<div class="px-5 pb-5 pt-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5 lg:gap-6">
<slot />
</div>
</div>
</section>
</template>
<style scoped>
section {
transition: box-shadow var(--transition-normal), transform var(--transition-normal),
border-color var(--transition-normal), background-color var(--transition-normal);
}
section:hover {
transform: translateY(-1px);
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.09);
border-color: rgba(244, 63, 94, 0.18);
}
</style>

View File

@@ -184,13 +184,12 @@ const navigateTo = (path) => {
</header>
<!-- 页面内容 -->
<!-- <main class="pt-20 px-6 pb-6">-->
<!-- <div data-aos="fade-up">-->
<!-- <router-view />-->
<!-- </div>-->
<!-- </main>-->
<main class="pt-20 px-6 pb-6">
<router-view />
<router-view v-slot="{ Component }">
<transition name="page-fade-slide" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
</div>
</div>

View File

@@ -8,6 +8,7 @@ import App from './App.vue'
// 注册路由
import router from './router'
const app = createApp(App)
// 初始化AOS动画

View File

@@ -40,6 +40,7 @@ const routes = [
{ path: '', redirect: '/admin/dashboard' },
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/admin/Dashboard.vue') },
{ path: 'projects', name: 'ProjectManage', component: () => import('@/views/admin/Projects.vue') },
{ path: 'projects/create', name: 'ProjectCreate', component: () => import('@/views/admin/ProjectCreate.vue') },
{ path: 'users', name: 'UsersManage', component: () => import('@/views/admin/Users.vue') },
{ path: 'setting', name: 'SettingManage', component: () => import('@/views/admin/Setting.vue') },
{ path: 'orders', name: 'OrdersManage', component: () => import('@/views/admin/Orders.vue') }
@@ -69,4 +70,4 @@ router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !userStore.user) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
})

View File

@@ -1 +1,2 @@
@import "tailwindcss";
@import "./styles/theme.css";

164
src/styles/theme.css Normal file
View File

@@ -0,0 +1,164 @@
/* 全局主题变量:颜色、阴影、圆角、过渡 */
:root {
/* 主题色 */
--color-primary: #e11d48; /* rose-600 */
--color-primary-soft: #fecaca;
--color-bg-soft: #f9fafb;
--color-border-soft: #e5e7eb;
/* 中性色 */
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-700: #374151;
--color-gray-800: #1f2933;
/* 阴影与圆角 */
--shadow-soft: 0 10px 30px rgba(15, 23, 42, 0.08);
--shadow-strong: 0 24px 60px rgba(15, 23, 42, 0.16);
--radius-lg: 1rem;
--radius-xl: 1.5rem;
/* 过渡 */
--transition-fast: 150ms ease-out;
--transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 400ms cubic-bezier(0.22, 1, 0.36, 1);
}
.dark {
--color-primary: #fb7185;
--color-primary-soft: rgba(248, 113, 113, 0.12);
--color-bg-soft: #020617;
--color-border-soft: rgba(148, 163, 184, 0.25);
--color-gray-50: #020617;
--color-gray-100: #111827;
--color-gray-200: #1f2937;
--color-gray-700: #9ca3af;
--color-gray-800: #020617;
}
/* 通用卡片样式 */
.ui-card {
border-radius: var(--radius-xl);
box-shadow: var(--shadow-soft);
background: radial-gradient(circle at top left, rgba(244, 63, 94, 0.06), transparent 55%),
radial-gradient(circle at bottom right, rgba(59, 130, 246, 0.05), transparent 55%),
#ffffff;
transition: transform var(--transition-normal), box-shadow var(--transition-normal),
background var(--transition-slow);
}
.dark .ui-card {
background:
radial-gradient(circle at top left, rgba(248, 113, 113, 0.1), transparent 55%),
radial-gradient(circle at bottom right, rgba(56, 189, 248, 0.08), transparent 55%),
linear-gradient(135deg, #020617, #020617);
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.6);
}
.ui-card-hover:hover {
transform: translateY(-4px) translateZ(0);
box-shadow: var(--shadow-strong);
}
/* 主按钮统一动效 */
.ui-btn-primary {
border-radius: 9999px;
background: linear-gradient(135deg, #fb7185, #e11d48);
color: #ffffff;
box-shadow: 0 10px 25px rgba(244, 63, 94, 0.45);
transition: transform var(--transition-fast), box-shadow var(--transition-fast),
filter var(--transition-fast);
}
.ui-btn-primary:hover {
transform: translateY(-1px) scale(1.02);
box-shadow: 0 18px 40px rgba(244, 63, 94, 0.55);
filter: brightness(1.03);
}
.ui-btn-primary:active {
transform: translateY(0) scale(0.98);
box-shadow: 0 6px 18px rgba(244, 63, 94, 0.4);
}
/* 页面路由过渡 */
.page-fade-slide-enter-active,
.page-fade-slide-leave-active {
transition: opacity var(--transition-normal), transform var(--transition-normal);
}
.page-fade-slide-enter-from,
.page-fade-slide-leave-to {
opacity: 0;
transform: translateY(8px);
}
/* 表单焦点与错误状态统一样式 */
.ui-input,
.ui-select,
.ui-textarea {
border-radius: 0.9rem;
border-width: 1px;
border-color: var(--color-border-soft);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast),
background-color var(--transition-fast), transform var(--transition-fast);
}
.ui-input:focus,
.ui-select:focus,
.ui-textarea:focus,
.ui-input:focus-visible,
.ui-select:focus-visible,
.ui-textarea:focus-visible {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 1px rgba(244, 63, 94, 0.6), 0 18px 45px rgba(15, 23, 42, 0.08);
background-color: rgba(248, 250, 252, 0.9);
transform: translateY(-1px);
}
.dark .ui-input,
.dark .ui-select,
.dark .ui-textarea {
background-color: rgba(15, 23, 42, 0.95);
border-color: rgba(148, 163, 184, 0.45);
}
.ui-input-error,
.ui-select-error,
.ui-textarea-error {
border-color: #f97373 !important;
box-shadow: 0 0 0 1px rgba(248, 113, 113, 0.7);
}
.ui-error-text {
font-size: 0.75rem;
color: #f97373;
margin-top: 0.15rem;
opacity: 0;
transform: translateY(-2px);
transition: opacity var(--transition-fast), transform var(--transition-fast);
}
.ui-error-text.show {
opacity: 1;
transform: translateY(0);
}
/* 列表行动效 */
.ui-row-hover {
transition: background-color var(--transition-fast), transform var(--transition-fast),
box-shadow var(--transition-fast);
}
.ui-row-hover:hover {
background-color: rgba(248, 250, 252, 0.95);
transform: translateY(-1px);
}
.dark .ui-row-hover:hover {
background-color: rgba(30, 64, 175, 0.25);
}

View File

@@ -756,10 +756,9 @@ const batchDelete = () => {
</button>
</div>
</div>
</div>
<!-- 订单详情模态框 -->
<div v-if="showDetailModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- 订单详情模态框 -->
<div v-if="showDetailModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
@@ -1000,10 +999,10 @@ const batchDelete = () => {
</div>
</div>
</div>
</div>
</div>
<!-- 新建/编辑订单模态框 -->
<div v-if="showModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- 新建/编辑订单模态框 -->
<div v-if="showModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
@@ -1310,10 +1309,10 @@ const batchDelete = () => {
</div>
</div>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- 确认删除模态框 -->
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
@@ -1364,6 +1363,7 @@ const batchDelete = () => {
</div>
</div>
</div>
</div>
</div>
</template>
@@ -1410,4 +1410,4 @@ const batchDelete = () => {
.dark ::-webkit-scrollbar-thumb:hover {
background-color: rgba(75, 85, 99, 0.7);
}
</style>
</style>

View File

@@ -0,0 +1,645 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { message, Upload, Select } from 'ant-design-vue'
import AOS from 'aos'
import 'aos/dist/aos.css'
import FormSection from '@/components/FormSection.vue'
import { labelOptionApi } from '@/api/request/label.js'
import { categoryOptionApi } from '@/api/request/category.js'
import {
createProjectApi,
updateProjectApi,
projectDetailApi
} from '@/api/request/project.js'
import { uploadImageApi, uploadVideoApi } from '@/api/request/upload.js'
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const saving = ref(false)
const categoryOptions = ref([])
const tagsOption = ref([])
const currentItem = ref({
title: '',
category_id: 1,
labels: [],
price: '',
preferential_price: '',
status: 0,
description: '',
cover: '',
features: [],
front: '',
service: '',
deploy: '',
screenshots: [],
video_url: ''
})
const isEdit = computed(() => !!currentItem.value.id)
const statusOptions = [
{ value: 0, label: '草稿' },
{ value: 1, label: '已发布' },
{ value: 2, label: '已下架' }
]
const getLabelOption = () => {
labelOptionApi().then((res) => {
tagsOption.value = res
})
}
const getCategoryOption = () => {
categoryOptionApi().then((res) => {
categoryOptions.value = res
})
}
const initById = (id) => {
if (!id) return
loading.value = true
projectDetailApi({ id })
.then((res) => {
const detail = { ...res }
// 兼容标签字段:优先使用 label_ids
if (detail.label_ids) {
detail.labels = detail.label_ids
} else if (Array.isArray(detail.labels)) {
detail.labels = detail.labels.map((l) => l.id)
}
currentItem.value = detail
// 初始化封面
fileList.value = []
if (detail.cover) {
fileList.value.push({
uid: '-1',
name: 'cover.jpg',
status: 'done',
url: String(detail.cover)
})
}
// 初始化截图(同时兼容 string 和 { url } 两种结构)
screenshotsFileList.value = []
if (Array.isArray(detail.screenshots) && detail.screenshots.length > 0) {
detail.screenshots.forEach((item, index) => {
const url = typeof item === 'string' ? item : item?.url
if (!url) return
screenshotsFileList.value.push({
uid: `-${index + 1}`,
name: `screenshot-${index + 1}.jpg`,
status: 'done',
url: String(url)
})
})
}
// 初始化视频
videoFileList.value = []
if (detail.video_url) {
videoFileList.value.push({
uid: '-1',
name: 'video.mp4',
status: 'done',
url: String(detail.video_url)
})
}
})
.finally(() => {
loading.value = false
})
}
onMounted(() => {
AOS.init({
duration: 800,
offset: 120,
easing: 'ease-out-cubic',
once: false
})
getLabelOption()
getCategoryOption()
const id = route.query.id
if (id) {
initById(id)
}
})
// 上传相关状态(与原 Projects.vue 保持兼容)
const fileList = ref([])
const screenshotsFileList = ref([])
const videoFileList = ref([])
const previewVisible = ref(false)
const previewImage = ref('')
const videoPreviewVisible = ref(false)
const videoPreviewUrl = ref('')
const handlePreview = (file) => {
previewImage.value = file.url || file.thumbUrl
previewVisible.value = true
}
const handleVideoPreview = () => {
videoPreviewUrl.value = currentItem.value.video_url
videoPreviewVisible.value = true
}
const beforeUpload = (file) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
message.error('只能上传JPG或PNG格式的图片!')
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error('图片大小不能超过2MB!')
}
return isJpgOrPng && isLt2M
}
const beforeVideoUpload = (file) => {
const isVideo = file.type.startsWith('video/')
if (!isVideo) {
message.error('只能上传视频文件!')
}
const isLt50M = file.size / 1024 / 1024 < 50
if (!isLt50M) {
message.error('视频大小不能超过50MB!')
}
return isVideo && isLt50M
}
const customCoverRequest = ({ file, onSuccess }) => {
uploadImageApi(file).then((res) => {
currentItem.value.cover = res.url
onSuccess('ok', { status: 'done' })
})
}
const customRequest = ({ file, onSuccess }) => {
if (!currentItem.value.screenshots) {
currentItem.value.screenshots = []
}
uploadImageApi(file).then((res) => {
currentItem.value.screenshots.push(res.url)
onSuccess('ok', { status: 'done' })
})
}
const customVideoRequest = ({ file, onSuccess }) => {
uploadVideoApi(file).then((res) => {
currentItem.value.video_url = res.url
onSuccess('ok', { status: 'done' })
})
}
// 标签与功能
const featureInputValue = ref('')
const handleAddFeature = () => {
if (featureInputValue.value && !currentItem.value.features.includes(featureInputValue.value)) {
if (!currentItem.value.features) {
currentItem.value.features = []
}
currentItem.value.features.push(featureInputValue.value)
featureInputValue.value = ''
}
}
const removeFeature = (index) => {
currentItem.value.features.splice(index, 1)
}
const handleSubmit = () => {
saving.value = true
if (typeof currentItem.value.price === 'string') {
currentItem.value.price = parseFloat(String(currentItem.value.price).replace(/,/g, ''))
}
const api = currentItem.value.id ? updateProjectApi : createProjectApi
api(currentItem.value)
.then(() => {
message.success(currentItem.value.id ? '项目更新成功' : '项目创建成功')
router.push('/admin/projects')
})
.finally(() => {
saving.value = false
})
}
const goBack = () => {
router.push('/admin/projects')
}
</script>
<template>
<div class="space-y-6" data-aos="fade-up">
<!-- 顶部返回与标题 -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<button
type="button"
class="w-9 h-9 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors cursor-pointer"
@click="goBack"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white tracking-tight">
{{ isEdit ? '编辑项目' : '新建项目' }}
</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ isEdit ? '更新已有项目的信息。' : '完整填写项目信息,用更沉浸的页面替代原有模态框。' }}
</p>
</div>
</div>
</div>
<!-- 内容卡片 -->
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6 lg:p-7">
<form class="space-y-5" @submit.prevent="handleSubmit">
<!-- 基本信息与标签 -->
<FormSection
title="基础信息"
description="项目信息将展示在前台列表与详情页,请尽量保持简洁清晰。"
>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">项目名称</label>
<input
v-model="currentItem.title"
type="text"
class="ui-input w-full px-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm"
placeholder="请输入项目名称"
required
/>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">项目分类</label>
<select
v-model="currentItem.category_id"
class="ui-select w-full px-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm cursor-pointer"
>
<option v-for="option in categoryOptions" :key="option.id" :value="option.id">
{{ option.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">项目价格</label>
<div class="relative">
<span
class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400 text-xs">¥</span>
<input
v-model="currentItem.price"
type="text"
class="ui-input w-full pl-7 pr-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm"
placeholder="请输入项目价格"
/>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">优惠价格</label>
<div class="relative">
<span
class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400 text-xs">¥</span>
<input
v-model="currentItem.preferential_price"
type="text"
class="ui-input w-full pl-7 pr-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm"
placeholder="请输入优惠价格"
/>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">项目状态</label>
<select
v-model="currentItem.status"
class="ui-select w-full px-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm cursor-pointer"
>
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<div class="lg:col-span-2">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">项目标签</label>
<div
class="ui-select w-full px-1 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm cursor-pointer"
>
<Select
class="w-full"
v-model:value="currentItem.labels"
:options="tagsOption"
mode="multiple"
placeholder="请选择标签"
:field-names="{ label: 'name', value: 'id' }"
/>
</div>
</div>
</FormSection>
<!-- 技术栈与功能 -->
<FormSection
title="技术栈与特性"
description="清晰标注前后端技术栈与核心功能,帮助用户快速理解项目能力。"
>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">前端技术栈</label>
<input
v-model="currentItem.front"
type="text"
class="ui-input w-full px-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm"
placeholder="如Vue3、Vite、Tailwind CSS"
required
/>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">后端技术栈</label>
<input
v-model="currentItem.service"
type="text"
class="ui-input w-full px-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm"
placeholder="如Laravel、Spring Boot"
required
/>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">部署架构</label>
<input
v-model="currentItem.deploy"
type="text"
class="ui-input w-full px-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm"
placeholder="如Docker + K8s / 宝塔面板 / 云服务器"
required
/>
</div>
<div class="lg:col-span-2">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">核心功能</label>
<div class="flex flex-wrap gap-2 mb-2">
<span
v-for="(feature, index) in currentItem.features"
:key="index"
class="px-3 py-1.5 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 text-xs font-medium rounded-lg flex items-center"
>
{{ feature }}
<button
type="button"
class="ml-1.5 text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-200 cursor-pointer"
@click="removeFeature(index)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
</div>
<div class="flex">
<input
v-model="featureInputValue"
type="text"
class="ui-input flex-1 px-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm rounded-r-none"
placeholder="输入功能后回车或点击添加"
@keyup.enter="handleAddFeature"
/>
<button
type="button"
class="ui-btn-primary px-4 py-2.5 text-sm font-medium rounded-l-none"
@click="handleAddFeature"
>
添加
</button>
</div>
</div>
</FormSection>
<!-- 媒体与描述 -->
<FormSection
title="媒体与描述"
description="封面、截图和演示视频会在前台页面中重点展示。"
>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">项目封面</label>
<div class="border border-gray-200 dark:border-gray-700 rounded-2xl p-4 dark:bg-gray-800/80">
<Upload
v-model:file-list="fileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customCoverRequest"
@preview="handlePreview"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:max-count="1"
>
<div v-if="fileList.length < 1">
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400 text-xs">
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 mb-1" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<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" />
</svg>
<span>点击上传封面</span>
</div>
</div>
</Upload>
<p class="mt-2 text-[11px] text-gray-500 dark:text-gray-400">
支持 JPG / PNG大小不超过 2MB
</p>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">演示视频</label>
<div class="border border-gray-200 dark:border-gray-700 rounded-2xl p-4 dark:bg-gray-800/80">
<Upload
v-model:file-list="videoFileList"
list-type="picture-card"
:before-upload="beforeVideoUpload"
:custom-request="customVideoRequest"
:show-upload-list="{ showPreviewIcon: false, showRemoveIcon: true }"
:max-count="1"
>
<div v-if="videoFileList.length < 1">
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400 text-xs">
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 mb-1" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<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" />
</svg>
<span>点击上传视频</span>
</div>
</div>
</Upload>
<p class="mt-2 text-[11px] text-gray-500 dark:text-gray-400">
支持常见格式大小不超过 50MB
</p>
<div v-if="currentItem.video_url" class="mt-3">
<button
type="button"
class="px-3 py-1.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium rounded-lg flex items-center hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors cursor-pointer"
@click="handleVideoPreview"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1.5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<circle cx="12" cy="12" r="9" />
</svg>
预览视频
</button>
</div>
</div>
</div>
<div class="lg:col-span-2">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">项目截图</label>
<div class="border border-gray-200 dark:border-gray-700 rounded-2xl p-4 dark:bg-gray-800/80">
<Upload
v-model:file-list="screenshotsFileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customRequest"
@preview="handlePreview"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
multiple
:max-count="10"
>
<div>
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400 text-xs">
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 mb-1" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<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" />
</svg>
<span>点击上传截图</span>
</div>
</div>
</Upload>
<p class="mt-2 text-[11px] text-gray-500 dark:text-gray-400">
支持多张截图建议展示关键页面和交互
</p>
</div>
</div>
<div class="lg:col-span-2">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1.5">项目描述</label>
<textarea
v-model="currentItem.description"
rows="4"
class="ui-textarea w-full px-4 py-2.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm"
placeholder="简要介绍项目背景、目标用户、使用场景等信息。"
></textarea>
</div>
</FormSection>
<!-- 底部操作栏 -->
<div class="flex justify-end gap-3 pt-3 border-t border-gray-100 dark:border-gray-800">
<button
type="button"
class="px-4 py-2 rounded-full border border-gray-300 dark:border-gray-600 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="goBack"
>
取消返回
</button>
<button
type="submit"
class="ui-btn-primary px-5 py-2 text-sm font-medium flex items-center justify-center"
:disabled="saving"
>
<span v-if="saving" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
{{ isEdit ? '保存修改' : '保存项目' }}
</button>
</div>
</form>
</div>
<!-- 图片预览模态框 -->
<div v-if="previewVisible" class="fixed inset-0 z-50" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen px-4 text-center">
<div
class="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
@click="previewVisible = false"
></div>
<span class="inline-block align-middle h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-middle max-w-3xl w-full relative">
<div class="relative rounded-2xl overflow-hidden shadow-2xl">
<img :src="previewImage" class="w-full max-h-[80vh] object-contain bg-black/80" />
<button
class="absolute top-3 right-3 w-8 h-8 flex items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80 transition-colors cursor-pointer"
@click="previewVisible = false"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- 视频预览模态框 -->
<div v-if="videoPreviewVisible" class="fixed inset-0 z-50" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen px-4 text-center">
<div
class="fixed inset-0 bg-black/70 backdrop-blur-sm transition-opacity"
@click="videoPreviewVisible = false"
></div>
<span class="inline-block align-middle h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-middle max-w-4xl w-full relative">
<div class="relative rounded-2xl overflow-hidden shadow-2xl bg-black">
<button
class="absolute top-3 right-3 w-8 h-8 flex items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80 transition-colors cursor-pointer z-10"
@click="videoPreviewVisible = false"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div class="aspect-video">
<video
:src="videoPreviewUrl"
class="w-full h-full"
controls
autoplay
></video>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -241,12 +241,13 @@ const sendTestEmail = () => {
</script>
<template>
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
<!-- 页面标题 -->
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">系统设置</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">管理系统的全局配置和参数设置</p>
</div>
<div>
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
<!-- 页面标题 -->
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">系统设置</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">管理系统的全局配置和参数设置</p>
</div>
<!-- 设置选项卡 -->
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
@@ -605,27 +606,27 @@ const sendTestEmail = () => {
</div>
</div>
<!-- 保存按钮 -->
<div class="mt-8 flex justify-end">
<button
type="button"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
@click="saveSettings"
:disabled="saveLoading"
>
<span v-if="saveLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
保存设置
</button>
<!-- 保存按钮 -->
<div class="mt-8 flex justify-end">
<button
type="button"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
@click="saveSettings"
:disabled="saveLoading"
>
<span v-if="saveLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
保存设置
</button>
</div>
</div>
</div>
<!-- Logo预览模态框 -->
<div v-if="logoPreviewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- Logo预览模态框 -->
<div v-if="logoPreviewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
@@ -654,8 +655,8 @@ const sendTestEmail = () => {
</div>
</div>
<!-- Favicon预览模态框 -->
<div v-if="faviconPreviewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- Favicon预览模态框 -->
<div v-if="faviconPreviewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
@@ -684,8 +685,8 @@ const sendTestEmail = () => {
</div>
</div>
<!-- 测试邮件模态框 -->
<div v-if="showTestEmailModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- 测试邮件模态框 -->
<div v-if="showTestEmailModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
@@ -753,6 +754,7 @@ const sendTestEmail = () => {
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@@ -808,4 +810,4 @@ const sendTestEmail = () => {
.dark ::-webkit-scrollbar-thumb:hover {
background-color: rgba(75, 85, 99, 0.7);
}
</style>
</style>

View File

@@ -305,9 +305,10 @@ const reset = () => {
</script>
<template>
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
<!-- 页面标题和新建按钮 -->
<div class="mb-6 flex justify-between items-center">
<div>
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
<!-- 页面标题和新建按钮 -->
<div class="mb-6 flex justify-between items-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">用户管理</h2>
<button
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
@@ -551,12 +552,12 @@ const reset = () => {
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 用户表单模态框 -->
<div v-if="showModal" class="fixed inset-0 z-50 overflow-y-scroll" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 用户表单模态框 -->
<div v-if="showModal" class="fixed inset-0 z-50 overflow-y-scroll" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
@@ -718,11 +719,11 @@ const reset = () => {
</div>
</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 删除确认模态框 -->
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
@@ -772,10 +773,10 @@ const reset = () => {
</div>
</div>
</div>
</div>
</div>
<!-- 重置密码模态框 -->
<div v-if="showResetPasswordModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- 重置密码模态框 -->
<div v-if="showResetPasswordModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
@@ -856,10 +857,9 @@ const reset = () => {
</div>
</div>
</div>
</div>
<!-- 图片预览模态框 -->
<div v-if="previewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- 图片预览模态框 -->
<div v-if="previewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
@@ -884,9 +884,11 @@ const reset = () => {
</button>
<img :src="previewImage" class="max-h-[80vh] max-w-full rounded-lg shadow-xl" />
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@@ -942,4 +944,4 @@ const reset = () => {
.dark ::-webkit-scrollbar-thumb:hover {
background-color: rgba(75, 85, 99, 0.7);
}
</style>
</style>

View File

@@ -256,8 +256,31 @@ const stopPropagation = (e) => {
/>
</div>
</ImagePreviewGroup>
</div>
<!-- <div class="grid grid-cols-2 sm:grid-cols-3 gap-4">-->
<!-- <div-->
<!-- v-for="(screenshot, index) in project.screenshots"-->
<!-- :key="index"-->
<!-- class="relative group rounded-xl overflow-hidden cursor-pointer h-40"-->
<!-- @click="handlePreview(typeof screenshot === 'string' ? screenshot : screenshot?.url)"-->
<!-- >-->
<!-- <img-->
<!-- :src="typeof screenshot === 'string' ? screenshot : screenshot?.url"-->
<!-- :alt="`Screenshot ${index + 1}`"-->
<!-- class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"-->
<!-- />-->
<!-- <div-->
<!-- class="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors duration-300 flex items-center justify-center opacity-0 group-hover:opacity-100">-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-white" fill="none" viewBox="0 0 24 24"-->
<!-- stroke="currentColor">-->
<!-- <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 0zM10 7v3m0 0v3m0-3h3m-3 0H7"/>-->
<!-- </svg>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
<!-- 右侧信息 -->

View File

@@ -12,7 +12,7 @@ import 'swiper/css/effect-fade'
import {homeProjectDetailApi} from "@/api/request/home.js";
import {DollarSign, User, X} from "lucide-vue-next";
import {createCollectionApi, unCollectionApi} from "@/api/request/collection.js";
import {message, Image} from "ant-design-vue";
import {message, Image, ImagePreviewGroup} from "ant-design-vue";
import {useUserStore} from "@/stores/user.js";
import {MdPreview} from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
@@ -359,32 +359,59 @@ const unCollection = () => {
<!-- 项目截图 -->
<div v-else-if="activeTab === '2'">
<swiper
:modules="modules"
:effect="'fade'"
:autoplay="swiperOptions"
:pagination="{
clickable: true,
dynamicBullets: true,
dynamicMainBullets: 3
}"
:navigation="true"
:loop="true"
class="rounded-2xl overflow-hidden shadow-lg"
>
<swiper-slide
v-for="(img, index) in project.screenshots"
:key="index"
>
<div class="relative h-[50vh] min-h-[400px] w-full">
<img
:src="img.url"
class="w-full h-full object-contain bg-gray-100 dark:bg-gray-900"
:alt="`项目截图 ${index + 1}`"
/>
</div>
</swiper-slide>
</swiper>
<!-- <swiper-->
<!-- :modules="modules"-->
<!-- :effect="'fade'"-->
<!-- :autoplay="swiperOptions"-->
<!-- :pagination="{-->
<!-- clickable: true,-->
<!-- dynamicBullets: true,-->
<!-- dynamicMainBullets: 3-->
<!-- }"-->
<!-- :navigation="true"-->
<!-- :loop="true"-->
<!-- class="rounded-2xl overflow-hidden shadow-lg"-->
<!-- >-->
<!-- <swiper-slide-->
<!-- v-for="(img, index) in project.screenshots"-->
<!-- :key="index"-->
<!-- >-->
<!-- <div class="relative h-[50vh] min-h-[400px] w-full">-->
<!-- <img-->
<!-- :src="img.url"-->
<!-- class="w-full h-full object-contain bg-gray-100 dark:bg-gray-900"-->
<!-- :alt="`项目截图 ${index + 1}`"-->
<!-- />-->
<!-- </div>-->
<!-- </swiper-slide>-->
<!-- </swiper>-->
<!-- <ImagePreviewGroup>-->
<!-- <div class="grid grid-cols-2 sm:grid-cols-3 gap-4">-->
<!-- <Image-->
<!-- v-for="(screenshot, index) in project.screenshots"-->
<!-- :key="index"-->
<!-- :src="screenshot.url"-->
<!-- :alt="`Screenshot ${index + 1}`"-->
<!-- class="w-full object-cover transition-transform duration-300 cursor-pointer"-->
<!-- style="height: calc(var(&#45;&#45;spacing) * 40); border-radius: 15px"-->
<!-- />-->
<!-- </div>-->
<!-- </ImagePreviewGroup>-->
<ImagePreviewGroup>
<div class="grid grid-cols-1 sm:grid-cols-1 gap-1">
<Image
v-for="(screenshot, index) in project.screenshots"
:key="index"
:src="screenshot.url"
:alt="`Screenshot ${index + 1}`"
class="w-full object-cover transition-transform duration-300 cursor-pointer"
style="height: calc(var(--spacing) * 80); border-radius: 15px"
/>
</div>
</ImagePreviewGroup>
</div>
<!-- 演示视频 -->
@@ -686,4 +713,4 @@ const unCollection = () => {
.cursor-grab:active {
cursor: grabbing;
}
</style>
</style>

View File

@@ -408,49 +408,49 @@ const breakpoints = {
</section>
<!-- 合作伙伴 - 高端品牌墙 -->
<section class="mb-20 lg:mb-28" data-aos="fade-up">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">
<span class="relative">
合作伙伴
<span class="absolute -bottom-2 left-1/4 right-1/4 h-1 bg-gradient-to-r from-rose-400 to-rose-600 rounded-full"></span>
</span>
</h2>
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
我们与以下品牌的合作夥伴一起合作提供更好的服务体验
</p>
</div>
<!-- <section class="mb-20 lg:mb-28" data-aos="fade-up">-->
<!-- <div class="text-center mb-16">-->
<!-- <h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-6">-->
<!-- <span class="relative">-->
<!-- 合作伙伴-->
<!-- <span class="absolute -bottom-2 left-1/4 right-1/4 h-1 bg-gradient-to-r from-rose-400 to-rose-600 rounded-full"></span>-->
<!-- </span>-->
<!-- </h2>-->
<!-- <p class="text-lg text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">-->
<!-- 我们与以下品牌的合作夥伴一起合作提供更好的服务体验-->
<!-- </p>-->
<!-- </div>-->
<div class="relative overflow-hidden py-8">
<swiper
:modules="[Autoplay]"
:slides-per-view="2"
:space-between="30"
:autoplay="partnerSwiperOptions"
:loop="true"
:breakpoints="breakpoints"
class="partner-swiper"
>
<swiper-slide v-for="(partner, index) in partners" :key="index">
<div
class="flex flex-col items-center justify-center h-48 bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-all duration-500 hover:-translate-y-2 border border-gray-100 dark:border-gray-700"
>
<div class="w-full h-24 flex items-center justify-center mb-4">
<img
:src="partner.logo"
class="h-24 object-contain opacity-80 hover:opacity-100 rounded-lg transition-opacity grayscale hover:grayscale-0 transition-all duration-500"
alt="Partner Logo"
/>
</div>
<div class="text-center">
<span class="text-gray-900 dark:text-white text-lg font-bold">{{ partner.name }}</span>
<div class="w-12 h-1 bg-rose-500 mx-auto mt-3 rounded-full"></div>
</div>
</div>
</swiper-slide>
</swiper>
</div>
</section>
<!-- <div class="relative overflow-hidden py-8">-->
<!-- <swiper-->
<!-- :modules="[Autoplay]"-->
<!-- :slides-per-view="2"-->
<!-- :space-between="30"-->
<!-- :autoplay="partnerSwiperOptions"-->
<!-- :loop="true"-->
<!-- :breakpoints="breakpoints"-->
<!-- class="partner-swiper"-->
<!-- >-->
<!-- <swiper-slide v-for="(partner, index) in partners" :key="index">-->
<!-- <div-->
<!-- class="flex flex-col items-center justify-center h-48 bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-lg hover:shadow-xl transition-all duration-500 hover:-translate-y-2 border border-gray-100 dark:border-gray-700"-->
<!-- >-->
<!-- <div class="w-full h-24 flex items-center justify-center mb-4">-->
<!-- <img-->
<!-- :src="partner.logo"-->
<!-- class="h-24 object-contain opacity-80 hover:opacity-100 rounded-lg transition-opacity grayscale hover:grayscale-0 transition-all duration-500"-->
<!-- alt="Partner Logo"-->
<!-- />-->
<!-- </div>-->
<!-- <div class="text-center">-->
<!-- <span class="text-gray-900 dark:text-white text-lg font-bold">{{ partner.name }}</span>-->
<!-- <div class="w-12 h-1 bg-rose-500 mx-auto mt-3 rounded-full"></div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </swiper-slide>-->
<!-- </swiper>-->
<!-- </div>-->
<!-- </section>-->
</div>
</div>
</template>