init
This commit is contained in:
parent
19e64b0c7f
commit
cf5e291af5
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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>
|
||||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
@ -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,113 +164,166 @@ export default {
|
|||
settings: '设置',
|
||||
notifications: '通知'
|
||||
},
|
||||
pages: {
|
||||
content: {
|
||||
title: '内容管理',
|
||||
add: '添加内容',
|
||||
status: '状态',
|
||||
type: '类型',
|
||||
search: '搜索内容',
|
||||
author: '作者',
|
||||
publishDate: '发布时间',
|
||||
views: '浏览量',
|
||||
actions: '操作',
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
statusOptions: {
|
||||
published: '已发布',
|
||||
pending: '待审核',
|
||||
draft: '草稿',
|
||||
rejected: '已拒绝'
|
||||
},
|
||||
typeOptions: {
|
||||
article: '文章',
|
||||
image: '图片',
|
||||
video: '视频'
|
||||
}
|
||||
content: {
|
||||
title: '内容管理',
|
||||
add: '添加内容',
|
||||
status: '状态',
|
||||
type: '类型',
|
||||
search: '搜索内容',
|
||||
author: '作者',
|
||||
publishDate: '发布时间',
|
||||
views: '浏览量',
|
||||
actions: '操作',
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
statusOptions: {
|
||||
published: '已发布',
|
||||
pending: '待审核',
|
||||
draft: '草稿',
|
||||
rejected: '已拒绝'
|
||||
},
|
||||
orders: {
|
||||
title: '订单管理',
|
||||
export: '导出订单',
|
||||
search: '搜索订单',
|
||||
status: '状态',
|
||||
dateRange: '日期范围',
|
||||
orderNumber: '订单号',
|
||||
customer: '客户',
|
||||
total: '总金额',
|
||||
payment: '支付方式',
|
||||
date: '下单日期',
|
||||
actions: '操作',
|
||||
view: '查看',
|
||||
updateStatus: '更新状态',
|
||||
detail: '订单详情',
|
||||
basicInfo: '基本信息',
|
||||
items: '订单商品',
|
||||
itemName: '商品名称',
|
||||
quantity: '数量',
|
||||
price: '价格',
|
||||
currentStatus: '当前状态',
|
||||
newStatus: '新状态',
|
||||
selectStatus: '选择状态',
|
||||
stats: {
|
||||
total: '总订单',
|
||||
pending: '待处理',
|
||||
completed: '已完成',
|
||||
revenue: '总收入'
|
||||
},
|
||||
statusOptions: {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
shipped: '已发货',
|
||||
delivered: '已送达',
|
||||
cancelled: '已取消'
|
||||
},
|
||||
paymentOptions: {
|
||||
alipay: '支付宝',
|
||||
wechat: '微信支付',
|
||||
credit: '信用卡'
|
||||
}
|
||||
typeOptions: {
|
||||
article: '文章',
|
||||
image: '图片',
|
||||
video: '视频'
|
||||
}
|
||||
},
|
||||
orders: {
|
||||
title: '订单管理',
|
||||
export: '导出订单',
|
||||
search: '搜索订单',
|
||||
status: '状态',
|
||||
dateRange: '日期范围',
|
||||
orderNumber: '订单号',
|
||||
customer: '客户',
|
||||
total: '总金额',
|
||||
payment: '支付方式',
|
||||
date: '下单日期',
|
||||
actions: '操作',
|
||||
view: '查看',
|
||||
updateStatus: '更新状态',
|
||||
detail: '订单详情',
|
||||
basicInfo: '基本信息',
|
||||
items: '订单商品',
|
||||
itemName: '商品名称',
|
||||
quantity: '数量',
|
||||
price: '价格',
|
||||
currentStatus: '当前状态',
|
||||
newStatus: '新状态',
|
||||
selectStatus: '选择状态',
|
||||
stats: {
|
||||
total: '总订单',
|
||||
pending: '待处理',
|
||||
completed: '已完成',
|
||||
revenue: '总收入'
|
||||
},
|
||||
users: {
|
||||
title: '用户管理',
|
||||
add: '添加用户',
|
||||
search: '搜索用户',
|
||||
status: '状态',
|
||||
role: '角色',
|
||||
registerDate: '注册日期',
|
||||
username: '用户名',
|
||||
email: '邮箱',
|
||||
phone: '手机号',
|
||||
lastLogin: '最后登录',
|
||||
loginCount: '登录次数',
|
||||
actions: '操作',
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
resetPassword: '重置密码',
|
||||
ban: '封禁',
|
||||
unban: '解封',
|
||||
detail: '用户详情',
|
||||
selectRole: '选择角色',
|
||||
selectStatus: '选择状态',
|
||||
save: '保存',
|
||||
stats: {
|
||||
total: '总用户',
|
||||
active: '活跃用户',
|
||||
inactive: '非活跃用户',
|
||||
vip: 'VIP用户'
|
||||
},
|
||||
statusOptions: {
|
||||
active: '活跃',
|
||||
inactive: '非活跃',
|
||||
banned: '已封禁'
|
||||
},
|
||||
roleOptions: {
|
||||
admin: '管理员',
|
||||
user: '普通用户',
|
||||
vip: 'VIP用户'
|
||||
}
|
||||
statusOptions: {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
shipped: '已发货',
|
||||
delivered: '已送达',
|
||||
cancelled: '已取消'
|
||||
},
|
||||
paymentOptions: {
|
||||
alipay: '支付宝',
|
||||
wechat: '微信支付',
|
||||
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: '添加用户',
|
||||
search: '搜索用户',
|
||||
status: '状态',
|
||||
role: '角色',
|
||||
registerDate: '注册日期',
|
||||
username: '用户名',
|
||||
email: '邮箱',
|
||||
phone: '手机号',
|
||||
lastLogin: '最后登录',
|
||||
loginCount: '登录次数',
|
||||
actions: '操作',
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
resetPassword: '重置密码',
|
||||
ban: '封禁',
|
||||
unban: '解封',
|
||||
detail: '用户详情',
|
||||
selectRole: '选择角色',
|
||||
selectStatus: '选择状态',
|
||||
save: '保存',
|
||||
stats: {
|
||||
total: '总用户',
|
||||
active: '活跃用户',
|
||||
inactive: '非活跃用户',
|
||||
vip: 'VIP用户'
|
||||
},
|
||||
statusOptions: {
|
||||
active: '活跃',
|
||||
inactive: '非活跃',
|
||||
banned: '已封禁'
|
||||
},
|
||||
roleOptions: {
|
||||
admin: '管理员',
|
||||
user: '普通用户',
|
||||
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: '导出失败'
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@
|
|||
clearable
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
// 设置3D模型URL,如果review中有modelUrl则使用,否则使用默认模型
|
||||
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>
|
||||
|
|
@ -117,23 +117,22 @@
|
|||
</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">
|
||||
<div class="legend-color completed"></div>
|
||||
<span>{{ t('admin.orders.statusOptions.completed') }} ({{ orderStats.completed }}%)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color pending"></div>
|
||||
<span>{{ t('admin.orders.statusOptions.pending') }} ({{ orderStats.pending }}%)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color cancelled"></div>
|
||||
<span>{{ t('admin.orders.statusOptions.cancelled') }} ({{ orderStats.cancelled }}%)</span>
|
||||
</div>
|
||||
<div class="legend-color completed"></div>
|
||||
<span>{{ t('admin.orders.statusOptions.completed') }} ({{ orderStats.completed }}%)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color pending"></div>
|
||||
<span>{{ t('admin.orders.statusOptions.pending') }} ({{ orderStats.pending }}%)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color cancelled"></div>
|
||||
<span>{{ t('admin.orders.statusOptions.cancelled') }} ({{ orderStats.cancelled }}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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. 添加以下环境变量:
|
||||
|
||||
#### 生产环境变量
|
||||
|
|
|
|||
|
|
@ -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. 监控应用性能
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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问题
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue