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>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<!-- <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>
|
||||
|
||||
@@ -8,6 +8,7 @@ import App from './App.vue'
|
||||
// 注册路由
|
||||
import router from './router'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 初始化AOS动画
|
||||
|
||||
@@ -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') }
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
@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,7 +756,6 @@ 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">
|
||||
@@ -1365,6 +1364,7 @@ const batchDelete = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import {ref, onMounted, computed} from "vue"
|
||||
import {useRouter} from "vue-router"
|
||||
import AOS from "aos"
|
||||
import "aos/dist/aos.css"
|
||||
import {message, Upload, Select} from "ant-design-vue"
|
||||
@@ -19,6 +20,8 @@ import {MdEditor} from 'md-editor-v3';
|
||||
|
||||
import 'md-editor-v3/lib/style.css';
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
AOS.init({
|
||||
duration: 800,
|
||||
@@ -111,6 +114,18 @@ const videoPreviewUrl = ref("")
|
||||
const showDetailModal = ref(false)
|
||||
const currentDetailItem = ref(null)
|
||||
|
||||
const goCreatePage = () => {
|
||||
router.push('/admin/projects/create')
|
||||
}
|
||||
|
||||
const goEditPage = (item) => {
|
||||
if (!item || !item.id) return
|
||||
router.push({
|
||||
name: 'ProjectCreate',
|
||||
query: {id: item.id}
|
||||
})
|
||||
}
|
||||
|
||||
const handlePreview = (file) => {
|
||||
previewImage.value = file.url || file.thumbUrl
|
||||
previewVisible.value = true
|
||||
@@ -576,13 +591,14 @@ const changeTypeProject = (id, type, index, feild) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
@click="openModal()"
|
||||
class="ui-btn-primary px-5 py-2 rounded-full text-sm font-medium flex items-center cursor-pointer"
|
||||
@click="goCreatePage"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
@@ -800,7 +816,7 @@ const changeTypeProject = (id, type, index, feild) => {
|
||||
<button
|
||||
class="p-1.5 text-amber-600 hover:text-amber-800 dark:text-amber-400 dark:hover:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded-lg transition-colors cursor-pointer"
|
||||
title="编辑"
|
||||
@click="openModal(item)"
|
||||
@click="goEditPage(item)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
@@ -865,456 +881,8 @@ const changeTypeProject = (id, type, index, feild) => {
|
||||
</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
|
||||
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
|
||||
aria-hidden="true"
|
||||
@click="closeModal"
|
||||
></div>
|
||||
|
||||
<!-- 使模态框居中 -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div
|
||||
class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-5xl sm:w-full relative z-50">
|
||||
<div class="bg-white dark:bg-gray-800 px-6 pt-5 pb-4 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-5">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white" id="modal-title">
|
||||
{{ currentItem?.id ? '编辑项目' : '新建项目' }}
|
||||
</h3>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
|
||||
@click="closeModal"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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>
|
||||
|
||||
<form @submit.prevent="handleSubmit" @keydown.enter.prevent="event.preventDefault()" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="mr-5">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- 项目名称 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目名称</label>
|
||||
<input
|
||||
v-model="currentItem.title"
|
||||
type="text"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="请输入项目名称"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 价格 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目价格</label>
|
||||
<div class="relative">
|
||||
<span
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400">¥</span>
|
||||
<input
|
||||
v-model="currentItem.price"
|
||||
type="text"
|
||||
class="w-full pl-8 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="请输入项目价格"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 价格 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">优惠价格</label>
|
||||
<div class="relative">
|
||||
<span
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400">¥</span>
|
||||
<input
|
||||
v-model="currentItem.preferential_price"
|
||||
type="text"
|
||||
class="w-full pl-8 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="请输入项目价格"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目分类</label>
|
||||
<select
|
||||
v-model="currentItem.category_id"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
|
||||
>
|
||||
<option v-for="option in categoryOptions" :key="option.id" :value="option.id">
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 状态 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目状态</label>
|
||||
<select
|
||||
v-model="currentItem.status"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer"
|
||||
>
|
||||
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 技术栈 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">前端技术栈</label>
|
||||
<div class="flex">
|
||||
<input
|
||||
v-model="currentItem.front"
|
||||
type="text"
|
||||
class="w-full pl-8 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="输入前端技术栈"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">后端技术栈</label>
|
||||
<div class="flex">
|
||||
<input
|
||||
v-model="currentItem.service"
|
||||
type="text"
|
||||
class="w-full pl-8 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="输入后端技术栈"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">部署架构</label>
|
||||
<div class="flex">
|
||||
<input
|
||||
v-model="currentItem.deploy"
|
||||
type="text"
|
||||
class="w-full pl-8 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="输入部署架构"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- 标签 -->
|
||||
<div class="mt-3">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目标签</label>
|
||||
<div
|
||||
class="w-full px-1 py-1 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer">
|
||||
<Select
|
||||
class="w-full"
|
||||
v-model:value="currentItem.labels"
|
||||
:options="tagsOption"
|
||||
mode="multiple"
|
||||
placeholder="请选择标签"
|
||||
:fieldNames="{
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目功能 -->
|
||||
<div class="mt-3">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">核心功能</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-sm 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-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>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<input
|
||||
v-model="featureInputValue"
|
||||
type="text"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-l-xl focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="输入功能"
|
||||
@keyup.enter="handleAddFeature"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-r-xl transition-colors cursor-pointer"
|
||||
@click="handleAddFeature"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 项目描述 -->
|
||||
<div class="mt-3">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目描述</label>
|
||||
<textarea
|
||||
v-model="currentItem.description"
|
||||
rows="4"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="请输入项目描述"
|
||||
></textarea>
|
||||
<!-- <MdEditor-->
|
||||
<!-- v-model="currentItem.description"-->
|
||||
<!-- :toolbars-exclude="['github']"-->
|
||||
<!-- class="md-editor-container"-->
|
||||
<!-- editor-id="description-md"-->
|
||||
<!-- theme="dark"-->
|
||||
<!-- :config="editorConfig"-->
|
||||
<!-- @on-upload-img="handleMdImageUpload"-->
|
||||
<!-- />-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 截图视频 -->
|
||||
<div class="ml-5">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||
|
||||
<!-- 项目封面上传 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目封面</label>
|
||||
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
|
||||
<Upload
|
||||
v-model:file-list="fileList"
|
||||
list-type="picture-card"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customCoverRequest"
|
||||
@preview="handlePreview"
|
||||
@change="handleUploadChange"
|
||||
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
|
||||
:maxCount="1"
|
||||
>
|
||||
<div v-if="fileList.length < 1">
|
||||
<div
|
||||
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 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>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
支持JPG、PNG格式,大小不超过10MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频上传 -->
|
||||
<div class="ml-5">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">演示视频</label>
|
||||
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
|
||||
<Upload
|
||||
v-model:file-list="videoFileList"
|
||||
list-type="picture-card"
|
||||
:before-upload="beforeVideoUpload"
|
||||
:custom-request="customVideoRequest"
|
||||
@change="handleVideoUploadChange"
|
||||
:show-upload-list="{ showPreviewIcon: false, showRemoveIcon: true }"
|
||||
:maxCount="1"
|
||||
>
|
||||
<div v-if="videoFileList.length < 1">
|
||||
<div
|
||||
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 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>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
支持MP4、WebM等常见视频格式,大小不超过500MB
|
||||
</div>
|
||||
|
||||
<!-- 视频预览按钮 -->
|
||||
<div v-if="currentItem.video_url" class="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1.5 bg-gray-100 dark:bg-gray-600 text-gray-700 dark:text-gray-200 text-sm font-medium rounded-lg flex items-center hover:bg-gray-200 dark:hover:bg-gray-500 transition-colors cursor-pointer"
|
||||
@click="handleVideoPreview"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 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"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
预览视频
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模块图上传 -->
|
||||
<div class="mt-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">模块图</label>
|
||||
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
|
||||
<Upload
|
||||
v-model:file-list="moduleImageFileList"
|
||||
list-type="picture-card"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customModuleImageRequest"
|
||||
@preview="handlePreview"
|
||||
@change="handleModuleImageUploadChange"
|
||||
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
|
||||
:maxCount="1"
|
||||
>
|
||||
<div v-if="moduleImageFileList.length < 1">
|
||||
<div
|
||||
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 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>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
支持JPG、PNG格式,大小不超过10MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 流程图上传 -->
|
||||
<div class="ml-5 mt-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">流程图</label>
|
||||
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
|
||||
<Upload
|
||||
v-model:file-list="flowchartFileList"
|
||||
list-type="picture-card"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customFlowchartRequest"
|
||||
@preview="handlePreview"
|
||||
@change="handleFlowchartUploadChange"
|
||||
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
|
||||
:maxCount="1"
|
||||
>
|
||||
<div v-if="flowchartFileList.length < 1">
|
||||
<div
|
||||
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 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>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
支持JPG、PNG格式,大小不超过10MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 项目截图上传 -->
|
||||
<div class="mt-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目截图</label>
|
||||
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
|
||||
<Upload
|
||||
v-model:file-list="screenshotsFileList"
|
||||
list-type="picture-card"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customRequest"
|
||||
@preview="handlePreview"
|
||||
@change="handleScreenshotsUploadChange"
|
||||
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
|
||||
multiple
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 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>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
支持JPG、PNG格式,大小不超过10MB,可上传多张
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 表单按钮 -->
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="px-5 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
|
||||
@click="closeModal"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
:disabled="formLoading"
|
||||
>
|
||||
<span v-if="formLoading" 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>
|
||||
{{ currentItem?.id ? '保存修改' : '创建项目' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目详细说明 -->
|
||||
<div v-if="showContentModal" 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">
|
||||
@@ -1389,6 +957,19 @@ const changeTypeProject = (id, type, index, feild) => {
|
||||
</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
|
||||
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
|
||||
aria-hidden="true"
|
||||
@click="closeModal"
|
||||
></div>
|
||||
|
||||
<!-- 使模态框居中 -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!-- 删除确认模态框 -->
|
||||
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog"
|
||||
@@ -1449,85 +1030,15 @@ const changeTypeProject = (id, type, index, feild) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片预览模态框 -->
|
||||
<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="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
|
||||
aria-hidden="true"
|
||||
@click="previewVisible = false"
|
||||
></div>
|
||||
|
||||
<!-- 使模态框居中 -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div
|
||||
class="inline-block align-bottom bg-transparent rounded-lg text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl sm:w-full relative z-50">
|
||||
<div class="relative">
|
||||
<button
|
||||
class="absolute top-2 right-2 p-2 rounded-full bg-gray-800/50 text-white hover:bg-gray-700/50 transition-colors cursor-pointer"
|
||||
@click="previewVisible = false"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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>
|
||||
<img :src="previewImage" class="w-full max-h-[80vh] max-w-full rounded-lg shadow-xl"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览模态框 -->
|
||||
<div v-if="videoPreviewVisible" 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="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
|
||||
aria-hidden="true"
|
||||
@click="videoPreviewVisible = false"
|
||||
></div>
|
||||
|
||||
<!-- 使模态框居中 -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div
|
||||
class="inline-block align-bottom bg-transparent rounded-lg text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full relative z-50">
|
||||
{{ videoPreviewUrl }}
|
||||
<div class="relative">
|
||||
<button
|
||||
class="absolute top-2 right-2 p-2 rounded-full bg-gray-800/50 text-white hover:bg-gray-700/50 transition-colors cursor-pointer z-10"
|
||||
@click="videoPreviewVisible = false"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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-w-16 aspect-h-9 rounded-lg overflow-hidden shadow-xl">
|
||||
<video
|
||||
:src="videoPreviewUrl"
|
||||
class="w-full h-full"
|
||||
controls
|
||||
autoplay
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目详情模态框 -->
|
||||
<ProjectDetails
|
||||
v-model:visible="showDetailModal"
|
||||
:project="currentDetailItem"
|
||||
@close="showDetailModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -241,6 +241,7 @@ const sendTestEmail = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-6" data-aos="fade-up">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-6">
|
||||
@@ -753,6 +754,7 @@ const sendTestEmail = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -305,6 +305,7 @@ const reset = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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">
|
||||
@@ -856,7 +857,6 @@ 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">
|
||||
@@ -887,6 +887,8 @@ const reset = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 右侧信息 -->
|
||||
|
||||
@@ -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"
|
||||
<!-- <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(--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"
|
||||
>
|
||||
<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}`"
|
||||
: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>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</ImagePreviewGroup>
|
||||
</div>
|
||||
|
||||
<!-- 演示视频 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user