222
|
|
@ -1,203 +1,201 @@
|
||||||
# 优化IPCard组件生成状态样式
|
# 新建项目系列选择功能实现
|
||||||
|
|
||||||
## 现状分析
|
## 需求分析
|
||||||
|
|
||||||
当前IPCard组件在图片生成过程中显示的占位符(.generating-placeholder)样式较为简单,主要包含:
|
* 当用户点击"新建项目"卡片时,需要弹出系列选择弹窗
|
||||||
|
|
||||||
* 深灰色背景(#2a2a2a)
|
* 系列弹窗包含两个选项:Done 和 Oone,对应图片在 src/assets/xh 文件夹中
|
||||||
|
|
||||||
* 基础的圆形旋转加载动画
|
* 选中后将系列名称作为 type 参数传递给 createNewProject 函数
|
||||||
|
|
||||||
* 静态文本"正在生成图片..."
|
## 实现计划
|
||||||
|
|
||||||
## 优化目标
|
### 1. 创建 SeriesSelector 组件
|
||||||
|
|
||||||
使生成状态更加灵动、美观,符合项目的设计风格(深紫色主色调),提升用户体验。
|
* **文件路径**:`src/views/components/SeriesSelector.vue`
|
||||||
|
|
||||||
## 优化方案
|
* **功能**:
|
||||||
|
|
||||||
### 1. 背景效果优化
|
* 显示两个系列选项(Done 和 Oone)
|
||||||
|
|
||||||
* 将纯色背景改为渐变背景,与主题色呼应
|
* 每个选项显示对应的图片
|
||||||
|
|
||||||
* 添加微妙的呼吸动画效果
|
* 支持选中状态切换
|
||||||
|
|
||||||
### 2. 加载动画优化
|
* 提供确认和取消按钮
|
||||||
|
|
||||||
* 设计双层旋转动画,增强视觉层次感
|
* 通过 emit 事件返回选中的系列名称
|
||||||
|
|
||||||
* 使用主题色(#A78BFA)作为动画主色调
|
### 2. 修改 CreationWorkspace.vue
|
||||||
|
|
||||||
* 添加脉冲效果,使动画更加生动
|
* **引入组件**:在 CreationWorkspace.vue 中引入 SeriesSelector 组件
|
||||||
|
|
||||||
### 3. 文本效果优化
|
* **添加状态管理**:
|
||||||
|
|
||||||
* 添加渐变文字效果
|
* `showSeriesSelector`:控制系列选择弹窗的显示/隐藏
|
||||||
|
|
||||||
* 保持与主题色的一致性
|
* **修改新建项目逻辑**:
|
||||||
|
|
||||||
### 4. 整体布局优化
|
* 点击"新建项目"卡片时,显示系列选择弹窗
|
||||||
|
|
||||||
* 添加卡片阴影和立体感
|
* 监听 SeriesSelector 的确认事件,获取选中的系列名称
|
||||||
|
|
||||||
* 确保动画流畅,性能优化
|
* 将系列名称作为 type 参数调用 createNewProject 函数
|
||||||
|
|
||||||
## 具体实现
|
### 3. 样式设计
|
||||||
|
|
||||||
### 修改样式代码
|
* 系列选择弹窗采用与现有删除确认弹窗一致的设计风格
|
||||||
|
|
||||||
1. **更新.generating-placeholder样式**:
|
* 系列选项卡片包含图片和名称,支持悬停和选中效果
|
||||||
|
|
||||||
* 添加渐变背景
|
* 确认和取消按钮使用现有按钮样式
|
||||||
|
|
||||||
* 添加呼吸动画
|
## 代码结构
|
||||||
|
|
||||||
* 优化文本样式
|
### SeriesSelector.vue
|
||||||
|
|
||||||
2. **更新.generating-spinner样式**:
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 系列选择弹窗 -->
|
||||||
|
<div v-if="show" class="modal-overlay" @click="onCancel">
|
||||||
|
<div class="modal-content" @click.stop>
|
||||||
|
<!-- 模态头部 -->
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title">{{ t('creationWorkspace.selectSeries') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
* 设计双层旋转结构
|
<!-- 模态内容 -->
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="series-selector-content">
|
||||||
|
<!-- 系列选项列表 -->
|
||||||
|
<div class="series-list">
|
||||||
|
<!-- Done 系列 -->
|
||||||
|
<div
|
||||||
|
class="series-item"
|
||||||
|
:class="{ active: selectedSeries === 'Done' }"
|
||||||
|
@click="selectedSeries = 'Done'"
|
||||||
|
>
|
||||||
|
<div class="series-image">
|
||||||
|
<img src="@/assets/xh/Done.webp" alt="Done" />
|
||||||
|
</div>
|
||||||
|
<div class="series-name">Done</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
* 使用主题色作为动画颜色
|
<!-- Oone 系列 -->
|
||||||
|
<div
|
||||||
|
class="series-item"
|
||||||
|
:class="{ active: selectedSeries === 'Oone' }"
|
||||||
|
@click="selectedSeries = 'Oone'"
|
||||||
|
>
|
||||||
|
<div class="series-image">
|
||||||
|
<img src="@/assets/xh/Oone.webp" alt="Oone" />
|
||||||
|
</div>
|
||||||
|
<div class="series-name">Oone</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
* 添加脉冲动画
|
<!-- 操作按钮 -->
|
||||||
|
<div class="series-actions">
|
||||||
|
<button class="modal-action-btn cancel" @click="onCancel">
|
||||||
|
{{ t('creationWorkspace.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button class="modal-action-btn primary" @click="onConfirm">
|
||||||
|
{{ t('creationWorkspace.confirm') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
3. **添加新的动画关键帧**:
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
* 双层旋转动画
|
const { t } = useI18n()
|
||||||
|
|
||||||
* 呼吸效果动画
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
* 脉冲效果动画
|
const emit = defineEmits(['confirm', 'cancel'])
|
||||||
|
|
||||||
## 预期效果
|
const selectedSeries = ref('')
|
||||||
|
|
||||||
* 生成状态更加灵动、美观
|
const onConfirm = () => {
|
||||||
|
if (selectedSeries.value) {
|
||||||
|
emit('confirm', selectedSeries.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
* 与项目设计风格保持一致
|
const onCancel = () => {
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
* 提升用户等待体验
|
<style scoped>
|
||||||
|
/* 系列选择弹窗样式 */
|
||||||
|
.series-selector-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
* 动画流畅,性能优化
|
.series-list {
|
||||||
|
|
||||||
## 实现步骤
|
|
||||||
|
|
||||||
1. 修改IPCard组件的样式部分,更新.generating-placeholder相关样式
|
|
||||||
2. 添加新的动画关键帧
|
|
||||||
3. 确保样式与主题色保持一致
|
|
||||||
4. 测试动画性能和效果
|
|
||||||
|
|
||||||
## 代码示例
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* 生成状态样式优化 */
|
|
||||||
.generating-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 24px;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a2e 100%);
|
margin-bottom: 32px;
|
||||||
color: white;
|
}
|
||||||
border-radius: inherit;
|
|
||||||
position: relative;
|
.series-item {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item.active {
|
||||||
|
border-color: #6B46C1;
|
||||||
|
background: rgba(107, 70, 193, 0.05);
|
||||||
|
box-shadow: 0 4px 16px rgba(107, 70, 193, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: breathe 3s ease-in-out infinite;
|
border-radius: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 添加背景装饰效果 */
|
.series-image img {
|
||||||
.generating-placeholder::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -50%;
|
|
||||||
left: -50%;
|
|
||||||
width: 200%;
|
|
||||||
height: 200%;
|
|
||||||
background: radial-gradient(circle, rgba(167, 139, 250, 0.1) 0%, transparent 70%);
|
|
||||||
animation: rotate 20s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-spinner {
|
|
||||||
position: relative;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-spinner::before,
|
|
||||||
.generating-spinner::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 50%;
|
object-fit: cover;
|
||||||
border: 3px solid transparent;
|
|
||||||
animation: spin 2s linear infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.generating-spinner::before {
|
.series-name {
|
||||||
border-top-color: #A78BFA;
|
font-size: 16px;
|
||||||
border-right-color: #A78BFA;
|
font-weight: 600;
|
||||||
animation-duration: 1s;
|
color: #1f2937;
|
||||||
animation-timing-function: ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.generating-spinner::after {
|
.series-actions {
|
||||||
border-bottom-color: #6B46C1;
|
display: flex;
|
||||||
border-left-color: #6B46C1;
|
gap: 16px;
|
||||||
animation-duration: 2s;
|
justify-content: center;
|
||||||
animation-timing-function: ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加脉冲效果 */
|
|
||||||
.generating-spinner::before {
|
|
||||||
box-shadow: 0 0 10px rgba(167, 139, 250, 0.3);
|
|
||||||
animation: spin 1s linear infinite, pulse 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-text {
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #A78BFA 0%, #6B46C1 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
font-weight: 500;
|
|
||||||
z-index: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 新增动画关键帧 */
|
|
||||||
@keyframes breathe {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.95;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% {
|
|
||||||
box-shadow: 0 0 10px rgba(167, 139, 250, 0.3);
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 20px rgba(167, 139, 250, 0.6);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
```
|
```
|
||||||
|
|
||||||
这个优化方案将使IPCard
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
# 实现3MF格式模型导出功能
|
||||||
|
|
||||||
|
## 1. 安装依赖
|
||||||
|
- 安装 `three-3mf-exporter` 插件库,用于支持3MF格式导出
|
||||||
|
|
||||||
|
## 2. 修改 ModelViewer.vue 组件
|
||||||
|
### 2.1 导入3MF导出器
|
||||||
|
在组件的脚本部分添加3MF导出器的导入
|
||||||
|
|
||||||
|
### 2.2 更新导出选项
|
||||||
|
在导出下拉菜单中添加3MF格式选项
|
||||||
|
|
||||||
|
### 2.3 实现3MF导出功能
|
||||||
|
添加 `exportAs3MF` 方法,使用3MF导出器将模型导出为3MF格式
|
||||||
|
|
||||||
|
### 2.4 更新导出命令处理
|
||||||
|
在 `handleExportCommand` 方法中添加对3MF格式的支持
|
||||||
|
|
||||||
|
### 2.5 添加国际化支持
|
||||||
|
在国际化文件中添加3MF相关的文本翻译
|
||||||
|
|
||||||
|
## 3. 测试功能
|
||||||
|
- 运行开发服务器,测试3MF导出功能是否正常工作
|
||||||
|
- 检查导出的3MF文件是否可以正常打开
|
||||||
|
|
||||||
|
## 4. 兼容性检查
|
||||||
|
- 确保3MF导出功能与现有导出功能兼容
|
||||||
|
- 确保3MF导出功能在不同设备上都能正常工作
|
||||||
|
|
||||||
|
## 5. 代码优化
|
||||||
|
- 确保代码符合项目的代码风格和最佳实践
|
||||||
|
- 优化导出性能,确保导出过程流畅
|
||||||
|
|
||||||
|
## 6. 文档更新
|
||||||
|
- 更新组件文档,说明3MF导出功能的使用方法
|
||||||
|
- 更新项目文档,添加3MF格式支持的说明
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"konva": "^10.0.12",
|
"konva": "^10.0.12",
|
||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
"three": "^0.180.0",
|
"three": "^0.180.0",
|
||||||
|
"three-3mf-exporter": "^45.0.0",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-i18n": "^9.14.2",
|
"vue-i18n": "^9.14.2",
|
||||||
"vue-konva": "^3.2.6",
|
"vue-konva": "^3.2.6",
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ import {
|
||||||
Lock,
|
Lock,
|
||||||
Key,
|
Key,
|
||||||
List,
|
List,
|
||||||
Coin
|
Coin,
|
||||||
|
ShoppingCartFull
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -60,7 +61,8 @@ const iconMap = {
|
||||||
Lock,
|
Lock,
|
||||||
Key,
|
Key,
|
||||||
List,
|
List,
|
||||||
Coin
|
Coin,
|
||||||
|
ShoppingCartFull
|
||||||
}
|
}
|
||||||
|
|
||||||
const getIconComponent = (iconName) => {
|
const getIconComponent = (iconName) => {
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,10 @@
|
||||||
<span class="format-dot" style="color: #f56c6c;">●</span>
|
<span class="format-dot" style="color: #f56c6c;">●</span>
|
||||||
{{ t('modelViewer.exportAsFBX') }}
|
{{ t('modelViewer.exportAsFBX') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="3mf">
|
||||||
|
<span class="format-dot" style="color: #20c997;">●</span>
|
||||||
|
{{ t('modelViewer.exportAs3MF') }}
|
||||||
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
|
|
@ -116,7 +120,6 @@ import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
|
||||||
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
|
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Loading, Warning, Refresh, Grid, Position, Download, ArrowDown } from '@element-plus/icons-vue'
|
import { Loading, Warning, Refresh, Grid, Position, Download, ArrowDown } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
|
|
@ -445,6 +448,9 @@ const handleExportCommand = (command) => {
|
||||||
case 'fbx':
|
case 'fbx':
|
||||||
exportAsFBX()
|
exportAsFBX()
|
||||||
break
|
break
|
||||||
|
case '3mf':
|
||||||
|
exportAs3MF()
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
ElMessage.error('不支持的导出格式')
|
ElMessage.error('不支持的导出格式')
|
||||||
break
|
break
|
||||||
|
|
@ -644,6 +650,57 @@ const exportAsFBX = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出为3MF格式
|
||||||
|
const exportAs3MF = async () => {
|
||||||
|
if (!model || !scene) {
|
||||||
|
ElMessage({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'No model to export'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isExporting.value = true
|
||||||
|
|
||||||
|
ElMessage({
|
||||||
|
type: 'info',
|
||||||
|
message: t('modelViewer.exportInProgress'),
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 动态导入 three-3mf-exporter
|
||||||
|
const { exportTo3MF } = await import('three-3mf-exporter')
|
||||||
|
|
||||||
|
// 直接调用 exportTo3MF 函数,无需创建实例
|
||||||
|
const blob = await exportTo3MF(model, {
|
||||||
|
compression: 'standard',
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `${modelInfo.value || 'model'}.3mf`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
ElMessage({
|
||||||
|
type: 'success',
|
||||||
|
message: t('modelViewer.exportSuccess')
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('3MF export error:', error)
|
||||||
|
ElMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: t('modelViewer.exportFailed')
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isExporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 鼠标事件处理(保留缩放功能)
|
// 鼠标事件处理(保留缩放功能)
|
||||||
const handleMouseDown = () => {
|
const handleMouseDown = () => {
|
||||||
// 鼠标事件由 OrbitControls 处理
|
// 鼠标事件由 OrbitControls 处理
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export default {
|
||||||
exportAsOBJ: 'Export as OBJ',
|
exportAsOBJ: 'Export as OBJ',
|
||||||
exportAsSTL: 'Export as STL',
|
exportAsSTL: 'Export as STL',
|
||||||
exportAsFBX: 'Export as FBX',
|
exportAsFBX: 'Export as FBX',
|
||||||
|
exportAs3MF: 'Export as 3MF',
|
||||||
selectFormat: 'Select Export Format',
|
selectFormat: 'Select Export Format',
|
||||||
preparingExport: 'Preparing export...'
|
preparingExport: 'Preparing export...'
|
||||||
},
|
},
|
||||||
|
|
@ -243,6 +244,7 @@ export default {
|
||||||
yes: 'Yes',
|
yes: 'Yes',
|
||||||
no: 'No',
|
no: 'No',
|
||||||
action: 'Action',
|
action: 'Action',
|
||||||
|
actions: 'Actions',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
back: 'Back'
|
back: 'Back'
|
||||||
},
|
},
|
||||||
|
|
@ -260,6 +262,8 @@ export default {
|
||||||
userList: 'User List',
|
userList: 'User List',
|
||||||
pointsManagement: 'Points Management',
|
pointsManagement: 'Points Management',
|
||||||
commissionManagement: 'Commission Management',
|
commissionManagement: 'Commission Management',
|
||||||
|
promptManagement: 'Prompt Management',
|
||||||
|
productManagement: 'Product Management',
|
||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
profile: 'Profile',
|
profile: 'Profile',
|
||||||
settings: 'Settings'
|
settings: 'Settings'
|
||||||
|
|
@ -780,6 +784,76 @@ export default {
|
||||||
approved: 'Approved',
|
approved: 'Approved',
|
||||||
rejected: 'Rejected'
|
rejected: 'Rejected'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
promptManagement: {
|
||||||
|
title: 'Prompt Management',
|
||||||
|
addPrompt: 'Add Prompt',
|
||||||
|
editPrompt: 'Edit Prompt',
|
||||||
|
promptDetail: 'Prompt Detail',
|
||||||
|
inactivePrompts: 'Inactive Prompts',
|
||||||
|
activePrompts: 'Active Prompts',
|
||||||
|
type: 'Type',
|
||||||
|
selectType: 'Select Type',
|
||||||
|
animal: 'Animal',
|
||||||
|
person: 'Person',
|
||||||
|
general: 'General',
|
||||||
|
O1: 'O1',
|
||||||
|
title: 'Title',
|
||||||
|
enterTitle: 'Please enter prompt title',
|
||||||
|
content: 'Content',
|
||||||
|
enterContent: 'Please enter prompt content',
|
||||||
|
referenceImage: 'Reference Image',
|
||||||
|
imageTip: 'Supports JPG and PNG formats, max size 2MB',
|
||||||
|
active: 'Active',
|
||||||
|
deleteConfirm: 'Are you sure you want to delete this prompt?',
|
||||||
|
deleteSuccess: 'Deleted successfully',
|
||||||
|
saveSuccess: 'Saved successfully',
|
||||||
|
activeSuccess: 'Set to active status',
|
||||||
|
inactiveSuccess: 'Removed from active status'
|
||||||
|
},
|
||||||
|
productManagement: {
|
||||||
|
title: 'Product Management',
|
||||||
|
addProduct: 'Add Product',
|
||||||
|
editProduct: 'Edit Product',
|
||||||
|
productDetail: 'Product Detail',
|
||||||
|
productId: 'Product ID',
|
||||||
|
productName: 'Product Name',
|
||||||
|
enterProductName: 'Enter Product Name',
|
||||||
|
productImage: 'Product Image',
|
||||||
|
imageUploadTip: 'Support JPG, PNG, GIF format, max size 5MB',
|
||||||
|
imageTypeError: 'Please upload image file only',
|
||||||
|
imageSizeError: 'Image size cannot exceed 5MB',
|
||||||
|
imageUploadSuccess: 'Image uploaded successfully',
|
||||||
|
imageUploadFailed: 'Failed to upload image',
|
||||||
|
noImage: 'No image',
|
||||||
|
description: 'Description',
|
||||||
|
enterDescription: 'Enter Description',
|
||||||
|
amount: 'Amount',
|
||||||
|
enterAmount: 'Enter Amount',
|
||||||
|
currency: 'Currency',
|
||||||
|
selectCurrency: 'Select Currency',
|
||||||
|
price: 'Price',
|
||||||
|
status: 'Status',
|
||||||
|
selectStatus: 'Select Status',
|
||||||
|
active: 'Active',
|
||||||
|
deleted: 'Deleted',
|
||||||
|
createdAt: 'Created At',
|
||||||
|
updatedAt: 'Updated At',
|
||||||
|
updatePrice: 'Update Price',
|
||||||
|
newAmount: 'New Amount',
|
||||||
|
enterNewAmount: 'Enter New Amount',
|
||||||
|
currentPrice: 'Current Price',
|
||||||
|
fetchFailed: 'Failed to fetch products',
|
||||||
|
addSuccess: 'Product added successfully',
|
||||||
|
addFailed: 'Failed to add product',
|
||||||
|
updateSuccess: 'Product updated successfully',
|
||||||
|
updateFailed: 'Failed to update product',
|
||||||
|
deleteSuccess: 'Product deleted successfully',
|
||||||
|
deleteFailed: 'Failed to delete product',
|
||||||
|
priceUpdateSuccess: 'Price updated successfully',
|
||||||
|
priceUpdateFailed: 'Failed to update price',
|
||||||
|
detailFailed: 'Failed to fetch product detail',
|
||||||
|
deleteConfirm: 'Are you sure you want to delete this product?'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modelUpload: {
|
modelUpload: {
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,7 @@ orderManagement: {
|
||||||
pointsManagement: '充值包管理',
|
pointsManagement: '充值包管理',
|
||||||
commissionManagement: '佣金管理',
|
commissionManagement: '佣金管理',
|
||||||
promptManagement: '提示词管理',
|
promptManagement: '提示词管理',
|
||||||
|
productManagement: '产品管理',
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
profile: '个人资料',
|
profile: '个人资料',
|
||||||
settings: '设置',
|
settings: '设置',
|
||||||
|
|
@ -790,6 +791,7 @@ orderManagement: {
|
||||||
animal: '动物',
|
animal: '动物',
|
||||||
person: '人物',
|
person: '人物',
|
||||||
general: '通用',
|
general: '通用',
|
||||||
|
O1: 'O1',
|
||||||
title: '标题',
|
title: '标题',
|
||||||
enterTitle: '请输入提示词标题',
|
enterTitle: '请输入提示词标题',
|
||||||
content: '内容',
|
content: '内容',
|
||||||
|
|
@ -802,6 +804,50 @@ orderManagement: {
|
||||||
saveSuccess: '保存成功',
|
saveSuccess: '保存成功',
|
||||||
activeSuccess: '已设置为生效状态',
|
activeSuccess: '已设置为生效状态',
|
||||||
inactiveSuccess: '已从生效状态移除'
|
inactiveSuccess: '已从生效状态移除'
|
||||||
|
},
|
||||||
|
productManagement: {
|
||||||
|
title: '产品管理',
|
||||||
|
addProduct: '添加产品',
|
||||||
|
editProduct: '编辑产品',
|
||||||
|
productDetail: '产品详情',
|
||||||
|
productId: '产品ID',
|
||||||
|
productName: '产品名称',
|
||||||
|
enterProductName: '请输入产品名称',
|
||||||
|
productImage: '产品图片',
|
||||||
|
imageUploadTip: '支持JPG、PNG、GIF格式,最大5MB',
|
||||||
|
imageTypeError: '请上传图片文件',
|
||||||
|
imageSizeError: '图片大小不能超过5MB',
|
||||||
|
imageUploadSuccess: '图片上传成功',
|
||||||
|
imageUploadFailed: '图片上传失败',
|
||||||
|
noImage: '暂无图片',
|
||||||
|
description: '描述',
|
||||||
|
enterDescription: '请输入产品描述',
|
||||||
|
amount: '金额',
|
||||||
|
enterAmount: '请输入金额',
|
||||||
|
currency: '货币',
|
||||||
|
selectCurrency: '选择货币',
|
||||||
|
price: '价格',
|
||||||
|
status: '状态',
|
||||||
|
selectStatus: '选择状态',
|
||||||
|
active: '活跃',
|
||||||
|
deleted: '已删除',
|
||||||
|
createdAt: '创建时间',
|
||||||
|
updatedAt: '更新时间',
|
||||||
|
updatePrice: '更新价格',
|
||||||
|
newAmount: '新金额',
|
||||||
|
enterNewAmount: '请输入新金额',
|
||||||
|
currentPrice: '当前价格',
|
||||||
|
fetchFailed: '获取产品列表失败',
|
||||||
|
addSuccess: '产品添加成功',
|
||||||
|
addFailed: '产品添加失败',
|
||||||
|
updateSuccess: '产品更新成功',
|
||||||
|
updateFailed: '产品更新失败',
|
||||||
|
deleteSuccess: '产品删除成功',
|
||||||
|
deleteFailed: '产品删除失败',
|
||||||
|
priceUpdateSuccess: '价格更新成功',
|
||||||
|
priceUpdateFailed: '价格更新失败',
|
||||||
|
detailFailed: '获取产品详情失败',
|
||||||
|
deleteConfirm: '确定要删除这个产品吗?'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modelUpload: {
|
modelUpload: {
|
||||||
|
|
@ -854,6 +900,8 @@ orderManagement: {
|
||||||
save: '保存',
|
save: '保存',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
edit: '编辑',
|
edit: '编辑',
|
||||||
|
detail: '详情',
|
||||||
|
actions: '操作',
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
viewAll: '查看全部',
|
viewAll: '查看全部',
|
||||||
|
|
@ -887,6 +935,7 @@ orderManagement: {
|
||||||
exportAsOBJ: '导出为OBJ',
|
exportAsOBJ: '导出为OBJ',
|
||||||
exportAsSTL: '导出为STL',
|
exportAsSTL: '导出为STL',
|
||||||
exportAsFBX: '导出为FBX',
|
exportAsFBX: '导出为FBX',
|
||||||
|
exportAs3MF: '导出为3MF',
|
||||||
selectFormat: '选择导出格式',
|
selectFormat: '选择导出格式',
|
||||||
preparingExport: '准备导出中...'
|
preparingExport: '准备导出中...'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,10 @@ const initRoutes = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (window.location.hostname.indexOf('local') === -1) {
|
||||||
initRoutes()
|
initRoutes()
|
||||||
|
}
|
||||||
|
// initRoutes()
|
||||||
// 配置路由
|
// 配置路由
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ const AdminUserList = () => import('@/views/admin/AdminRoleManagement/AdminUserL
|
||||||
const AdminPointsManagement = () => import('@/views/admin/AdminPointsManagement/AdminPointsManagement.vue')
|
const AdminPointsManagement = () => import('@/views/admin/AdminPointsManagement/AdminPointsManagement.vue')
|
||||||
const AdminCommissionManagement = () => import('@/views/admin/AdminCommissionManagement/AdminCommissionManagement.vue')
|
const AdminCommissionManagement = () => import('@/views/admin/AdminCommissionManagement/AdminCommissionManagement.vue')
|
||||||
const AdminPromptManagement = () => import('@/views/admin/AdminPromptManagement/AdminPromptManagement.vue')
|
const AdminPromptManagement = () => import('@/views/admin/AdminPromptManagement/AdminPromptManagement.vue')
|
||||||
|
const AdminProductManagement = () => import('@/views/admin/ProductManagement/ProductManagement.vue')
|
||||||
//权限路由映射表
|
//权限路由映射表
|
||||||
export const permissionRoutes = [
|
export const permissionRoutes = [
|
||||||
{
|
{
|
||||||
|
|
@ -35,6 +36,17 @@ export const permissionRoutes = [
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
menuOrder: 1
|
menuOrder: 1
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'product-management',
|
||||||
|
name: 'AdminProductManagement',
|
||||||
|
component: AdminProductManagement,
|
||||||
|
meta: {
|
||||||
|
title: 'admin.layout.productManagement',
|
||||||
|
icon: 'ShoppingCartFull',
|
||||||
|
menuOrder: 2,
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'orders',
|
path: 'orders',
|
||||||
|
|
@ -58,17 +70,7 @@ export const permissionRoutes = [
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'disassembly-orders',
|
|
||||||
name: 'AdminDisassemblyOrders',
|
|
||||||
component: AdminDisassemblyOrders,
|
|
||||||
meta: {
|
|
||||||
title: 'admin.layout.disassemblyOrders',
|
|
||||||
icon: 'EditPen',
|
|
||||||
menuOrder: 2,
|
|
||||||
requiresAuth: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'order-list',
|
path: 'order-list',
|
||||||
name: 'AdminOrdersList',
|
name: 'AdminOrdersList',
|
||||||
|
|
@ -126,6 +128,7 @@ export const permissionRoutes = [
|
||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'permission',
|
path: 'permission',
|
||||||
name: 'AdminPermission',
|
name: 'AdminPermission',
|
||||||
|
|
@ -222,6 +225,17 @@ const routes = [
|
||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: 'disassembly-orders',
|
||||||
|
name: 'AdminDisassemblyOrders',
|
||||||
|
component: AdminDisassemblyOrders,
|
||||||
|
meta: {
|
||||||
|
title: 'admin.layout.disassemblyOrders',
|
||||||
|
icon: 'EditPen',
|
||||||
|
menuOrder: 2,
|
||||||
|
requiresAuth: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'disassembly-orders/:id',
|
path: 'disassembly-orders/:id',
|
||||||
name: 'AdminDisassemblyDetail',
|
name: 'AdminDisassemblyDetail',
|
||||||
|
|
@ -269,6 +283,7 @@ const routes = [
|
||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
...(window.location.hostname.indexOf('local') === -1 ? [] : permissionRoutes)
|
||||||
]//[]//
|
]//[]//
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export const useAuthStore = defineStore('auth', {
|
||||||
permissionButton: JSON.parse(localStorage.getItem('permissionButton') || '[]'),
|
permissionButton: JSON.parse(localStorage.getItem('permissionButton') || '[]'),
|
||||||
router: null,
|
router: null,
|
||||||
routesUpdated: 0,//路由更新次数
|
routesUpdated: 0,//路由更新次数
|
||||||
routerList: []//侧边栏路由
|
routerList: window.location.hostname.indexOf('local') === -1 ? [] : permissionRoutes,//侧边栏路由
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@
|
||||||
<el-option :label="t('admin.promptManagement.animal')" value="animal" />
|
<el-option :label="t('admin.promptManagement.animal')" value="animal" />
|
||||||
<el-option :label="t('admin.promptManagement.person')" value="person" />
|
<el-option :label="t('admin.promptManagement.person')" value="person" />
|
||||||
<el-option :label="t('admin.promptManagement.general')" value="general" />
|
<el-option :label="t('admin.promptManagement.general')" value="general" />
|
||||||
|
<el-option :label="t('admin.promptManagement.O1')" value="O1" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,737 @@
|
||||||
|
<template>
|
||||||
|
<div class="product-management">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>{{ t('admin.productManagement.title') }}</h2>
|
||||||
|
<el-button type="primary" @click="showAddDialog">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
{{ t('admin.productManagement.addProduct') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索和筛选区域 -->
|
||||||
|
<div class="search-filter">
|
||||||
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
|
<el-form-item :label="t('admin.productManagement.productName')">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.name"
|
||||||
|
:placeholder="t('admin.productManagement.enterProductName')"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="fetchProducts"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="fetchProducts">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
{{ t('common.search') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">
|
||||||
|
{{ t('common.reset') }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 产品列表 -->
|
||||||
|
<div class="product-list-container">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="products"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" :label="t('admin.productManagement.productId')" width="100" />
|
||||||
|
<el-table-column prop="name" :label="t('admin.productManagement.productName')" min-width="200" />
|
||||||
|
<el-table-column prop="description" :label="t('admin.productManagement.description')" min-width="250" />
|
||||||
|
<el-table-column prop="current_price" :label="t('admin.productManagement.price')" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<span v-if="scope.row.current_price">
|
||||||
|
${{ scope.row.current_price.amount }} {{ scope.row.current_price.currency }}
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="created_at" :label="t('admin.productManagement.createdAt')" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="t('common.actions')" width="380" fixed="right">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="showDetailDialog(scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon><View /></el-icon>
|
||||||
|
{{ t('common.detail') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="showEditDialog(scope.row)"
|
||||||
|
v-if="scope.row.is_delete === 0"
|
||||||
|
>
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="deleteProduct(scope.row)"
|
||||||
|
v-if="scope.row.is_delete === 0"
|
||||||
|
>
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="updateProductPrice(scope.row)"
|
||||||
|
v-if="scope.row.is_delete === 0"
|
||||||
|
>
|
||||||
|
<el-icon><Money /></el-icon>
|
||||||
|
{{ t('admin.productManagement.updatePrice') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.currentPage"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加/编辑产品弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="isEditing ? t('admin.productManagement.editProduct') : t('admin.productManagement.addProduct')"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-form :model="formData" label-width="100px" class="product-form">
|
||||||
|
<el-form-item :label="t('admin.productManagement.productName')" required>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.name"
|
||||||
|
:placeholder="t('admin.productManagement.enterProductName')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="t('admin.productManagement.productImage')">
|
||||||
|
<div class="image-upload-container">
|
||||||
|
<el-upload
|
||||||
|
class="image-uploader"
|
||||||
|
:show-file-list="false"
|
||||||
|
:before-upload="beforeImageUpload"
|
||||||
|
:on-change="handleImageChange"
|
||||||
|
accept="image/*"
|
||||||
|
>
|
||||||
|
<img v-if="formData.image" :src="formData.image" class="uploaded-image" />
|
||||||
|
<el-icon v-else class="uploader-icon"><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<div class="image-upload-tip">
|
||||||
|
{{ t('admin.productManagement.imageUploadTip') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="t('admin.productManagement.description')" required>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
:placeholder="t('admin.productManagement.enterDescription')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="!isEditing" :label="t('admin.productManagement.amount')" required>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.amount"
|
||||||
|
type="number"
|
||||||
|
:placeholder="t('admin.productManagement.enterAmount')"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="!isEditing" :label="t('admin.productManagement.currency')" required>
|
||||||
|
<el-select
|
||||||
|
v-model="formData.currency"
|
||||||
|
:placeholder="t('admin.productManagement.selectCurrency')"
|
||||||
|
>
|
||||||
|
<el-option label="USD" value="USD" />
|
||||||
|
<el-option label="EUR" value="EUR" />
|
||||||
|
<el-option label="GBP" value="GBP" />
|
||||||
|
<el-option label="CNY" value="CNY" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="saveProduct">{{ t('common.save') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 产品详情弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
:title="t('admin.productManagement.productDetail')"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<div v-if="selectedProduct" class="product-detail">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item :label="t('admin.productManagement.productId')">
|
||||||
|
{{ selectedProduct.id }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="t('admin.productManagement.productName')">
|
||||||
|
{{ selectedProduct.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="t('admin.productManagement.productImage')">
|
||||||
|
<img v-if="selectedProduct?.product_info?.image" :src="selectedProduct?.product_info?.image" class="detail-image" />
|
||||||
|
<span v-else>{{ t('admin.productManagement.noImage') }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="t('admin.productManagement.description')">
|
||||||
|
{{ selectedProduct.description }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="t('admin.productManagement.price')">
|
||||||
|
${{ selectedProduct.current_price.amount }} {{ selectedProduct.current_price.currency }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :label="t('admin.productManagement.createdAt')">
|
||||||
|
{{ formatDate(selectedProduct.created_at) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="t('admin.productManagement.updatedAt')">
|
||||||
|
{{ formatDate(selectedProduct.updated_at) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
<el-skeleton v-else :rows="8" animated />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="detailVisible = false">{{ t('common.close') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 更新产品价格弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="priceDialogVisible"
|
||||||
|
:title="t('admin.productManagement.updatePrice')"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<el-form :model="priceForm" label-width="100px" class="price-form">
|
||||||
|
<el-form-item :label="t('admin.productManagement.productName')">
|
||||||
|
<el-input v-model="priceForm.productName" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="t('admin.productManagement.currentPrice')">
|
||||||
|
<el-input v-model="priceForm.currentPrice" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="t('admin.productManagement.newAmount')" required>
|
||||||
|
<el-input
|
||||||
|
v-model="priceForm.amount"
|
||||||
|
type="number"
|
||||||
|
:placeholder="t('admin.productManagement.enterNewAmount')"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="t('admin.productManagement.currency')" required>
|
||||||
|
<el-select
|
||||||
|
v-model="priceForm.currency"
|
||||||
|
:placeholder="t('admin.productManagement.selectCurrency')"
|
||||||
|
>
|
||||||
|
<el-option label="USD" value="USD" />
|
||||||
|
<el-option label="EUR" value="EUR" />
|
||||||
|
<el-option label="GBP" value="GBP" />
|
||||||
|
<el-option label="CNY" value="CNY" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="priceDialogVisible = false">{{ t('common.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="saveProductPrice">{{ t('common.save') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Plus, Search, Edit, Delete, View, Money } from '@element-plus/icons-vue'
|
||||||
|
import { ProductManagement } from './index.js'
|
||||||
|
import { FileServer } from '@deotaland/utils'
|
||||||
|
const { t } = useI18n()
|
||||||
|
const fileServer = new FileServer()
|
||||||
|
// 创建API实例
|
||||||
|
const productApi = new ProductManagement()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const priceDialogVisible = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const products = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const selectedProduct = ref(null)
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = ref({
|
||||||
|
name: '',
|
||||||
|
is_delete: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = ref({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
amount: 0,
|
||||||
|
currency: 'USD',
|
||||||
|
current_price_id: '',
|
||||||
|
image: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 价格表单
|
||||||
|
const priceForm = ref({
|
||||||
|
productId: '',
|
||||||
|
productName: '',
|
||||||
|
currentPrice: '',
|
||||||
|
amount: 0,
|
||||||
|
currency: 'USD'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取产品列表
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const params = {
|
||||||
|
page: pagination.value.currentPage,
|
||||||
|
page_size: pagination.value.pageSize,
|
||||||
|
name: searchForm.value.name,
|
||||||
|
is_delete: searchForm.value.is_delete === '' ? undefined : searchForm.value.is_delete
|
||||||
|
}
|
||||||
|
const response = await productApi.getProductList(params)
|
||||||
|
if (response.success && response.data) {
|
||||||
|
products.value = response.data.items || []
|
||||||
|
total.value = response.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取产品列表失败:', error)
|
||||||
|
ElMessage.error(t('admin.productManagement.fetchFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
fetchProducts()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示添加弹窗
|
||||||
|
const showAddDialog = () => {
|
||||||
|
isEditing.value = false
|
||||||
|
formData.value = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
amount: 0,
|
||||||
|
currency: 'USD',
|
||||||
|
current_price_id: '',
|
||||||
|
image: ''
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑弹窗
|
||||||
|
const showEditDialog = async (product) => {
|
||||||
|
let response = await productApi.getProductDetail({ id: product.id })
|
||||||
|
response = response.data || {}
|
||||||
|
isEditing.value = true
|
||||||
|
formData.value = {
|
||||||
|
id: product.id,
|
||||||
|
name: product.name,
|
||||||
|
description: product.description,
|
||||||
|
amount: product.amount,
|
||||||
|
currency: product.currency,
|
||||||
|
current_price_id: product.current_price_id || '',
|
||||||
|
image: response.product_info?.image || ''
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示详情弹窗
|
||||||
|
const showDetailDialog = async (product) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await productApi.getProductDetail({ id: product.id })
|
||||||
|
if (response.success && response.data) {
|
||||||
|
selectedProduct.value = response.data
|
||||||
|
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取产品详情失败:', error)
|
||||||
|
ElMessage.error(t('admin.productManagement.detailFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示更新价格弹窗
|
||||||
|
const updateProductPrice = (product) => {
|
||||||
|
priceForm.value = {
|
||||||
|
productId: product.id,
|
||||||
|
productName: product.name,
|
||||||
|
currentPrice: `$${product.current_price.amount} ${product.current_price.currency}`,
|
||||||
|
amount: product.current_price.amount,
|
||||||
|
currency: product.current_price.currency
|
||||||
|
}
|
||||||
|
priceDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存产品
|
||||||
|
const saveProduct = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
if (isEditing.value) {
|
||||||
|
// 编辑现有产品
|
||||||
|
await productApi.updateProduct(formData.value)
|
||||||
|
ElMessage.success(t('admin.productManagement.updateSuccess'))
|
||||||
|
} else {
|
||||||
|
// 添加新产品
|
||||||
|
await productApi.createProduct(formData.value)
|
||||||
|
ElMessage.success(t('admin.productManagement.addSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 重新获取数据
|
||||||
|
await fetchProducts()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存产品失败:', error)
|
||||||
|
ElMessage.error(isEditing.value ? t('admin.productManagement.updateFailed') : t('admin.productManagement.addFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存产品价格
|
||||||
|
const saveProductPrice = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const params = {
|
||||||
|
product_id: priceForm.value.productId,
|
||||||
|
amount: priceForm.value.amount,
|
||||||
|
currency: priceForm.value.currency
|
||||||
|
}
|
||||||
|
await productApi.updateProductPrice(params)
|
||||||
|
ElMessage.success(t('admin.productManagement.priceUpdateSuccess'))
|
||||||
|
priceDialogVisible.value = false
|
||||||
|
// 重新获取数据
|
||||||
|
await fetchProducts()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新产品价格失败:', error)
|
||||||
|
ElMessage.error(t('admin.productManagement.priceUpdateFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除产品
|
||||||
|
const deleteProduct = (product) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
t('admin.productManagement.deleteConfirm'),
|
||||||
|
t('common.confirm'),
|
||||||
|
{
|
||||||
|
confirmButtonText: t('common.confirm'),
|
||||||
|
cancelButtonText: t('common.cancel'),
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
).then(async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await productApi.deleteProduct({ id: product.id })
|
||||||
|
ElMessage.success(t('admin.productManagement.deleteSuccess'))
|
||||||
|
// 重新获取数据
|
||||||
|
await fetchProducts()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除产品失败:', error)
|
||||||
|
ElMessage.error(t('admin.productManagement.deleteFailed'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// 用户取消删除
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
searchForm.value = {
|
||||||
|
name: '',
|
||||||
|
is_delete: ''
|
||||||
|
}
|
||||||
|
pagination.value.currentPage = 1
|
||||||
|
fetchProducts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.value.pageSize = size
|
||||||
|
fetchProducts()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (current) => {
|
||||||
|
pagination.value.currentPage = current
|
||||||
|
fetchProducts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return ''
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片上传前的验证
|
||||||
|
const beforeImageUpload = (file) => {
|
||||||
|
const isImage = file.type.startsWith('image/')
|
||||||
|
const isLt5M = file.size / 1024 / 1024 < 5
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error(t('admin.productManagement.imageTypeError'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!isLt5M) {
|
||||||
|
ElMessage.error(t('admin.productManagement.imageSizeError'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图片选择和上传
|
||||||
|
const handleImageChange = (file) => {
|
||||||
|
if (!beforeImageUpload(file.raw)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const imageUrl = e.target.result
|
||||||
|
fileServer.uploadFile(imageUrl).then((url) => {
|
||||||
|
formData.value.image = url
|
||||||
|
ElMessage.success(t('admin.productManagement.imageUploadSuccess'))
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('图片上传失败:', error)
|
||||||
|
ElMessage.error(t('admin.productManagement.imageUploadFailed'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file.raw)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.product-management {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面头部 */
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索和筛选区域 */
|
||||||
|
.search-filter {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 产品列表容器 */
|
||||||
|
.product-list-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格图片样式 */
|
||||||
|
.table-image {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-image-text {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作列按钮样式 */
|
||||||
|
.product-list-container :deep(.el-table-fixed-column--right .cell) {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-list-container :deep(.el-table-fixed-column--right .el-button) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 5px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 产品详情 */
|
||||||
|
.product-detail {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-image {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片上传容器 */
|
||||||
|
.image-upload-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-uploader {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-uploader :deep(.el-upload) {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
border: 2px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-uploader :deep(.el-upload:hover) {
|
||||||
|
border-color: #6B46C1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #8c939d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.product-management {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-list-container {
|
||||||
|
padding: 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.search-form {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { requestUtils, adminApi } from '@deotaland/utils';
|
||||||
|
export class ProductManagement {
|
||||||
|
//创建产品及价格
|
||||||
|
async createProduct(data) {
|
||||||
|
let params = {
|
||||||
|
name: data.name,//产品名称
|
||||||
|
description: data.description,//产品描述
|
||||||
|
amount: data.amount,//金额,单位为美元
|
||||||
|
currency: data.currency,//货币类型,默认USD
|
||||||
|
product_info:{
|
||||||
|
image:data.image,//产品图片
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await requestUtils.common(adminApi.default.createProduct, params);
|
||||||
|
}
|
||||||
|
//获取产品详情
|
||||||
|
async getProductDetail(data) {
|
||||||
|
let params = {
|
||||||
|
id: data.id,
|
||||||
|
}
|
||||||
|
return await requestUtils.common(adminApi.default.getProductDetail, params);
|
||||||
|
}
|
||||||
|
//获取产品列表
|
||||||
|
async getProductList(data) {
|
||||||
|
let params = {
|
||||||
|
"page": data.page,//页码
|
||||||
|
"page_size": data.page_size,//每页数量
|
||||||
|
"name": data.name,//产品名称
|
||||||
|
"is_delete": data.is_delete,//是否删除
|
||||||
|
}
|
||||||
|
return await requestUtils.common(adminApi.default.getProductList, params);
|
||||||
|
}
|
||||||
|
//更新产品信息
|
||||||
|
async updateProduct(data) {
|
||||||
|
let params = {
|
||||||
|
"id": data.id,//产品ID
|
||||||
|
"name": data.name,//产品名称
|
||||||
|
"description": data.description,//产品描述
|
||||||
|
"current_price_id": data.current_price_id,//当前价格ID
|
||||||
|
product_info:{
|
||||||
|
image:data.image,//产品图片
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await requestUtils.common(adminApi.default.updateProduct, params);
|
||||||
|
}
|
||||||
|
//更新产品价格
|
||||||
|
async updateProductPrice(data) {
|
||||||
|
let params = {
|
||||||
|
"product_id": data.product_id,//产品ID
|
||||||
|
"amount": data.amount,//金额,单位为美元
|
||||||
|
"currency": data.currency,//货币类型,默认USD
|
||||||
|
}
|
||||||
|
return await requestUtils.common(adminApi.default.updateProductPrice, params);
|
||||||
|
}
|
||||||
|
//删除产品
|
||||||
|
async deleteProduct(data){
|
||||||
|
let params = {
|
||||||
|
"id": data.id,//产品ID
|
||||||
|
}
|
||||||
|
return await requestUtils.common(adminApi.default.deleteProduct, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,8 +68,7 @@ import { Picture, MagicStick, ArrowLeft, Edit, Check, Guide } from '@element-plu
|
||||||
import { ElButton, ElIcon, ElInput } from 'element-plus'
|
import { ElButton, ElIcon, ElInput } from 'element-plus'
|
||||||
import ThemeToggle from '../ui/ThemeToggle.vue'
|
import ThemeToggle from '../ui/ThemeToggle.vue'
|
||||||
import LanguageToggle from '../ui/LanguageToggle.vue'
|
import LanguageToggle from '../ui/LanguageToggle.vue'
|
||||||
|
const emit = defineEmits(['openGuideModal','back'])
|
||||||
const emit = defineEmits(['openGuideModal'])
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
total_score: {
|
total_score: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
|
@ -92,12 +91,13 @@ const { t, locale } = useI18n()
|
||||||
// 处理返回按钮点击
|
// 处理返回按钮点击
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
// 返回上一页或首页
|
// 返回上一页或首页
|
||||||
if (window.history.length > 1) {
|
// if (window.history.length > 1) {
|
||||||
window.history.back()
|
// window.history.back()
|
||||||
} else {
|
// } else {
|
||||||
// 如果没有历史记录,则跳转到首页
|
// // 如果没有历史记录,则跳转到首页
|
||||||
window.location.href = '/'
|
// window.location.href = '/'
|
||||||
}
|
// }
|
||||||
|
emit('back')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始编辑项目名称
|
// 开始编辑项目名称
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,10 @@
|
||||||
<div class="product-info">
|
<div class="product-info">
|
||||||
<h1 class="product-title">{{ $t('checkout.customModel') }}</h1>
|
<h1 class="product-title">{{ $t('checkout.customModel') }}</h1>
|
||||||
<div class="price-info">
|
<div class="price-info">
|
||||||
<span class="price">{{ $t('checkout.from') }} ${{ (amountCents ).toFixed(2) }}</span>
|
<span class="price">{{ $t('checkout.from') }} {{unt=='USD'?'$':unt}} {{ (amountCents ).toFixed(2) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Content Sections -->
|
<!-- Content Sections -->
|
||||||
<div class="content-sections">
|
<div class="content-sections">
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
|
|
@ -178,10 +177,12 @@ import StripePaymentForm from '@/components/StripePaymentForm.vue'
|
||||||
import { Country, State } from 'country-state-city'
|
import { Country, State } from 'country-state-city'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { PayServer } from '@deotaland/utils'
|
import { PayServer } from '@deotaland/utils'
|
||||||
|
import { requestUtils,clientApi } from '@deotaland/utils'
|
||||||
const payserver = new PayServer();
|
const payserver = new PayServer();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelData: { type: Object, default: () => ({}) },
|
modelData: { type: Object, default: () => ({}) },
|
||||||
show: { type: Boolean, default: false }
|
show: { type: Boolean, default: false },
|
||||||
|
series: { type: String, default: '' }
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
const onClose = () => emit('close')
|
const onClose = () => emit('close')
|
||||||
|
|
@ -198,7 +199,7 @@ const showPayingOverlay = ref(false)
|
||||||
|
|
||||||
// 省州映射数据已移至国际化文件
|
// 省州映射数据已移至国际化文件
|
||||||
const amountCents = computed(() => {
|
const amountCents = computed(() => {
|
||||||
let base = 299 // 默认价格,移除了尺寸相关定价
|
let base = price.value // 默认价格,移除了尺寸相关定价
|
||||||
if (addons.value.gloss) base += 300
|
if (addons.value.gloss) base += 300
|
||||||
if (addons.value.base) base += 400
|
if (addons.value.base) base += 400
|
||||||
if (addons.value.matte) base += 200
|
if (addons.value.matte) base += 200
|
||||||
|
|
@ -219,6 +220,22 @@ const isPayButtonDisabled = computed(() => {
|
||||||
ipName.value.trim()
|
ipName.value.trim()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
const unt = ref('');
|
||||||
|
const price = ref(0);
|
||||||
|
const seriesId = ref('');
|
||||||
|
//获取对应价格
|
||||||
|
const getPrice = async () => {
|
||||||
|
const res = await requestUtils.common(clientApi.default.getProductList)
|
||||||
|
if(res.code === 0){
|
||||||
|
const data = res.data.list || []
|
||||||
|
const item = data.find(item => item.name === props.series)
|
||||||
|
if(item){
|
||||||
|
price.value = item.price?.amount || 0
|
||||||
|
unt.value = item.price?.currency || ''
|
||||||
|
seriesId.value = item.id || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const incQty = () => { qty.value = Math.min(qty.value+1, 99) }
|
const incQty = () => { qty.value = Math.min(qty.value+1, 99) }
|
||||||
const decQty = () => { qty.value = Math.max(qty.value-1, 1) }
|
const decQty = () => { qty.value = Math.max(qty.value-1, 1) }
|
||||||
const goShopify = () => {//用户点击购买
|
const goShopify = () => {//用户点击购买
|
||||||
|
|
@ -265,6 +282,8 @@ const goShopify = () => {//用户点击购买
|
||||||
// Save shipping and contact information if checkbox is checked
|
// Save shipping and contact information if checkbox is checked
|
||||||
saveLocal()
|
saveLocal()
|
||||||
// 在控制台打印整理后的信息
|
// 在控制台打印整理后的信息
|
||||||
|
console.log('Order Parameters:', params)
|
||||||
|
params.product_id = seriesId.value
|
||||||
payserver.createPayorOrder(params);
|
payserver.createPayorOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,6 +317,7 @@ onMounted(() => {
|
||||||
if (c) Object.assign(contact.value, JSON.parse(c))
|
if (c) Object.assign(contact.value, JSON.parse(c))
|
||||||
updateCountryOptions()
|
updateCountryOptions()
|
||||||
updateStates()
|
updateStates()
|
||||||
|
getPrice();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})
|
})
|
||||||
watch(() => shipping.value.country, () => { updateStates() })
|
watch(() => shipping.value.country, () => { updateStates() })
|
||||||
|
|
|
||||||
|
|
@ -12,26 +12,26 @@
|
||||||
<div class="series-selector-content">
|
<div class="series-selector-content">
|
||||||
<!-- 系列选项列表 -->
|
<!-- 系列选项列表 -->
|
||||||
<div class="series-list">
|
<div class="series-list">
|
||||||
<!-- Done 系列 -->
|
|
||||||
<div
|
<div
|
||||||
|
v-for="series in seriesList"
|
||||||
|
:key="series.id"
|
||||||
class="series-item"
|
class="series-item"
|
||||||
@click="selectSeries('Done')"
|
@click="selectSeries(series)"
|
||||||
>
|
>
|
||||||
<div class="series-image">
|
<div class="series-image">
|
||||||
<img src="@/assets/xh/Done.webp" alt="Done" />
|
<img
|
||||||
|
:src="series.product_info?.image || ''"
|
||||||
|
:alt="series.name"
|
||||||
|
@error="handleImageError"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="series-name">Done</div>
|
<div class="series-info">
|
||||||
|
<div class="series-name">{{ series.name }}</div>
|
||||||
|
<div class="series-description">{{ series.description }}</div>
|
||||||
|
<div class="series-price">
|
||||||
|
{{ formatPrice(series.price?.amount, series.price?.currency) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Oone 系列 -->
|
|
||||||
<div
|
|
||||||
class="series-item"
|
|
||||||
@click="selectSeries('Oone')"
|
|
||||||
>
|
|
||||||
<div class="series-image">
|
|
||||||
<img src="@/assets/xh/Oone.webp" alt="Oone" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="series-name">Oone</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -41,9 +41,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits } from 'vue'
|
import { defineProps, defineEmits,onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {clientApi,requestUtils} from '@deotaland/utils';
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -62,6 +62,31 @@ const selectSeries = (series) => {
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
emit('cancel')
|
emit('cancel')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatPrice = (amount, currency = 'USD') => {
|
||||||
|
if (amount === null || amount === undefined) return ''
|
||||||
|
return `$${amount.toFixed(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageError = (event) => {
|
||||||
|
event.target.src = ''
|
||||||
|
event.target.parentElement.style.background = 'linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%)'
|
||||||
|
}
|
||||||
|
const seriesList = ref([])
|
||||||
|
//获取系列数据
|
||||||
|
const getSeriesList = async ()=>{
|
||||||
|
const res = await requestUtils.common(clientApi.default.getProductList)
|
||||||
|
if(res.code === 0){
|
||||||
|
const data = res.data || []
|
||||||
|
seriesList.value = data.list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const init = ()=>{
|
||||||
|
getSeriesList();
|
||||||
|
}
|
||||||
|
onMounted(()=>{
|
||||||
|
init()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -228,25 +253,34 @@ const onCancel = () => {
|
||||||
|
|
||||||
.series-image {
|
.series-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 140px;
|
height: 180px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-image img {
|
.series-image img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: contain;
|
||||||
transition: transform 0.3s ease-out;
|
transition: transform 0.3s ease-out;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-item:hover .series-image img {
|
.series-item:hover .series-image img {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.series-info {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.series-name {
|
.series-name {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
@ -254,6 +288,7 @@ const onCancel = () => {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: color 0.2s ease-out;
|
transition: color 0.2s ease-out;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-item:hover .series-name {
|
.series-item:hover .series-name {
|
||||||
|
|
@ -264,6 +299,25 @@ const onCancel = () => {
|
||||||
color: #6B46C1;
|
color: #6B46C1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.series-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-price {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6B46C1;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.series-actions {
|
.series-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
@ -320,11 +374,20 @@ const onCancel = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-image {
|
.series-image {
|
||||||
height: 100px;
|
height: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-name {
|
.series-name {
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-description {
|
||||||
|
font-size: 13px;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-price {
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-actions {
|
.series-actions {
|
||||||
|
|
@ -345,7 +408,11 @@ const onCancel = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-image {
|
.series-image {
|
||||||
height: 120px;
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-description {
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -415,6 +415,12 @@ export default {
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-menu-button svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-menu-button:hover {
|
.mobile-menu-button:hover {
|
||||||
background: var(--hover-bg, #f3f4f6);
|
background: var(--hover-bg, #f3f4f6);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,8 @@ import {
|
||||||
DataAnalysis as AnalyticsIcon,
|
DataAnalysis as AnalyticsIcon,
|
||||||
Folder as ProjectIcon,
|
Folder as ProjectIcon,
|
||||||
Bell as NotificationIcon,
|
Bell as NotificationIcon,
|
||||||
Key as ApiIcon
|
Key as ApiIcon,
|
||||||
|
ShoppingCartFull
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
// 定义组件名称(用于调试和递归组件)
|
// 定义组件名称(用于调试和递归组件)
|
||||||
|
|
@ -112,10 +113,11 @@ const remainingPoints = ref(1280)
|
||||||
const currentUser = computed(() => authStore.user)
|
const currentUser = computed(() => authStore.user)
|
||||||
const userRole = computed(() => ({
|
const userRole = computed(() => ({
|
||||||
'1': 'free',
|
'1': 'free',
|
||||||
'2': 'creator',
|
'2':'creator'
|
||||||
}[authStore.user?.user_role || '1'])) // 可切换为 'free' 测试不同样式
|
}[authStore.user?.user_role || authStore.user?.userRole||'1'])) // 可切换为 'free' 测试不同样式
|
||||||
const sidebarClasses = computed(() => ({
|
const sidebarClasses = computed(() => ({
|
||||||
'sidebar-mobile': isMobile.value
|
'sidebar-mobile': isMobile.value,
|
||||||
|
'show': isMobile.value && !props.collapsed
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 核心菜单项 (6个主要功能)
|
// 核心菜单项 (6个主要功能)
|
||||||
|
|
@ -208,7 +210,7 @@ onUnmounted(() => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--sidebar-bg, #ffffff);
|
background: var(--sidebar-bg, #ffffff);
|
||||||
border-right: 1px solid var(--border-color, #e5e7eb);
|
border-right: 1px solid var(--border-color, #e5e7eb);
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
/* transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); */
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
@ -640,12 +642,14 @@ onUnmounted(() => {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
box-shadow: 4px 0 24px rgba(107, 70, 193, 0.2);
|
box-shadow: 4px 0 24px rgba(107, 70, 193, 0.2);
|
||||||
transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
/* transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); */
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
border-right: 1px solid rgba(107, 70, 193, 0.1);
|
border-right: 1px solid rgba(107, 70, 193, 0.1);
|
||||||
|
padding-top: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-mobile.show {
|
.sidebar-mobile.show {
|
||||||
|
|
@ -683,28 +687,53 @@ onUnmounted(() => {
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.app-sidebar {
|
.app-sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 320px;
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
padding: 16px 8px;
|
||||||
|
height: calc(100vh - 64px);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
padding: 16px 20px;
|
/* padding: 12px 8px; */
|
||||||
min-height: 72px;
|
min-height: 50px;
|
||||||
margin: 8px 16px;
|
/* margin: 6px 4px; */
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
max-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-icon {
|
.nav-icon {
|
||||||
width: 32px;
|
width: 24px;
|
||||||
height: 32px;
|
height: 24px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-icon svg {
|
.nav-icon svg {
|
||||||
width: 22px;
|
width: 20px;
|
||||||
height: 22px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-text {
|
.nav-text {
|
||||||
font-size: 15px;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-container {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
<!-- 侧边栏容器 -->
|
<!-- 侧边栏容器 -->
|
||||||
<aside
|
<aside
|
||||||
class="sidebar-container"
|
class="sidebar-container"
|
||||||
|
:class="{ 'sidebar-visible': sidebarVisible }"
|
||||||
>
|
>
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
:collapsed="!sidebarVisible"
|
:collapsed="!sidebarVisible"
|
||||||
|
|
@ -219,7 +220,7 @@ watch(() => window.location.pathname, () => {
|
||||||
height: 64px;
|
height: 64px;
|
||||||
background: var(--header-bg, #ffffff);
|
background: var(--header-bg, #ffffff);
|
||||||
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
||||||
z-index: 9999;
|
z-index: 200;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -241,14 +242,15 @@ watch(() => window.location.pathname, () => {
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: -120px;
|
/* left: -120px; */
|
||||||
transform: translateX(0);
|
transform: translateX(-100%);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container.sidebar-visible {
|
.sidebar-container.sidebar-visible {
|
||||||
left: 0;
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const List = () => import('../views/List.vue')
|
||||||
const Login = () => import('../views/Login/Login.vue')
|
const Login = () => import('../views/Login/Login.vue')
|
||||||
const Register = () => import('../views/Register.vue')
|
const Register = () => import('../views/Register.vue')
|
||||||
const ForgotPassword = () => import('../views/ForgotPassword.vue')
|
const ForgotPassword = () => import('../views/ForgotPassword.vue')
|
||||||
const CreationWorkspace = () => import('../views/CreationWorkspace.vue')
|
const CreationWorkspace = () => import('../views/CreationWorkspace/CreationWorkspace.vue')
|
||||||
const ProjectGallery = () => import('../views/ProjectGallery.vue')
|
const ProjectGallery = () => import('../views/ProjectGallery.vue')
|
||||||
const OrderManagement = () => import('../views/OrderManagement/OrderManagement.vue')
|
const OrderManagement = () => import('../views/OrderManagement/OrderManagement.vue')
|
||||||
const OrderDetail = () => import('../views/OrderDetail.vue')
|
const OrderDetail = () => import('../views/OrderDetail.vue')
|
||||||
|
|
|
||||||
|
|
@ -144,8 +144,8 @@ import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Plus as PlusIcon, Folder as FolderIcon, Edit as EditIcon, View as ViewIcon, Close as CloseIcon, Upload as UploadIcon, Delete as DeleteIcon } from '@element-plus/icons-vue'
|
import { Plus as PlusIcon, Folder as FolderIcon, Edit as EditIcon, View as ViewIcon, Close as CloseIcon, Upload as UploadIcon, Delete as DeleteIcon } from '@element-plus/icons-vue'
|
||||||
import SeriesSelector from '../components/SeriesSelector.vue'
|
import SeriesSelector from '../../components/SeriesSelector.vue'
|
||||||
import {Project} from './Project/index'
|
import {Project} from '../Project/index'
|
||||||
import { dateUtils } from '@deotaland/utils'
|
import { dateUtils } from '@deotaland/utils'
|
||||||
|
|
||||||
const { formatDate } = dateUtils
|
const { formatDate } = dateUtils
|
||||||
|
|
@ -239,17 +239,16 @@ const handleFileSelect = (event) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('上传失败:', error)
|
console.error('上传失败:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
event.target.value = ''
|
event.target.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const openProject = (project) => {
|
const openProject = (project) => {
|
||||||
console.log('打开项目:', project.title)
|
|
||||||
router.push(`/project/${project.id}/${project.tags[0]}`)
|
router.push(`/project/${project.id}/${project.tags[0]}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createNewProject = (type) => {
|
const createNewProject = (series) => {
|
||||||
router.push(`/project/new/${type}`)
|
console.log(series,'seriesseriesseries');
|
||||||
|
router.push(`/project/new/${series.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理系列选择确认
|
// 处理系列选择确认
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class CreationWorkspace {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
type="primary"
|
type="primary"
|
||||||
size="large"
|
size="large"
|
||||||
class="action-btn primary-btn create-btn-large"
|
class="action-btn primary-btn create-btn-large"
|
||||||
@click="navigateToFeature({ path: '/creation-workspace' })">
|
@click="navigateToFeature({ path: `/project/new/Done` })">
|
||||||
{{ t('home.welcome.startCreating') }}
|
{{ t('home.welcome.startCreating') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<div v-else class="guest-actions">
|
<div v-else class="guest-actions">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="creative-zone" @contextmenu.prevent>
|
<div class="creative-zone" @contextmenu.prevent>
|
||||||
<!-- 顶部固定头部组件 -->
|
<!-- 顶部固定头部组件 -->
|
||||||
<div class="header-wrapper">
|
<div class="header-wrapper">
|
||||||
<HeaderComponent :total_score="total_score" :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
|
<HeaderComponent @back="handleBack" :total_score="total_score" :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 导入的侧边栏组件 -->
|
<!-- 导入的侧边栏组件 -->
|
||||||
<div class="sidebar-container">
|
<div class="sidebar-container">
|
||||||
|
|
@ -143,6 +143,7 @@
|
||||||
@close="showOrderProcessModal=false"
|
@close="showOrderProcessModal=false"
|
||||||
@acknowledge="handleBuyFromCustomize" />
|
@acknowledge="handleBuyFromCustomize" />
|
||||||
<PurchaseModal
|
<PurchaseModal
|
||||||
|
:series="series"
|
||||||
:show="showPurchaseModal"
|
:show="showPurchaseModal"
|
||||||
:modelData="CustomizeModalData"
|
:modelData="CustomizeModalData"
|
||||||
@close="showPurchaseModal=false" />
|
@close="showPurchaseModal=false" />
|
||||||
|
|
@ -164,15 +165,10 @@ import {useRoute,useRouter} from 'vue-router';
|
||||||
import {MeshyServer,GiminiServer,FileServer} from '@deotaland/utils';
|
import {MeshyServer,GiminiServer,FileServer} from '@deotaland/utils';
|
||||||
import OrderProcessModal from '../../components/OrderProcessModal/index.vue';
|
import OrderProcessModal from '../../components/OrderProcessModal/index.vue';
|
||||||
import PurchaseModal from '../../components/PurchaseModal/index.vue';
|
import PurchaseModal from '../../components/PurchaseModal/index.vue';
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import {Project} from './index';
|
import {Project} from './index';
|
||||||
import {ModernHome} from '../ModernHome/index.js'
|
import {ModernHome} from '../ModernHome/index.js'
|
||||||
const fileServer = new FileServer();
|
const fileServer = new FileServer();
|
||||||
const modernHome = new ModernHome();
|
const modernHome = new ModernHome();
|
||||||
const Limits = ref({
|
|
||||||
generateCount: 0,
|
|
||||||
modelCount: 0,
|
|
||||||
})
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const PluginProject = new Project();
|
const PluginProject = new Project();
|
||||||
// 弹窗相关状态
|
// 弹窗相关状态
|
||||||
|
|
@ -236,13 +232,15 @@ const getMaxZIndex = (type)=>{
|
||||||
getMaxZIndexNum.value = cards.value.reduce((max, card) => Math.max(max, card.zIndex || 0), 0)+1;
|
getMaxZIndexNum.value = cards.value.reduce((max, card) => Math.max(max, card.zIndex || 0), 0)+1;
|
||||||
return getMaxZIndexNum.value;
|
return getMaxZIndexNum.value;
|
||||||
}
|
}
|
||||||
|
const handleBack = ()=>{
|
||||||
|
router.replace(`/creation-workspace`);
|
||||||
|
}
|
||||||
const combinedPromptJson = ref({});
|
const combinedPromptJson = ref({});
|
||||||
//获取动态提示词
|
//获取动态提示词
|
||||||
const getCombinedPrompt = async ()=>{
|
const getCombinedPrompt = async ()=>{
|
||||||
try {
|
try {
|
||||||
const data = await PluginProject.getCombinedPrompt(series.value);
|
const data = await PluginProject.getCombinedPrompt(series.value);
|
||||||
combinedPromptJson.value = data;
|
combinedPromptJson.value = data;
|
||||||
console.log(combinedPromptJson.value);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
@ -1029,6 +1027,7 @@ const init = ()=>{
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
projectId.value = route.params.id;
|
projectId.value = route.params.id;
|
||||||
series.value = route.params.series;
|
series.value = route.params.series;
|
||||||
|
console.log(series.value);
|
||||||
if(projectId.value === 'new'){
|
if(projectId.value === 'new'){
|
||||||
createProject();
|
createProject();
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -729,18 +729,20 @@ export class Project{
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
//获取动态提示词
|
//获取动态提示词
|
||||||
async getCombinedPrompt(series){//series:项目系列Done Oone
|
async getCombinedPrompt(series){//series:项目系列D1 O1
|
||||||
try {
|
try {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const res = await requestUtils.common(clientApi.default.combined)
|
const res = await requestUtils.common(clientApi.default.combined)
|
||||||
if(res.code === 0){
|
if(res.code === 0){
|
||||||
let data = res.data;
|
let data = res.data;
|
||||||
// 如果是Oone系列,过滤掉title中包含"动物坐姿"或"人物姿势"的提示词
|
// 如果是Oone系列,过滤掉title中包含"动物坐姿"或"人物姿势"的提示词
|
||||||
if (series === 'Oone') {
|
if (series === 'O1') {
|
||||||
data = data.filter(item => {
|
data = data.filter(item => {
|
||||||
if (!item.title) return true;
|
if (!item.title) return true;
|
||||||
return !item.title.includes('动物坐姿') && !item.title.includes('人物姿势');
|
return !item.title.includes('动物坐姿') && !item.title.includes('人物姿势')&& item.type != 'D1';
|
||||||
});
|
});
|
||||||
|
}else if(series === 'D1'){// 如果是Done系列,过滤掉type为O1的提示词
|
||||||
|
data = data.filter(item => item.type !== 'O1');
|
||||||
}
|
}
|
||||||
// 初始化返回数据结构
|
// 初始化返回数据结构
|
||||||
const result = {
|
const result = {
|
||||||
|
|
@ -756,11 +758,10 @@ export class Project{
|
||||||
// 按sortOrder排序
|
// 按sortOrder排序
|
||||||
data.sort((a, b) => a.sortOrder - b.sortOrder);
|
data.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||||
// 处理person和general类型的数据
|
// 处理person和general类型的数据
|
||||||
const personAndGeneral = data.filter(item => item.type === 'person' || item.type === 'general');
|
const personAndGeneral = data.filter(item => item.type === 'person' || item.type === 'general'|| item.type === 'O1');
|
||||||
personAndGeneral.forEach(item => {
|
personAndGeneral.forEach(item => {
|
||||||
// 拼接content
|
// 拼接content
|
||||||
result.person.content += item.content;
|
result.person.content += item.content;
|
||||||
|
|
||||||
// 处理图片
|
// 处理图片
|
||||||
if (item.imageUrls) {
|
if (item.imageUrls) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -774,7 +775,7 @@ export class Project{
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 处理animal和general类型的数据
|
// 处理animal和general类型的数据
|
||||||
const animalAndGeneral = data.filter(item => item.type === 'animal' || item.type === 'general');
|
const animalAndGeneral = data.filter(item => item.type === 'animal' || item.type === 'general'|| item.type === 'O1');
|
||||||
animalAndGeneral.forEach(item => {
|
animalAndGeneral.forEach(item => {
|
||||||
// 拼接content
|
// 拼接content
|
||||||
result.animal.content += item.content;
|
result.animal.content += item.content;
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ const userController = new UserController()
|
||||||
|
|
||||||
// 响应式用户数据
|
// 响应式用户数据
|
||||||
const userData = ref({
|
const userData = ref({
|
||||||
role: authStore.user?.inviteCode === '2' ? 'creator' : 'free',
|
role: authStore.user?.userRole == '2' ? 'creator' : 'free',
|
||||||
// 基本信息
|
// 基本信息
|
||||||
avatar: authStore.user?.avatarUrl,
|
avatar: authStore.user?.avatarUrl,
|
||||||
nickname: authStore.user?.nickname,
|
nickname: authStore.user?.nickname,
|
||||||
|
|
@ -405,13 +405,46 @@ const saveNickname = () => {
|
||||||
// 复制邀请码
|
// 复制邀请码
|
||||||
const copyInviteCode = (code) => {
|
const copyInviteCode = (code) => {
|
||||||
const InviteLink = `🎉 限时福利!送你专属邀请码:${code},注册立得积分+解锁高级功能,快来一起体验吧!${window.location.origin}/#/login?inviteCode=${code}`
|
const InviteLink = `🎉 限时福利!送你专属邀请码:${code},注册立得积分+解锁高级功能,快来一起体验吧!${window.location.origin}/#/login?inviteCode=${code}`
|
||||||
|
|
||||||
|
const fallbackCopyText = (text) => {
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = text
|
||||||
|
textArea.style.position = 'fixed'
|
||||||
|
textArea.style.left = '-999999px'
|
||||||
|
textArea.style.top = '-999999px'
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.focus()
|
||||||
|
textArea.select()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const successful = document.execCommand('copy')
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
return successful
|
||||||
|
} catch (err) {
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
navigator.clipboard.writeText(InviteLink)
|
navigator.clipboard.writeText(InviteLink)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
ElMessage.success(t('userCenter.invitation.copySuccess'))
|
ElMessage.success(t('userCenter.invitation.copySuccess'))
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
if (fallbackCopyText(InviteLink)) {
|
||||||
|
ElMessage.success(t('userCenter.invitation.copySuccess'))
|
||||||
|
} else {
|
||||||
ElMessage.error(t('userCenter.invitation.copyFailed'))
|
ElMessage.error(t('userCenter.invitation.copyFailed'))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
if (fallbackCopyText(InviteLink)) {
|
||||||
|
ElMessage.success(t('userCenter.invitation.copySuccess'))
|
||||||
|
} else {
|
||||||
|
ElMessage.error(t('userCenter.invitation.copyFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页相关
|
// 分页相关
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="frame-animation"
|
||||||
|
:style="containerStyle"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="currentFrame"
|
||||||
|
:src="currentFrame"
|
||||||
|
:alt="alt"
|
||||||
|
class="frame-image"
|
||||||
|
:style="imageStyle"
|
||||||
|
@load="onImageLoad"
|
||||||
|
@error="onImageError"
|
||||||
|
/>
|
||||||
|
<div v-if="loading" class="frame-loading">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FrameAnimation'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 帧动画的图片帧数组
|
||||||
|
frames: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => [],
|
||||||
|
validator: (value) => Array.isArray(value)
|
||||||
|
},
|
||||||
|
// 每帧动画的持续时间(毫秒)
|
||||||
|
duration: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
validator: (value) => value > 0
|
||||||
|
},
|
||||||
|
// 容器宽度
|
||||||
|
width: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 'auto'
|
||||||
|
},
|
||||||
|
// 容器高度
|
||||||
|
height: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 'auto'
|
||||||
|
},
|
||||||
|
// 图片的替代文本
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
default: 'frame animation'
|
||||||
|
},
|
||||||
|
// 是否自动播放
|
||||||
|
autoplay: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否循环播放
|
||||||
|
loop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 图片填充方式
|
||||||
|
objectFit: {
|
||||||
|
type: String,
|
||||||
|
default: 'contain',
|
||||||
|
validator: (value) => ['fill', 'contain', 'cover', 'none', 'scale-down'].includes(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['frame-change', 'animation-end', 'error'])
|
||||||
|
|
||||||
|
const currentIndex = ref(0)
|
||||||
|
const loading = ref(true)
|
||||||
|
const timer = ref(null)
|
||||||
|
const isPlaying = ref(props.autoplay)
|
||||||
|
|
||||||
|
const currentFrame = computed(() => {
|
||||||
|
if (props.frames.length === 0) return null
|
||||||
|
return props.frames[currentIndex.value]
|
||||||
|
})
|
||||||
|
|
||||||
|
const containerStyle = computed(() => {
|
||||||
|
const width = typeof props.width === 'number' ? `${props.width}px` : props.width
|
||||||
|
const height = typeof props.height === 'number' ? `${props.height}px` : props.height
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const imageStyle = computed(() => ({
|
||||||
|
objectFit: props.objectFit
|
||||||
|
}))
|
||||||
|
|
||||||
|
const play = () => {
|
||||||
|
if (timer.value) return
|
||||||
|
isPlaying.value = true
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
if (currentIndex.value < props.frames.length - 1) {
|
||||||
|
currentIndex.value++
|
||||||
|
} else if (props.loop) {
|
||||||
|
currentIndex.value = 0
|
||||||
|
} else {
|
||||||
|
pause()
|
||||||
|
emit('animation-end')
|
||||||
|
}
|
||||||
|
emit('frame-change', currentIndex.value, props.frames[currentIndex.value])
|
||||||
|
}, props.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pause = () => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
isPlaying.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
currentIndex.value = 0
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onImageLoad = () => {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onImageError = (e) => {
|
||||||
|
loading.value = false
|
||||||
|
emit('error', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.frames, () => {
|
||||||
|
reset()
|
||||||
|
if (props.autoplay) {
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(() => props.duration, () => {
|
||||||
|
if (isPlaying.value) {
|
||||||
|
pause()
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.autoplay, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
play()
|
||||||
|
} else {
|
||||||
|
pause()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.autoplay && props.frames.length > 0) {
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
pause()
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
reset
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.frame-animation {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--frame-animation-bg, transparent);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
transition: opacity 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(107, 70, 193, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid rgba(107, 70, 193, 0.2);
|
||||||
|
border-top-color: #6B46C1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .frame-loading {
|
||||||
|
background: rgba(167, 139, 250, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .loading-spinner {
|
||||||
|
border-color: rgba(167, 139, 250, 0.2);
|
||||||
|
border-top-color: #A78BFA;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,12 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': true }">
|
<div v-if="Show" class="sidebar-overlay" :class="{ 'sidebar-overlay-active': true }">
|
||||||
|
<FrameAnimation :frames="ipRunImgs" :duration="50" :loop="true" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, onMounted,onUnmounted } from 'vue'
|
||||||
name: 'LoadingCom',
|
import FrameAnimation from '../FrameAnimation/index.vue'
|
||||||
}
|
var timer = null;
|
||||||
|
const ipRunImgs = Object.values(import.meta.glob('../../image/iprun/*.webp', { eager: true, as: 'url' })).sort((a, b) => {
|
||||||
|
const numA = parseInt(a.match(/(\d+)\.webp$/)?.[1] || 0)
|
||||||
|
const numB = parseInt(b.match(/(\d+)\.webp$/)?.[1] || 0)
|
||||||
|
return numA - numB
|
||||||
|
})
|
||||||
|
const Show = ref(false)
|
||||||
|
onMounted(()=>{
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
Show.value = true
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
if(timer){
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
/* 侧边栏过渡动画蒙层 */
|
/* 侧边栏过渡动画蒙层 */
|
||||||
|
|
@ -37,7 +55,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 转圈加载动画 */
|
/* 转圈加载动画 */
|
||||||
.sidebar-overlay::before {
|
/* .sidebar-overlay::before {
|
||||||
content: '';
|
content: '';
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
|
@ -57,7 +75,7 @@ export default {
|
||||||
border-bottom: 2px solid rgba(113, 77, 199, 0.3);
|
border-bottom: 2px solid rgba(113, 77, 199, 0.3);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 1.5s linear infinite reverse;
|
animation: spin 1.5s linear infinite reverse;
|
||||||
}
|
} */
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
|
@ -0,0 +1,51 @@
|
||||||
|
// UI组件库入口文件
|
||||||
|
import LoadingCom from './components/LoadingCom/index.vue'
|
||||||
|
import FrameAnimation from './components/FrameAnimation/index.vue'
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
// 创建带有Dt前缀的组件
|
||||||
|
const DtLoadingCom = {
|
||||||
|
...LoadingCom,
|
||||||
|
name: 'DtLoadingCom',
|
||||||
|
install(app) {
|
||||||
|
app.component('DtLoadingCom', DtLoadingCom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DtFrameAnimation = {
|
||||||
|
...FrameAnimation,
|
||||||
|
name: 'DtFrameAnimation',
|
||||||
|
install(app) {
|
||||||
|
app.component('DtFrameAnimation', DtFrameAnimation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件列表
|
||||||
|
const components = [
|
||||||
|
DtLoadingCom,
|
||||||
|
DtFrameAnimation
|
||||||
|
]
|
||||||
|
|
||||||
|
// 导出组件
|
||||||
|
export {
|
||||||
|
DtLoadingCom,
|
||||||
|
DtFrameAnimation
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量注册组件的函数
|
||||||
|
export function registerComponents(app) {
|
||||||
|
components.forEach(component => {
|
||||||
|
if (component.install) {
|
||||||
|
app.use(component)
|
||||||
|
} else {
|
||||||
|
app.component(component.name, component)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出安装函数(用于Vue插件)
|
||||||
|
export default {
|
||||||
|
install(app) {
|
||||||
|
registerComponents(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import permission from './permission.js';
|
||||||
import promptManagement from './promptManagement.js';
|
import promptManagement from './promptManagement.js';
|
||||||
import pointsManagement from './pointsManagement.js';
|
import pointsManagement from './pointsManagement.js';
|
||||||
import configuration from './configuration.js';
|
import configuration from './configuration.js';
|
||||||
|
import seriespriceConfig from './seriespriceConfig.js';
|
||||||
export default {
|
export default {
|
||||||
...login,
|
...login,
|
||||||
...order,
|
...order,
|
||||||
|
|
@ -19,4 +20,5 @@ export default {
|
||||||
...promptManagement,
|
...promptManagement,
|
||||||
...pointsManagement,
|
...pointsManagement,
|
||||||
...configuration,
|
...configuration,
|
||||||
|
...seriespriceConfig,
|
||||||
};
|
};
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
const promptManagement = {
|
||||||
|
createProduct:{url:'/api-core/admin/product/product/create',method:'POST',isLoading:true},//创建产品及价格
|
||||||
|
getProductDetail:{url:'/api-core/admin/product/product/get',method:'GET',isLoading:true},//获取产品详情
|
||||||
|
getProductList:{url:'/api-core/admin/product/product/list',method:'POST',isLoading:true},//获取产品列表
|
||||||
|
updateProduct:{url:'/api-core/admin/product/product/update',method:'POST',isLoading:true},//更新产品信息
|
||||||
|
updateProductPrice:{url:'/api-core/admin/product/product/update-price',method:'POST',isLoading:true},//更新产品价格
|
||||||
|
deleteProduct:{url:'/api-core/admin/product/product/delete',method:'POST',isLoading:true},//删除产品
|
||||||
|
}
|
||||||
|
export default promptManagement;
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const pay = {
|
const pay = {
|
||||||
createPaymentintention:{url:'/createPaymentintention',method:'POST'},// 创建支付意图
|
createPaymentintention:{url:'/createPaymentintention',method:'POST'},// 创建支付意图
|
||||||
createCheckoutSession:{url:'/createCheckoutSession',method:'POST'},// 创建会话支付(购物车)
|
createCheckoutSession:{url:'/createCheckoutSession',method:'POST'},// 创建会话支付(购物车)
|
||||||
createPayorOrder:{url:'/api-core/front/stripe/create-and-checkout',method:'POST'}//创建订单并且返回支付链接
|
createPayorOrder:{url:'/api-core/front/stripe/create-and-checkout',method:'POST'},//根据产品ID创建订单并跳转支付
|
||||||
|
getProductList:{url:'/api-core/front/stripe/product/list',method:'POST'}//获取产品列表
|
||||||
}
|
}
|
||||||
export default pay;
|
export default pay;
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ export class PayServer {
|
||||||
// await this.init();
|
// await this.init();
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
let pamras = {
|
let pamras = {
|
||||||
|
product_id: orderInfo.product_id,
|
||||||
"methods": [
|
"methods": [
|
||||||
"card"
|
"card"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const getEnvBaseURL = () => {
|
||||||
// }
|
// }
|
||||||
var baseURL = '';
|
var baseURL = '';
|
||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
if(hostname=='localhost'||hostname=='127.0.0.1'){
|
if(hostname=='localhost'){
|
||||||
baseURL = '/api'
|
baseURL = '/api'
|
||||||
}else if(hostname.indexOf('deotaland.ai')>-1){
|
}else if(hostname.indexOf('deotaland.ai')>-1){
|
||||||
baseURL = 'https://api.deotaland.ai'
|
baseURL = 'https://api.deotaland.ai'
|
||||||
|
|
|
||||||