项目相关优化

This commit is contained in:
2025-05-08 13:31:22 +08:00
parent 3ede84b678
commit 7cd1a58cac
10 changed files with 1273 additions and 202 deletions

View File

@@ -44,6 +44,8 @@ service.interceptors.response.use(
msg.destroy()
})
}
throw new Error(message);
},
error => {
console.log('err' + error); // 打印错误信息

View File

@@ -19,4 +19,7 @@ export function updateProjectApi(credentials) {
}
export function deleteProjectApi(credentials) {
return post(`${prefix}/delete`, credentials)
}
export function changeContentProjectApi(credentials) {
return post(`${prefix}/change-content`, credentials)
}

View File

@@ -17,7 +17,7 @@ const mobileMenuVisible = ref(false)
const menuItems = [
{ path: '/', title: '首页', icon: 'home' },
{ path: '/projects', title: '项目列表', icon: 'grid' },
{ path: '/projects', title: '项目合集', icon: 'grid' },
{ path: '/services', title: '服务项目', icon: 'briefcase' },
{ path: '/about', title: '关于我们', icon: 'info' },
]

View File

@@ -1,11 +1,12 @@
<script setup>
import { ref, onMounted, computed } from "vue"
import {ref, onMounted, computed} from "vue"
import AOS from "aos"
import "aos/dist/aos.css"
import { message, Upload, Select } from "ant-design-vue"
import {message, Upload, Select} from "ant-design-vue"
import {labelOptionApi} from "@/api/request/label.js";
import {categoryOptionApi} from "@/api/request/category.js";
import {
changeContentProjectApi,
createProjectApi,
deleteProjectApi,
projectDetailApi,
@@ -14,6 +15,9 @@ import {
} from "@/api/request/project.js";
import {uploadImageApi, uploadVideoApi} from "@/api/request/upload.js";
import ProjectDetails from './common/ProjectDetails.vue'
import {MdEditor} from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
onMounted(() => {
AOS.init({
@@ -58,9 +62,9 @@ const getList = () => {
}
const statusOptions = [
{ value: 0, label: "草稿" },
{ value: 1, label: "已发布" },
{ value: 2, label: "已下架" },
{value: 0, label: "草稿"},
{value: 1, label: "已发布"},
{value: 2, label: "已下架"},
]
// 分页
@@ -87,6 +91,8 @@ const formLoading = ref(false)
// 图片上传相关
const fileList = ref([])
const moduleImageFileList = ref([])
const flowchartFileList = ref([])
const uploading = ref(false)
const previewVisible = ref(false)
const previewImage = ref("")
@@ -131,6 +137,38 @@ const handleUploadChange = (info) => {
}
}
const handleModuleImageUploadChange = (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 handleFlowchartUploadChange = (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
@@ -186,9 +224,9 @@ const beforeUpload = (file) => {
if (!isJpgOrPng) {
message.error("只能上传JPG或PNG格式的图片!")
}
const isLt2M = file.size / 1024 / 1024 < 2
const isLt2M = file.size / 1024 / 1024 < 10
if (!isLt2M) {
message.error("图片大小不能超过2MB!")
message.error("图片大小不能超过10MB!")
}
return isJpgOrPng && isLt2M
}
@@ -198,34 +236,50 @@ const beforeVideoUpload = (file) => {
if (!isVideo) {
message.error("只能上传视频文件!")
}
const isLt50M = file.size / 1024 / 1024 < 50
const isLt50M = file.size / 1024 / 1024 < 500
if (!isLt50M) {
message.error("视频大小不能超过50MB!")
message.error("视频大小不能超过500MB!")
}
return isVideo && isLt50M
}
const customCoverRequest = ({ file, onSuccess }) => {
const customCoverRequest = ({file, onSuccess}) => {
// 模拟上传
uploadImageApi(file).then((res) => {
currentItem.value.cover = res.url;
onSuccess("ok", { status: "done" })
onSuccess("ok", {status: "done"})
})
}
const customRequest = ({ file, onSuccess }) => {
const customModuleImageRequest = ({file, onSuccess}) => {
// 模拟上传
uploadImageApi(file).then((res) => {
currentItem.value.module_image = res.url;
onSuccess("ok", {status: "done"})
})
}
const customFlowchartRequest = ({file, onSuccess}) => {
// 模拟上传
uploadImageApi(file).then((res) => {
currentItem.value.flowchart = res.url;
onSuccess("ok", {status: "done"})
})
}
const customRequest = ({file, onSuccess}) => {
// 模拟上传
uploadImageApi(file).then((res) => {
currentItem.value.screenshots.push(res.url);
onSuccess("ok", { status: "done" })
onSuccess("ok", {status: "done"})
})
}
const customVideoRequest = ({ file, onSuccess }) => {
const customVideoRequest = ({file, onSuccess}) => {
// 模拟上传
uploadVideoApi(file).then((res) => {
currentItem.value.video_url = res.url
onSuccess("ok", { status: "done" })
onSuccess("ok", {status: "done"})
})
}
@@ -235,6 +289,7 @@ const openModal = (item = null) => {
if (item) {
item = JSON.parse(JSON.stringify(item));
item.labels = item.label_ids;
item.features = item.features_names
}
currentItem.value = item
? JSON.parse(JSON.stringify(item)) // 深拷贝避免直接修改原对象
@@ -251,6 +306,9 @@ const openModal = (item = null) => {
front: "",
server: "",
deploy: "",
module_image: "",
flowchart: "",
content: "",
screenshots: [],
video_url: ""
}
@@ -261,7 +319,7 @@ const openModal = (item = null) => {
fileList.value = [
{
uid: "-1",
name: "cover.jpg",
name: "封面",
status: "done",
url: currentItem.value.cover,
},
@@ -287,13 +345,37 @@ const openModal = (item = null) => {
videoFileList.value = [
{
uid: "-1",
name: "video.mp4",
name: "演示视频",
status: "done",
url: currentItem.value.video_url,
},
]
}
moduleImageFileList.value = []
if (currentItem.value.module_image) {
moduleImageFileList.value = [
{
uid: "-1",
name: "模块图",
status: "done",
url: currentItem.value.module_image,
},
]
}
flowchartFileList.value = []
if (currentItem.value.flowchart) {
flowchartFileList.value = [
{
uid: "-1",
name: "流程图",
status: "done",
url: currentItem.value.flowchart,
},
]
}
showModal.value = true
}
@@ -328,12 +410,12 @@ const handleDelete = () => {
}
const formatPrice = (price) => {
return "¥ " + parseFloat(price).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
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" })
return date.toLocaleDateString("zh-CN", {year: "numeric", month: "2-digit", day: "2-digit"})
}
const handleSubmit = () => {
@@ -348,16 +430,10 @@ const handleSubmit = () => {
if (currentItem.value.id) {
updateProjectApi(currentItem.value).then((res) => {
message.success("项目更新成功")
closeModal()
getList();
})
// 更新现有项目
const index = dataSource.value.findIndex((item) => item.id === currentItem.value.id)
if (index !== -1) {
dataSource.value[index] = {...currentItem.value}
}
} else {
createProjectApi(currentItem.value).then((res) => {
console.log(res)
@@ -371,6 +447,24 @@ const handleSubmit = () => {
}, 800)
}
const handleProjectSubmit = () => {
formLoading.value = true
if (currentItem.value.id) {
changeContentProjectApi(currentItem.value).then((res) => {
message.success("更新成功")
closeContentModal()
getList();
})
}
formLoading.value = false
}
const closeContentModal = () => {
showContentModal.value = false
}
// 标签管理
const tagInputValue = ref("")
const handleAddTag = () => {
@@ -405,12 +499,16 @@ const service = ref("")
const deploy = ref("")
const handleAddFeature = () => {
if (featureInputValue.value && !currentItem.value.features.includes(featureInputValue.value)) {
if (featureInputValue.value) {
if (!currentItem.value.features) {
currentItem.value.features = []
}
currentItem.value.features.push(featureInputValue.value)
featureInputValue.value = ""
if (currentItem.value.features) {
if (!currentItem.value.features.includes(featureInputValue.value)) {
currentItem.value.features.push(featureInputValue.value)
featureInputValue.value = ""
}
}
}
}
@@ -426,8 +524,44 @@ const openDetailModal = (item) => {
showDetailModal.value = true
})
}
const showContentModal = ref(false)
const openContentModal = (item) => {
currentItem.value = {
id: item.id,
content: item.content || ''
}
showContentModal.value = true
}
getList();
const isUpload = ref(false);
const editorConfig = ref({
preview: {
show: false // 确保预览框显示
}
});
// 处理Markdown编辑器的图片上传
const handleMdImageUpload = async (
files,
callback,
) => {
if (!files || files.length === 0) return;
const urls = [];
for (const file of files) {
if (isUpload.value === true) continue;
isUpload.value = true;
const url = await uploadImageApi(file).then((res) => {
return res.url;
});
if (url) urls.push(url);
isUpload.value = false;
}
callback(urls);
};
</script>
<template>
@@ -478,6 +612,10 @@ getList();
class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
创建时间
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
最后编辑时间
</th>
<th scope="col"
class="px-6 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
操作
@@ -530,7 +668,10 @@ getList();
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ formatDate(item.createdAt) }}
{{ item.created_at }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{{ item.updated_at }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end space-x-2">
@@ -547,6 +688,53 @@ getList();
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"/>
</svg>
</button>
<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="补充详细介绍"
@click="openContentModal(item)"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path fill="url(#fluentColorAppsListDetail240)"
d="M11 6a1 1 0 0 1 1-1h9a1 1 0 1 1 0 2h-9a1 1 0 0 1-1-1"/>
<path fill="url(#fluentColorAppsListDetail241)"
d="M11 9a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1"/>
<path fill="url(#fluentColorAppsListDetail242)"
d="M11 15a1 1 0 0 1 1-1h9a1 1 0 1 1 0 2h-9a1 1 0 0 1-1-1"/>
<path fill="url(#fluentColorAppsListDetail243)"
d="M11 18a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1"/>
<path fill="url(#fluentColorAppsListDetail244)"
d="M4.25 4A2.25 2.25 0 0 0 2 6.25v2.5A2.25 2.25 0 0 0 4.25 11h2.5A2.25 2.25 0 0 0 9 8.75v-2.5A2.25 2.25 0 0 0 6.75 4zm0 9A2.25 2.25 0 0 0 2 15.25v2.5A2.25 2.25 0 0 0 4.25 20h2.5A2.25 2.25 0 0 0 9 17.75v-2.5A2.25 2.25 0 0 0 6.75 13z"/>
<defs>
<linearGradient id="fluentColorAppsListDetail240" x1="9.35" x2="20.9" y1="3" y2="19"
gradientUnits="userSpaceOnUse">
<stop stop-color="#36dff1"/>
<stop offset="1" stop-color="#0094f0"/>
</linearGradient>
<linearGradient id="fluentColorAppsListDetail241" x1="9.35" x2="20.9" y1="3" y2="19"
gradientUnits="userSpaceOnUse">
<stop stop-color="#36dff1"/>
<stop offset="1" stop-color="#0094f0"/>
</linearGradient>
<linearGradient id="fluentColorAppsListDetail242" x1="9.35" x2="20.9" y1="3" y2="19"
gradientUnits="userSpaceOnUse">
<stop stop-color="#36dff1"/>
<stop offset="1" stop-color="#0094f0"/>
</linearGradient>
<linearGradient id="fluentColorAppsListDetail243" x1="9.35" x2="20.9" y1="3" y2="19"
gradientUnits="userSpaceOnUse">
<stop stop-color="#36dff1"/>
<stop offset="1" stop-color="#0094f0"/>
</linearGradient>
<linearGradient id="fluentColorAppsListDetail244" x1="3.665" x2="7.232" y1="6.127" y2="19.147"
gradientUnits="userSpaceOnUse">
<stop offset=".125" stop-color="#9c6cfe"/>
<stop offset="1" stop-color="#7a41dc"/>
</linearGradient>
</defs>
</g>
</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="编辑"
@@ -652,7 +840,7 @@ getList();
<div class="grid grid-cols-1 md:grid-cols-2">
<!-- 基本信息 -->
<div class="mr-10">
<div class="mr-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 项目名称 -->
<div>
@@ -833,99 +1021,173 @@ getList();
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入项目描述"
></textarea>
<!-- <MdEditor-->
<!-- v-model="currentItem.description"-->
<!-- :toolbars-exclude="['github']"-->
<!-- class="md-editor-container"-->
<!-- editor-id="description-md"-->
<!-- theme="dark"-->
<!-- :config="editorConfig"-->
<!-- @on-upload-img="handleMdImageUpload"-->
<!-- />-->
</div>
</div>
</div>
<!-- 截图视频 -->
<div class="ml-10">
<div class="ml-5">
<div class="grid grid-cols-1 md:grid-cols-2">
<!-- 项目封面上传 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目封面</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
v-model:file-list="fileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customCoverRequest"
@preview="handlePreview"
@change="handleUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="fileList.length < 1">
<div
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>点击上传封面</span>
<!-- 项目封面上传 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目封面</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
v-model:file-list="fileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customCoverRequest"
@preview="handlePreview"
@change="handleUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="fileList.length < 1">
<div
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>点击上传封面</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持JPGPNG格式大小不超过10MB
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持JPGPNG格式大小不超过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="customVideoRequest"
@change="handleVideoUploadChange"
:show-upload-list="{ showPreviewIcon: false, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="videoFileList.length < 1">
<div
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24"
<!-- 视频上传 -->
<div class="ml-5">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">演示视频</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
v-model:file-list="videoFileList"
list-type="picture-card"
:before-upload="beforeVideoUpload"
:custom-request="customVideoRequest"
@change="handleVideoUploadChange"
:show-upload-list="{ showPreviewIcon: false, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="videoFileList.length < 1">
<div
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<span>点击上传视频</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持MP4WebM等常见视频格式大小不超过500MB
</div>
<!-- 视频预览按钮 -->
<div v-if="currentItem.video_url" class="mt-3">
<button
type="button"
class="px-3 py-1.5 bg-gray-100 dark:bg-gray-600 text-gray-700 dark:text-gray-200 text-sm font-medium rounded-lg flex items-center hover:bg-gray-200 dark:hover:bg-gray-500 transition-colors cursor-pointer"
@click="handleVideoPreview"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="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"/>
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>
<span>点击上传视频</span>
</div>
预览视频
</button>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持MP4WebM等常见视频格式大小不超过50MB
</div>
</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"
<!-- 模块图上传 -->
<div class="mt-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">模块图</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
v-model:file-list="moduleImageFileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customModuleImageRequest"
@preview="handlePreview"
@change="handleModuleImageUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:maxCount="1"
>
<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 v-if="moduleImageFileList.length < 1">
<div
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>点击上传封面</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持JPGPNG格式大小不超过10MB
</div>
</div>
</div>
<!-- 流程图上传 -->
<div class="ml-5 mt-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">流程图</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
v-model:file-list="flowchartFileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="customFlowchartRequest"
@preview="handlePreview"
@change="handleFlowchartUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
:maxCount="1"
>
<div v-if="flowchartFileList.length < 1">
<div
class="ant-upload-text flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mb-1" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>点击上传封面</span>
</div>
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持JPGPNG格式大小不超过10MB
</div>
</div>
</div>
</div>
<!-- 项目截图上传 -->
<div>
<div class="mt-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目截图</label>
<div class="border border-gray-300 dark:border-gray-600 rounded-xl p-4 dark:bg-gray-700">
<Upload
@@ -937,7 +1199,6 @@ getList();
@change="handleScreenshotsUploadChange"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
multiple
:maxCount="10"
>
<div>
<div
@@ -952,7 +1213,7 @@ getList();
</div>
</Upload>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持JPGPNG格式大小不超过2MB可上传多张
支持JPGPNG格式大小不超过10MB可上传多张
</div>
</div>
</div>
@@ -990,6 +1251,81 @@ getList();
</div>
</div>
</div>
<!-- 项目表单模态框 -->
<div v-if="showContentModal" class="fixed inset-0 z-50 overflow-y-scroll" aria-labelledby="modal-title" role="dialog"
aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<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>
<!-- 模态框内容 -->
<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">
补充项目说明
</h3>
<button
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
@click="closeContentModal"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<form @submit.prevent="handleProjectSubmit" @keydown.enter.prevent="event.preventDefault()" class="space-y-6">
<MdEditor
v-model="currentItem.content"
:toolbars-exclude="['github']"
class="md-editor-container"
editor-id="description-md"
theme="dark"
:config="editorConfig"
@on-upload-img="handleMdImageUpload"
/>
<!-- 表单按钮 -->
<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="closeContentModal"
>
取消
</button>
<button
type="submit"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
:disabled="formLoading"
>
<span v-if="formLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
保存修改
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog"
@@ -1278,6 +1614,7 @@ getList();
font-weight: 600;
background-color: var(--color-purple-500);
}
:where(.css-dev-only-do-not-override-1p3hq3p).ant-select-multiple .ant-select-selection-placeholder {
position: absolute;
top: 50%;

View File

@@ -17,6 +17,8 @@ import {
X
} from "lucide-vue-next"
import {Image, ImagePreviewGroup} from 'ant-design-vue'
const props = defineProps({
visible: {
type: Boolean,
@@ -241,28 +243,19 @@ const stopPropagation = (e) => {
<ImageIcon class="w-5 h-5 mr-2 text-rose-500" />
项目截图
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
<div
v-for="(screenshot, index) in project.screenshots"
:key="index"
class="relative group rounded-xl overflow-hidden cursor-pointer h-40"
@click="handlePreview(screenshot.url)"
>
<img
:src="screenshot.url"
:alt="`Screenshot ${index + 1}`"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
<div
class="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors duration-300 flex items-center justify-center opacity-0 group-hover:opacity-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-white" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"/>
</svg>
<ImagePreviewGroup>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
<Image
v-for="(screenshot, index) in project.screenshots"
:key="index"
:src="screenshot.url"
:alt="`Screenshot ${index + 1}`"
class="w-full object-cover transition-transform duration-300 cursor-pointer"
style="height: calc(var(--spacing) * 40); border-radius: 15px"
/>
</div>
</div>
</div>
</ImagePreviewGroup>
</div>
</div>

View File

@@ -22,38 +22,23 @@ useHead({
const teamMembers = [
{
name: '张三',
role: '创始人 & CEO',
name: '年糕崽崽',
role: '站长 & 研发工程师',
avatar: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
description: '拥有15年互联网行业经验曾任职于多家知名科技企业,专注于企业数字化转型战略'
description: '拥有5年互联网行业经验熟练使用PHP、Vue、Golang等编程技术'
},
{
name: '李四',
role: '技术总监',
name: '年糕',
role: '助理 & 小红书推广负责人',
avatar: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
description: '全栈开发专家,精通前后端技术栈,负责公司核心产品架构设计与技术团队管理'
description: '内人'
},
{
name: '王五',
role: '产品经理',
avatar: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
description: '拥有丰富的产品设计与用户体验经验,致力于打造简洁高效的企业级应用'
},
{
name: '赵六',
role: '市场总监',
avatar: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
description: '营销策略专家,擅长品牌建设与市场拓展,负责公司整体市场战略规划'
}
]
const milestones = [
{ year: '2018', title: '公司成立', description: '团队初创,确立企业数字化服务方向' },
{ year: '2019', title: '首轮融资', description: '获得天使轮投资团队扩展到15人' },
{ year: '2020', title: '产品上线', description: '核心产品正式发布,服务首批企业客户' },
{ year: '2021', title: '业务扩展', description: '客户数量突破100家业务覆盖全国主要城市' },
{ year: '2022', title: 'A轮融资', description: '完成A轮融资估值达到1亿元' },
{ year: '2023', title: '国际化', description: '开始拓展海外市场,建立国际合作伙伴网络' }
{ year: '2024', title: '立', description: '为更多没有技术人员的小微企业提供技术化转型方案、成果' },
{ year: '2025', title: '起步', description: '项目案例达到10个' },
{ year: '未来', title: '憧憬', description: '坚信美好的事情即将发生……' },
]
</script>
@@ -148,7 +133,7 @@ const milestones = [
</span>
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div :class="teamMembers.length > 1 ? 'grid grid-cols-1 md:grid-cols-2 gap-8' : ''">
<div
v-for="(member, index) in teamMembers"
:key="index"
@@ -191,7 +176,7 @@ const milestones = [
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-rose-500 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span class="text-gray-700 dark:text-gray-200">workerqi@163.com</span>
<a href="mailto:workerqi@163.com"><span class="text-gray-700 dark:text-gray-200">workerqi@163.com</span></a>
</div>
<div class="flex items-center bg-white dark:bg-gray-800/80 rounded-full px-6 py-3 shadow-md">
@@ -206,7 +191,7 @@ const milestones = [
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="text-gray-700 dark:text-gray-200">杭州市滨江区长河商厦</span>
<span class="text-gray-700 dark:text-gray-200">杭州市 - 滨江区 - 长河商厦</span>
</div>
</div>
</div>

View File

@@ -10,10 +10,12 @@ import 'swiper/css/pagination'
import 'swiper/css/navigation'
import 'swiper/css/effect-fade'
import {homeProjectDetailApi} from "@/api/request/home.js";
import {DollarSign, User} from "lucide-vue-next";
import {DollarSign, User, X} from "lucide-vue-next";
import {createCollectionApi, unCollectionApi} from "@/api/request/collection.js";
import {message} from "ant-design-vue";
import {message, Image} from "ant-design-vue";
import {useUserStore} from "@/stores/user.js";
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
const route = useRoute()
const activeTab = ref('1')
@@ -201,7 +203,28 @@ const unCollection = () => {
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === '1' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
项目详情
项目简介
</button>
<button
@click="activeTab = '6'"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === '6' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
详细说明
</button>
<button
@click="activeTab = '4'"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === '4' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
项目模块图
</button>
<button
@click="activeTab = '5'"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === '5' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
项目流程图
</button>
<button
@click="activeTab = '2'"
@@ -224,42 +247,6 @@ const unCollection = () => {
<div>
<!-- 项目详情 -->
<div v-if="activeTab === '1'" class="space-y-8">
<div>
<h3 class="text-xl font-bold mb-4 text-gray-900 dark:text-white flex items-center">
<span class="w-8 h-8 rounded-full bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400 flex items-center justify-center mr-3">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</span>
项目描述
</h3>
<p class="text-gray-600 dark:text-gray-300 leading-relaxed">{{ project.description }}</p>
</div>
<div>
<h3 class="text-xl font-bold mb-4 text-gray-900 dark:text-white flex items-center">
<span class="w-8 h-8 rounded-full bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400 flex items-center justify-center mr-3">
<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="M5 13l4 4L19 7" />
</svg>
</span>
核心功能
</h3>
<ul class="grid grid-cols-1 md:grid-cols-2 gap-3">
<li
v-for="(feature, index) in project.features"
:key="index"
class="flex items-start"
>
<div class="flex-shrink-0 w-5 h-5 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-400 mr-3 mt-0.5">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<span class="text-gray-700 dark:text-gray-300">{{ feature.name }}</span>
</li>
</ul>
</div>
<div>
<h3 class="text-xl font-bold mb-4 text-gray-900 dark:text-white flex items-center">
@@ -271,18 +258,18 @@ const unCollection = () => {
技术栈
</h3>
<ul class="space-y-2">
<!-- <li-->
<!-- v-for="(tech, index) in project.technicalStack"-->
<!-- :key="index"-->
<!-- class="flex items-center bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3"-->
<!-- >-->
<!-- <div class="flex-shrink-0 w-5 h-5 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-400 mr-3">-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">-->
<!-- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />-->
<!-- </svg>-->
<!-- </div>-->
<!-- <span class="text-gray-700 dark:text-gray-300">{{ tech }}</span>-->
<!-- </li>-->
<!-- <li-->
<!-- v-for="(tech, index) in project.technicalStack"-->
<!-- :key="index"-->
<!-- class="flex items-center bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3"-->
<!-- >-->
<!-- <div class="flex-shrink-0 w-5 h-5 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-400 mr-3">-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">-->
<!-- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />-->
<!-- </svg>-->
<!-- </div>-->
<!-- <span class="text-gray-700 dark:text-gray-300">{{ tech }}</span>-->
<!-- </li>-->
<li
class="flex items-center bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3"
>
@@ -315,6 +302,43 @@ const unCollection = () => {
</li>
</ul>
</div>
<div>
<h3 class="text-xl font-bold mb-4 text-gray-900 dark:text-white flex items-center">
<span class="w-8 h-8 rounded-full bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400 flex items-center justify-center mr-3">
<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="M5 13l4 4L19 7" />
</svg>
</span>
核心功能
</h3>
<ul class="grid grid-cols-1 md:grid-cols-2 gap-3">
<li
v-for="(feature, index) in project.features"
:key="index"
class="flex items-start"
>
<div class="flex-shrink-0 w-5 h-5 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-400 mr-3 mt-0.5">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<span class="text-gray-700 dark:text-gray-300">{{ feature.name }}</span>
</li>
</ul>
</div>
<div>
<h3 class="text-xl font-bold mb-4 text-gray-900 dark:text-white flex items-center">
<span class="w-8 h-8 rounded-full bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400 flex items-center justify-center mr-3">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</span>
项目描述
</h3>
<p class="text-gray-600 dark:text-gray-300 leading-relaxed" style="text-indent: 2em">{{ project.description }}</p>
</div>
</div>
<!-- 项目截图 -->
@@ -358,6 +382,66 @@ const unCollection = () => {
></iframe>
</div>
</div>
<div v-else-if="activeTab === '4'">
<div
v-if="project.module_image === ''"
class="relative group rounded-xl overflow-hidden cursor-pointer"
>
<div class=" text-center text-gray-500 dark:text-gray-400 items-center" style="margin: 200px auto;">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
作者没有上传
</div>
</div>
<div
v-else
class="relative group rounded-xl overflow-hidden cursor-pointer"
>
<Image :src="project.module_image" class="w-full h-full object-cover transition-transform duration-300" />
</div>
</div>
<div v-else-if="activeTab === '5'">
<div
v-if="project.flowchart === ''"
class="relative group rounded-xl overflow-hidden cursor-pointer"
>
<div class=" text-center text-gray-500 dark:text-gray-400 items-center" style="margin: 200px auto;">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
作者没有上传
</div>
</div>
<div
v-else
class="relative group rounded-xl overflow-hidden cursor-pointer"
>
<Image :src="project.flowchart" class="w-full h-full object-cover transition-transform duration-300" />
</div>
</div>
<div v-else-if="activeTab === '6'">
<div
v-if="project.content === ''"
class="relative group rounded-xl overflow-hidden cursor-pointer"
>
<div class=" text-center text-gray-500 dark:text-gray-400 items-center" style="margin: 200px auto;">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
作者没有上传
</div>
</div>
<MdPreview
v-else
v-model="project.content"
style="padding: 10px 20px"
theme="dark"
/>
</div>
</div>
</div>
</div>
@@ -375,7 +459,7 @@ const unCollection = () => {
<span class="text-2xl font-bold text-rose-400">
{{ formatPrice(project.preferential_price) }}
</span>
<span class="ml-2 text-sm text-gray-400 line-through">
<span v-if="project.preferential_price !== project.price" class="ml-2 text-sm text-gray-400 line-through">
{{ formatPrice(project.price) }}
</span>
</div>
@@ -490,6 +574,7 @@ const unCollection = () => {
</div>
</section>
</div>
</div>
</template>
@@ -544,4 +629,12 @@ const unCollection = () => {
background: linear-gradient(135deg, #1a1d23 0%, #111827 100%);
}
}
/* 自定义样式 */
.cursor-grab {
cursor: grab;
}
.cursor-grab:active {
cursor: grabbing;
}
</style>