This commit is contained in:
13121765685 2025-11-18 17:53:34 +08:00
parent 19e64b0c7f
commit cf5e291af5
18 changed files with 1938 additions and 239 deletions

View File

@ -12,8 +12,10 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@types/three": "^0.180.0",
"element-plus": "^2.11.7",
"pinia": "^2.2.6",
"three": "^0.180.0",
"vue": "^3.5.24",
"vue-i18n": "^9.14.2",
"vue-router": "^4.4.5"

View File

@ -133,12 +133,7 @@ onUnmounted(() => {
<main class="app-main">
<div class="main-container">
<router-view v-slot="{ Component, route }">
<transition
:name="route.meta.transition || 'fade'"
mode="out-in"
>
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</div>
</main>

Binary file not shown.

View File

@ -0,0 +1,649 @@
<template>
<div class="model-viewer" ref="modelViewerContainer">
<div v-if="loading" class="loading-overlay">
<el-icon class="loading-icon" size="32"><Loading /></el-icon>
<p>{{ loadingText }}</p>
</div>
<div v-if="error" class="error-overlay">
<el-icon size="32" color="#ff6b6b"><Warning /></el-icon>
<p>{{ error }}</p>
<el-button size="small" @click="retryLoad">
{{ t('common.retry') }}
</el-button>
</div>
<!-- WebGL 渲染容器 -->
<canvas
ref="canvas"
:class="{ 'hidden': loading || error }"
@wheel="handleWheel"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
/>
<!-- 控制面板 -->
<div v-if="!loading && !error && showControls" class="model-controls">
<div class="control-group">
<el-button-group>
<el-button
size="small"
@click="resetView"
:title="t('modelViewer.resetView')"
>
<el-icon><Refresh /></el-icon>
</el-button>
<el-button
size="small"
@click="toggleWireframe"
:title="t('modelViewer.toggleWireframe')"
>
<el-icon><Grid /></el-icon>
</el-button>
<el-button
size="small"
@click="centerModel"
:title="t('modelViewer.centerModel')"
>
<el-icon><Position /></el-icon>
</el-button>
<el-button
size="small"
@click="exportModel"
:title="t('modelViewer.exportModel')"
>
<el-icon><Download /></el-icon>
</el-button>
</el-button-group>
</div>
<!-- 模型信息 -->
<div v-if="modelInfo" class="model-info">
<p>{{ t('modelViewer.modelInfo') }}: {{ modelInfo }}</p>
<p>{{ t('modelViewer.fileSize') }}: {{ formatFileSize(fileSize) }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { Loading, Warning, Refresh, Grid, Position, Download } from '@element-plus/icons-vue'
const { t } = useI18n()
// Props
const props = defineProps({
modelUrl: {
type: String,
default: null
},
width: {
type: [String, Number],
default: '100%'
},
height: {
type: [String, Number],
default: '400px'
},
showControls: {
type: Boolean,
default: true
},
autoRotate: {
type: Boolean,
default: false
},
loadingText: {
type: String,
default: ''
}
})
//
const DEFAULT_MODEL = '/src/assets/demo/model.glb'
// Refs
const modelViewerContainer = ref(null)
const canvas = ref(null)
//
const loading = ref(true)
const error = ref('')
const modelInfo = ref('')
const fileSize = ref(0)
// Three.js
let scene, camera, renderer, controls
let model, mixer
let animationId
//
let cameraDistance = 5
// Three.js
const initThreeJS = () => {
if (!canvas.value || !modelViewerContainer.value) return
//
scene = new THREE.Scene()
createLaboratoryBackground()
//
const container = modelViewerContainer.value
const rect = container.getBoundingClientRect()
camera = new THREE.PerspectiveCamera(
45,
rect.width / rect.height,
0.1,
1000
)
camera.position.set(0, 2, 5)
//
renderer = new THREE.WebGLRenderer({
canvas: canvas.value,
antialias: true,
alpha: true
})
renderer.setSize(rect.width, rect.height)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
//
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight)
//
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0)
directionalLight.position.set(10, 15, 8)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.width = 2048
directionalLight.shadow.mapSize.height = 2048
directionalLight.shadow.camera.near = 0.5
directionalLight.shadow.camera.far = 50
directionalLight.shadow.camera.left = -10
directionalLight.shadow.camera.right = 10
directionalLight.shadow.camera.top = 10
directionalLight.shadow.camera.bottom = -10
scene.add(directionalLight)
//
const auxiliaryLight = new THREE.DirectionalLight(0xffffff, 0.3)
auxiliaryLight.position.set(-8, 10, -5)
scene.add(auxiliaryLight)
//
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.enableZoom = true
controls.enableRotate = true //
controls.enablePan = true
controls.zoomSpeed = 0.8
controls.panSpeed = 0.8
controls.panSpeed = 0.8
//
animate()
}
//
const createLaboratoryBackground = () => {
//
const gridHelper = new THREE.GridHelper(30, 30, 0x444444, 0x222222)
gridHelper.material.opacity = 0.4
gridHelper.material.transparent = true
gridHelper.position.y = -2
scene.add(gridHelper)
//
scene.background = new THREE.Color(0x1a1a1a)
}
//
const loadModel = async (modelPath) => {
if (!scene) return
loading.value = true
error.value = ''
try {
//
if (model) {
scene.remove(model)
model = null
}
const loader = new GLTFLoader()
//
const gltf = await new Promise((resolve, reject) => {
loader.load(
modelPath,
resolve,
(progress) => {
//
},
reject
)
})
model = gltf.scene
//
try {
const response = await fetch(modelPath)
if (response.ok) {
fileSize.value = response.headers.get('content-length') || 0
}
} catch (e) {
console.warn('Could not get file size:', e)
}
//
const box = new THREE.Box3().setFromObject(model)
const center = box.getCenter(new THREE.Vector3())
const size = box.getSize(new THREE.Vector3())
//
model.position.sub(center)
//
const maxDim = Math.max(size.x, size.y, size.z)
const fov = camera.fov * (Math.PI / 180)
let cameraZ = Math.abs(maxDim / Math.tan(fov / 2))
cameraZ *= 1.5 //
camera.position.z = cameraZ
cameraDistance = cameraZ
//
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true
child.receiveShadow = true
}
})
scene.add(model)
modelInfo.value = gltf.scene?.name || '3D Model'
loading.value = false
//
if (props.autoRotate) {
controls.autoRotate = true
}
} catch (err) {
console.error('Error loading model:', err)
loading.value = false
error.value = t('modelViewer.loadError')
}
}
//
const animate = () => {
animationId = requestAnimationFrame(animate)
if (controls) {
controls.update()
}
if (mixer) {
mixer.update(0.016)
}
renderer.render(scene, camera)
}
//
const resetView = () => {
if (controls) {
controls.reset()
}
camera.position.set(0, 2, cameraDistance)
camera.lookAt(0, 0, 0)
}
// 线
const toggleWireframe = () => {
if (!model) return
model.traverse((child) => {
if (child.isMesh && child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(mat => {
mat.wireframe = !mat.wireframe
})
} else {
child.material.wireframe = !child.material.wireframe
}
}
})
}
//
const centerModel = () => {
if (!model) return
const box = new THREE.Box3().setFromObject(model)
const center = box.getCenter(new THREE.Vector3())
//
model.position.sub(center)
//
const newBox = new THREE.Box3().setFromObject(model)
const size = newBox.getSize(new THREE.Vector3())
const maxDim = Math.max(size.x, size.y, size.z)
//
const fov = camera.fov * (Math.PI / 180)
let cameraZ = Math.abs(maxDim / Math.tan(fov / 2))
cameraZ *= 1.5
camera.position.z = cameraZ
cameraDistance = cameraZ
camera.lookAt(0, 0, 0)
controls.target.set(0, 0, 0)
controls.update()
}
//
const exportModel = () => {
if (!model || !scene) {
console.warn('No model to export')
return
}
//
const exportOptions = {
binary: false,
embedImages: true,
animations: true,
forcePowerOfTwoTextures: false
}
//
// GLTF
//
try {
//
ElMessage({
type: 'info',
message: t('modelViewer.exportInProgress'),
duration: 2000
})
setTimeout(() => {
//
const dataStr = JSON.stringify({
model: '3D Model Data',
info: modelInfo.value,
fileSize: formatFileSize(fileSize.value),
timestamp: new Date().toISOString()
})
const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
link.download = `${modelInfo.value || 'model'}_export.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage({
type: 'success',
message: t('modelViewer.exportSuccess')
})
}, 1500)
} catch (error) {
console.error('Export failed:', error)
ElMessage({
type: 'error',
message: t('modelViewer.exportFailed')
})
}
}
//
const handleMouseDown = () => {
// OrbitControls
}
const handleWheel = (event) => {
event.preventDefault()
const scale = event.deltaY > 0 ? 1.1 : 0.9
camera.position.multiplyScalar(scale)
cameraDistance *= scale
}
//
let lastTouchDistance = 0
const handleTouchStart = (event) => {
if (event.touches.length === 2) {
const dx = event.touches[0].clientX - event.touches[1].clientX
const dy = event.touches[0].clientY - event.touches[1].clientY
lastTouchDistance = Math.sqrt(dx * dx + dy * dy)
}
}
const handleTouchMove = (event) => {
event.preventDefault()
if (event.touches.length === 2) {
//
const dx = event.touches[0].clientX - event.touches[1].clientX
const dy = event.touches[0].clientY - event.touches[1].clientY
const distance = Math.sqrt(dx * dx + dy * dy)
if (lastTouchDistance > 0) {
const scale = distance / lastTouchDistance
camera.position.multiplyScalar(1 / scale)
cameraDistance /= scale
}
lastTouchDistance = distance
}
// OrbitControls
}
const handleTouchEnd = () => {
lastTouchDistance = 0
}
//
const retryLoad = () => {
const modelToLoad = props.modelUrl || DEFAULT_MODEL
loadModel(modelToLoad)
}
//
const formatFileSize = (bytes) => {
if (!bytes) return 'Unknown'
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`
}
//
const handleResize = () => {
if (!camera || !renderer || !modelViewerContainer.value) return
const rect = modelViewerContainer.value.getBoundingClientRect()
camera.aspect = rect.width / rect.height
camera.updateProjectionMatrix()
renderer.setSize(rect.width, rect.height)
}
//
watch(() => props.modelUrl, (newUrl) => {
if (newUrl) {
loadModel(newUrl)
}
}, { immediate: false })
//
onMounted(async () => {
await nextTick()
initThreeJS()
//
const modelToLoad = props.modelUrl || DEFAULT_MODEL
loadModel(modelToLoad)
//
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
//
if (animationId) {
cancelAnimationFrame(animationId)
}
if (renderer) {
renderer.dispose()
}
if (controls) {
controls.dispose()
}
window.removeEventListener('resize', handleResize)
})
//
defineExpose({
resetView,
toggleWireframe,
centerModel
})
</script>
<style scoped>
.model-viewer {
position: relative;
width: 100%;
height: 100%;
min-height: 300px;
background: #f0f2f5;
border-radius: 8px;
overflow: hidden;
}
.model-viewer canvas {
width: 100%;
height: 100%;
display: block;
cursor: grab;
}
.model-viewer canvas:active {
cursor: grabbing;
}
.loading-overlay,
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
color: #666;
z-index: 10;
}
.loading-icon {
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.error-overlay {
color: #ff6b6b;
}
.error-overlay p {
margin: 16px 0;
text-align: center;
}
.model-controls {
position: absolute;
top: 12px;
right: 12px;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 5;
}
.control-group {
background: rgba(255, 255, 255, 0.9);
border-radius: 6px;
padding: 8px;
backdrop-filter: blur(4px);
}
.model-info {
background: rgba(255, 255, 255, 0.9);
border-radius: 6px;
padding: 8px;
font-size: 12px;
color: #666;
max-width: 200px;
backdrop-filter: blur(4px);
}
.model-info p {
margin: 4px 0;
word-break: break-word;
}
.hidden {
display: none;
}
/* 移动端优化 */
@media (max-width: 768px) {
.model-controls {
top: 8px;
right: 8px;
}
.control-group,
.model-info {
padding: 6px;
}
.model-info {
max-width: 150px;
font-size: 11px;
}
}
</style>

View File

@ -7,10 +7,11 @@ export default {
save: 'Save',
delete: 'Delete',
edit: 'Edit',
add: 'Add',
search: 'Search',
reset: 'Reset',
loading: 'Loading...',
viewAll: 'View All',
reset: 'Reset',
retry: 'Retry',
noData: 'No Data',
error: 'Error',
success: 'Success',
@ -18,6 +19,22 @@ export default {
info: 'Info'
},
// 3D Model Viewer
modelViewer: {
resetView: 'Reset View',
toggleWireframe: 'Toggle Wireframe',
centerModel: 'Center Model',
modelInfo: 'Model Info',
fileSize: 'File Size',
loadError: 'Model Load Error',
touchControls: 'Touch Controls Available',
mouseControls: 'Mouse Controls Available',
exportModel: 'Export Model',
exportInProgress: 'Export in progress...',
exportSuccess: 'Export successful',
exportFailed: 'Export failed'
},
// 应用
app: {
title: 'Vue3 Frontend Designer Tool',
@ -114,7 +131,19 @@ export default {
reset: 'Reset',
startDate: 'Start Date',
endDate: 'End Date',
loading: 'Loading...'
loading: 'Loading...',
refreshSuccess: 'Refresh successful',
confirm: 'Confirm',
cancel: 'Cancel',
save: 'Save',
delete: 'Delete',
edit: 'Edit',
add: 'Add',
noData: 'No Data',
error: 'Error',
success: 'Success',
warning: 'Warning',
info: 'Info'
},
layout: {
dashboard: 'Dashboard',
@ -218,6 +247,61 @@ export default {
credit: 'Credit Card'
}
},
review: {
title: 'IP Review Management',
subtitle: 'Manage creator-submitted IP content review process',
list: 'Review List',
search: 'Search Creator',
status: 'Review Status',
dateRange: 'Date Range',
orderPrice: 'Order Price',
creator: 'Creator',
ipName: 'IP Name',
thumbnail: 'Thumbnail',
preview: 'Preview',
preview3D: '3D Preview',
preview3DModel: '3D Model Preview',
previewImage: 'Image Preview',
approve: 'Approve',
reject: 'Reject',
approved: 'Approved',
rejected: 'Rejected',
pending: 'Pending Review',
rejectReason: 'Reject Reason',
enterRejectReason: 'Please enter reject reason',
pleaseInputReason: 'Please enter reject reason',
modelPreview: 'Model Preview',
proceedToParts: 'Parts',
noRejectReason: 'Please enter reject reason',
confirmApprove: 'Confirm approve this IP?',
confirmReject: 'Confirm reject this IP?',
enterFeedback: 'Please enter feedback',
approveSuccess: 'IP approved successfully',
rejectSuccess: 'IP rejected successfully',
disassemblyComingSoon: 'Disassembly feature coming soon',
redirectToDisassembly: 'Redirecting to disassembly page:',
model3DPlaceholder: '3D Model Display Area',
creatorStudioA: 'Creative Studio A',
creatorStudioB: '3D Design Team B',
creatorStudioC: 'Digital Art Studio C',
nonComplianceReason: 'Does not meet content compliance requirements',
dateFormat: 'en-US',
rejectReview: 'Reject Review',
createTime: 'Create Time',
actions: 'Actions',
goToDisassembly: 'Disassembly',
stats: {
total: 'Total Reviews',
pending: 'Pending Review',
approved: 'Approved',
rejected: 'Rejected'
},
statusOptions: {
pending: 'Pending Review',
approved: 'Approved',
rejected: 'Rejected'
}
},
users: {
title: 'User Management',
add: 'Add User',
@ -257,23 +341,5 @@ export default {
vip: 'VIP User'
}
}
},
// 通用文本
common: {
confirm: 'Confirm',
cancel: 'Cancel',
save: 'Save',
delete: 'Delete',
edit: 'Edit',
search: 'Search',
loading: 'Loading...',
viewAll: 'View All',
reset: 'Reset',
noData: 'No Data',
error: 'Error',
success: 'Success',
warning: 'Warning',
info: 'Info'
}
}

View File

@ -139,7 +139,19 @@ export default {
reset: '重置',
startDate: '开始日期',
endDate: '结束日期',
loading: '加载中...'
loading: '加载中...',
refreshSuccess: '刷新成功',
confirm: '确认',
cancel: '取消',
save: '保存',
delete: '删除',
edit: '编辑',
add: '添加',
noData: '暂无数据',
error: '错误',
success: '成功',
warning: '警告',
info: '信息'
},
layout: {
dashboard: '仪表板',
@ -152,7 +164,6 @@ export default {
settings: '设置',
notifications: '通知'
},
pages: {
content: {
title: '内容管理',
add: '添加内容',
@ -221,6 +232,61 @@ export default {
credit: '信用卡'
}
},
review: {
title: 'IP审核管理',
subtitle: '管理创作者提交的IP内容审核流程',
list: '审核列表',
search: '搜索创作者',
status: '审核状态',
dateRange: '日期范围',
orderPrice: '订单价格',
creator: '创作者',
ipName: 'IP名称',
thumbnail: '缩略图',
preview: '预览',
preview3D: '3D预览',
preview3DModel: '3D模型预览',
previewImage: '图片预览',
approve: '通过',
reject: '拒绝',
approved: '已通过',
rejected: '已拒绝',
pending: '待审核',
rejectReason: '拒绝原因',
enterRejectReason: '请输入拒绝原因',
pleaseInputReason: '请输入拒绝原因',
modelPreview: '模型预览',
proceedToParts: '拆件',
noRejectReason: '请输入拒绝原因',
confirmApprove: '确认通过此IP',
confirmReject: '确认拒绝此IP',
enterFeedback: '请输入反馈',
approveSuccess: '审核通过成功',
rejectSuccess: '审核拒绝成功',
disassemblyComingSoon: '拆件功能即将上线',
redirectToDisassembly: '跳转到拆件页面:',
model3DPlaceholder: '3D模型展示区域',
creatorStudioA: '创意工作室A',
creatorStudioB: '3D设计团队B',
creatorStudioC: '数字艺术工作室C',
nonComplianceReason: '不符合内容规范要求',
dateFormat: 'zh-CN',
rejectReview: '拒绝审核',
createTime: '创建时间',
actions: '操作',
goToDisassembly: '拆件',
stats: {
total: '总审核数',
pending: '待审核',
approved: '已通过',
rejected: '已拒绝'
},
statusOptions: {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝'
}
},
users: {
title: '用户管理',
add: '添加用户',
@ -260,7 +326,6 @@ export default {
vip: 'VIP用户'
}
}
}
},
// 头部导航
@ -290,10 +355,27 @@ export default {
loading: '加载中...',
viewAll: '查看全部',
reset: '重置',
retry: '重试',
noData: '暂无数据',
error: '错误',
success: '成功',
warning: '警告',
info: '信息'
},
// 3D模型预览器
modelViewer: {
resetView: '重置视图',
toggleWireframe: '切换线框',
centerModel: '居中模型',
modelInfo: '模型信息',
fileSize: '文件大小',
loadError: '模型加载失败',
touchControls: '触摸控制可用',
mouseControls: '鼠标控制可用',
exportModel: '导出模型',
exportInProgress: '正在导出...',
exportSuccess: '导出成功',
exportFailed: '导出失败'
}
}

View File

@ -9,6 +9,7 @@ const AdminDashboard = () => import('@/views/admin/AdminDashboard.vue')
const AdminContent = () => import('@/views/admin/AdminContent.vue')
const AdminOrders = () => import('@/views/admin/AdminOrders.vue')
const AdminUsers = () => import('@/views/admin/AdminUsers.vue')
const AdminContentReview = () => import('@/views/admin/AdminContentReview.vue')
const routes = [
{
@ -64,6 +65,14 @@ const routes = [
title: '内容管理'
}
},
{
path: 'content-review',
name: 'AdminContentReview',
component: AdminContentReview,
meta: {
title: '内容审核'
}
},
{
path: 'orders',
name: 'AdminOrders',

View File

@ -22,9 +22,6 @@
clearable
@keyup.enter="handleLogin"
>
<template #prefix>
<el-icon><UserFilled /></el-icon>
</template>
</el-input>
</el-form-item>

View File

@ -0,0 +1,871 @@
<template>
<div class="admin-content-review">
<!-- 统计卡片 -->
<div class="review-stats">
<div class="stat-card">
<div class="stat-icon total">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ reviewStats.total }}</div>
<div class="stat-label">{{ t('admin.review.stats.total') }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon pending">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ reviewStats.pending }}</div>
<div class="stat-label">{{ t('admin.review.stats.pending') }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon approved">
<el-icon><Check /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ reviewStats.approved }}</div>
<div class="stat-label">{{ t('admin.review.stats.approved') }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon rejected">
<el-icon><Close /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ reviewStats.rejected }}</div>
<div class="stat-label">{{ t('admin.review.stats.rejected') }}</div>
</div>
</div>
</div>
<!-- 筛选和搜索 -->
<div class="review-filters">
<div class="filter-group">
<el-select v-model="selectedStatus" :placeholder="t('admin.review.status')" clearable>
<el-option :label="t('admin.review.statusOptions.pending')" value="pending" />
<el-option :label="t('admin.review.statusOptions.approved')" value="approved" />
<el-option :label="t('admin.review.statusOptions.rejected')" value="rejected" />
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
:placeholder="t('admin.review.dateRange')"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</div>
<div class="search-group">
<el-input
v-model="searchQuery"
:placeholder="t('admin.review.search')"
:prefix-icon="Search"
clearable
/>
</div>
<div class="filter-actions">
<el-button :icon="Refresh" @click="refresh">
{{ t('admin.common.refresh') }}
</el-button>
</div>
</div>
<!-- 审核列表 -->
<div class="review-table">
<el-table
:data="filteredReviewList"
stripe
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="id" label="ID" min-width="60" />
<el-table-column prop="creatorName" :label="t('admin.review.creator')" min-width="140" />
<el-table-column :label="t('admin.review.thumbnail')" min-width="80">
<template #default="{ row }">
<div
class="thumbnail-container"
@click="previewThumbnail(row.thumbnail)"
>
<el-image
:src="row.thumbnail"
:lazy="true"
style="width: 50px; height: 50px; cursor: pointer;"
fit="cover"
>
<template #error>
<div class="image-error">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
</template>
</el-table-column>
<el-table-column prop="orderPrice" :label="t('admin.review.orderPrice')" min-width="120">
<template #default="{ row }">
¥{{ row.orderPrice.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="status" :label="t('admin.review.status')" min-width="120">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">
{{ t(`admin.review.statusOptions.${row.status}`) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="rejectionReason" :label="t('admin.review.rejectionReason')" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.status === 'rejected'" class="rejection-text">
{{ row.rejectionReason || '-' }}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="t('admin.review.createTime')" min-width="160">
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column :label="t('admin.review.actions')" min-width="280" fixed="right">
<template #default="{ row }">
<div class="actions-container">
<el-button size="small" @click="previewModel(row)">
{{ t('admin.review.preview3D') }}
</el-button>
<el-button
size="small"
type="primary"
@click="approveReview(row)"
v-if="row.status === 'pending'"
>
{{ t('admin.review.approve') }}
</el-button>
<el-button
size="small"
type="danger"
@click="rejectReview(row)"
v-if="row.status === 'pending'"
>
{{ t('admin.review.reject') }}
</el-button>
<el-button
size="small"
type="success"
@click="goToDisassembly(row)"
v-if="row.status === 'approved'"
>
{{ t('admin.review.goToDisassembly') }}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="totalReviews"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 预览图片对话框 -->
<el-dialog
:title="t('admin.review.previewImage')"
v-model="previewImageVisible"
width="80%"
top="8vh"
:z-index="1000"
:append-to-body="true"
class="image-preview-dialog"
>
<div class="image-preview-container">
<div class="image-preview-content">
<img
:src="currentImage"
class="preview-image"
:style="{ transform: `scale(${imageScale})` }"
/>
</div>
<div class="image-actions">
<el-button
size="small"
@click="zoomIn"
>
<el-icon><ZoomIn /></el-icon>
放大
</el-button>
<el-button
size="small"
@click="zoomOut"
>
<el-icon><ZoomOut /></el-icon>
缩小
</el-button>
<el-button
size="small"
@click="resetZoom"
>
<el-icon><Refresh /></el-icon>
重置
</el-button>
<el-button
size="small"
type="primary"
@click="openInNewTab"
>
<el-icon><Link /></el-icon>
新窗口打开
</el-button>
</div>
</div>
</el-dialog>
<!-- 3D模型预览对话框 -->
<el-dialog
:title="t('admin.review.preview3DModel')"
v-model="preview3DVisible"
width="85%"
top="5vh"
:z-index="2000"
:append-to-body="true"
class="model-preview-dialog"
>
<div class="model-preview-content">
<ModelViewer
ref="modelViewerRef"
:model-url="currentModelUrl"
:show-controls="true"
:loading-text="t('modelViewer.loadingModel')"
style="height: 70vh; width: 100%;"
/>
</div>
</el-dialog>
<!-- 拒绝审核对话框 -->
<el-dialog
:title="t('admin.review.rejectReview')"
v-model="rejectDialogVisible"
width="500px"
>
<el-form :model="rejectForm" :rules="rejectRules" ref="rejectFormRef" label-width="100px">
<el-form-item :label="t('admin.review.rejectionReason')" prop="reason">
<el-input
v-model="rejectForm.reason"
type="textarea"
:rows="4"
:placeholder="t('admin.review.pleaseInputReason')"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="rejectDialogVisible = false">
{{ t('common.cancel') }}
</el-button>
<el-button type="primary" @click="confirmRejectReview">
{{ t('common.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Refresh,
Search,
Document,
Clock,
Check,
Close,
Picture,
Box,
ZoomIn,
ZoomOut,
Link
} from '@element-plus/icons-vue'
import ModelViewer from '@/components/common/ModelViewer.vue'
const { t } = useI18n()
//
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(20)
const totalReviews = ref(0)
const selectedStatus = ref('')
const searchQuery = ref('')
const dateRange = ref([])
//
const previewImageVisible = ref(false)
const preview3DVisible = ref(false)
const rejectDialogVisible = ref(false)
const currentImage = ref('')
const currentModelUrl = ref('') // URL
//
const imageScale = ref(1)
//
const rejectFormRef = ref(null)
const rejectForm = ref({
reason: ''
})
const rejectRules = {
reason: [
{ required: true, message: t('admin.review.pleaseInputReason'), trigger: 'blur' }
]
}
//
const selectedReview = ref(null)
//
const reviewStats = ref({
total: 156,
pending: 23,
approved: 108,
rejected: 25
})
//
const reviewList = ref([
{
id: 1,
creatorName: t('admin.review.creatorStudioA'),
thumbnail: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=150',
orderPrice: 299.99,
status: 'pending',
rejectionReason: '',
createTime: '2025-11-18 10:30:00'
},
{
id: 2,
creatorName: t('admin.review.creatorStudioB'),
thumbnail: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=150',
orderPrice: 459.99,
status: 'approved',
rejectionReason: '',
createTime: '2025-11-18 09:15:00'
},
{
id: 3,
creatorName: t('admin.review.creatorStudioC'),
thumbnail: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=150',
orderPrice: 189.99,
status: 'rejected',
rejectionReason: t('admin.review.nonComplianceReason'),
createTime: '2025-11-18 08:45:00'
}
])
//
const filteredReviewList = computed(() => {
let list = reviewList.value
if (selectedStatus.value) {
list = list.filter(item => item.status === selectedStatus.value)
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
list = list.filter(item =>
item.creatorName.toLowerCase().includes(query)
)
}
if (dateRange.value && dateRange.value.length === 2) {
const [start, end] = dateRange.value
list = list.filter(item => {
const itemDate = item.createTime.split(' ')[0]
return itemDate >= start && itemDate <= end
})
}
totalReviews.value = list.length
return list.slice(
(currentPage.value - 1) * pageSize.value,
currentPage.value * pageSize.value
)
})
//
const refresh = () => {
loading.value = true
setTimeout(() => {
loading.value = false
ElMessage.success(t('admin.common.refreshSuccess'))
}, 1000)
}
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
}
const handleCurrentChange = (page) => {
currentPage.value = page
}
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString(
t('admin.review.dateFormat') === 'en-US' ? 'en-US' : 'zh-CN',
{
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}
)
}
const getStatusTagType = (status) => {
const typeMap = {
pending: 'warning',
approved: 'success',
rejected: 'danger'
}
return typeMap[status] || 'info'
}
const previewThumbnail = (imageUrl) => {
currentImage.value = imageUrl
imageScale.value = 1
previewImageVisible.value = true
}
//
const zoomIn = () => {
if (imageScale.value < 3) {
imageScale.value = Math.round((imageScale.value + 0.2) * 10) / 10
}
}
const zoomOut = () => {
if (imageScale.value > 0.3) {
imageScale.value = Math.round((imageScale.value - 0.2) * 10) / 10
}
}
const resetZoom = () => {
imageScale.value = 1
}
const openInNewTab = () => {
window.open(currentImage.value, '_blank')
}
const previewModel = (review) => {
// 3DURLreviewmodelUrl使使
if (review.modelUrl) {
currentModelUrl.value = review.modelUrl
} else {
currentModelUrl.value = '/src/assets/demo/model.glb'
}
preview3DVisible.value = true
}
const approveReview = async (review) => {
try {
await ElMessageBox.confirm(
t('admin.review.confirmApprove'),
t('admin.review.approve'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'success'
}
)
//
review.status = 'approved'
reviewStats.value.pending--
reviewStats.value.approved++
ElMessage.success(t('admin.review.approveSuccess'))
} catch {
//
}
}
const rejectReview = (review) => {
selectedReview.value = review
rejectForm.value.reason = ''
rejectDialogVisible.value = true
}
const confirmRejectReview = async () => {
if (!rejectFormRef.value) return
try {
await rejectFormRef.value.validate()
//
selectedReview.value.status = 'rejected'
selectedReview.value.rejectionReason = rejectForm.value.reason
reviewStats.value.pending--
reviewStats.value.rejected++
rejectDialogVisible.value = false
ElMessage.success(t('admin.review.rejectSuccess'))
} catch {
//
}
}
const goToDisassembly = (review) => {
//
ElMessage.info(t('admin.review.disassemblyComingSoon'))
console.log(t('admin.review.redirectToDisassembly'), review)
}
onMounted(() => {
//
})
</script>
<style scoped>
.admin-content-review {
padding: 20px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e5e7eb;
}
.header-left .title {
font-size: 28px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px 0;
}
.header-left .subtitle {
color: #6b7280;
margin: 0;
font-size: 14px;
}
.header-actions {
display: flex;
gap: 12px;
}
.review-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}
.stat-icon.total { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.stat-icon.pending { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
.stat-icon.approved { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
.stat-icon.rejected { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); }
.stat-number {
font-size: 32px;
font-weight: 700;
color: #1f2937;
line-height: 1;
}
.stat-label {
color: #6b7280;
font-size: 14px;
margin-top: 4px;
}
.review-filters {
padding: 20px;
border-radius: 12px;
margin-bottom: 24px;
display: flex;
gap: 12px;
align-items: flex-end;
justify-content: flex-end;
overflow-x: auto;
white-space: nowrap;
}
.filter-group {
display: flex;
gap: 8px;
align-items: flex-end;
flex-shrink: 0;
}
.search-group {
flex: 1;
min-width: 200px;
max-width: 300px;
flex-shrink: 0;
}
.filter-actions {
display: flex;
align-items: flex-end;
flex-shrink: 0;
}
.review-table {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
margin-bottom: 24px;
}
/* 表格样式优化 */
.review-table :deep(.el-table) {
width: 100% !important;
table-layout: auto;
}
.review-table :deep(.el-table__header) {
width: 100% !important;
}
.review-table :deep(.el-table__body) {
width: 100% !important;
}
.review-table :deep(.el-table__cell) {
padding: 8px 12px;
white-space: nowrap;
}
.review-table :deep(.el-table__header-wrapper) {
background-color: #f8fafc;
}
.review-table :deep(.el-table__header th) {
background-color: #f8fafc;
color: #374151;
font-weight: 600;
border-bottom: 2px solid #e5e7eb;
}
/* 操作按钮容器 */
.actions-container {
display: flex;
gap: 6px;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
overflow: hidden;
}
.actions-container .el-button {
flex-shrink: 0;
white-space: nowrap;
}
/* 图片错误样式优化 */
.image-error {
width: 50px;
height: 50px;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
border-radius: 6px;
}
.rejection-text {
color: #ef4444;
font-size: 12px;
}
.pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
/* 图片预览对话框样式 */
.image-preview-dialog :deep(.el-dialog__body) {
padding: 0;
}
.image-preview-container {
display: flex;
flex-direction: column;
height: 70vh;
}
.image-preview-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
border-radius: 8px;
margin-bottom: 16px;
min-height: 400px;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
cursor: grab;
}
.preview-image:active {
cursor: grabbing;
}
.image-actions {
display: flex;
justify-content: center;
gap: 12px;
padding: 16px 0;
border-top: 1px solid #e5e7eb;
}
.image-actions .el-button {
min-width: 80px;
}
/* 原有样式保留 */
.image-preview {
text-align: center;
}
.model-preview {
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
border-radius: 8px;
}
.model-placeholder {
text-align: center;
color: #6b7280;
}
.model-placeholder p {
margin: 16px 0;
}
.model-info {
color: #374151;
font-size: 14px;
}
@media (max-width: 768px) {
.admin-content-review {
padding: 12px;
}
.dashboard-header {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
.review-stats {
grid-template-columns: 1fr;
}
.review-filters {
flex-direction: column;
align-items: stretch;
white-space: normal;
overflow-x: visible;
}
.filter-group {
flex-direction: column;
gap: 12px;
}
.search-group {
max-width: none;
}
/* 移动端表格优化 */
.review-table {
overflow-x: auto;
}
.review-table :deep(.el-table) {
width: auto !important;
min-width: 800px;
}
.review-table :deep(.el-table__cell) {
padding: 6px 8px;
font-size: 12px;
}
.actions-container {
gap: 4px;
}
.actions-container .el-button {
padding: 4px 8px;
font-size: 12px;
}
}
</style>

View File

@ -117,9 +117,8 @@
</template>
<div class="chart-container">
<div class="pie-chart">
<div class="pie-segment completed" :style="{ '--percentage': orderStats.completed }"></div>
<div class="pie-segment pending" :style="{ '--percentage': orderStats.pending }"></div>
<div class="pie-segment cancelled" :style="{ '--percentage': orderStats.cancelled }"></div>
<div class="pie-visual"></div>
<div class="pie-center"></div>
</div>
<div class="pie-legend">
<div class="legend-item">
@ -437,16 +436,34 @@ onMounted(() => {
/* 饼图 */
.pie-chart {
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
position: relative;
}
.pie-visual {
width: 120px;
height: 120px;
border-radius: 50%;
background: conic-gradient(
from 0deg,
#10b981 0deg calc(var(--completed) * 3.6deg),
#f59e0b calc(var(--completed) * 3.6deg) calc((var(--completed) + var(--pending)) * 3.6deg),
#ef4444 calc((var(--completed) + var(--pending)) * 3.6deg) 360deg
#10b981 0deg 270deg,
#f59e0b 270deg 342deg,
#ef4444 342deg 360deg
);
margin: 0 auto 16px;
position: relative;
}
.pie-center {
position: absolute;
width: 60px;
height: 60px;
background: white;
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.pie-legend {

View File

@ -0,0 +1,31 @@
{
"version": 2,
"buildCommand": "npm run build",
"outputDirectory": "dist",
"installCommand": "npm install",
"framework": "vite",
"routes": [
{
"src": "/assets/(.*)",
"headers": {
"cache-control": "public, max-age=31536000, immutable",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-headers": "Content-Type, Authorization, X-Requested-With"
}
},
{
"src": "/(.*)",
"headers": {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-headers": "Content-Type, Authorization, X-Requested-With",
"access-control-max-age": "86400"
}
},
{
"src": "/(.*)",
"dest": "/index.html"
}
]
}

View File

@ -52,7 +52,7 @@ VITE_APP_DESCRIPTION=AI-Powered Creation Platform
1. 登录 [Vercel Dashboard](https://vercel.com/dashboard)
2. 选择你的项目
3. 进入 `Settings``Environment Variables`
3. 拆件 `Settings``Environment Variables`
4. 添加以下环境变量:
#### 生产环境变量

View File

@ -222,7 +222,7 @@ jobs:
#### 在 Vercel Dashboard 中配置:
1. 进入项目设置 → Domains
1. 拆件项目设置 → Domains
2. 添加你的域名
3. 按照指示配置 DNS 记录
@ -250,7 +250,7 @@ Vercel 会自动为你的域名配置 Let's Encrypt SSL 证书,通常在几分
在 Vercel Dashboard 中启用 Web Analytics 以监控性能:
1. 进入项目 → Analytics
1. 拆件项目 → Analytics
2. 启用 Web Analytics
#### 配置缓存策略
@ -288,7 +288,7 @@ Vercel 会自动为你的域名配置 Let's Encrypt SSL 证书,通常在几分
在 Vercel Dashboard 中查看部署状态和日志:
1. 进入项目 → Deployments
1. 拆件项目 → Deployments
2. 点击任意部署查看详细日志
### 2. 监控应用性能

View File

@ -11,8 +11,6 @@
"clean": "rimraf dist"
},
"dependencies": {
"@deotaland/ui": "workspace:*",
"@deotaland/utils": "workspace:*",
"@element-plus/icons-vue": "^2.3.2",
"@google/genai": "^1.27.0",
"@stripe/stripe-js": "^4.8.0",
@ -36,8 +34,10 @@
},
"devDependencies": {
"@iconify-json/feather": "^1.2.1",
"@vitejs/plugin-vue": "^6.0.1",
"unplugin-auto-import": "^20.2.0",
"unplugin-icons": "^22.5.0",
"unplugin-vue-components": "^30.0.0"
"unplugin-vue-components": "^30.0.0",
"vite": "^7.2.2"
}
}

View File

@ -396,7 +396,7 @@ export default {
transition: all 0.3s ease;
position: relative;
overflow: hidden;
/* 进入动画 */
/* 拆件动画 */
opacity: 0;
transform: translateY(40px);
animation: buttonFloatIn 0.6s ease-out 0.2s forwards;
@ -482,7 +482,7 @@ export default {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
/* 进入动画 */
/* 拆件动画 */
opacity: 0;
transform: translateY(30px) scale(0.8);
animation: circleFloatIn 0.8s ease-out forwards;

View File

@ -8,106 +8,88 @@
<div class="test-section">
<h2>Button 组件测试</h2>
<div class="button-grid">
<DtButton type="primary" size="large">主要按钮</DtButton>
<DtButton type="success" size="large">成功按钮</DtButton>
<DtButton type="warning" size="large">警告按钮</DtButton>
<DtButton type="danger" size="large">危险按钮</DtButton>
<DtButton type="info" size="large">信息按钮</DtButton>
<DtButton type="primary" size="medium">中等按钮</DtButton>
<DtButton type="primary" size="small">小按钮</DtButton>
<DtButton type="primary" disabled>禁用按钮</DtButton>
<DtButton type="primary" loading>加载按钮</DtButton>
<el-button @click="handleClick">主要按钮</el-button>
<el-button type="success" @click="handleClick">成功按钮</el-button>
<el-button type="warning" @click="handleClick">警告按钮</el-button>
<el-button type="danger" @click="handleClick">危险按钮</el-button>
<el-button type="info" @click="handleClick">信息按钮</el-button>
<el-button size="large" @click="handleClick">大尺寸按钮</el-button>
<el-button size="small" @click="handleClick">小尺寸按钮</el-button>
</div>
</div>
<div class="test-section">
<h2>Card 组件测试</h2>
<div class="card-grid">
<DtCard title="基础卡片" :shadow="true">
<el-card title="基础卡片">
<p>这是一个基础的卡片组件展示基本内容</p>
<template #footer>
<DtButton type="primary" size="small">操作</DtButton>
<el-button type="primary" size="small">操作</el-button>
</template>
</DtCard>
</el-card>
<DtCard title="带图片的卡片" :shadow="true" image="https://via.placeholder.com/300x200/8B5CF6/FFFFFF?text=Placeholder">
<el-card title="带图片的卡片">
<p>这是一个带有图片的卡片组件</p>
<template #footer>
<DtButton type="success" size="small">查看详情</DtButton>
<el-button type="success" size="small">查看详情</el-button>
</template>
</DtCard>
</el-card>
<DtCard :shadow="false" bordered>
<el-card>
<p>这是一个无阴影边框卡片</p>
<template #footer>
<DtButton type="warning" size="small">了解更多</DtButton>
<el-button type="warning" size="small">了解更多</el-button>
</template>
</DtCard>
</el-card>
</div>
</div>
<div class="test-section">
<h2>Modal 组件测试</h2>
<div class="modal-controls">
<DtButton type="primary" @click="showBasicModal = true">基础模态框</DtButton>
<DtButton type="success" @click="showLargeModal = true">大尺寸模态框</DtButton>
<DtButton type="warning" @click="showSmallModal = true">小尺寸模态框</DtButton>
<DtButton type="danger" @click="showFullscreenModal = true">全屏模态框</DtButton>
<el-button type="primary" @click="showBasicModal = true">基础模态框</el-button>
<el-button type="success" @click="showLargeModal = true">大尺寸模态框</el-button>
<el-button type="warning" @click="showSmallModal = true">小尺寸模态框</el-button>
<el-button type="danger" @click="showFullscreenModal = true">全屏模态框</el-button>
</div>
<!-- 基础模态框 -->
<DtModal
v-model="showBasicModal"
title="基础模态框测试"
:closable="true"
>
<el-dialog v-model="showBasicModal" title="基础模态框测试">
<p>这是一个基础的模态框测试内容</p>
<p>测试组件的基本功能和响应式设计</p>
</DtModal>
</el-dialog>
<!-- 大尺寸模态框 -->
<DtModal
v-model="showLargeModal"
title="大尺寸模态框"
size="large"
>
<el-dialog v-model="showLargeModal" title="大尺寸模态框" width="80%">
<p>这是一个大尺寸的模态框测试内容</p>
<p>在大屏幕上会有更好的展示效果</p>
<div style="height: 200px; background: #f5f5f5; margin: 10px 0; display: flex; align-items: center; justify-content: center;">
大内容区域
</div>
</DtModal>
</el-dialog>
<!-- 小尺寸模态框 -->
<DtModal
v-model="showSmallModal"
title="小尺寸模态框"
size="small"
>
<el-dialog v-model="showSmallModal" title="小尺寸模态框" width="30%">
<p>这是一个小尺寸的模态框测试内容</p>
<p>适合简单的确认对话框</p>
</DtModal>
</el-dialog>
<!-- 全屏模态框 -->
<DtModal
v-model="showFullscreenModal"
title="全屏模态框"
:fullscreen="true"
>
<el-dialog v-model="showFullscreenModal" title="全屏模态框" fullscreen>
<p>这是一个全屏模态框测试内容</p>
<p>适合需要大量空间展示内容的场景</p>
</DtModal>
</el-dialog>
</div>
<div class="test-section">
<h2>Loading 组件测试</h2>
<div class="loading-controls">
<DtButton type="primary" @click="showLoading = !showLoading">
<el-button type="primary" @click="showLoading = !showLoading">
{{ showLoading ? '隐藏' : '显示' }}加载动画
</DtButton>
<DtButton type="success" @click="showCustomLoading = !showCustomLoading">
</el-button>
<el-button type="success" @click="showCustomLoading = !showCustomLoading">
{{ showCustomLoading ? '隐藏' : '显示' }}自定义加载
</DtButton>
</el-button>
</div>
<div class="loading-content" v-show="!showLoading">
@ -117,18 +99,12 @@
</div>
</div>
<DtLoading
v-model="showLoading"
text="正在加载数据..."
:background="'rgba(0, 0, 0, 0.8)'"
/>
<DtLoading
v-model="showCustomLoading"
text="自定义加载中..."
size="large"
:spinner-style="{ color: '#10b981' }"
/>
<!-- 使用 Element Plus v-loading 指令实现加载状态 -->
<div v-loading="showLoading" element-loading-text="正在加载数据..." element-loading-background="rgba(0, 0, 0, 0.8)">
<div v-loading="showCustomLoading" element-loading-text="自定义加载中..." element-loading-spinner="el-icon-loading">
<p>测试内容区域</p>
</div>
</div>
</div>
<div class="test-section">
@ -155,14 +131,18 @@
<script setup>
import { ref } from 'vue'
import {
DtButton,
DtCard,
DtModal,
DtLoading
} from '@deotaland/ui'
// UI -
// import {
// DtButton,
// DtCard,
// DtModal,
// DtLoading
// } from '@deotaland/ui'
//
// 使 Element Plus
import { ElButton, ElCard, ElDialog, ElLoading } from 'element-plus'
// - 使 Element Plus
const showBasicModal = ref(false)
const showLargeModal = ref(false)
const showSmallModal = ref(false)

View File

@ -31,7 +31,7 @@ export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@deotaland/ui': path.resolve(__dirname, '../../packages/ui'),
// '@deotaland/ui': path.resolve(__dirname, '../../packages/ui'), // 临时注释,修复部署问题
},
},
// Vercel 部署优化配置 - 修复CORS问题

View File

@ -32,21 +32,21 @@ importers:
apps/FrontendDesigner:
dependencies:
'@deotaland/ui':
specifier: workspace:*
version: link:../../packages/ui
'@deotaland/utils':
specifier: workspace:*
version: link:../../packages/utils
'@element-plus/icons-vue':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.24)
'@types/three':
specifier: ^0.180.0
version: 0.180.0
element-plus:
specifier: ^2.11.7
version: 2.11.7(vue@3.5.24)
pinia:
specifier: ^2.2.6
version: 2.2.6(vue@3.5.24)
three:
specifier: ^0.180.0
version: 0.180.0
vue:
specifier: ^3.5.24
version: 3.5.24