UI优化
This commit is contained in:
BIN
public/wx.jpg
Normal file
BIN
public/wx.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
50
src/components/FormSection.vue
Normal file
50
src/components/FormSection.vue
Normal 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>
|
||||||
|
|
||||||
|
|
||||||
@@ -184,13 +184,12 @@ const navigateTo = (path) => {
|
|||||||
</header>
|
</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">
|
<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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import App from './App.vue'
|
|||||||
// 注册路由
|
// 注册路由
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
// 初始化AOS动画
|
// 初始化AOS动画
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const routes = [
|
|||||||
{ path: '', redirect: '/admin/dashboard' },
|
{ path: '', redirect: '/admin/dashboard' },
|
||||||
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/admin/Dashboard.vue') },
|
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/admin/Dashboard.vue') },
|
||||||
{ path: 'projects', name: 'ProjectManage', component: () => import('@/views/admin/Projects.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: 'users', name: 'UsersManage', component: () => import('@/views/admin/Users.vue') },
|
||||||
{ path: 'setting', name: 'SettingManage', component: () => import('@/views/admin/Setting.vue') },
|
{ path: 'setting', name: 'SettingManage', component: () => import('@/views/admin/Setting.vue') },
|
||||||
{ path: 'orders', name: 'OrdersManage', component: () => import('@/views/admin/Orders.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) {
|
if (to.meta.requiresAuth && !userStore.user) {
|
||||||
return { name: 'Login', query: { redirect: to.fullPath } }
|
return { name: 'Login', query: { redirect: to.fullPath } }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "./styles/theme.css";
|
||||||
|
|||||||
164
src/styles/theme.css
Normal file
164
src/styles/theme.css
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -756,10 +756,9 @@ const batchDelete = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
@@ -1000,10 +999,10 @@ const batchDelete = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
@@ -1310,10 +1309,10 @@ const batchDelete = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
@@ -1364,6 +1363,7 @@ const batchDelete = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1410,4 +1410,4 @@ const batchDelete = () => {
|
|||||||
.dark ::-webkit-scrollbar-thumb:hover {
|
.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: rgba(75, 85, 99, 0.7);
|
background-color: rgba(75, 85, 99, 0.7);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
645
src/views/admin/ProjectCreate.vue
Normal file
645
src/views/admin/ProjectCreate.vue
Normal 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">​</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">​</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
@@ -241,12 +241,13 @@ const sendTestEmail = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
|
<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>
|
<div class="mb-6">
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">管理系统的全局配置和参数设置</p>
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">系统设置</h2>
|
||||||
</div>
|
<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">
|
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
@@ -605,27 +606,27 @@ const sendTestEmail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 保存按钮 -->
|
<!-- 保存按钮 -->
|
||||||
<div class="mt-8 flex justify-end">
|
<div class="mt-8 flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="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"
|
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"
|
@click="saveSettings"
|
||||||
:disabled="saveLoading"
|
:disabled="saveLoading"
|
||||||
>
|
>
|
||||||
<span v-if="saveLoading" class="mr-2">
|
<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">
|
<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>
|
<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>
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
保存设置
|
保存设置
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Logo预览模态框 -->
|
<!-- Logo预览模态框 -->
|
||||||
<div v-if="logoPreviewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
<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 class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
@@ -654,8 +655,8 @@ const sendTestEmail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Favicon预览模态框 -->
|
<!-- Favicon预览模态框 -->
|
||||||
<div v-if="faviconPreviewVisible" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
<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 class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
@@ -684,8 +685,8 @@ const sendTestEmail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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 class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
@@ -753,6 +754,7 @@ const sendTestEmail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -808,4 +810,4 @@ const sendTestEmail = () => {
|
|||||||
.dark ::-webkit-scrollbar-thumb:hover {
|
.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: rgba(75, 85, 99, 0.7);
|
background-color: rgba(75, 85, 99, 0.7);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -305,9 +305,10 @@ const reset = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
|
<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">
|
<!-- 页面标题和新建按钮 -->
|
||||||
|
<div class="mb-6 flex justify-between items-center">
|
||||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">用户管理</h2>
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">用户管理</h2>
|
||||||
<button
|
<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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 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="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
|
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>
|
||||||
</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 class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
|
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>
|
||||||
</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 class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
@@ -856,10 +857,9 @@ const reset = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div
|
<div
|
||||||
@@ -884,9 +884,11 @@ const reset = () => {
|
|||||||
</button>
|
</button>
|
||||||
<img :src="previewImage" class="max-h-[80vh] max-w-full rounded-lg shadow-xl" />
|
<img :src="previewImage" class="max-h-[80vh] max-w-full rounded-lg shadow-xl" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -942,4 +944,4 @@ const reset = () => {
|
|||||||
.dark ::-webkit-scrollbar-thumb:hover {
|
.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: rgba(75, 85, 99, 0.7);
|
background-color: rgba(75, 85, 99, 0.7);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -256,8 +256,31 @@ const stopPropagation = (e) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ImagePreviewGroup>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧信息 -->
|
<!-- 右侧信息 -->
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import 'swiper/css/effect-fade'
|
|||||||
import {homeProjectDetailApi} from "@/api/request/home.js";
|
import {homeProjectDetailApi} from "@/api/request/home.js";
|
||||||
import {DollarSign, User, X} from "lucide-vue-next";
|
import {DollarSign, User, X} from "lucide-vue-next";
|
||||||
import {createCollectionApi, unCollectionApi} from "@/api/request/collection.js";
|
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 {useUserStore} from "@/stores/user.js";
|
||||||
import {MdPreview} from 'md-editor-v3';
|
import {MdPreview} from 'md-editor-v3';
|
||||||
import 'md-editor-v3/lib/style.css';
|
import 'md-editor-v3/lib/style.css';
|
||||||
@@ -359,32 +359,59 @@ const unCollection = () => {
|
|||||||
|
|
||||||
<!-- 项目截图 -->
|
<!-- 项目截图 -->
|
||||||
<div v-else-if="activeTab === '2'">
|
<div v-else-if="activeTab === '2'">
|
||||||
<swiper
|
<!-- <swiper-->
|
||||||
:modules="modules"
|
<!-- :modules="modules"-->
|
||||||
:effect="'fade'"
|
<!-- :effect="'fade'"-->
|
||||||
:autoplay="swiperOptions"
|
<!-- :autoplay="swiperOptions"-->
|
||||||
:pagination="{
|
<!-- :pagination="{-->
|
||||||
clickable: true,
|
<!-- clickable: true,-->
|
||||||
dynamicBullets: true,
|
<!-- dynamicBullets: true,-->
|
||||||
dynamicMainBullets: 3
|
<!-- dynamicMainBullets: 3-->
|
||||||
}"
|
<!-- }"-->
|
||||||
:navigation="true"
|
<!-- :navigation="true"-->
|
||||||
:loop="true"
|
<!-- :loop="true"-->
|
||||||
class="rounded-2xl overflow-hidden shadow-lg"
|
<!-- class="rounded-2xl overflow-hidden shadow-lg"-->
|
||||||
>
|
<!-- >-->
|
||||||
<swiper-slide
|
<!-- <swiper-slide-->
|
||||||
v-for="(img, index) in project.screenshots"
|
<!-- v-for="(img, index) in project.screenshots"-->
|
||||||
:key="index"
|
<!-- :key="index"-->
|
||||||
>
|
<!-- >-->
|
||||||
<div class="relative h-[50vh] min-h-[400px] w-full">
|
<!-- <div class="relative h-[50vh] min-h-[400px] w-full">-->
|
||||||
<img
|
<!-- <img-->
|
||||||
:src="img.url"
|
<!-- :src="img.url"-->
|
||||||
class="w-full h-full object-contain bg-gray-100 dark:bg-gray-900"
|
<!-- class="w-full h-full object-contain bg-gray-100 dark:bg-gray-900"-->
|
||||||
:alt="`项目截图 ${index + 1}`"
|
<!-- :alt="`项目截图 ${index + 1}`"-->
|
||||||
/>
|
<!-- />-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</swiper-slide>
|
<!-- </swiper-slide>-->
|
||||||
</swiper>
|
<!-- </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(--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>
|
</div>
|
||||||
|
|
||||||
<!-- 演示视频 -->
|
<!-- 演示视频 -->
|
||||||
@@ -686,4 +713,4 @@ const unCollection = () => {
|
|||||||
.cursor-grab:active {
|
.cursor-grab:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -408,49 +408,49 @@ const breakpoints = {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 合作伙伴 - 高端品牌墙 -->
|
<!-- 合作伙伴 - 高端品牌墙 -->
|
||||||
<section class="mb-20 lg:mb-28" data-aos="fade-up">
|
<!-- <section class="mb-20 lg:mb-28" data-aos="fade-up">-->
|
||||||
<div class="text-center mb-16">
|
<!-- <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">
|
<!-- <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="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 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>
|
<!-- </span>-->
|
||||||
</h2>
|
<!-- </h2>-->
|
||||||
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
|
<!-- <p class="text-lg text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">-->
|
||||||
我们与以下品牌的合作夥伴一起合作,提供更好的服务体验
|
<!-- 我们与以下品牌的合作夥伴一起合作,提供更好的服务体验-->
|
||||||
</p>
|
<!-- </p>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
|
|
||||||
<div class="relative overflow-hidden py-8">
|
<!-- <div class="relative overflow-hidden py-8">-->
|
||||||
<swiper
|
<!-- <swiper-->
|
||||||
:modules="[Autoplay]"
|
<!-- :modules="[Autoplay]"-->
|
||||||
:slides-per-view="2"
|
<!-- :slides-per-view="2"-->
|
||||||
:space-between="30"
|
<!-- :space-between="30"-->
|
||||||
:autoplay="partnerSwiperOptions"
|
<!-- :autoplay="partnerSwiperOptions"-->
|
||||||
:loop="true"
|
<!-- :loop="true"-->
|
||||||
:breakpoints="breakpoints"
|
<!-- :breakpoints="breakpoints"-->
|
||||||
class="partner-swiper"
|
<!-- class="partner-swiper"-->
|
||||||
>
|
<!-- >-->
|
||||||
<swiper-slide v-for="(partner, index) in partners" :key="index">
|
<!-- <swiper-slide v-for="(partner, index) in partners" :key="index">-->
|
||||||
<div
|
<!-- <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"
|
<!-- 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">
|
<!-- <div class="w-full h-24 flex items-center justify-center mb-4">-->
|
||||||
<img
|
<!-- <img-->
|
||||||
:src="partner.logo"
|
<!-- :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"
|
<!-- 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"
|
<!-- alt="Partner Logo"-->
|
||||||
/>
|
<!-- />-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="text-center">
|
<!-- <div class="text-center">-->
|
||||||
<span class="text-gray-900 dark:text-white text-lg font-bold">{{ partner.name }}</span>
|
<!-- <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 class="w-12 h-1 bg-rose-500 mx-auto mt-3 rounded-full"></div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</swiper-slide>
|
<!-- </swiper-slide>-->
|
||||||
</swiper>
|
<!-- </swiper>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</section>
|
<!-- </section>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user