Files
spa-view/src/views/admin/Projects.vue

1291 lines
49 KiB
Vue
Raw Normal View History

2025-04-24 16:59:40 +08:00
<script setup>
import { ref, onMounted, computed } from "vue"
import AOS from "aos"
import "aos/dist/aos.css"
2025-05-06 16:59:12 +08:00
import { message, Upload, Select } from "ant-design-vue"
import {labelOptionApi} from "@/api/request/label.js";
import {categoryOptionApi} from "@/api/request/category.js";
2025-05-07 08:58:51 +08:00
import {
createProjectApi,
deleteProjectApi,
projectDetailApi,
projectListApi,
updateProjectApi
} from "@/api/request/project.js";
2025-05-06 16:59:12 +08:00
import {uploadImageApi, uploadVideoApi} from "@/api/request/upload.js";
2025-05-07 08:58:51 +08:00
import ProjectDetails from './common/ProjectDetails.vue'
2025-04-24 20:47:00 +08:00
onMounted(() => {
AOS.init({
duration: 800,
2025-04-24 20:47:00 +08:00
offset: 120,
easing: "ease-out-cubic",
once: false,
2025-04-24 20:47:00 +08:00
})
})
2025-04-24 16:59:40 +08:00
// 分类数据
2025-05-06 16:59:12 +08:00
const categoryOptions = ref([])
2025-05-06 16:59:12 +08:00
// 表单相关
const tagsOption = ref([]);
const getLabelOption = () => {
labelOptionApi().then((res) => {
tagsOption.value = res;
})
}
const getCategoryOption = () => {
categoryOptionApi().then((res) => {
categoryOptions.value = res;
})
}
2025-05-07 22:23:48 +08:00
const dataSource = ref([])
2025-04-24 20:47:00 +08:00
2025-05-06 16:59:12 +08:00
const getList = () => {
2025-05-07 08:58:51 +08:00
projectListApi({
2025-05-06 16:59:12 +08:00
page: currentPage.value,
pageSize: pageSize.value,
}).then((res) => {
dataSource.value = res.items;
currentPage.value = res.page;
pageSize.value = res.size;
totalItems.value = res.total;
pageCount.value = res.page_count;
})
}
2025-04-24 20:47:00 +08:00
const statusOptions = [
2025-05-06 16:59:12 +08:00
{ value: 0, label: "草稿" },
{ value: 1, label: "已发布" },
{ value: 2, label: "已下架" },
2025-04-24 16:59:40 +08:00
]
2025-04-24 20:47:00 +08:00
// 分页
const currentPage = ref(1)
2025-05-06 16:59:12 +08:00
const pageSize = ref(20)
const totalItems = ref(0)
const pageCount = ref(0)
2025-04-24 20:47:00 +08:00
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return dataSource.value.slice(start, end)
})
const changePage = (page) => {
currentPage.value = page
2025-05-07 23:12:06 +08:00
getList();
2025-04-24 20:47:00 +08:00
}
// 模态框
const showModal = ref(false)
2025-04-24 16:59:40 +08:00
const currentItem = ref(null)
2025-04-24 20:47:00 +08:00
const formLoading = ref(false)
// 图片上传相关
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("")
2025-05-07 08:58:51 +08:00
// 详情模态框
const showDetailModal = ref(false)
const currentDetailItem = ref(null)
const handlePreview = (file) => {
2025-05-07 22:23:48 +08:00
previewImage.value = file.url || file.thumbUrl
previewVisible.value = true
}
const handleVideoPreview = () => {
2025-05-06 16:59:12 +08:00
videoPreviewUrl.value = currentItem.value.video_url
videoPreviewVisible.value = true
}
const handleUploadChange = (info) => {
if (info.file.status === "uploading") {
uploading.value = true
return
}
if (info.file.status === "done") {
// 模拟上传成功后获取图片URL
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 = []
}
screenshotsUploading.value = false
message.success("截图上传成功")
}
if (info.file.status === "error") {
screenshotsUploading.value = false
message.error("截图上传失败")
}
}
const handleVideoUploadChange = (info) => {
videoFileList.value = info.fileList.slice(-1) // 只保留最后一个文件
2025-05-06 16:59:12 +08:00
console.log(info, 'ssssssss')
if (info.file.status === "uploading") {
videoUploading.value = true
return
}
if (info.file.status === "done") {
// 模拟上传成功后获取视频URL
2025-05-06 16:59:12 +08:00
// currentItem.value.video_url = info.file.response.url
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
}
2025-05-07 23:12:06 +08:00
const customCoverRequest = ({ file, onSuccess }) => {
// 模拟上传
uploadImageApi(file).then((res) => {
currentItem.value.cover = res.url;
onSuccess("ok", { status: "done" })
})
}
2025-05-06 16:59:12 +08:00
const customRequest = ({ file, onSuccess }) => {
// 模拟上传
2025-05-06 16:59:12 +08:00
uploadImageApi(file).then((res) => {
currentItem.value.screenshots.push(res.url);
onSuccess("ok", { status: "done" })
2025-05-06 16:59:12 +08:00
})
}
const customVideoRequest = ({ file, onSuccess }) => {
// 模拟上传
uploadVideoApi(file).then((res) => {
currentItem.value.video_url = res.url
onSuccess("ok", { status: "done" })
})
}
2025-04-24 20:47:00 +08:00
const openModal = (item = null) => {
2025-05-06 16:59:12 +08:00
getLabelOption();
getCategoryOption();
if (item) {
item = JSON.parse(JSON.stringify(item));
item.labels = item.label_ids;
}
currentItem.value = item
? JSON.parse(JSON.stringify(item)) // 深拷贝避免直接修改原对象
: {
title: "",
2025-05-06 16:59:12 +08:00
category_id: 1,
tags: [],
price: "",
2025-05-06 16:59:12 +08:00
status: 0,
preferential_price: '',
description: "",
cover: "",
features: [],
2025-05-06 16:59:12 +08:00
front: "",
server: "",
deploy: "",
screenshots: [],
2025-05-06 16:59:12 +08:00
video_url: ""
}
// 重置文件列表
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 = []
2025-05-06 16:59:12 +08:00
if (currentItem.value.video_url) {
videoFileList.value = [
{
uid: "-1",
name: "video.mp4",
status: "done",
2025-05-06 16:59:12 +08:00
url: currentItem.value.video_url,
},
]
2025-04-24 20:47:00 +08:00
}
2025-04-24 20:47:00 +08:00
showModal.value = true
}
2025-04-24 16:59:40 +08:00
2025-04-24 20:47:00 +08:00
const closeModal = () => {
showModal.value = false
}
// 确认删除模态框
const showDeleteConfirm = ref(false)
const itemToDelete = ref(null)
const openDeleteConfirm = (id) => {
itemToDelete.value = id
showDeleteConfirm.value = true
}
const closeDeleteConfirm = () => {
showDeleteConfirm.value = false
itemToDelete.value = null
}
const handleDelete = () => {
if (itemToDelete.value) {
2025-05-07 08:58:51 +08:00
deleteProjectApi({
ids: [itemToDelete.value]
}).then(() => {
message.success("项目删除成功")
closeDeleteConfirm()
getList();
})
2025-04-24 20:47:00 +08:00
}
2025-04-24 16:59:40 +08:00
}
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" })
}
2025-04-24 20:47:00 +08:00
const handleSubmit = () => {
formLoading.value = true
// 模拟API请求
setTimeout(() => {
// 确保price是数字
if (typeof currentItem.value.price === "string") {
currentItem.value.price = parseFloat(currentItem.value.price.replace(/,/g, ""))
}
2025-04-24 20:47:00 +08:00
if (currentItem.value.id) {
2025-05-06 16:59:12 +08:00
updateProjectApi(currentItem.value).then((res) => {
message.success("项目更新成功")
closeModal()
getList();
})
2025-04-24 20:47:00 +08:00
// 更新现有项目
const index = dataSource.value.findIndex((item) => item.id === currentItem.value.id)
2025-04-24 20:47:00 +08:00
if (index !== -1) {
2025-05-07 08:58:51 +08:00
dataSource.value[index] = {...currentItem.value}
2025-04-24 20:47:00 +08:00
}
} else {
2025-05-06 16:59:12 +08:00
createProjectApi(currentItem.value).then((res) => {
console.log(res)
message.success("项目创建成功")
closeModal()
getList();
2025-04-24 20:47:00 +08:00
})
2025-04-24 16:59:40 +08:00
}
2025-04-24 20:47:00 +08:00
formLoading.value = false
}, 800)
}
// 标签管理
const tagInputValue = ref("")
2025-04-24 20:47:00 +08:00
const handleAddTag = () => {
if (tagInputValue.value) {
// 检查是否已存在该标签
2025-05-06 16:59:12 +08:00
const existingTag = tagsOption.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) {
2025-05-06 16:59:12 +08:00
const newTagId = Math.max(...tagsOption.map((tag) => tag.id), 0) + 1
2025-05-07 08:58:51 +08:00
const newTag = {id: newTagId, name: tagInputValue.value}
2025-05-06 16:59:12 +08:00
tagsOption.push(newTag)
currentItem.value.tags.push(newTag)
}
tagInputValue.value = ""
2025-04-24 20:47:00 +08:00
}
}
const removeTag = (index) => {
currentItem.value.tags.splice(index, 1)
2025-04-24 16:59:40 +08:00
}
// 功能和技术栈管理
const featureInputValue = ref("")
2025-05-06 16:59:12 +08:00
const front = ref("")
const service = ref("")
const deploy = 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)
}
2025-05-07 08:58:51 +08:00
const openDetailModal = (item) => {
projectDetailApi({
id: item.id
}).then((res) => {
currentDetailItem.value = res
showDetailModal.value = true
})
}
2025-05-06 16:59:12 +08:00
getList();
2025-04-24 16:59:40 +08:00
</script>
<template>
2025-04-24 20:47:00 +08:00
<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()"
>
2025-05-07 08:58:51 +08:00
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
2025-04-24 20:47:00 +08:00
</svg>
2025-04-24 16:59:40 +08:00
新建项目
2025-04-24 20:47:00 +08:00
</button>
</div>
<!-- 表格 -->
<div class="rounded-2xl border border-gray-200 dark:border-gray-700">
<div class="overflow-x-auto" style="height: 60vh">
2025-04-24 20:47:00 +08:00
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<!-- 表头 -->
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
2025-05-07 08:58:51 +08:00
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
2025-04-24 20:47:00 +08:00
项目名称
</th>
2025-05-07 08:58:51 +08:00
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
2025-04-24 20:47:00 +08:00
分类
</th>
2025-05-07 08:58:51 +08:00
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
2025-04-24 20:47:00 +08:00
标签
</th>
2025-05-07 08:58:51 +08:00
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
2025-04-24 20:47:00 +08:00
价格
</th>
2025-05-07 08:58:51 +08:00
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
2025-04-24 20:47:00 +08:00
状态
</th>
2025-05-07 08:58:51 +08:00
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
2025-04-24 20:47:00 +08:00
创建时间
</th>
2025-05-07 08:58:51 +08:00
<th scope="col"
class="px-6 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
2025-04-24 20:47:00 +08:00
操作
</th>
</tr>
</thead>
<!-- 表格内容 -->
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
2025-05-07 08:58:51 +08:00
<tr v-for="item in paginatedData" :key="item.id"
class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
2025-04-24 20:47:00 +08:00
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
<div class="flex items-center space-x-3">
<div class="flex-shrink-0 h-10 w-10 rounded-lg overflow-hidden">
2025-05-07 08:58:51 +08:00
<img :src="item.cover" :alt="item.title" class="h-full w-full object-cover"/>
</div>
<div>
{{ item.title }}
</div>
</div>
2025-04-24 20:47:00 +08:00
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ item.category.name }}
2025-04-24 20:47:00 +08:00
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex flex-wrap gap-1">
<span
2025-05-06 16:59:12 +08:00
v-for="(tag, index) in item.labels"
2025-04-24 20:47:00 +08:00
: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.name }}
2025-04-24 20:47:00 +08:00
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{{ formatPrice(item.price) }}
2025-04-24 20:47:00 +08:00
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
:class="`px-2 py-1 text-xs font-medium rounded-full ${
2025-05-07 16:47:11 +08:00
item.status === 1
2025-04-24 20:47:00 +08:00
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
2025-05-07 16:47:11 +08:00
: item.status === 2
? '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'
2025-04-24 20:47:00 +08:00
}`"
>
2025-05-06 16:59:12 +08:00
{{ item.status_text }}
2025-04-24 20:47:00 +08:00
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ formatDate(item.createdAt) }}
2025-04-24 20:47:00 +08:00
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end space-x-2">
<button
class="p-1.5 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors cursor-pointer"
title="查看"
2025-05-07 08:58:51 +08:00
@click="openDetailModal(item)"
2025-04-24 20:47:00 +08:00
>
2025-05-07 08:58:51 +08:00
<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 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
2025-04-24 20:47:00 +08:00
</svg>
</button>
<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)"
>
2025-05-07 08:58:51 +08:00
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
2025-04-24 20:47:00 +08:00
</svg>
</button>
<button
class="p-1.5 text-rose-600 hover:text-rose-800 dark:text-rose-400 dark:hover:text-rose-300 hover:bg-rose-50 dark:hover:bg-rose-900/20 rounded-lg transition-colors cursor-pointer"
title="删除"
@click="openDeleteConfirm(item.id)"
>
2025-05-07 08:58:51 +08:00
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
2025-04-24 20:47:00 +08:00
</svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
2025-05-07 08:58:51 +08:00
<div
class="bg-white dark:bg-gray-800 px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
2025-04-24 20:47:00 +08:00
<div class="text-sm text-gray-700 dark:text-gray-300">
{{ totalItems }} 条记录
</div>
<div class="flex space-x-1">
<button
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
:disabled="currentPage === 1"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }"
@click="currentPage > 1 && changePage(currentPage - 1)"
>
上一页
</button>
<button
2025-05-06 16:59:12 +08:00
v-for="page in pageCount"
2025-04-24 20:47:00 +08:00
:key="page"
class="px-3 py-1 border rounded-md text-sm font-medium cursor-pointer"
:class="page === currentPage
? 'bg-rose-600 text-white border-rose-600 dark:bg-rose-600 dark:border-rose-600'
: 'border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'"
@click="changePage(page)"
>
{{ page }}
</button>
<button
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
2025-05-06 16:59:12 +08:00
:disabled="currentPage === pageCount"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === pageCount }"
@click="currentPage < pageCount && changePage(currentPage + 1)"
2025-04-24 20:47:00 +08:00
>
下一页
</button>
</div>
</div>
2025-04-24 16:59:40 +08:00
</div>
2025-04-24 20:47:00 +08:00
</div>
<!-- 项目表单模态框 -->
2025-05-07 08:58:51 +08:00
<div v-if="showModal" class="fixed inset-0 z-50 overflow-y-scroll" aria-labelledby="modal-title" role="dialog"
aria-modal="true">
2025-04-24 20:47:00 +08:00
<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">&#8203;</span>
<!-- 模态框内容 -->
2025-05-07 08:58:51 +08:00
<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">
2025-04-24 20:47:00 +08:00
<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"
>
2025-05-07 08:58:51 +08:00
<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"/>
2025-04-24 20:47:00 +08:00
</svg>
</button>
</div>
2025-05-06 16:59:12 +08:00
<form @submit.prevent="handleSubmit" @keydown.enter.prevent="event.preventDefault()" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2">
2025-04-24 20:47:00 +08:00
2025-05-06 16:59:12 +08:00
<!-- 基本信息 -->
<div class="mr-10">
<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>
2025-05-06 16:59:12 +08:00
<!-- 价格 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目价格</label>
<div class="relative">
2025-05-07 08:58:51 +08:00
<span
class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400">¥</span>
2025-05-06 16:59:12 +08:00
<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>
2025-05-06 16:59:12 +08:00
<!-- 价格 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">优惠价格</label>
<div class="relative">
2025-05-07 08:58:51 +08:00
<span
class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400">¥</span>
2025-05-06 16:59:12 +08:00
<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>
2025-04-24 20:47:00 +08:00
2025-05-06 16:59:12 +08:00
<!-- 分类 -->
<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"
2025-04-24 20:47:00 +08:00
>
2025-05-06 16:59:12 +08:00
<option v-for="option in categoryOptions" :key="option.id" :value="option.id">
{{ option.name }}
</option>
</select>
</div>
2025-04-24 20:47:00 +08:00
2025-05-06 16:59:12 +08:00
<!-- 状态 -->
<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>
2025-04-24 20:47:00 +08:00
2025-05-06 16:59:12 +08:00
<div>
<!-- 标签 -->
2025-05-07 22:23:48 +08:00
<div class="mt-3">
2025-05-06 16:59:12 +08:00
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目标签</label>
2025-05-07 08:58:51 +08:00
<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">
2025-05-06 16:59:12 +08:00
<Select
class="w-full"
v-model:value="currentItem.labels"
:options="tagsOption"
mode="multiple"
placeholder="请选择标签"
:fieldNames="{
2025-05-07 22:23:48 +08:00
label: 'name',
value: 'id',
}"
2025-05-06 16:59:12 +08:00
/>
</div>
</div>
<!-- 项目功能 -->
2025-05-07 22:23:48 +08:00
<div class="mt-3">
2025-05-06 16:59:12 +08:00
<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)"
>
2025-05-07 08:58:51 +08:00
<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>
2025-05-06 16:59:12 +08:00
</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>
2025-05-06 16:59:12 +08:00
<!-- 项目描述 -->
2025-05-07 22:23:48 +08:00
<div class="mt-3">
2025-05-06 16:59:12 +08:00
<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>
</div>
</div>
</div>
2025-05-06 16:59:12 +08:00
<!-- 截图视频 -->
<div class="ml-10">
<!-- 项目封面上传 -->
<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"
2025-05-07 23:12:06 +08:00
:custom-request="customCoverRequest"
2025-05-06 16:59:12 +08:00
@preview="handlePreview"
@change="handleUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="fileList.length < 1">
2025-05-07 08:58:51 +08:00
<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"/>
2025-05-06 16:59:12 +08:00
</svg>
<span>点击上传封面</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持JPGPNG格式大小不超过2MB
</div>
</div>
</div>
2025-05-06 16:59:12 +08:00
<!-- 视频上传 -->
<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="customVideoRequest"
@change="handleVideoUploadChange"
:show-upload-list="{ showPreviewIcon: false, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="videoFileList.length < 1">
2025-05-07 08:58:51 +08:00
<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"/>
2025-05-06 16:59:12 +08:00
</svg>
<span>点击上传视频</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持MP4WebM等常见视频格式大小不超过50MB
</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"
>
2025-05-07 08:58:51 +08:00
<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"/>
2025-05-06 16:59:12 +08:00
</svg>
预览视频
</button>
</div>
</div>
</div>
2025-05-06 16:59:12 +08:00
<!-- 项目截图上传 -->
<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
2025-05-07 22:23:48 +08:00
:maxCount="10"
2025-05-06 16:59:12 +08:00
>
<div>
2025-05-07 08:58:51 +08:00
<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"/>
2025-05-06 16:59:12 +08:00
</svg>
<span>点击上传截图</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持JPGPNG格式大小不超过2MB可上传多张
</div>
</div>
</div>
</div>
</div>
2025-05-06 16:59:12 +08:00
2025-04-24 20:47:00 +08:00
<!-- 表单按钮 -->
<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">
2025-05-07 08:58:51 +08:00
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
2025-04-24 20:47:00 +08:00
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
2025-05-07 08:58:51 +08:00
<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>
2025-04-24 20:47:00 +08:00
</svg>
</span>
{{ currentItem?.id ? '保存修改' : '创建项目' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
2025-05-07 08:58:51 +08:00
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog"
aria-modal="true">
2025-04-24 20:47:00 +08:00
<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="closeDeleteConfirm"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
2025-05-07 08:58:51 +08:00
<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">
2025-04-24 20:47:00 +08:00
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
2025-05-07 08:58:51 +08:00
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/30 sm:mx-0 sm:h-10 sm:w-10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-600 dark:text-red-400" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
2025-04-24 20:47:00 +08:00
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white" id="modal-title">
确认删除
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
确定要删除这个项目吗此操作无法撤销
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
2025-04-24 20:47:00 +08:00
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"
>
确认
</button>
<button
2025-04-24 20:47:00 +08:00
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"
>
取消
</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">&#8203;</span>
<!-- 模态框内容 -->
2025-05-07 08:58:51 +08:00
<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"
>
2025-05-07 08:58:51 +08:00
<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>
2025-05-07 22:23:48 +08:00
<img :src="previewImage" class="w-full max-h-[80vh] max-w-full rounded-lg shadow-xl"/>
</div>
</div>
</div>
</div>
<!-- 视频预览模态框 -->
2025-05-07 08:58:51 +08:00
<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">&#8203;</span>
<!-- 模态框内容 -->
2025-05-07 08:58:51 +08:00
<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"
>
2025-05-07 08:58:51 +08:00
<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>
2025-04-24 20:47:00 +08:00
</div>
</div>
</div>
2025-04-24 16:59:40 +08:00
</div>
2025-05-07 08:58:51 +08:00
<!-- 项目详情模态框 -->
<ProjectDetails
v-model:visible="showDetailModal"
:project="currentDetailItem"
@close="showDetailModal = false"
/>
2025-04-24 16:59:40 +08:00
</template>
<style lang="scss" scoped>
2025-04-24 20:47:00 +08:00
/* 自定义样式 */
/* 毛玻璃效果增强 */
.backdrop-blur-xl {
backdrop-filter: blur(16px);
2025-04-24 16:59:40 +08:00
}
2025-04-24 20:47:00 +08:00
.backdrop-blur-md {
backdrop-filter: blur(12px);
}
.backdrop-blur-sm {
backdrop-filter: blur(4px);
2025-04-24 16:59:40 +08:00
}
/* 上传组件暗黑模式适配 */
: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;
2025-05-07 08:58:51 +08:00
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);
}
}
2025-05-06 16:59:12 +08:00
:deep .ant-select-show-search:where(.css-dev-only-do-not-override-1p3hq3p).ant-select:not(.ant-select-customize-input) .ant-select-selector {
cursor: text;
border-color: #364153;
background-color: #364153;
color: white;
}
2025-05-07 08:58:51 +08:00
2025-05-06 16:59:12 +08:00
:deep :where(.css-dev-only-do-not-override-1p3hq3p).ant-select-multiple .ant-select-selection-item {
position: relative;
display: flex;
flex: none;
box-sizing: border-box;
max-width: 100%;
height: 24px;
margin-top: 2px;
margin-bottom: 2px;
line-height: 22px;
background: rgba(255, 32, 86, 0.35);
border: 1px solid;
border-radius: 4px;
cursor: default;
transition: font-size 0.3s, line-height 0.3s, height 0.3s;
user-select: none;
margin-inline-end: 4px;
padding-inline-start: 8px;
padding-inline-end: 4px;
2025-05-07 08:58:51 +08:00
border-color: var(--color-gray-600);
2025-05-06 16:59:12 +08:00
}
:deep :where(.css-dev-only-do-not-override-1p3hq3p).ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled) {
//background-color: #364153;
background-color: var(--color-gray-600);
}
:deep :where(.css-dev-only-do-not-override-1p3hq3p).ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
color: rgba(0, 0, 0, 0.88);
font-weight: 600;
background-color: var(--color-purple-500);
}
2025-05-07 22:23:48 +08:00
:where(.css-dev-only-do-not-override-1p3hq3p).ant-select-multiple .ant-select-selection-placeholder {
position: absolute;
top: 50%;
inset-inline-start: 11px;
inset-inline-end: 11px;
transform: translateY(-50%);
transition: all 0.3s;
color: var(--color-gray-300);
}
</style>