统一项目字段,优化表单页面
This commit is contained in:
@@ -31,12 +31,12 @@ const menuData = [
|
||||
icon: 'layout-grid',
|
||||
title: '项目管理'
|
||||
},
|
||||
// {
|
||||
// path: '/admin/users',
|
||||
// name: 'UserManage',
|
||||
// icon: 'users',
|
||||
// title: '用户管理'
|
||||
// },
|
||||
{
|
||||
path: '/admin/users',
|
||||
name: 'UserManage',
|
||||
icon: 'users',
|
||||
title: '用户管理'
|
||||
},
|
||||
// {
|
||||
// path: '/admin/settings',
|
||||
// name: 'Settings',
|
||||
|
||||
@@ -40,7 +40,8 @@ const routes = [
|
||||
children: [
|
||||
{ path: '', redirect: '/admin/dashboard' },
|
||||
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/admin/Dashboard.vue') },
|
||||
{ path: 'projects', name: 'ProjectManage', component: () => import('@/views/admin/ProjectsCRUD.vue') }
|
||||
{ path: 'projects', name: 'ProjectManage', component: () => import('@/views/admin/ProjectsCRUD.vue') },
|
||||
{ path: 'users', name: 'UsersManage', component: () => import('@/views/admin/Users.vue') }
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,61 +1,99 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import AOS from 'aos'
|
||||
import 'aos/dist/aos.css'
|
||||
import { ref, onMounted, computed } from "vue"
|
||||
import AOS from "aos"
|
||||
import "aos/dist/aos.css"
|
||||
import { message, Upload } from "ant-design-vue"
|
||||
|
||||
onMounted(() => {
|
||||
AOS.init({
|
||||
duration: 800,
|
||||
offset: 120,
|
||||
easing: 'ease-out-cubic',
|
||||
once: false
|
||||
easing: "ease-out-cubic",
|
||||
once: false,
|
||||
})
|
||||
})
|
||||
|
||||
// 分类数据
|
||||
const categoryOptions = [
|
||||
{ id: 1, name: "Web应用", value: "Web应用" },
|
||||
{ id: 2, name: "移动端", value: "移动端" },
|
||||
{ id: 3, name: "小程序", value: "小程序" },
|
||||
{ id: 4, name: "企业系统", value: "企业系统" },
|
||||
]
|
||||
|
||||
// 标签数据
|
||||
const allTags = [
|
||||
{ id: 1, name: "Vue3" },
|
||||
{ id: 2, name: "Node.js" },
|
||||
{ id: 3, name: "TypeScript" },
|
||||
{ id: 4, name: "React" },
|
||||
{ id: 5, name: "Express" },
|
||||
{ id: 6, name: "PHP" },
|
||||
{ id: 7, name: "Python" },
|
||||
{ id: 8, name: "MongoDB" },
|
||||
{ id: 9, name: "Docker" },
|
||||
]
|
||||
|
||||
const dataSource = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '电商平台',
|
||||
category: 'Web应用',
|
||||
tags: ['Vue3', 'Node.js'],
|
||||
author: 'Admin',
|
||||
price: '29,999',
|
||||
status: '已发布',
|
||||
createdAt: '2024-03-20'
|
||||
title: "电商平台",
|
||||
category: categoryOptions[0],
|
||||
tags: [allTags[0], allTags[1], allTags[5]],
|
||||
author: "Admin",
|
||||
price: 29999,
|
||||
status: "已发布",
|
||||
createdAt: "2024-03-20T00:00:00Z",
|
||||
cover: "https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280",
|
||||
description: "基于Vue3和Node.js开发的电商平台,提供完整的购物流程,包括注册、登录、购物、支付等功能。",
|
||||
features: ["响应式设计,支持多端访问", "完整的商品管理系统", "订单处理和物流跟踪"],
|
||||
technicalStack: ["前端:Vue3 + TypeScript", "后端:Node.js + MongoDB", "部署:Docker"],
|
||||
screenshots: [
|
||||
"https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280",
|
||||
"https://img2.baidu.com/it/u=1629222614,1358629025&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
|
||||
],
|
||||
video: "http://d-jy.nailaoyun.cn//storage/video//20250327/2cc1abf9bd69410ce7c69660daf54260.mp4"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'CRM系统',
|
||||
category: '企业系统',
|
||||
tags: ['React', 'Express'],
|
||||
author: 'Admin',
|
||||
price: '19,999',
|
||||
status: '已发布',
|
||||
createdAt: '2024-03-15'
|
||||
title: "CRM系统",
|
||||
category: categoryOptions[3],
|
||||
tags: [allTags[3], allTags[4]],
|
||||
author: "Admin",
|
||||
price: 19999,
|
||||
status: "已发布",
|
||||
createdAt: "2024-03-15T00:00:00Z",
|
||||
cover: "https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280",
|
||||
description: "基于React和Express开发的CRM系统,提供客户管理、销售跟踪和数据分析功能。",
|
||||
features: ["客户信息管理", "销售漏斗跟踪", "数据分析和报表"],
|
||||
technicalStack: ["前端:React + Redux", "后端:Express + MySQL", "部署:AWS"],
|
||||
screenshots: [
|
||||
"https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280"
|
||||
],
|
||||
video: ""
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '数据分析平台',
|
||||
category: 'Web应用',
|
||||
tags: ['Vue3', 'Python'],
|
||||
author: 'Admin',
|
||||
price: '24,999',
|
||||
status: '草稿',
|
||||
createdAt: '2024-03-10'
|
||||
}
|
||||
title: "数据分析平台",
|
||||
category: categoryOptions[0],
|
||||
tags: [allTags[0], allTags[6]],
|
||||
author: "Admin",
|
||||
price: 24999,
|
||||
status: "草稿",
|
||||
createdAt: "2024-03-10T00:00:00Z",
|
||||
cover: "https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280",
|
||||
description: "基于Vue3和Python开发的数据分析平台,提供数据可视化和深度分析功能。",
|
||||
features: ["实时数据仪表盘", "自定义报表生成", "数据导入导出"],
|
||||
technicalStack: ["前端:Vue3 + ECharts", "后端:Python + Django", "部署:Docker + Kubernetes"],
|
||||
screenshots: [],
|
||||
video: ""
|
||||
},
|
||||
])
|
||||
|
||||
const categoryOptions = [
|
||||
{ value: 'Web应用', label: 'Web应用' },
|
||||
{ value: '移动端', label: '移动端' },
|
||||
{ value: '小程序', label: '小程序' },
|
||||
{ value: '企业系统', label: '企业系统' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ value: '草稿', label: '草稿' },
|
||||
{ value: '已发布', label: '已发布' },
|
||||
{ value: '已下架', label: '已下架' }
|
||||
{ value: "草稿", label: "草稿" },
|
||||
{ value: "已发布", label: "已发布" },
|
||||
{ value: "已下架", label: "已下架" },
|
||||
]
|
||||
|
||||
// 分页
|
||||
@@ -78,15 +116,193 @@ const showModal = ref(false)
|
||||
const currentItem = ref(null)
|
||||
const formLoading = ref(false)
|
||||
|
||||
const openModal = (item = null) => {
|
||||
currentItem.value = item ? { ...item } : {
|
||||
title: '',
|
||||
category: 'Web应用',
|
||||
tags: [],
|
||||
price: '',
|
||||
status: '草稿',
|
||||
description: ''
|
||||
// 图片上传相关
|
||||
const fileList = ref([])
|
||||
const uploading = ref(false)
|
||||
const previewVisible = ref(false)
|
||||
const previewImage = ref("")
|
||||
|
||||
// 多图上传相关
|
||||
const screenshotsFileList = ref([])
|
||||
const screenshotsUploading = ref(false)
|
||||
|
||||
// 视频上传相关
|
||||
const videoFileList = ref([])
|
||||
const videoUploading = ref(false)
|
||||
const videoPreviewVisible = ref(false)
|
||||
const videoPreviewUrl = ref("")
|
||||
|
||||
const handlePreview = (file) => {
|
||||
previewImage.value = file.url || file.preview
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
const handleVideoPreview = () => {
|
||||
videoPreviewUrl.value = currentItem.value.video
|
||||
videoPreviewVisible.value = true
|
||||
}
|
||||
|
||||
const handleUploadChange = (info) => {
|
||||
if (info.file.status === "uploading") {
|
||||
uploading.value = true
|
||||
return
|
||||
}
|
||||
if (info.file.status === "done") {
|
||||
// 模拟上传成功后获取图片URL
|
||||
currentItem.value.cover =
|
||||
"https://img1.baidu.com/it/u=2172818577,3783888802&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1422"
|
||||
uploading.value = false
|
||||
message.success("封面上传成功")
|
||||
}
|
||||
if (info.file.status === "error") {
|
||||
uploading.value = false
|
||||
message.error("封面上传失败")
|
||||
}
|
||||
}
|
||||
|
||||
const handleScreenshotsUploadChange = (info) => {
|
||||
screenshotsFileList.value = info.fileList
|
||||
|
||||
if (info.file.status === "uploading") {
|
||||
screenshotsUploading.value = true
|
||||
return
|
||||
}
|
||||
if (info.file.status === "done") {
|
||||
// 模拟上传成功后获取图片URL
|
||||
if (!currentItem.value.screenshots) {
|
||||
currentItem.value.screenshots = []
|
||||
}
|
||||
|
||||
// 确保不重复添加
|
||||
const newUrl = "https://img2.baidu.com/it/u=1629222614,1358629025&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
|
||||
if (!currentItem.value.screenshots.includes(newUrl)) {
|
||||
currentItem.value.screenshots.push(newUrl)
|
||||
}
|
||||
|
||||
screenshotsUploading.value = false
|
||||
message.success("截图上传成功")
|
||||
}
|
||||
if (info.file.status === "error") {
|
||||
screenshotsUploading.value = false
|
||||
message.error("截图上传失败")
|
||||
}
|
||||
}
|
||||
|
||||
const handleVideoUploadChange = (info) => {
|
||||
videoFileList.value = info.fileList.slice(-1) // 只保留最后一个文件
|
||||
|
||||
if (info.file.status === "uploading") {
|
||||
videoUploading.value = true
|
||||
return
|
||||
}
|
||||
if (info.file.status === "done") {
|
||||
// 模拟上传成功后获取视频URL
|
||||
currentItem.value.video = "http://d-jy.nailaoyun.cn//storage/video//20250327/2cc1abf9bd69410ce7c69660daf54260.mp4"
|
||||
videoUploading.value = false
|
||||
message.success("视频上传成功")
|
||||
}
|
||||
if (info.file.status === "error") {
|
||||
videoUploading.value = false
|
||||
message.error("视频上传失败")
|
||||
}
|
||||
}
|
||||
|
||||
const removeScreenshot = (index) => {
|
||||
currentItem.value.screenshots.splice(index, 1)
|
||||
// 同步更新文件列表
|
||||
if (screenshotsFileList.value.length > index) {
|
||||
screenshotsFileList.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
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 customRequest = ({ onSuccess }) => {
|
||||
// 模拟上传
|
||||
setTimeout(() => {
|
||||
onSuccess("ok", { status: "done" })
|
||||
}, 800)
|
||||
}
|
||||
|
||||
const openModal = (item = null) => {
|
||||
currentItem.value = item
|
||||
? JSON.parse(JSON.stringify(item)) // 深拷贝避免直接修改原对象
|
||||
: {
|
||||
title: "",
|
||||
category: categoryOptions[0],
|
||||
tags: [],
|
||||
price: "",
|
||||
status: "草稿",
|
||||
description: "",
|
||||
cover: "",
|
||||
author: "Admin",
|
||||
features: [],
|
||||
technicalStack: [],
|
||||
screenshots: [],
|
||||
video: ""
|
||||
}
|
||||
|
||||
// 重置文件列表
|
||||
fileList.value = []
|
||||
if (currentItem.value.cover) {
|
||||
fileList.value = [
|
||||
{
|
||||
uid: "-1",
|
||||
name: "cover.jpg",
|
||||
status: "done",
|
||||
url: currentItem.value.cover,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 重置截图文件列表
|
||||
screenshotsFileList.value = []
|
||||
if (currentItem.value.screenshots && currentItem.value.screenshots.length > 0) {
|
||||
currentItem.value.screenshots.forEach((url, index) => {
|
||||
screenshotsFileList.value.push({
|
||||
uid: `-${index + 1}`,
|
||||
name: `screenshot-${index + 1}.jpg`,
|
||||
status: "done",
|
||||
url: url,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 重置视频文件列表
|
||||
videoFileList.value = []
|
||||
if (currentItem.value.video) {
|
||||
videoFileList.value = [
|
||||
{
|
||||
uid: "-1",
|
||||
name: "video.mp4",
|
||||
status: "done",
|
||||
url: currentItem.value.video,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
@@ -110,33 +326,49 @@ const closeDeleteConfirm = () => {
|
||||
|
||||
const handleDelete = () => {
|
||||
if (itemToDelete.value) {
|
||||
dataSource.value = dataSource.value.filter(item => item.id !== itemToDelete.value)
|
||||
dataSource.value = dataSource.value.filter((item) => item.id !== itemToDelete.value)
|
||||
totalItems.value = dataSource.value.length
|
||||
closeDeleteConfirm()
|
||||
}
|
||||
}
|
||||
|
||||
const formatPrice = (price) => {
|
||||
return "¥ " + parseFloat(price).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" })
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
formLoading.value = true
|
||||
|
||||
// 模拟API请求
|
||||
setTimeout(() => {
|
||||
// 确保price是数字
|
||||
if (typeof currentItem.value.price === "string") {
|
||||
currentItem.value.price = parseFloat(currentItem.value.price.replace(/,/g, ""))
|
||||
}
|
||||
|
||||
if (currentItem.value.id) {
|
||||
// 更新现有项目
|
||||
const index = dataSource.value.findIndex(item => item.id === currentItem.value.id)
|
||||
const index = dataSource.value.findIndex((item) => item.id === currentItem.value.id)
|
||||
if (index !== -1) {
|
||||
dataSource.value[index] = { ...currentItem.value }
|
||||
}
|
||||
message.success("项目更新成功")
|
||||
} else {
|
||||
// 添加新项目
|
||||
const newId = Math.max(...dataSource.value.map(item => item.id), 0) + 1
|
||||
const newId = Math.max(...dataSource.value.map((item) => item.id), 0) + 1
|
||||
dataSource.value.push({
|
||||
...currentItem.value,
|
||||
id: newId,
|
||||
author: 'Admin',
|
||||
createdAt: new Date().toISOString().split('T')[0]
|
||||
author: "Admin",
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
totalItems.value = dataSource.value.length
|
||||
message.success("项目创建成功")
|
||||
}
|
||||
|
||||
formLoading.value = false
|
||||
@@ -145,17 +377,63 @@ const handleSubmit = () => {
|
||||
}
|
||||
|
||||
// 标签管理
|
||||
const tagInputValue = ref('')
|
||||
const tagInputValue = ref("")
|
||||
const handleAddTag = () => {
|
||||
if (tagInputValue.value && !currentItem.value.tags.includes(tagInputValue.value)) {
|
||||
currentItem.value.tags.push(tagInputValue.value)
|
||||
tagInputValue.value = ''
|
||||
if (tagInputValue.value) {
|
||||
// 检查是否已存在该标签
|
||||
const existingTag = allTags.find((tag) => tag.name.toLowerCase() === tagInputValue.value.toLowerCase())
|
||||
|
||||
// 如果存在且未添加到当前项目,则添加
|
||||
if (existingTag && !currentItem.value.tags.some((tag) => tag.id === existingTag.id)) {
|
||||
currentItem.value.tags.push(existingTag)
|
||||
}
|
||||
// 如果不存在,创建新标签并添加
|
||||
else if (!existingTag) {
|
||||
const newTagId = Math.max(...allTags.map((tag) => tag.id), 0) + 1
|
||||
const newTag = { id: newTagId, name: tagInputValue.value }
|
||||
allTags.push(newTag)
|
||||
currentItem.value.tags.push(newTag)
|
||||
}
|
||||
|
||||
tagInputValue.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = (index) => {
|
||||
currentItem.value.tags.splice(index, 1)
|
||||
}
|
||||
|
||||
// 功能和技术栈管理
|
||||
const featureInputValue = ref("")
|
||||
const techStackInputValue = 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 handleAddTechStack = () => {
|
||||
if (techStackInputValue.value && !currentItem.value.technicalStack.includes(techStackInputValue.value)) {
|
||||
if (!currentItem.value.technicalStack) {
|
||||
currentItem.value.technicalStack = []
|
||||
}
|
||||
currentItem.value.technicalStack.push(techStackInputValue.value)
|
||||
techStackInputValue.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const removeTechStack = (index) => {
|
||||
currentItem.value.technicalStack.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -176,7 +454,7 @@ const removeTag = (index) => {
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-700">
|
||||
<div class="overflow-x-auto" style="height: 75vh">
|
||||
<div class="overflow-x-auto" style="height: 60vh">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<!-- 表头 -->
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
@@ -209,10 +487,17 @@ const removeTag = (index) => {
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="item in paginatedData" :key="item.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ item.title }}
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex-shrink-0 h-10 w-10 rounded-lg overflow-hidden">
|
||||
<img :src="item.cover" :alt="item.title" class="h-full w-full object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ item.category }}
|
||||
{{ item.category.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@@ -221,26 +506,28 @@ const removeTag = (index) => {
|
||||
:key="index"
|
||||
class="px-2 py-1 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-xs font-medium rounded-full"
|
||||
>
|
||||
{{ tag }}
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
||||
¥ {{ item.price }}
|
||||
{{ formatPrice(item.price) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
:class="`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
item.status === '已发布'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
||||
: item.status === '已下架'
|
||||
? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
||||
}`"
|
||||
>
|
||||
{{ item.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ item.createdAt }}
|
||||
{{ formatDate(item.createdAt) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex justify-end space-x-2">
|
||||
@@ -317,7 +604,7 @@ const removeTag = (index) => {
|
||||
|
||||
</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 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
|
||||
@@ -330,7 +617,7 @@ const removeTag = (index) => {
|
||||
<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-lg sm:w-full relative z-50">
|
||||
<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">
|
||||
@@ -347,31 +634,48 @@ const removeTag = (index) => {
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="space-y-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 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 class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 价格 -->
|
||||
<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>
|
||||
<select
|
||||
v-model="currentItem.category"
|
||||
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.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
<option v-for="option in categoryOptions" :key="option.id" :value="option">
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 状态 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目状态</label>
|
||||
<select
|
||||
@@ -385,20 +689,6 @@ const removeTag = (index) => {
|
||||
</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.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>
|
||||
@@ -408,7 +698,7 @@ const removeTag = (index) => {
|
||||
:key="index"
|
||||
class="px-3 py-1.5 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-sm font-medium rounded-lg flex items-center"
|
||||
>
|
||||
{{ tag }}
|
||||
{{ tag.name }}
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1.5 text-rose-500 hover:text-rose-700 dark:text-rose-400 dark:hover:text-rose-200 cursor-pointer"
|
||||
@@ -449,6 +739,185 @@ const removeTag = (index) => {
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 项目功能 -->
|
||||
<div>
|
||||
<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>
|
||||
<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="(tech, index) in currentItem.technicalStack"
|
||||
:key="index"
|
||||
class="px-3 py-1.5 bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-300 text-sm font-medium rounded-lg flex items-center"
|
||||
>
|
||||
{{ tech }}
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1.5 text-purple-500 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-200 cursor-pointer"
|
||||
@click="removeTechStack(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="techStackInputValue"
|
||||
type="text"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-l-xl focus:ring-purple-500 focus:border-purple-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="输入技术栈"
|
||||
@keyup.enter="handleAddTechStack"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-r-xl transition-colors cursor-pointer"
|
||||
@click="handleAddTechStack"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目封面上传 -->
|
||||
<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="customRequest"
|
||||
@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格式,大小不超过2MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目截图上传 -->
|
||||
<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="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格式,大小不超过2MB,可上传多张
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频上传 -->
|
||||
<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="videoFileList"
|
||||
list-type="picture-card"
|
||||
:before-upload="beforeVideoUpload"
|
||||
:custom-request="customRequest"
|
||||
@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等常见视频格式,大小不超过50MB
|
||||
</div>
|
||||
|
||||
<!-- 视频预览按钮 -->
|
||||
<div v-if="currentItem.video" 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="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
@@ -513,20 +982,87 @@ const removeTag = (index) => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<a-button
|
||||
<button
|
||||
type="button"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm cursor-pointer"
|
||||
@click="handleDelete"
|
||||
>
|
||||
确认
|
||||
</a-button>
|
||||
<a-button
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm cursor-pointer"
|
||||
@click="closeDeleteConfirm"
|
||||
>
|
||||
取消
|
||||
</a-button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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="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">
|
||||
<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>
|
||||
@@ -548,4 +1084,95 @@ const removeTag = (index) => {
|
||||
.backdrop-blur-sm {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
</style>
|
||||
|
||||
/* 上传组件暗黑模式适配 */
|
||||
:deep(.ant-upload-list-picture-card .ant-upload-list-item) {
|
||||
border-color: #4b5563;
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-select-picture-card) {
|
||||
background-color: transparent;
|
||||
border-color: #4b5563;
|
||||
}
|
||||
|
||||
:deep(.ant-upload-list-picture-card-container) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-select-picture-card) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
/* 视频播放器样式 */
|
||||
.aspect-w-16 {
|
||||
position: relative;
|
||||
padding- {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
/* 视频播放器样式 */
|
||||
.aspect-w-16 {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 */
|
||||
}
|
||||
|
||||
.aspect-h-9 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.aspect-w-16 > *, .aspect-h-9 > * {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* 模态框动画 */
|
||||
.transform {
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
/* 表单元素暗黑模式适配 */
|
||||
:deep(.dark input),
|
||||
:deep(.dark select),
|
||||
:deep(.dark textarea) {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* 滚动条美化 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.7);
|
||||
}
|
||||
|
||||
/* 暗黑模式滚动条 */
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(75, 85, 99, 0.5);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(75, 85, 99, 0.7);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -23,14 +23,33 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const project = {
|
||||
id: route.params.id,
|
||||
// 分类数据
|
||||
const categories = [
|
||||
{ id: 1, name: 'Web应用', value: 'Web应用' },
|
||||
{ id: 2, name: '移动端', value: '移动端' },
|
||||
{ id: 3, name: '小程序', value: '小程序' },
|
||||
{ id: 4, name: '企业系统', value: '企业系统' }
|
||||
]
|
||||
|
||||
// 标签数据
|
||||
const allTags = [
|
||||
{ id: 1, name: 'Vue3' },
|
||||
{ id: 2, name: 'Node.js' },
|
||||
{ id: 3, name: 'TypeScript' },
|
||||
{ id: 4, name: 'MongoDB' },
|
||||
{ id: 5, name: 'Docker' }
|
||||
]
|
||||
|
||||
const project = ref({
|
||||
id: parseInt(route.params.id),
|
||||
title: '电商平台',
|
||||
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
category: 'Web应用',
|
||||
tags: ['Vue3', 'Node.js', 'TypeScript', 'MongoDB', 'Docker'],
|
||||
category: categories[0],
|
||||
tags: allTags,
|
||||
author: '技术团队',
|
||||
price: '¥ 29,999',
|
||||
price: 29999,
|
||||
status: '已发布',
|
||||
createdAt: new Date().toISOString(),
|
||||
description: '基于微服务架构的电商平台,包含商品管理、订单系统、支付对接等模块。采用前后端分离设计,提供完整的API文档和部署方案。系统具有高度可扩展性和定制化能力,适合中小型企业快速搭建自有电商平台。',
|
||||
features: [
|
||||
'响应式设计,支持多端访问',
|
||||
@@ -48,37 +67,50 @@ const project = {
|
||||
],
|
||||
video: 'http://d-jy.nailaoyun.cn//storage/video//20250327/2cc1abf9bd69410ce7c69660daf54260.mp4',
|
||||
wechatQR: '/assets/qr-code.png'
|
||||
}
|
||||
})
|
||||
|
||||
const relatedProjects = [
|
||||
const relatedProjects = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: 'CRM系统',
|
||||
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
tags: ['Vue3', 'Express'],
|
||||
price: '¥ 19,999'
|
||||
tags: [allTags[0], allTags[3]],
|
||||
category: categories[3],
|
||||
price: 19999,
|
||||
status: '已发布',
|
||||
createdAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '数据分析平台',
|
||||
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
tags: ['React', 'Python'],
|
||||
price: '¥ 24,999'
|
||||
tags: [allTags[1], allTags[2]],
|
||||
category: categories[0],
|
||||
price: 24999,
|
||||
status: '已发布',
|
||||
createdAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '企业官网',
|
||||
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
tags: ['Nuxt.js', 'Tailwind'],
|
||||
price: '¥ 9,999'
|
||||
tags: [allTags[0], allTags[4]],
|
||||
category: categories[0],
|
||||
price: 9999,
|
||||
status: '已发布',
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
// 轮播图自动播放配置
|
||||
const swiperOptions = {
|
||||
delay: 5000,
|
||||
disableOnInteraction: false
|
||||
}
|
||||
|
||||
const formatPrice = (price) => {
|
||||
return '¥ ' + price.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -99,7 +131,7 @@ const swiperOptions = {
|
||||
<span
|
||||
class="px-4 py-1.5 bg-white/10 backdrop-blur-md text-white text-sm font-medium rounded-full border border-white/20"
|
||||
>
|
||||
{{ project.category }}
|
||||
{{ project.category.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -108,10 +140,10 @@ const swiperOptions = {
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
<span
|
||||
v-for="tag in project.tags"
|
||||
:key="tag"
|
||||
:key="tag.id"
|
||||
class="px-3 py-1 bg-rose-500/20 backdrop-blur-md text-rose-100 text-sm font-medium rounded-full border border-rose-500/30"
|
||||
>
|
||||
{{ tag }}
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,7 +299,7 @@ const swiperOptions = {
|
||||
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-8 sticky top-8">
|
||||
<div class="mb-6">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">价格</p>
|
||||
<p class="text-3xl font-bold text-rose-600 dark:text-rose-400">{{ project.price }}</p>
|
||||
<p class="text-3xl font-bold text-rose-600 dark:text-rose-400">{{ formatPrice(project.price) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
@@ -291,11 +323,6 @@ const swiperOptions = {
|
||||
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-center text-gray-500 dark:text-gray-400 mb-4">扫码添加微信</p>
|
||||
<div class="flex justify-center">
|
||||
<!-- <img-->
|
||||
<!-- :src="project.wechatQR"-->
|
||||
<!-- class="w-32 h-32 object-cover rounded-lg border border-gray-200 dark:border-gray-700"-->
|
||||
<!-- alt="微信二维码"-->
|
||||
<!-- />-->
|
||||
<img
|
||||
src="@/assets/qr-code.png"
|
||||
class="w-48 h-68 object-cover rounded-lg border border-gray-200 dark:border-gray-700"
|
||||
@@ -339,15 +366,15 @@ const swiperOptions = {
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span
|
||||
v-for="tag in related.tags"
|
||||
:key="tag"
|
||||
:key="tag.id"
|
||||
class="px-3 py-1 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-xs font-medium rounded-lg"
|
||||
>
|
||||
{{ tag }}
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="font-bold text-rose-600 dark:text-rose-400">{{ related.price }}</p>
|
||||
<p class="font-bold text-rose-600 dark:text-rose-400">{{ formatPrice(related.price) }}</p>
|
||||
<button class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
@@ -413,4 +440,4 @@ const swiperOptions = {
|
||||
background: linear-gradient(135deg, #1a1d23 0%, #111827 100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -12,15 +12,36 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 分类数据
|
||||
const categories = [
|
||||
{ id: 1, name: '全部分类', value: 'all' },
|
||||
{ id: 2, name: 'Web应用', value: 'Web应用' },
|
||||
{ id: 3, name: '移动端', value: '移动端' },
|
||||
{ id: 4, name: '小程序', value: '小程序' },
|
||||
{ id: 5, name: '企业系统', value: '企业系统' }
|
||||
]
|
||||
|
||||
// 标签数据
|
||||
const allTags = [
|
||||
{ id: 1, name: 'Vue3' },
|
||||
{ id: 2, name: 'Node.js' },
|
||||
{ id: 3, name: 'TypeScript' },
|
||||
{ id: 4, name: 'React' },
|
||||
{ id: 5, name: 'PHP' },
|
||||
{ id: 6, name: 'MongoDB' },
|
||||
{ id: 7, name: 'Docker' }
|
||||
]
|
||||
|
||||
// 模拟项目数据
|
||||
const projects = ref(Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
title: `项目 ${i + 1}`,
|
||||
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
tags: ['Vue3', 'Node.js', 'TypeScript'].slice(0, Math.floor(Math.random() * 3) + 1),
|
||||
category: ['Web应用', '移动端', '小程序', '企业系统'][i % 4],
|
||||
tags: allTags.slice(0, Math.floor(Math.random() * 3) + 1),
|
||||
category: categories.find(c => c.value === ['Web应用', '移动端', '小程序', '企业系统'][i % 4]),
|
||||
author: '开发者' + (i + 1),
|
||||
price: (Math.random() * 10000 + 5000).toFixed(2),
|
||||
price: Math.random() * 10000 + 5000,
|
||||
status: ['已发布', '草稿', '已下架'][i % 3],
|
||||
createdAt: new Date(Date.now() - Math.floor(Math.random() * 10000000000)).toISOString(),
|
||||
description: '这是一个示例项目描述,包含了项目的主要功能和技术特点。实际项目中会有更详细的描述信息。'
|
||||
})))
|
||||
@@ -30,14 +51,6 @@ const selectedCategory = ref('all')
|
||||
const sortBy = ref('date')
|
||||
const searchQuery = ref('')
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: '全部分类' },
|
||||
{ value: 'Web应用', label: 'Web应用' },
|
||||
{ value: '移动端', label: '移动端' },
|
||||
{ value: '小程序', label: '小程序' },
|
||||
{ value: '企业系统', label: '企业系统' }
|
||||
]
|
||||
|
||||
const sortOptions = [
|
||||
{ value: 'date', label: '最新发布' },
|
||||
{ value: 'price-high', label: '价格从高到低' },
|
||||
@@ -47,7 +60,7 @@ const sortOptions = [
|
||||
const filteredProjects = computed(() => {
|
||||
return projects.value
|
||||
.filter(p =>
|
||||
(selectedCategory.value === 'all' || p.category === selectedCategory.value) &&
|
||||
(selectedCategory.value === 'all' || p.category.value === selectedCategory.value) &&
|
||||
(searchQuery.value === '' || p.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
p.description.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
)
|
||||
@@ -55,9 +68,9 @@ const filteredProjects = computed(() => {
|
||||
if (sortBy.value === 'date') {
|
||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||
} else if (sortBy.value === 'price-high') {
|
||||
return parseFloat(b.price) - parseFloat(a.price)
|
||||
return b.price - a.price
|
||||
} else if (sortBy.value === 'price-low') {
|
||||
return parseFloat(a.price) - parseFloat(b.price)
|
||||
return a.price - b.price
|
||||
}
|
||||
return 0
|
||||
})
|
||||
@@ -69,7 +82,7 @@ const formatDate = (dateString) => {
|
||||
}
|
||||
|
||||
const formatPrice = (price) => {
|
||||
return '¥ ' + parseFloat(price).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
return '¥ ' + price.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
const getAosEffect = (index) => {
|
||||
@@ -101,7 +114,8 @@ const getAosEffect = (index) => {
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-6 text-gray-900 dark:text-white">
|
||||
<span class="relative">
|
||||
项目展示
|
||||
<span class="absolute -bottom-2 left-0 right-0 mx-auto w-1/3 h-1 bg-gradient-to-r from-rose-400 to-rose-600 rounded-full"></span>
|
||||
<span
|
||||
class="absolute -bottom-2 left-0 right-0 mx-auto w-1/3 h-1 bg-gradient-to-r from-rose-400 to-rose-600 rounded-full"></span>
|
||||
</span>
|
||||
</h1>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
@@ -116,8 +130,10 @@ const getAosEffect = (index) => {
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" 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 0z" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" 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 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
@@ -136,10 +152,10 @@ const getAosEffect = (index) => {
|
||||
>
|
||||
<option
|
||||
v-for="category in categories"
|
||||
:key="category.value"
|
||||
:key="category.id"
|
||||
:value="category.value"
|
||||
>
|
||||
{{ category.label }}
|
||||
{{ category.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -178,13 +194,14 @@ const getAosEffect = (index) => {
|
||||
class="w-full h-full object-cover transform transition-transform duration-700 group-hover:scale-110"
|
||||
:alt="project.title"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div class="absolute top-4 right-4">
|
||||
<span
|
||||
class="px-3 py-1 bg-black/50 backdrop-blur-md text-white text-xs font-medium rounded-full"
|
||||
>
|
||||
{{ project.category }}
|
||||
{{ project.category.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,10 +210,10 @@ const getAosEffect = (index) => {
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
<span
|
||||
v-for="tag in project.tags"
|
||||
:key="tag"
|
||||
:key="tag.id"
|
||||
class="px-2 py-1 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-xs font-medium rounded-lg"
|
||||
>
|
||||
{{ tag }}
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -214,9 +231,11 @@ const getAosEffect = (index) => {
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ formatDate(project.createdAt) }}</p>
|
||||
</div>
|
||||
|
||||
<button class="w-10 h-10 rounded-full bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-400 flex items-center justify-center hover:bg-rose-100 dark:hover:bg-rose-900/40 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
<button
|
||||
class="w-10 h-10 rounded-full bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-400 flex items-center justify-center hover:bg-rose-100 dark:hover:bg-rose-900/40 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -227,9 +246,11 @@ const getAosEffect = (index) => {
|
||||
<!-- 分页 -->
|
||||
<div class="mt-16 flex justify-center" data-aos="fade-up">
|
||||
<nav class="flex items-center space-x-2">
|
||||
<button class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
<button
|
||||
class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -237,23 +258,28 @@ const getAosEffect = (index) => {
|
||||
1
|
||||
</button>
|
||||
|
||||
<button class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<button
|
||||
class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
2
|
||||
</button>
|
||||
|
||||
<button class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<button
|
||||
class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
3
|
||||
</button>
|
||||
|
||||
<span class="text-gray-500 dark:text-gray-400">...</span>
|
||||
|
||||
<button class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<button
|
||||
class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
10
|
||||
</button>
|
||||
|
||||
<button class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
<button
|
||||
class="w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user