项目管理完成
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"ant-design-vue": "^4.1.1",
|
||||
"aos": "^2.3.4",
|
||||
"axios": "^1.6.2",
|
||||
"lucide-vue-next": "^0.507.0",
|
||||
"pinia": "^2.1.7",
|
||||
"swiper": "^11.0.5",
|
||||
"tailwindcss": "^4.1.4",
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.6.2
|
||||
version: 1.8.4
|
||||
lucide-vue-next:
|
||||
specifier: ^0.507.0
|
||||
version: 0.507.0(vue@3.5.13)
|
||||
pinia:
|
||||
specifier: ^2.1.7
|
||||
version: 2.3.1(vue@3.5.13)
|
||||
@@ -878,6 +881,11 @@ packages:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
||||
lucide-vue-next@0.507.0:
|
||||
resolution: {integrity: sha512-n0AZRmez4xq5vu5ZOfnrhC5wKBpu5tpwDHA6Ue2+sKyJv3USAPxqIerXLbdYbkHRW1tVZ3U9GE1MACsu6X/Z7Q==}
|
||||
peerDependencies:
|
||||
vue: '>=3.0.1'
|
||||
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
|
||||
@@ -1769,6 +1777,10 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
lucide-vue-next@0.507.0(vue@3.5.13):
|
||||
dependencies:
|
||||
vue: 3.5.13
|
||||
|
||||
magic-string@0.30.17:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
@@ -2,12 +2,15 @@ import {get, post} from '../request.js'
|
||||
|
||||
const prefix = 'project'
|
||||
|
||||
export function labelListApi(credentials) {
|
||||
export function projectListApi(credentials) {
|
||||
return get(`${prefix}/list`, credentials)
|
||||
}
|
||||
export function labelOptionApi(credentials) {
|
||||
export function projectOptionApi(credentials) {
|
||||
return get(`${prefix}/option`, credentials)
|
||||
}
|
||||
export function projectDetailApi(credentials) {
|
||||
return get(`${prefix}/detail`, credentials)
|
||||
}
|
||||
export function createProjectApi(credentials) {
|
||||
return post(`${prefix}/create`, credentials)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,15 @@ import "aos/dist/aos.css"
|
||||
import { message, Upload, Select } from "ant-design-vue"
|
||||
import {labelOptionApi} from "@/api/request/label.js";
|
||||
import {categoryOptionApi} from "@/api/request/category.js";
|
||||
import {createProjectApi, labelListApi, updateProjectApi} from "@/api/request/project.js";
|
||||
import {
|
||||
createProjectApi,
|
||||
deleteProjectApi,
|
||||
projectDetailApi,
|
||||
projectListApi,
|
||||
updateProjectApi
|
||||
} from "@/api/request/project.js";
|
||||
import {uploadImageApi, uploadVideoApi} from "@/api/request/upload.js";
|
||||
import ProjectDetails from './common/ProjectDetails.vue'
|
||||
|
||||
onMounted(() => {
|
||||
AOS.init({
|
||||
@@ -110,7 +117,7 @@ const dataSource = ref([
|
||||
])
|
||||
|
||||
const getList = () => {
|
||||
labelListApi({
|
||||
projectListApi({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
}).then((res) => {
|
||||
@@ -165,6 +172,10 @@ const videoUploading = ref(false)
|
||||
const videoPreviewVisible = ref(false)
|
||||
const videoPreviewUrl = ref("")
|
||||
|
||||
// 详情模态框
|
||||
const showDetailModal = ref(false)
|
||||
const currentDetailItem = ref(null)
|
||||
|
||||
const handlePreview = (file) => {
|
||||
previewImage.value = file.url || file.preview
|
||||
previewVisible.value = true
|
||||
@@ -371,9 +382,13 @@ const closeDeleteConfirm = () => {
|
||||
|
||||
const handleDelete = () => {
|
||||
if (itemToDelete.value) {
|
||||
dataSource.value = dataSource.value.filter((item) => item.id !== itemToDelete.value)
|
||||
totalItems.value = dataSource.value.length
|
||||
closeDeleteConfirm()
|
||||
deleteProjectApi({
|
||||
ids: [itemToDelete.value]
|
||||
}).then(() => {
|
||||
message.success("项目删除成功")
|
||||
closeDeleteConfirm()
|
||||
getList();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +421,7 @@ const handleSubmit = () => {
|
||||
// 更新现有项目
|
||||
const index = dataSource.value.findIndex((item) => item.id === currentItem.value.id)
|
||||
if (index !== -1) {
|
||||
dataSource.value[index] = { ...currentItem.value }
|
||||
dataSource.value[index] = {...currentItem.value}
|
||||
}
|
||||
} else {
|
||||
createProjectApi(currentItem.value).then((res) => {
|
||||
@@ -435,7 +450,7 @@ const handleAddTag = () => {
|
||||
// 如果不存在,创建新标签并添加
|
||||
else if (!existingTag) {
|
||||
const newTagId = Math.max(...tagsOption.map((tag) => tag.id), 0) + 1
|
||||
const newTag = { id: newTagId, name: tagInputValue.value }
|
||||
const newTag = {id: newTagId, name: tagInputValue.value}
|
||||
tagsOption.push(newTag)
|
||||
currentItem.value.tags.push(newTag)
|
||||
}
|
||||
@@ -468,6 +483,15 @@ const removeFeature = (index) => {
|
||||
currentItem.value.features.splice(index, 1)
|
||||
}
|
||||
|
||||
const openDetailModal = (item) => {
|
||||
projectDetailApi({
|
||||
id: item.id
|
||||
}).then((res) => {
|
||||
currentDetailItem.value = res
|
||||
showDetailModal.value = true
|
||||
})
|
||||
}
|
||||
|
||||
getList();
|
||||
</script>
|
||||
|
||||
@@ -480,8 +504,9 @@ getList();
|
||||
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()"
|
||||
>
|
||||
<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" />
|
||||
<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"/>
|
||||
</svg>
|
||||
新建项目
|
||||
</button>
|
||||
@@ -494,25 +519,32 @@ getList();
|
||||
<!-- 表头 -->
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<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 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-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
<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-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
<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-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
<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-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
<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-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
<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">
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
@@ -520,11 +552,12 @@ getList();
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<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">
|
||||
<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">
|
||||
<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" />
|
||||
<img :src="item.cover" :alt="item.title" class="h-full w-full object-cover"/>
|
||||
</div>
|
||||
<div>
|
||||
{{ item.title }}
|
||||
@@ -569,10 +602,14 @@ getList();
|
||||
<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="openDetailModal(item)"
|
||||
>
|
||||
<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" />
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@@ -580,8 +617,10 @@ getList();
|
||||
title="编辑"
|
||||
@click="openModal(item)"
|
||||
>
|
||||
<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" />
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@@ -589,8 +628,10 @@ getList();
|
||||
title="删除"
|
||||
@click="openDeleteConfirm(item.id)"
|
||||
>
|
||||
<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" />
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -601,7 +642,8 @@ getList();
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<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">
|
||||
<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">
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||
共 {{ totalItems }} 条记录
|
||||
</div>
|
||||
@@ -639,7 +681,8 @@ getList();
|
||||
|
||||
</div>
|
||||
<!-- 项目表单模态框 -->
|
||||
<div v-if="showModal" class="fixed inset-0 z-50 overflow-y-scroll" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div v-if="showModal" class="fixed inset-0 z-50 overflow-y-scroll" aria-labelledby="modal-title" role="dialog"
|
||||
aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!-- 背景遮罩 -->
|
||||
<div
|
||||
@@ -652,7 +695,8 @@ getList();
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-5xl sm:w-full relative z-50">
|
||||
<div
|
||||
class="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">
|
||||
@@ -662,8 +706,9 @@ getList();
|
||||
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
|
||||
@click="closeModal"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<svg 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>
|
||||
@@ -690,7 +735,8 @@ getList();
|
||||
<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>
|
||||
<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"
|
||||
@@ -704,7 +750,8 @@ getList();
|
||||
<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>
|
||||
<span
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400">¥</span>
|
||||
<input
|
||||
v-model="currentItem.preferential_price"
|
||||
type="text"
|
||||
@@ -785,7 +832,8 @@ getList();
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">项目标签</label>
|
||||
<div class="w-full px-1 py-1 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer">
|
||||
<div
|
||||
class="w-full px-1 py-1 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white cursor-pointer">
|
||||
<Select
|
||||
class="w-full"
|
||||
v-model:value="currentItem.labels"
|
||||
@@ -815,8 +863,9 @@ getList();
|
||||
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 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>
|
||||
@@ -873,9 +922,12 @@ getList();
|
||||
: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" />
|
||||
<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>
|
||||
@@ -901,9 +953,12 @@ getList();
|
||||
: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" />
|
||||
<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>
|
||||
@@ -920,9 +975,12 @@ getList();
|
||||
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 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>
|
||||
@@ -946,9 +1004,12 @@ getList();
|
||||
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" />
|
||||
<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>
|
||||
@@ -978,9 +1039,11 @@ getList();
|
||||
: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">
|
||||
<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>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
{{ currentItem?.id ? '保存修改' : '创建项目' }}
|
||||
@@ -993,7 +1056,8 @@ getList();
|
||||
</div>
|
||||
|
||||
<!-- 删除确认模态框 -->
|
||||
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog"
|
||||
aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!-- 背景遮罩 -->
|
||||
<div
|
||||
@@ -1006,12 +1070,16 @@ getList();
|
||||
<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-lg sm:w-full relative z-50">
|
||||
<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">
|
||||
<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" />
|
||||
<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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
@@ -1060,24 +1128,27 @@ getList();
|
||||
<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="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 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" />
|
||||
<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 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
|
||||
@@ -1090,15 +1161,17 @@ getList();
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div class="inline-block align-bottom bg-transparent rounded-lg text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full relative z-50">
|
||||
{{videoPreviewUrl}}
|
||||
<div
|
||||
class="inline-block align-bottom bg-transparent rounded-lg text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full relative z-50">
|
||||
{{ videoPreviewUrl }}
|
||||
<div class="relative">
|
||||
<button
|
||||
class="absolute top-2 right-2 p-2 rounded-full bg-gray-800/50 text-white hover:bg-gray-700/50 transition-colors cursor-pointer z-10"
|
||||
@click="videoPreviewVisible = false"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<svg 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">
|
||||
@@ -1113,6 +1186,13 @@ getList();
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目详情模态框 -->
|
||||
<ProjectDetails
|
||||
v-model:visible="showDetailModal"
|
||||
:project="currentDetailItem"
|
||||
@close="showDetailModal = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -1154,7 +1234,8 @@ getList();
|
||||
/* 视频播放器样式 */
|
||||
.aspect-w-16 {
|
||||
position: relative;
|
||||
padding- {
|
||||
|
||||
padding- {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
@@ -1228,6 +1309,7 @@ getList();
|
||||
background-color: #364153;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep :where(.css-dev-only-do-not-override-1p3hq3p).ant-select-multiple .ant-select-selection-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -1247,32 +1329,13 @@ getList();
|
||||
margin-inline-end: 4px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: 4px;
|
||||
border-color: var(--color-gray-600);
|
||||
border-color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
: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 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-size: 14px;
|
||||
line-height: 1.5714285714285714;
|
||||
list-style: none;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
z-index: 1050;
|
||||
overflow: hidden;
|
||||
font-variant: initial;
|
||||
background-color: var(--color-gray-600) !important;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
: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);
|
||||
|
||||
453
src/views/admin/common/ProjectDetails.vue
Normal file
453
src/views/admin/common/ProjectDetails.vue
Normal file
@@ -0,0 +1,453 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
||||
import {
|
||||
Calendar,
|
||||
Tag,
|
||||
DollarSign,
|
||||
Award,
|
||||
Layers,
|
||||
Server,
|
||||
Database,
|
||||
Code,
|
||||
Monitor,
|
||||
CheckCircle,
|
||||
User,
|
||||
Image as ImageIcon,
|
||||
Play,
|
||||
X
|
||||
} from "lucide-vue-next"
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'close'])
|
||||
|
||||
const closeModal = () => {
|
||||
emit('update:visible', false)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 处理ESC键关闭模态框
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape' && props.visible) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听键盘事件
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
// 防止模态框打开时页面滚动
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
})
|
||||
|
||||
// 图片预览相关
|
||||
const previewVisible = ref(false)
|
||||
const previewImage = ref("")
|
||||
|
||||
const handlePreview = (url) => {
|
||||
previewImage.value = url
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
// 视频预览相关
|
||||
const videoPreviewVisible = ref(false)
|
||||
const videoPreviewUrl = ref("")
|
||||
|
||||
const handleVideoPreview = () => {
|
||||
videoPreviewUrl.value = props.project.video_url
|
||||
videoPreviewVisible.value = true
|
||||
}
|
||||
|
||||
// 格式化价格
|
||||
const formatPrice = (price) => {
|
||||
return "¥ " + parseFloat(price).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" })
|
||||
}
|
||||
|
||||
// 计算优惠比例
|
||||
const discountPercentage = computed(() => {
|
||||
if (!props.project.price || !props.project.preferential_price) return 0
|
||||
const originalPrice = parseFloat(props.project.price)
|
||||
const discountPrice = parseFloat(props.project.preferential_price)
|
||||
if (originalPrice <= 0 || discountPrice >= originalPrice) return 0
|
||||
return Math.round((1 - discountPrice / originalPrice) * 100)
|
||||
})
|
||||
|
||||
// 状态文本映射
|
||||
const statusTextMap = {
|
||||
0: '草稿',
|
||||
1: '已发布',
|
||||
2: '已下架'
|
||||
}
|
||||
|
||||
// 状态颜色映射
|
||||
const statusColorMap = {
|
||||
0: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
1: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
|
||||
2: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
||||
}
|
||||
|
||||
// 阻止冒泡
|
||||
const stopPropagation = (e) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 主模态框 -->
|
||||
<div v-if="props.visible" class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<!-- 背景遮罩 -->
|
||||
<div
|
||||
class="fixed inset-0 bg-gray-500/80 dark:bg-gray-900/80 backdrop-blur-sm transition-opacity"
|
||||
@click="closeModal"
|
||||
></div>
|
||||
|
||||
<!-- 模态框内容容器 -->
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<!-- 模态框内容 -->
|
||||
<div
|
||||
class="relative bg-gray-800 dark:bg-gray-800 rounded-2xl overflow-hidden shadow-xl transform transition-all max-w-5xl w-full animate-fadeIn"
|
||||
@click="stopPropagation"
|
||||
>
|
||||
<!-- 项目头部信息 -->
|
||||
<div class="relative">
|
||||
<!-- 封面图 -->
|
||||
<div class="h-64 w-full overflow-hidden relative">
|
||||
<img
|
||||
:src="project.cover"
|
||||
:alt="project.title"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent"></div>
|
||||
|
||||
<!-- 状态标签 -->
|
||||
<div class="absolute top-4 right-4">
|
||||
<span
|
||||
:class="`px-3 py-1.5 rounded-full text-sm font-medium ${statusColorMap[project.status] || 'bg-gray-100 text-gray-800'}`"
|
||||
>
|
||||
{{ statusTextMap[project.status] || '未知状态' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 标题和分类 -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-6 text-white">
|
||||
<div class="flex items-center mb-2">
|
||||
<span class="px-3 py-1 bg-rose-600/90 rounded-full text-sm font-medium mr-2">
|
||||
{{ project.category?.name || '未分类' }}
|
||||
</span>
|
||||
<span class="text-sm flex items-center">
|
||||
<Calendar class="w-4 h-4 mr-1" />
|
||||
{{ formatDate(project.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold">{{ project.title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button
|
||||
class="absolute top-4 left-4 p-2 rounded-full bg-black/30 text-white hover:bg-black/50 transition-colors"
|
||||
@click="closeModal"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 项目内容 -->
|
||||
<div class="p-6 grid grid-cols-1 md:grid-cols-3 gap-8 bg-gray-800 dark:bg-gray-800 text-white">
|
||||
<!-- 左侧信息 -->
|
||||
<div class="md:col-span-2 space-y-8">
|
||||
<!-- 标签 -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="(tag, index) in project.labels"
|
||||
:key="index"
|
||||
class="px-3 py-1.5 bg-rose-900/20 text-rose-300 text-sm font-medium rounded-full flex items-center"
|
||||
>
|
||||
<Tag class="w-3.5 h-3.5 mr-1.5" />
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 项目描述 -->
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center">
|
||||
<Layers class="w-5 h-5 mr-2 text-rose-500" />
|
||||
项目描述
|
||||
</h2>
|
||||
<p class="text-gray-300 leading-relaxed">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 技术栈 -->
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center">
|
||||
<Code class="w-5 h-5 mr-2 text-rose-500" />
|
||||
技术栈
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="bg-gray-700/50 p-4 rounded-xl">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2 flex items-center">
|
||||
<Monitor class="w-4 h-4 mr-1.5 text-rose-500" />
|
||||
前端技术
|
||||
</h3>
|
||||
<p class="text-gray-200">{{ project.front }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-700/50 p-4 rounded-xl">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2 flex items-center">
|
||||
<Server class="w-4 h-4 mr-1.5 text-rose-500" />
|
||||
后端技术
|
||||
</h3>
|
||||
<p class="text-gray-200">{{ project.service }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-700/50 p-4 rounded-xl">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2 flex items-center">
|
||||
<Database class="w-4 h-4 mr-1.5 text-rose-500" />
|
||||
部署架构
|
||||
</h3>
|
||||
<p class="text-gray-200">{{ project.deploy }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心功能 -->
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center">
|
||||
<Award class="w-5 h-5 mr-2 text-rose-500" />
|
||||
核心功能
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="(feature, index) in project.features"
|
||||
:key="index"
|
||||
class="flex items-start"
|
||||
>
|
||||
<CheckCircle class="w-5 h-5 text-rose-500 mr-2 flex-shrink-0 mt-0.5" />
|
||||
<span class="text-gray-300">{{ feature.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目截图 -->
|
||||
<div v-if="project.screenshots && project.screenshots.length > 0" class="space-y-4">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 演示视频 -->
|
||||
<div v-if="project.video_url" class="space-y-4">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center">
|
||||
<Play class="w-5 h-5 mr-2 text-rose-500"/>
|
||||
演示视频
|
||||
</h2>
|
||||
<div
|
||||
class="relative rounded-xl overflow-hidden cursor-pointer h-64 bg-gray-700"
|
||||
@click="handleVideoPreview"
|
||||
>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="w-16 h-16 rounded-full bg-rose-600 flex items-center justify-center shadow-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-white ml-1" 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"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-4 left-4 right-4 text-white bg-black/50 p-2 rounded-lg">
|
||||
<p class="text-sm font-medium">点击播放项目演示视频</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧信息 -->
|
||||
<div class="space-y-6">
|
||||
<!-- 价格卡片 -->
|
||||
<div class="bg-gray-700 rounded-xl shadow-md p-6 border border-gray-600">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-white flex items-center">
|
||||
<DollarSign class="w-5 h-5 mr-1.5 text-rose-500"/>
|
||||
项目价格
|
||||
</h3>
|
||||
<span v-if="discountPercentage > 0"
|
||||
class="px-2 py-1 bg-rose-900/30 text-rose-300 text-xs font-bold rounded-full">
|
||||
{{ discountPercentage }}% OFF
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div v-if="project.preferential_price" class="flex items-center">
|
||||
<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">
|
||||
{{ formatPrice(project.price) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="text-2xl font-bold text-white">
|
||||
{{ formatPrice(project.price) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 框架信息 -->
|
||||
<div v-if="project.frameworks && project.frameworks.length > 0"
|
||||
class="bg-gray-700 rounded-xl shadow-md p-6 border border-gray-600">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">使用框架</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="(framework, index) in project.frameworks"
|
||||
:key="index"
|
||||
class="px-3 py-1.5 bg-blue-900/20 text-blue-300 text-sm font-medium rounded-full"
|
||||
>
|
||||
{{ framework.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作者信息 -->
|
||||
<div v-if="project.author" class="bg-gray-700 rounded-xl shadow-md p-6 border border-gray-600">
|
||||
<h3 class="text-lg font-semibold text-white mb-4 flex items-center">
|
||||
<User class="w-5 h-5 mr-1.5 text-rose-500"/>
|
||||
项目作者
|
||||
</h3>
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-full overflow-hidden mr-4">
|
||||
<img
|
||||
:src="project.author.avatar"
|
||||
:alt="project.author.nick_name"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-white">{{ project.author.nick_name }}</h4>
|
||||
<p class="text-sm text-gray-400">ID: {{ project.author.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片预览模态框 -->
|
||||
<div v-if="previewVisible" class="fixed inset-0 z-[60] overflow-y-auto" aria-labelledby="modal-title" role="dialog"
|
||||
aria-modal="true">
|
||||
<div
|
||||
class="fixed inset-0 bg-gray-900/90 backdrop-blur-sm transition-opacity"
|
||||
@click="previewVisible = false"
|
||||
></div>
|
||||
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="relative max-w-4xl w-full animate-fadeIn" @click="stopPropagation">
|
||||
<button
|
||||
class="absolute top-4 right-4 p-2 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors z-10"
|
||||
@click="previewVisible = false"
|
||||
>
|
||||
<X class="h-6 w-6"/>
|
||||
</button>
|
||||
<img :src="previewImage" class="max-h-[80vh] max-w-full mx-auto rounded-lg"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览模态框 -->
|
||||
<div v-if="videoPreviewVisible" class="fixed inset-0 z-[60] overflow-y-auto" aria-labelledby="modal-title"
|
||||
role="dialog" aria-modal="true">
|
||||
<div
|
||||
class="fixed inset-0 bg-gray-900/90 backdrop-blur-sm transition-opacity"
|
||||
@click="videoPreviewVisible = false"
|
||||
></div>
|
||||
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="relative max-w-4xl w-full animate-fadeIn" @click="stopPropagation">
|
||||
<button
|
||||
class="absolute top-4 right-4 p-2 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors z-10"
|
||||
@click="videoPreviewVisible = false"
|
||||
>
|
||||
<X class="h-6 w-6"/>
|
||||
</button>
|
||||
<div class="relative pb-[56.25%] rounded-lg overflow-hidden">
|
||||
<video
|
||||
:src="videoPreviewUrl"
|
||||
class="absolute inset-0 w-full h-full"
|
||||
controls
|
||||
autoplay
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 添加淡入动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user