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;
|
||||
}
|
||||
|
||||
* 动画流畅,性能优化
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 修改IPCard组件的样式部分,更新.generating-placeholder相关样式
|
||||
2. 添加新的动画关键帧
|
||||
3. 确保样式与主题色保持一致
|
||||
4. 测试动画性能和效果
|
||||
|
||||
## 代码示例
|
||||
|
||||
```css
|
||||
/* 生成状态样式优化 */
|
||||
.generating-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.series-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a2e 100%);
|
||||
color: white;
|
||||
border-radius: inherit;
|
||||
position: relative;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.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;
|
||||
animation: breathe 3s ease-in-out infinite;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 添加背景装饰效果 */
|
||||
.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;
|
||||
.series-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
animation: spin 2s linear infinite;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.generating-spinner::before {
|
||||
border-top-color: #A78BFA;
|
||||
border-right-color: #A78BFA;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: ease-in-out;
|
||||
.series-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.generating-spinner::after {
|
||||
border-bottom-color: #6B46C1;
|
||||
border-left-color: #6B46C1;
|
||||
animation-duration: 2s;
|
||||
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);
|
||||
}
|
||||
.series-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
</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",
|
||||
"pinia": "^2.2.6",
|
||||
"three": "^0.180.0",
|
||||
"three-3mf-exporter": "^45.0.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^9.14.2",
|
||||
"vue-konva": "^3.2.6",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ import {
|
|||
Lock,
|
||||
Key,
|
||||
List,
|
||||
Coin
|
||||
Coin,
|
||||
ShoppingCartFull
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -60,7 +61,8 @@ const iconMap = {
|
|||
Lock,
|
||||
Key,
|
||||
List,
|
||||
Coin
|
||||
Coin,
|
||||
ShoppingCartFull
|
||||
}
|
||||
|
||||
const getIconComponent = (iconName) => {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@
|
|||
<span class="format-dot" style="color: #f56c6c;">●</span>
|
||||
{{ t('modelViewer.exportAsFBX') }}
|
||||
</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>
|
||||
</template>
|
||||
</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 { ElMessage } from 'element-plus'
|
||||
import { Loading, Warning, Refresh, Grid, Position, Download, ArrowDown } from '@element-plus/icons-vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Props
|
||||
|
|
@ -445,6 +448,9 @@ const handleExportCommand = (command) => {
|
|||
case 'fbx':
|
||||
exportAsFBX()
|
||||
break
|
||||
case '3mf':
|
||||
exportAs3MF()
|
||||
break
|
||||
default:
|
||||
ElMessage.error('不支持的导出格式')
|
||||
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 = () => {
|
||||
// 鼠标事件由 OrbitControls 处理
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export default {
|
|||
exportAsOBJ: 'Export as OBJ',
|
||||
exportAsSTL: 'Export as STL',
|
||||
exportAsFBX: 'Export as FBX',
|
||||
exportAs3MF: 'Export as 3MF',
|
||||
selectFormat: 'Select Export Format',
|
||||
preparingExport: 'Preparing export...'
|
||||
},
|
||||
|
|
@ -243,6 +244,7 @@ export default {
|
|||
yes: 'Yes',
|
||||
no: 'No',
|
||||
action: 'Action',
|
||||
actions: 'Actions',
|
||||
close: 'Close',
|
||||
back: 'Back'
|
||||
},
|
||||
|
|
@ -260,6 +262,8 @@ export default {
|
|||
userList: 'User List',
|
||||
pointsManagement: 'Points Management',
|
||||
commissionManagement: 'Commission Management',
|
||||
promptManagement: 'Prompt Management',
|
||||
productManagement: 'Product Management',
|
||||
logout: 'Logout',
|
||||
profile: 'Profile',
|
||||
settings: 'Settings'
|
||||
|
|
@ -780,6 +784,76 @@ export default {
|
|||
approved: 'Approved',
|
||||
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: {
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ orderManagement: {
|
|||
pointsManagement: '充值包管理',
|
||||
commissionManagement: '佣金管理',
|
||||
promptManagement: '提示词管理',
|
||||
productManagement: '产品管理',
|
||||
logout: '退出登录',
|
||||
profile: '个人资料',
|
||||
settings: '设置',
|
||||
|
|
@ -790,6 +791,7 @@ orderManagement: {
|
|||
animal: '动物',
|
||||
person: '人物',
|
||||
general: '通用',
|
||||
O1: 'O1',
|
||||
title: '标题',
|
||||
enterTitle: '请输入提示词标题',
|
||||
content: '内容',
|
||||
|
|
@ -802,6 +804,50 @@ orderManagement: {
|
|||
saveSuccess: '保存成功',
|
||||
activeSuccess: '已设置为生效状态',
|
||||
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: {
|
||||
|
|
@ -854,6 +900,8 @@ orderManagement: {
|
|||
save: '保存',
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
detail: '详情',
|
||||
actions: '操作',
|
||||
search: '搜索',
|
||||
loading: '加载中...',
|
||||
viewAll: '查看全部',
|
||||
|
|
@ -887,6 +935,7 @@ orderManagement: {
|
|||
exportAsOBJ: '导出为OBJ',
|
||||
exportAsSTL: '导出为STL',
|
||||
exportAsFBX: '导出为FBX',
|
||||
exportAs3MF: '导出为3MF',
|
||||
selectFormat: '选择导出格式',
|
||||
preparingExport: '准备导出中...'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -56,8 +56,10 @@ const initRoutes = () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
initRoutes()
|
||||
if (window.location.hostname.indexOf('local') === -1) {
|
||||
initRoutes()
|
||||
}
|
||||
// initRoutes()
|
||||
// 配置路由
|
||||
app.use(router)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const AdminUserList = () => import('@/views/admin/AdminRoleManagement/AdminUserL
|
|||
const AdminPointsManagement = () => import('@/views/admin/AdminPointsManagement/AdminPointsManagement.vue')
|
||||
const AdminCommissionManagement = () => import('@/views/admin/AdminCommissionManagement/AdminCommissionManagement.vue')
|
||||
const AdminPromptManagement = () => import('@/views/admin/AdminPromptManagement/AdminPromptManagement.vue')
|
||||
const AdminProductManagement = () => import('@/views/admin/ProductManagement/ProductManagement.vue')
|
||||
//权限路由映射表
|
||||
export const permissionRoutes = [
|
||||
{
|
||||
|
|
@ -35,6 +36,17 @@ export const permissionRoutes = [
|
|||
requiresAuth: true,
|
||||
menuOrder: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'product-management',
|
||||
name: 'AdminProductManagement',
|
||||
component: AdminProductManagement,
|
||||
meta: {
|
||||
title: 'admin.layout.productManagement',
|
||||
icon: 'ShoppingCartFull',
|
||||
menuOrder: 2,
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
|
|
@ -58,17 +70,7 @@ export const permissionRoutes = [
|
|||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'disassembly-orders',
|
||||
name: 'AdminDisassemblyOrders',
|
||||
component: AdminDisassemblyOrders,
|
||||
meta: {
|
||||
title: 'admin.layout.disassemblyOrders',
|
||||
icon: 'EditPen',
|
||||
menuOrder: 2,
|
||||
requiresAuth: true,
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'order-list',
|
||||
name: 'AdminOrdersList',
|
||||
|
|
@ -126,6 +128,7 @@ export const permissionRoutes = [
|
|||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'permission',
|
||||
name: 'AdminPermission',
|
||||
|
|
@ -222,6 +225,17 @@ const routes = [
|
|||
requiresAuth: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'disassembly-orders',
|
||||
name: 'AdminDisassemblyOrders',
|
||||
component: AdminDisassemblyOrders,
|
||||
meta: {
|
||||
title: 'admin.layout.disassemblyOrders',
|
||||
icon: 'EditPen',
|
||||
menuOrder: 2,
|
||||
requiresAuth: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'disassembly-orders/:id',
|
||||
name: 'AdminDisassemblyDetail',
|
||||
|
|
@ -269,6 +283,7 @@ const routes = [
|
|||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
...(window.location.hostname.indexOf('local') === -1 ? [] : permissionRoutes)
|
||||
]//[]//
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const useAuthStore = defineStore('auth', {
|
|||
permissionButton: JSON.parse(localStorage.getItem('permissionButton') || '[]'),
|
||||
router: null,
|
||||
routesUpdated: 0,//路由更新次数
|
||||
routerList: []//侧边栏路由
|
||||
routerList: window.location.hostname.indexOf('local') === -1 ? [] : permissionRoutes,//侧边栏路由
|
||||
}),
|
||||
|
||||
getters: {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@
|
|||
<el-option :label="t('admin.promptManagement.animal')" value="animal" />
|
||||
<el-option :label="t('admin.promptManagement.person')" value="person" />
|
||||
<el-option :label="t('admin.promptManagement.general')" value="general" />
|
||||
<el-option :label="t('admin.promptManagement.O1')" value="O1" />
|
||||
</el-select>
|
||||
</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 ThemeToggle from '../ui/ThemeToggle.vue'
|
||||
import LanguageToggle from '../ui/LanguageToggle.vue'
|
||||
|
||||
const emit = defineEmits(['openGuideModal'])
|
||||
const emit = defineEmits(['openGuideModal','back'])
|
||||
const props = defineProps({
|
||||
total_score: {
|
||||
type: Number,
|
||||
|
|
@ -92,12 +91,13 @@ const { t, locale } = useI18n()
|
|||
// 处理返回按钮点击
|
||||
const handleBack = () => {
|
||||
// 返回上一页或首页
|
||||
if (window.history.length > 1) {
|
||||
window.history.back()
|
||||
} else {
|
||||
// 如果没有历史记录,则跳转到首页
|
||||
window.location.href = '/'
|
||||
}
|
||||
// if (window.history.length > 1) {
|
||||
// window.history.back()
|
||||
// } else {
|
||||
// // 如果没有历史记录,则跳转到首页
|
||||
// window.location.href = '/'
|
||||
// }
|
||||
emit('back')
|
||||
}
|
||||
|
||||
// 开始编辑项目名称
|
||||
|
|
|
|||
|
|
@ -21,11 +21,10 @@
|
|||
<div class="product-info">
|
||||
<h1 class="product-title">{{ $t('checkout.customModel') }}</h1>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<!-- Content Sections -->
|
||||
<div class="content-sections">
|
||||
<div class="main-content">
|
||||
|
|
@ -178,10 +177,12 @@ import StripePaymentForm from '@/components/StripePaymentForm.vue'
|
|||
import { Country, State } from 'country-state-city'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { PayServer } from '@deotaland/utils'
|
||||
import { requestUtils,clientApi } from '@deotaland/utils'
|
||||
const payserver = new PayServer();
|
||||
const props = defineProps({
|
||||
modelData: { type: Object, default: () => ({}) },
|
||||
show: { type: Boolean, default: false }
|
||||
show: { type: Boolean, default: false },
|
||||
series: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['close'])
|
||||
const onClose = () => emit('close')
|
||||
|
|
@ -198,7 +199,7 @@ const showPayingOverlay = ref(false)
|
|||
|
||||
// 省州映射数据已移至国际化文件
|
||||
const amountCents = computed(() => {
|
||||
let base = 299 // 默认价格,移除了尺寸相关定价
|
||||
let base = price.value // 默认价格,移除了尺寸相关定价
|
||||
if (addons.value.gloss) base += 300
|
||||
if (addons.value.base) base += 400
|
||||
if (addons.value.matte) base += 200
|
||||
|
|
@ -219,6 +220,22 @@ const isPayButtonDisabled = computed(() => {
|
|||
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 decQty = () => { qty.value = Math.max(qty.value-1, 1) }
|
||||
const goShopify = () => {//用户点击购买
|
||||
|
|
@ -265,6 +282,8 @@ const goShopify = () => {//用户点击购买
|
|||
// Save shipping and contact information if checkbox is checked
|
||||
saveLocal()
|
||||
// 在控制台打印整理后的信息
|
||||
console.log('Order Parameters:', params)
|
||||
params.product_id = seriesId.value
|
||||
payserver.createPayorOrder(params);
|
||||
}
|
||||
|
||||
|
|
@ -298,6 +317,7 @@ onMounted(() => {
|
|||
if (c) Object.assign(contact.value, JSON.parse(c))
|
||||
updateCountryOptions()
|
||||
updateStates()
|
||||
getPrice();
|
||||
} catch (e) {}
|
||||
})
|
||||
watch(() => shipping.value.country, () => { updateStates() })
|
||||
|
|
|
|||
|
|
@ -12,26 +12,26 @@
|
|||
<div class="series-selector-content">
|
||||
<!-- 系列选项列表 -->
|
||||
<div class="series-list">
|
||||
<!-- Done 系列 -->
|
||||
<div
|
||||
v-for="series in seriesList"
|
||||
:key="series.id"
|
||||
class="series-item"
|
||||
@click="selectSeries('Done')"
|
||||
@click="selectSeries(series)"
|
||||
>
|
||||
<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 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>
|
||||
|
||||
<!-- Oone 系列 -->
|
||||
<div
|
||||
class="series-item"
|
||||
@click="selectSeries('Oone')"
|
||||
>
|
||||
<div class="series-image">
|
||||
<img src="@/assets/xh/Oone.webp" alt="Oone" />
|
||||
</div>
|
||||
<div class="series-name">Oone</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,9 +41,9 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
import { defineProps, defineEmits,onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {clientApi,requestUtils} from '@deotaland/utils';
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -62,6 +62,31 @@ const selectSeries = (series) => {
|
|||
const onCancel = () => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -228,25 +253,34 @@ const onCancel = () => {
|
|||
|
||||
.series-image {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.series-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
transition: transform 0.3s ease-out;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.series-item:hover .series-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.series-info {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.series-name {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
|
|
@ -254,6 +288,7 @@ const onCancel = () => {
|
|||
position: relative;
|
||||
z-index: 1;
|
||||
transition: color 0.2s ease-out;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.series-item:hover .series-name {
|
||||
|
|
@ -264,6 +299,25 @@ const onCancel = () => {
|
|||
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 {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
|
@ -320,11 +374,20 @@ const onCancel = () => {
|
|||
}
|
||||
|
||||
.series-image {
|
||||
height: 100px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.series-name {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.series-description {
|
||||
font-size: 13px;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.series-price {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.series-actions {
|
||||
|
|
@ -345,7 +408,11 @@ const onCancel = () => {
|
|||
}
|
||||
|
||||
.series-image {
|
||||
height: 120px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.series-description {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -415,6 +415,12 @@ export default {
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mobile-menu-button svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-button:hover {
|
||||
background: var(--hover-bg, #f3f4f6);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,8 @@ import {
|
|||
DataAnalysis as AnalyticsIcon,
|
||||
Folder as ProjectIcon,
|
||||
Bell as NotificationIcon,
|
||||
Key as ApiIcon
|
||||
Key as ApiIcon,
|
||||
ShoppingCartFull
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
// 定义组件名称(用于调试和递归组件)
|
||||
|
|
@ -112,10 +113,11 @@ const remainingPoints = ref(1280)
|
|||
const currentUser = computed(() => authStore.user)
|
||||
const userRole = computed(() => ({
|
||||
'1': 'free',
|
||||
'2': 'creator',
|
||||
}[authStore.user?.user_role || '1'])) // 可切换为 'free' 测试不同样式
|
||||
'2':'creator'
|
||||
}[authStore.user?.user_role || authStore.user?.userRole||'1'])) // 可切换为 'free' 测试不同样式
|
||||
const sidebarClasses = computed(() => ({
|
||||
'sidebar-mobile': isMobile.value
|
||||
'sidebar-mobile': isMobile.value,
|
||||
'show': isMobile.value && !props.collapsed
|
||||
}))
|
||||
|
||||
// 核心菜单项 (6个主要功能)
|
||||
|
|
@ -208,7 +210,7 @@ onUnmounted(() => {
|
|||
height: 100%;
|
||||
background: var(--sidebar-bg, #ffffff);
|
||||
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;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
|
|
@ -640,12 +642,14 @@ onUnmounted(() => {
|
|||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
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);
|
||||
border-right: 1px solid rgba(107, 70, 193, 0.1);
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.sidebar-mobile.show {
|
||||
|
|
@ -683,28 +687,53 @@ onUnmounted(() => {
|
|||
@media (max-width: 767px) {
|
||||
.app-sidebar {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
padding: 16px 8px;
|
||||
height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 16px 20px;
|
||||
min-height: 72px;
|
||||
margin: 8px 16px;
|
||||
/* padding: 12px 8px; */
|
||||
min-height: 50px;
|
||||
/* margin: 6px 4px; */
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-bottom: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-icon svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.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
|
||||
class="sidebar-container"
|
||||
:class="{ 'sidebar-visible': sidebarVisible }"
|
||||
>
|
||||
<AppSidebar
|
||||
:collapsed="!sidebarVisible"
|
||||
|
|
@ -219,7 +220,7 @@ watch(() => window.location.pathname, () => {
|
|||
height: 64px;
|
||||
background: var(--header-bg, #ffffff);
|
||||
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
||||
z-index: 9999;
|
||||
z-index: 200;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
|
@ -241,14 +242,15 @@ watch(() => window.location.pathname, () => {
|
|||
.sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -120px;
|
||||
transform: translateX(0);
|
||||
/* left: -120px; */
|
||||
transform: translateX(-100%);
|
||||
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 {
|
||||
left: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const List = () => import('../views/List.vue')
|
|||
const Login = () => import('../views/Login/Login.vue')
|
||||
const Register = () => import('../views/Register.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 OrderManagement = () => import('../views/OrderManagement/OrderManagement.vue')
|
||||
const OrderDetail = () => import('../views/OrderDetail.vue')
|
||||
|
|
|
|||
|
|
@ -144,8 +144,8 @@ import { ref, onMounted } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
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 SeriesSelector from '../components/SeriesSelector.vue'
|
||||
import {Project} from './Project/index'
|
||||
import SeriesSelector from '../../components/SeriesSelector.vue'
|
||||
import {Project} from '../Project/index'
|
||||
import { dateUtils } from '@deotaland/utils'
|
||||
|
||||
const { formatDate } = dateUtils
|
||||
|
|
@ -239,17 +239,16 @@ const handleFileSelect = (event) => {
|
|||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
}
|
||||
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
const openProject = (project) => {
|
||||
console.log('打开项目:', project.title)
|
||||
router.push(`/project/${project.id}/${project.tags[0]}`)
|
||||
}
|
||||
|
||||
const createNewProject = (type) => {
|
||||
router.push(`/project/new/${type}`)
|
||||
const createNewProject = (series) => {
|
||||
console.log(series,'seriesseriesseries');
|
||||
router.push(`/project/new/${series.name}`)
|
||||
}
|
||||
|
||||
// 处理系列选择确认
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export class CreationWorkspace {
|
||||
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
type="primary"
|
||||
size="large"
|
||||
class="action-btn primary-btn create-btn-large"
|
||||
@click="navigateToFeature({ path: '/creation-workspace' })">
|
||||
@click="navigateToFeature({ path: `/project/new/Done` })">
|
||||
{{ t('home.welcome.startCreating') }}
|
||||
</el-button>
|
||||
<div v-else class="guest-actions">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="creative-zone" @contextmenu.prevent>
|
||||
<!-- 顶部固定头部组件 -->
|
||||
<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 class="sidebar-container">
|
||||
|
|
@ -143,6 +143,7 @@
|
|||
@close="showOrderProcessModal=false"
|
||||
@acknowledge="handleBuyFromCustomize" />
|
||||
<PurchaseModal
|
||||
:series="series"
|
||||
:show="showPurchaseModal"
|
||||
:modelData="CustomizeModalData"
|
||||
@close="showPurchaseModal=false" />
|
||||
|
|
@ -164,15 +165,10 @@ import {useRoute,useRouter} from 'vue-router';
|
|||
import {MeshyServer,GiminiServer,FileServer} from '@deotaland/utils';
|
||||
import OrderProcessModal from '../../components/OrderProcessModal/index.vue';
|
||||
import PurchaseModal from '../../components/PurchaseModal/index.vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import {Project} from './index';
|
||||
import {ModernHome} from '../ModernHome/index.js'
|
||||
const fileServer = new FileServer();
|
||||
const modernHome = new ModernHome();
|
||||
const Limits = ref({
|
||||
generateCount: 0,
|
||||
modelCount: 0,
|
||||
})
|
||||
const router = useRouter();
|
||||
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;
|
||||
return getMaxZIndexNum.value;
|
||||
}
|
||||
const handleBack = ()=>{
|
||||
router.replace(`/creation-workspace`);
|
||||
}
|
||||
const combinedPromptJson = ref({});
|
||||
//获取动态提示词
|
||||
const getCombinedPrompt = async ()=>{
|
||||
try {
|
||||
const data = await PluginProject.getCombinedPrompt(series.value);
|
||||
combinedPromptJson.value = data;
|
||||
console.log(combinedPromptJson.value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
|
@ -1029,6 +1027,7 @@ const init = ()=>{
|
|||
const route = useRoute();
|
||||
projectId.value = route.params.id;
|
||||
series.value = route.params.series;
|
||||
console.log(series.value);
|
||||
if(projectId.value === 'new'){
|
||||
createProject();
|
||||
return
|
||||
|
|
|
|||
|
|
@ -729,18 +729,20 @@ export class Project{
|
|||
}, 1000);
|
||||
}
|
||||
//获取动态提示词
|
||||
async getCombinedPrompt(series){//series:项目系列Done Oone
|
||||
async getCombinedPrompt(series){//series:项目系列D1 O1
|
||||
try {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const res = await requestUtils.common(clientApi.default.combined)
|
||||
if(res.code === 0){
|
||||
let data = res.data;
|
||||
// 如果是Oone系列,过滤掉title中包含"动物坐姿"或"人物姿势"的提示词
|
||||
if (series === 'Oone') {
|
||||
if (series === 'O1') {
|
||||
data = data.filter(item => {
|
||||
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 = {
|
||||
|
|
@ -756,11 +758,10 @@ export class Project{
|
|||
// 按sortOrder排序
|
||||
data.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
// 处理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 => {
|
||||
// 拼接content
|
||||
result.person.content += item.content;
|
||||
|
||||
// 处理图片
|
||||
if (item.imageUrls) {
|
||||
try {
|
||||
|
|
@ -774,7 +775,7 @@ export class Project{
|
|||
}
|
||||
});
|
||||
// 处理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 => {
|
||||
// 拼接content
|
||||
result.animal.content += item.content;
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ const userController = new UserController()
|
|||
|
||||
// 响应式用户数据
|
||||
const userData = ref({
|
||||
role: authStore.user?.inviteCode === '2' ? 'creator' : 'free',
|
||||
role: authStore.user?.userRole == '2' ? 'creator' : 'free',
|
||||
// 基本信息
|
||||
avatar: authStore.user?.avatarUrl,
|
||||
nickname: authStore.user?.nickname,
|
||||
|
|
@ -405,13 +405,46 @@ const saveNickname = () => {
|
|||
// 复制邀请码
|
||||
const copyInviteCode = (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)
|
||||
.then(() => {
|
||||
ElMessage.success(t('userCenter.invitation.copySuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
if (fallbackCopyText(InviteLink)) {
|
||||
ElMessage.success(t('userCenter.invitation.copySuccess'))
|
||||
} else {
|
||||
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>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoadingCom',
|
||||
}
|
||||
<script setup>
|
||||
import { ref, onMounted,onUnmounted } from 'vue'
|
||||
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>
|
||||
<style>
|
||||
/* 侧边栏过渡动画蒙层 */
|
||||
|
|
@ -37,7 +55,7 @@ export default {
|
|||
}
|
||||
|
||||
/* 转圈加载动画 */
|
||||
.sidebar-overlay::before {
|
||||
/* .sidebar-overlay::before {
|
||||
content: '';
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
|
|
@ -57,7 +75,7 @@ export default {
|
|||
border-bottom: 2px solid rgba(113, 77, 199, 0.3);
|
||||
border-radius: 50%;
|
||||
animation: spin 1.5s linear infinite reverse;
|
||||
}
|
||||
} */
|
||||
|
||||
@keyframes spin {
|
||||
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 pointsManagement from './pointsManagement.js';
|
||||
import configuration from './configuration.js';
|
||||
import seriespriceConfig from './seriespriceConfig.js';
|
||||
export default {
|
||||
...login,
|
||||
...order,
|
||||
|
|
@ -19,4 +20,5 @@ export default {
|
|||
...promptManagement,
|
||||
...pointsManagement,
|
||||
...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 = {
|
||||
createPaymentintention:{url:'/createPaymentintention',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;
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ export class PayServer {
|
|||
// await this.init();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let pamras = {
|
||||
product_id: orderInfo.product_id,
|
||||
"methods": [
|
||||
"card"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const getEnvBaseURL = () => {
|
|||
// }
|
||||
var baseURL = '';
|
||||
const hostname = window.location.hostname;
|
||||
if(hostname=='localhost'||hostname=='127.0.0.1'){
|
||||
if(hostname=='localhost'){
|
||||
baseURL = '/api'
|
||||
}else if(hostname.indexOf('deotaland.ai')>-1){
|
||||
baseURL = 'https://api.deotaland.ai'
|
||||
|
|
|
|||