This commit is contained in:
13121765685 2025-12-16 14:13:37 +08:00
parent 0e7ab72c73
commit b86e6fb285
70 changed files with 2277 additions and 903 deletions

View File

@ -0,0 +1,49 @@
# 实现移动端手指缩放功能
## 现状分析
- 当前代码已经实现了鼠标滚轮+Ctrl键的缩放功能
- 实现了场景拖拽功能,支持触摸和鼠标事件
- 有一个`preventPinchZoom`函数阻止了缩放手势,需要修改
- 没有实现双指缩放功能
## 实现方案
### 1. 修改preventPinchZoom函数
- 移除或修改该函数,允许在主内容区域进行缩放手势
- 保持在侧边栏区域阻止缩放手势
### 2. 添加双指缩放支持
- 添加触摸事件处理,实现双指缩放
- 在`touchstart`事件中记录双指初始距离
- 在`touchmove`事件中计算双指距离变化,实现缩放
- 保持与现有缩放逻辑的一致性
### 3. 实现缩放中心点计算
- 计算双指中心点作为缩放中心
- 调整场景偏移量,使缩放围绕双指中心点进行
### 4. 保持现有功能不变
- 确保鼠标滚轮缩放功能正常
- 确保单指拖拽功能正常
## 代码修改点
1. **修改preventPinchZoom函数**:仅在侧边栏区域阻止缩放手势
2. **添加触摸事件处理变量**:记录双指初始距离和中心点
3. **修改startSceneDrag函数**:添加双指检测逻辑
4. **修改dragScene函数**:添加双指缩放逻辑
5. **调整缩放计算逻辑**:支持从触摸事件获取缩放中心点
## 预期效果
- 移动端支持双指缩放场景
- 缩放围绕双指中心点进行
- 与现有鼠标滚轮缩放效果一致
- 保持单指拖拽功能正常
## 实现步骤
1. 分析现有触摸事件处理逻辑
2. 添加双指缩放所需的状态变量
3. 修改触摸事件处理函数,添加双指缩放逻辑
4. 测试在移动端的缩放效果
5. 确保与现有功能兼容

View File

@ -0,0 +1,35 @@
# 优化物流信息布局方案
**需求分析**:将当前物流信息区域中承运信息卡片和物流时间线从上下垂直排列改为左右并排展示,提高空间利用率和视觉层次感。
**实现思路**
1. 修改物流信息主容器的布局方式
2. 将承运信息卡片和物流时间线作为并排的两个子元素
3. 调整宽度比例和间距
4. 保持响应式设计,在小屏幕下自动切换为垂直布局
**具体修改点**
1. **修改模板结构**将承运信息卡片和物流时间线包裹在一个flex/grid容器中
2. **调整CSS样式**
* 修改`.logistics-timeline`容器布局
* 调整`.logistics-info`和`.logistics-timeline-container`的宽度和排列方式
* 添加响应式断点
3. **优化间距和对齐**:确保左右两个区域视觉平衡
**预期效果**
* 桌面端:承运信息卡片在左侧,物流时间线在右侧,左右并排展示
* 移动端:保持原有的上下垂直排列,适配小屏幕
* 提高信息展示效率,减少页面滚动
**修改文件**
* `d:\work\Aiproject\DeotalandAi\apps\frontend\src\views\OrderDetail.vue`

View File

@ -0,0 +1,36 @@
# 实现图片预览功能
## 目标
在IPCard组件中添加点击图片预览功能使用Element Plus的Image组件实现。
## 实现步骤
1. **引入Element Plus Image组件**
- 在组件脚本中引入ElImage组件
2. **修改图片标签**
- 将当前的img标签替换为el-image标签
- 配置el-image的src、alt、class属性保持原有样式
- 添加preview-src-list属性以支持预览功能
- 添加@click事件处理预览逻辑
3. **确保样式一致性**
- 保持原有ip-card-image类的样式不变
- 确保el-image组件的布局与原有img标签一致
4. **测试功能**
- 确保点击图片时能够正常弹出预览窗口
- 确保预览窗口可以正常关闭
- 确保样式没有破坏原有布局
## 代码修改点
1. **引入组件**在第122行添加ElImage到Element Plus组件引入列表
2. **修改模板**将第36-43行的img标签替换为el-image标签
3. **添加预览配置**添加preview-src-list属性和相关逻辑
## 预期效果
- 点击IP卡片中的图片时会弹出一个全屏的图片预览窗口
- 预览窗口支持缩放、旋转等操作
- 点击预览窗口外部或关闭按钮可以关闭预览
- 原有卡片样式和布局保持不变

View File

@ -0,0 +1,87 @@
# 实现独立邀请码组件并集成到登录表单
## 1. 创建独立的邀请码组件
### 1.1 创建 InviteCodeInput.vue 组件
- 组件路径:`src/components/auth/InviteCodeInput.vue`
- 实现邀请码输入框包含label、input和错误提示
- 使用与现有表单相同的样式和结构
- 添加邀请码的必填验证逻辑
- 支持实时验证和错误提示
- 支持加载状态
### 1.2 组件功能设计
- 支持v-model双向绑定
- 支持自定义placeholder和label
- 支持错误信息显示
- 支持禁用状态
- 支持聚焦和失焦事件
## 2. 更新 LoginForm.vue 组件
### 2.1 引入并使用 InviteCodeInput 组件
- 在LoginForm.vue中引入InviteCodeInput组件
- 在登录按钮下方添加一个div容器使用`divider`类名进行隔离
- 在div容器中使用InviteCodeInput组件
- 传递必要的props和事件
### 2.2 更新表单数据和验证
- 在form对象中添加inviteCode字段
- 在rules中添加inviteCode的必填验证
- 添加inviteCodeError状态变量
- 更新表单有效性判断包含inviteCode验证
## 3. 更新 Login.vue 页面
### 3.1 添加邀请码状态管理
- 在Login.vue中添加inviteCode状态变量
- 添加inviteCodeError状态变量
- 添加validateInviteCode函数
### 3.2 更新Google登录按钮逻辑
- 将inviteCode状态传递给GoogleOAuthButton组件
- 更新GoogleOAuthButton的disabled属性只有inviteCode有效时才启用
- 确保Google登录流程能够获取到邀请码信息
## 4. 实现样式隔离
### 4.1 使用divider进行样式隔离
- 在LoginForm组件和InviteCodeInput组件之间添加divider
- 确保视觉上的清晰分隔
- 保持与现有分割线一致的样式
## 5. 实现Google登录与邀请码的关联
### 5.1 共享邀请码状态
- 确保LoginForm和Google登录按钮使用同一个邀请码状态
- 实现跨组件的状态同步
- 确保Google登录时能够传递邀请码信息到后端
## 6. 国际化支持
### 6.1 添加邀请码相关的i18n翻译
- 在i18n配置文件中添加邀请码相关的翻译文本
- 确保中英文都有对应的翻译
## 7. 响应式设计
### 7.1 确保邀请码组件在各种屏幕尺寸下正常显示
- 适配移动端、平板端和桌面端
- 确保在不同主题下正常显示
## 实现步骤
1. 创建独立的InviteCodeInput.vue组件
2. 更新LoginForm.vue引入并使用InviteCodeInput组件
3. 更新Login.vue添加邀请码状态管理和Google登录按钮的禁用逻辑
4. 实现样式隔离和响应式设计
5. 测试所有功能确保邀请码功能正常工作且Google登录按钮只有在填写邀请码后才能使用
## 预期效果
1. 登录表单下方新增邀请码输入组件用divider隔离
2. 邀请码为必填项,有实时验证和错误提示
3. Google登录按钮初始禁用填写邀请码后启用
4. 邀请码组件样式与现有表单保持一致
5. 支持响应式设计和国际化
6. 支持明暗主题切换

View File

@ -0,0 +1,65 @@
# 添加规则方格子四个角点
## 实现目标
在画布上添加有规则的点,类似方格子但只展示四个角上的点,支持缩放、拖动交互,并适配亮色和暗色主题。
## 实现步骤
### 1. 修改.scene-container的背景样式
- 将当前的随机装饰点替换为规则的方格子四个角点
- 使用CSS radial-gradient创建点效果
- 通过多个渐变层组合实现四个角的点
- 设置合适的background-size控制方格大小
### 2. 适配亮色主题
- 为亮色主题设置合适的点颜色和大小
- 确保点与背景对比度适中
### 3. 适配暗色主题
- 为暗色主题单独设置点颜色
- 保持与暗色背景的良好对比度
### 4. 确保交互兼容性
- 点网格应随画布缩放和拖动保持正确位置
- 不影响现有卡片元素的交互
## 技术实现
通过修改CSS样式使用多个radial-gradient层和background-position属性来创建规则的方格子四个角点。具体实现如下
```css
/* 亮色主题 */
.scene-container {
background:
/* 背景渐变 */
radial-gradient(ellipse at center, rgba(255, 255, 255, 0.9) 0%, rgba(243, 244, 246, 0.9) 100%),
/* 方格子四个角点 */
radial-gradient(circle at 0 0, rgba(107, 70, 193, 0.3) 2px, transparent 3px),
radial-gradient(circle at 100% 0, rgba(107, 70, 193, 0.3) 2px, transparent 3px),
radial-gradient(circle at 0 100%, rgba(107, 70, 193, 0.3) 2px, transparent 3px),
radial-gradient(circle at 100% 100%, rgba(107, 70, 193, 0.3) 2px, transparent 3px);
background-size: 100% 100%, 50px 50px, 50px 50px, 50px 50px, 50px 50px;
background-position: center center, 0 0, 0 0, 0 0, 0 0;
}
/* 暗色主题 */
html.dark .scene-container {
background:
/* 背景渐变 */
radial-gradient(ellipse at center, rgba(31, 41, 55, 0.9) 0%, rgba(17, 24, 39, 0.9) 100%),
/* 方格子四个角点 */
radial-gradient(circle at 0 0, rgba(167, 139, 250, 0.4) 2px, transparent 3px),
radial-gradient(circle at 100% 0, rgba(167, 139, 250, 0.4) 2px, transparent 3px),
radial-gradient(circle at 0 100%, rgba(167, 139, 250, 0.4) 2px, transparent 3px),
radial-gradient(circle at 100% 100%, rgba(167, 139, 250, 0.4) 2px, transparent 3px);
background-size: 100% 100%, 50px 50px, 50px 50px, 50px 50px, 50px 50px;
background-position: center center, 0 0, 0 0, 0 0, 0 0;
}
```
## 预期效果
- 画布上出现规则排列的方格子,每个方格的四个角上有一个点
- 方格大小为50px x 50px点大小为2px
- 点的颜色适配当前主题
- 缩放和拖动画布时,点网格保持正确的位置关系
- 不影响现有卡片元素的交互

View File

@ -0,0 +1,39 @@
# 重新设计div模块并适配中英文切换
## 目标
将当前的双按钮设计重新设计为参考图样式,并确保支持中英文切换功能。
## 修改内容
1. **修改文件**`d:\work\Aiproject\DeotalandAi\apps\frontend\src\views\home\index.vue`
2. **修改位置**
- 第181-194行的div容器按钮布局
- 第541-654行的i18n对象中英文文本
## 具体修改
### 1. 更新i18n配置
在i18n对象的en和zh部分添加新的文本键值对
- en: { hero: { joinWaitlist: 'Join Waitlist', enterInviteCode: 'Enter Invite Code' } }
- zh: { hero: { joinWaitlist: '加入候补名单', enterInviteCode: '输入邀请码' } }
### 2. 修改按钮布局
- 将flex容器的排列方式改为垂直居中
- 替换两个按钮为一个主按钮和一个次按钮
- 使用t()函数获取按钮文本,支持中英文切换
### 3. 样式设计
- 主按钮:白色背景、黑色文字、大圆角、居中显示
- 次按钮:使用渐变文字效果(用户提供的样式)
- 整体垂直排列,居中显示
## 实现细节
- 主按钮文字:通过`t('hero.joinWaitlist')`获取
- 次按钮文字:通过`t('hero.enterInviteCode')`获取
- 主按钮样式:白色背景、黑色文字、大圆角
- 次按钮样式:
- 渐变背景linear-gradient(90deg, #fff5c1, #a2e5ff)
- 文字填充透明:-webkit-text-fill-color: #0000
- 背景裁剪文字:-webkit-background-clip: text; background-clip: text
- 布局:垂直排列,主按钮在上,次按钮在下,整体居中
## 预期效果
修改后的div模块将呈现参考图所示的设计支持中英文切换功能。当切换语言时按钮文字会自动更新为对应语言同时保持参考图的视觉设计。

View File

@ -21,7 +21,6 @@
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^14.1.0",
"axios": "^1.13.2",
"country-state-city": "^3.2.1",
"dayjs": "^1.11.13",
"element-plus": "^2.11.7",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,28 +0,0 @@
<svg width="120" height="140" viewBox="0 0 120 140" xmlns="http://www.w3.org/2000/svg">
<!-- 背景 -->
<rect width="120" height="140" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" rx="8"/>
<!-- 头部 -->
<circle cx="60" cy="30" r="16" fill="#fbbf24" stroke="#f59e0b" stroke-width="2"/>
<!-- 身体 -->
<rect x="47" y="43" width="26" height="35" fill="#3b82f6" stroke="#2563eb" stroke-width="2" rx="4"/>
<!-- 手臂 -->
<rect x="35" y="48" width="12" height="20" fill="#ef4444" stroke="#dc2626" stroke-width="2" rx="3"/>
<rect x="73" y="48" width="12" height="20" fill="#ef4444" stroke="#dc2626" stroke-width="2" rx="3"/>
<!-- 大腿(坐姿) -->
<rect x="45" y="78" width="30" height="12" fill="#10b981" stroke="#059669" stroke-width="2" rx="2"/>
<!-- 小腿(垂直向下) -->
<rect x="48" y="90" width="8" height="25" fill="#10b981" stroke="#059669" stroke-width="2" rx="2"/>
<rect x="64" y="90" width="8" height="25" fill="#10b981" stroke="#059669" stroke-width="2" rx="2"/>
<!-- 脚部 -->
<ellipse cx="52" cy="120" rx="7" ry="3" fill="#6b7280" stroke="#4b5563" stroke-width="1"/>
<ellipse cx="68" cy="120" rx="7" ry="3" fill="#6b7280" stroke="#4b5563" stroke-width="1"/>
<!-- 标签 -->
<text x="60" y="135" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#374151">Sitting</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,25 +0,0 @@
<svg width="120" height="160" viewBox="0 0 120 160" xmlns="http://www.w3.org/2000/svg">
<!-- 背景 -->
<rect width="120" height="160" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" rx="8"/>
<!-- 头部 -->
<circle cx="60" cy="35" r="18" fill="#fbbf24" stroke="#f59e0b" stroke-width="2"/>
<!-- 身体 -->
<rect x="45" y="50" width="30" height="45" fill="#3b82f6" stroke="#2563eb" stroke-width="2" rx="4"/>
<!-- 手臂 -->
<rect x="30" y="55" width="15" height="25" fill="#ef4444" stroke="#dc2626" stroke-width="2" rx="3"/>
<rect x="75" y="55" width="15" height="25" fill="#ef4444" stroke="#dc2626" stroke-width="2" rx="3"/>
<!-- 腿部 -->
<rect x="48" y="95" width="10" height="35" fill="#10b981" stroke="#059669" stroke-width="2" rx="2"/>
<rect x="62" y="95" width="10" height="35" fill="#10b981" stroke="#059669" stroke-width="2" rx="2"/>
<!-- 脚部 -->
<ellipse cx="53" cy="135" rx="8" ry="4" fill="#6b7280" stroke="#4b5563" stroke-width="1"/>
<ellipse cx="67" cy="135" rx="8" ry="4" fill="#6b7280" stroke="#4b5563" stroke-width="1"/>
<!-- 标签 -->
<text x="60" y="150" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#374151">Standing</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

View File

@ -1,29 +0,0 @@
<svg width="100" height="120" viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg">
<!-- 背景 -->
<rect width="100" height="120" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" rx="8"/>
<!-- 头部(较小) -->
<circle cx="50" cy="25" r="14" fill="#fbbf24" stroke="#f59e0b" stroke-width="2"/>
<!-- 身体(紧凑) -->
<rect x="40" y="35" width="20" height="30" fill="#06b6d4" stroke="#0891b2" stroke-width="2" rx="3"/>
<!-- 手臂(贴身) -->
<rect x="30" y="40" width="10" height="15" fill="#ef4444" stroke="#dc2626" stroke-width="2" rx="2"/>
<rect x="60" y="40" width="10" height="15" fill="#ef4444" stroke="#dc2626" stroke-width="2" rx="2"/>
<!-- 腿部(并拢) -->
<rect x="43" y="65" width="6" height="25" fill="#10b981" stroke="#059669" stroke-width="2" rx="2"/>
<rect x="51" y="65" width="6" height="25" fill="#10b981" stroke="#059669" stroke-width="2" rx="2"/>
<!-- 脚部(小巧) -->
<ellipse cx="46" cy="95" rx="5" ry="3" fill="#6b7280" stroke="#4b5563" stroke-width="1"/>
<ellipse cx="54" cy="95" rx="5" ry="3" fill="#6b7280" stroke="#4b5563" stroke-width="1"/>
<!-- 紧凑指示器 -->
<rect x="20" y="50" width="60" height="1" fill="#f59e0b" opacity="0.5"/>
<rect x="20" y="70" width="60" height="1" fill="#f59e0b" opacity="0.5"/>
<!-- 标签 -->
<text x="50" y="110" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#374151">Compact</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

View File

@ -1,33 +0,0 @@
<svg width="140" height="160" viewBox="0 0 140 160" xmlns="http://www.w3.org/2000/svg">
<!-- 背景 -->
<rect width="140" height="160" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" rx="8"/>
<!-- 头部 -->
<circle cx="70" cy="30" r="18" fill="#fbbf24" stroke="#f59e0b" stroke-width="2"/>
<!-- 身体(略微倾斜) -->
<rect x="55" y="45" width="30" height="40" fill="#8b5cf6" stroke="#7c3aed" stroke-width="2" rx="4" transform="rotate(5 70 65)"/>
<!-- 左臂(向前伸展) -->
<rect x="35" y="50" width="20" height="8" fill="#ef4444" stroke="#dc2626" stroke-width="2" rx="3" transform="rotate(-15 45 54)"/>
<!-- 右臂(向上举起) -->
<rect x="85" y="35" width="8" height="20" fill="#ef4444" stroke="#dc2626" stroke-width="2" rx="3" transform="rotate(30 89 45)"/>
<!-- 左腿(向前迈步) -->
<rect x="55" y="85" width="10" height="35" fill="#10b981" stroke="#059669" stroke-width="2" rx="2" transform="rotate(10 60 102)"/>
<!-- 右腿(支撑腿) -->
<rect x="70" y="85" width="10" height="35" fill="#10b981" stroke="#059669" stroke-width="2" rx="2"/>
<!-- 脚部 -->
<ellipse cx="62" cy="125" rx="8" ry="4" fill="#6b7280" stroke="#4b5563" stroke-width="1" transform="rotate(10 62 125)"/>
<ellipse cx="75" cy="125" rx="8" ry="4" fill="#6b7280" stroke="#4b5563" stroke-width="1"/>
<!-- 动作线条 -->
<path d="M 95 40 Q 105 35 110 45" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="3,3"/>
<path d="M 45 60 Q 35 65 30 75" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="3,3"/>
<!-- 标签 -->
<text x="70" y="150" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#374151">Action</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

View File

@ -11,7 +11,7 @@
<div class="template-grid">
<div :class="['template-card', { selected: selected==='white' }]" @click="select('white')">
<div class="thumb"><img :src="whiteThumb" alt="白膜预览" /></div>
<!-- <div class="thumb"><img :src="whiteThumb" alt="白膜预览" /></div> -->
<div class="info">
<div class="name">白膜模型</div>
<div class="desc">基础打样适合二次涂装</div>
@ -63,8 +63,6 @@ import { ref, watch } from 'vue'
//
import { ElIcon } from 'element-plus'
import { CloseBold } from '@element-plus/icons-vue'
import whiteThumb from '@/assets/demo.png'
import colorThumb from '@/assets/demo4.png'
const props = defineProps({
show: { type: Boolean, default: false },

View File

@ -113,28 +113,28 @@ const guideSteps = computed(() => [
id: 1,
title: t('guideModal.step1.title'),
description: t('guideModal.step1.description'),
image: new URL('@/assets/step/creatProject/step1.png', import.meta.url).href,
image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/de7142df-ceb9-48f9-9367-af2a65e786a5.png',
tips: t('guideModal.step1.tips')
},
{
id: 2,
title: t('guideModal.step2.title'),
description: t('guideModal.step2.description'),
image: new URL('@/assets/step/creatProject/step2.png', import.meta.url).href,
image:'https://draft-user.s3.us-east-2.amazonaws.com/images/40773ee8-7f85-40c9-8c23-7ea1718e58a8.png',
tips: t('guideModal.step2.tips')
},
{
id: 3,
title: t('guideModal.step3.title'),
description: t('guideModal.step3.description'),
image: new URL('@/assets/step/creatProject/step3.png', import.meta.url).href,
image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/47cdd95f-23fb-486b-bd19-5fca67c2ce45.png',
tips: t('guideModal.step3.tips')
},
{
id: 4,
title: t('guideModal.step4.title'),
description: t('guideModal.step4.description'),
image: new URL('@/assets/step/creatProject/step4.png', import.meta.url).href,
image: 'https://draft-user.s3.us-east-2.amazonaws.com/images/690f2da4-400e-4cb0-a815-246e614d79b1.png',
tips: t('guideModal.step4.tips')
}
]);

View File

@ -1,5 +1,5 @@
<template>
<div class="ip-card-container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
<div class="ip-card-container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" @touchstart="handleTouchStart" @touchend="handleTouchEnd" :class="{ 'controls-visible': isControlsVisible }">
<!-- 主卡片区域 -->
<div class="ip-card-wrapper">
<!-- 文本输入框弹出层 -->
@ -33,11 +33,13 @@
</div>
</div>
<div class="ip-card" :style="cardStyle">
<img
<el-image
v-if="formData.internalImageUrl"
:src="formData.internalImageUrl"
@contextmenu.prevent
alt="IP"
class="ip-card-image"
:teleported="true"
@error="handleImageError"
@load="handleImageLoad"
/>
@ -54,76 +56,51 @@
</div>
</div>
<!-- 右侧控件区域 -->
<div class="right-controls-container" v-if="formData.internalImageUrl&&!(props.cardData.imgyt)">
<div class="right-controls-container" v-if="formData.internalImageUrl">
<!-- 右侧圆形按钮控件 -->
<div class="right-circular-controls">
<!-- v-if="props.generateFourView" -->
<button class="control-button share-btn" title="3DMODEL" @click="handleGenerateModel">
<button class="control-button share-btn" title="Preview Image" @click="handleImageClick">
<el-icon class="btn-icon"><View /></el-icon>
</button>
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="model" @click="handleGenerateModel">
<el-icon class="btn-icon"><Cpu /></el-icon>
</button>
<!-- v-if="props.generateSmoothWhiteModelStatus" -->
<!-- <button class="control-button share-btn" title="生成四视图" @click="handleCreateNewCard">
<el-icon class="btn-icon"><View /></el-icon>
</button> -->
<!-- v-if="!props.generateFourView&&!props.generateSmoothWhiteModelStatus" -->
<!-- <button class="control-button share-btn" title="生成白膜" @click="emitGenerateSmoothWhiteModel">
<el-icon class="btn-icon"><Picture /></el-icon>
</button> -->
<!-- 文本输入功能按钮 -->
<button class="control-button share-btn" title="textInput" @click="toggleTextInput">
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="textInput" @click="toggleTextInput">
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
</button>
<button class="control-button share-btn" title="scene graph" @click="handleTextcjt">
<button v-if="!(props.cardData.imgyt)" class="control-button share-btn" title="scene graph" @click="handleTextcjt">
<el-icon class="btn-icon"><Grid /></el-icon>
</button>
<!-- 发型脱离功能按钮 -->
<!-- <button class="control-button share-btn" title="发型脱离" @click="handleHairDetach">
<el-icon class="btn-icon"><MagicStick /></el-icon>
</button> -->
<!-- <button class="control-button image-btn" @click="handleGenerateImage">
<span class="btn-icon">🖼</span>
</button> -->
<!-- <button class="control-button more-btn" @click="toggleActions">
<span class="btn-icon"></span>
</button> -->
<!-- <div class="right-actions-controls" v-if="showRightControls">
<button class="control-button">
<span class="btn-icon">📏</span>
</button>
<button class="control-button">
<span class="btn-icon">🖌</span>
</button>
<button class="control-button">
<span class="btn-icon"></span>
</button>
</div> -->
</div>
</div>
</div>
</template>
<script setup>
import demoImage from '@/assets/demo.png'
import {cjt} from './tsc.js'
import cjimg from '@/assets/sketches/cjt.png';
// import cjimg from '@/assets/sketches/cjt.png';
import { computed, ref, onMounted, watch, nextTick } from 'vue';
import { GiminiServer } from '@deotaland/utils';
// import humanTypeImg from '@/assets/sketches/tcww.png'
import humanTypeImg from '@/assets/sketches/tcww2.webp'
import anTypeImg from '@/assets/sketches/dwww.webp';
// import humanTypeImg from '@/assets/sketches/tcww2.webp'
// import anTypeImg from '@/assets/sketches/dwww.webp';
// import anTypeImg from '@/assets/sketches/dwww2.png';
import cz2 from '@/assets/material/cz2.png';
// import cz2 from '@/assets/material/cz2.png';
// Element Plus
import { Cpu, ChatDotRound, CloseBold,Grid } from '@element-plus/icons-vue'
import { ElIcon,ElMessage,ElSkeleton } from 'element-plus'
import { Cpu, ChatDotRound, CloseBold,Grid,View } from '@element-plus/icons-vue'
import { ElIcon,ElMessage,ElSkeleton,ElImage } from 'element-plus'
const formData = ref({
internalImageUrl: '',//URL
status:'loading',//
})
});
//
const isTouching = ref(false);
const isControlsVisible = ref(false);
const cjimg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/14f98f33-06a7-4629-a42e-d7cfbced786f';
const anTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/1e82b2b6-0e5d-4a62-b65f-098952eb2f67';
const humanTypeImg = 'https://draft-user.s3.us-east-2.amazonaws.com/images/6569f75b-5534-49f4-9c3a-68eb35def196';
const giminiServer = new GiminiServer();
//
const showRightControls = ref(false);
@ -141,7 +118,10 @@ const textInputRef = ref(null);
const handleMouseEnter = () => {
isHovered.value = true;
};
const handleImageClick = () => {
//
emit('preview-image', formData.value.internalImageUrl);
}
//
const handleMouseLeave = () => {
isHovered.value = false;
@ -164,7 +144,7 @@ const toggleTextInput = () => {
};
const handleTextcjt = ()=>{
emit('create-prompt-card', {
img: formData.value.internalImageUrl,
img: formData.value.internalImageUrl||'',
imgyt:props?.cardData?.inspirationImage||'',
diyPromptText:cjt,
cardData: props.cardData
@ -188,6 +168,22 @@ const handleTextInputCancel = () => {
showTextInput.value = false;
textInputValue.value = '';
};
//
const handleTouchStart = () => {
isTouching.value = true;
setTimeout(() => {
if (isTouching.value) {
isControlsVisible.value = true;
}
}, 200); //
};
//
const handleTouchEnd = () => {
isTouching.value = false;
setTimeout(() => {
isControlsVisible.value = false;
}, 3000); // 3
};
//
const props = defineProps({
// URL
@ -223,7 +219,7 @@ const props = defineProps({
});
//
const emit = defineEmits(['generate-model-requested', 'create-new-card','create-prompt-card','delete']);
const emit = defineEmits(['generate-model-requested', 'create-new-card','create-prompt-card','delete','preview-image']);
//
const handleGenerateImage = async () => {
@ -242,6 +238,11 @@ const handleGenerateImage = async () => {
referenceImages.push(anTypeImg);
}
}
if(iscjt){
props.cardData.imgyt&&referenceImages.push(props.cardData.imgyt);
referenceImages.push(props.cardData.diyPromptImg);
referenceImages.push(cjimg);
}
// if(props.cardData.diyPromptText){
// console.log(props.cardData.diyPromptImg,'diyPromptImgdiyPromptImgdiyPromptImg');
// referenceImages.push(props.cardData.diyPromptImg);
@ -261,15 +262,12 @@ const handleGenerateImage = async () => {
//
let prompt = props.cardData.diyPromptText|| `
首先保证生成的角色符合以下要求
要求1一个通体由单一纯色木材雕刻而成的角色全身包括服装皮肤头发均为木质材质衣服一定不要有其他颜色全部统一无布料无皮肤无金属表面光滑颜色均匀一致无纹理变化整体呈现木质雕塑或木偶风格极简设计纯色例如暖棕色
角色肤色和衣服材质都为纯色一种颜色如下
保证角色全身都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
重点保证角色所有的服饰衣服都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
一个通体由单一纯色木材雕刻而成的角色全身包括服装皮肤头发均为木质材质无布料无皮肤无金属表面光滑颜色均匀一致无纹理变化整体呈现木质雕塑或木偶风格极简设计纯色例如暖棕色
一个通体由单一纯色木材雕刻而成的角色全身无布料无皮肤无金属表面光滑颜色均匀一致无纹理变化整体呈现木质雕塑或木偶风格极简设计.
A full-body character portrait
角色特征Q 版萌系造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征)色彩搭配和谐且富有层次.
角色特征Q 版萌系造型头身比例夸张大头小身神态纯真服饰设计融合童话风与复古感(简化一下复杂衣服纹理,只保留特征).
Style:潮玩盲盒角色设计采用 3D 立体建模渲染呈现细腻的质感与精致的细节
如果参考图是动物使用疯狂动物城的动物风格设计动物的特征要保留
${props?.cardData?.prompt? `Appearance: ${props?.cardData?.prompt}.`:``}
Note: The image should not have white borders.
去除原图中复杂的背景只保留人物角色的主体
@ -281,15 +279,16 @@ const handleGenerateImage = async () => {
材质处理
整体需光滑稳固边缘柔和防止打印时断裂
模型应呈现专业3D效果
${props.cardData?.ipType==1?`
调整角色的发型使其厚实蓬松且结构坚固轮廓清晰扎实适合3D打印
确保头发具备足够的厚度与结构完整性避免在打印过程中出现脆弱断裂同时保留原有的可爱美感
头发纹理细节需针对3D制造进行优化层次平滑且分明兼顾视觉吸引力与可打印性维持整体俏皮且高品质的盲盒角色风格
`:`采用疯狂动物城的设计风格`}
调整背景为极简风格换成中性纯白色,让图片中的人物呈现3D立体效果
保证生成的图片一定要有眼睛一定要有嘴巴
角色肤色和衣服材质都为纯色一种颜色如下
保证角色全身都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
衣服如果不适合做木制一定要简化衣服不能用复杂的衣服设计保留衣服特征即可衣服一定要纯色木质材质
如果参考图是动物保证动物双腿是向前伸展并分开的膝盖弯曲脚掌朝上或朝前它的双手前爪放在两腿之间靠近脚踝的位置整个身体是直立的面带微笑W坐姿W型坐姿盘腿坐V字坐
保证角色所有的服饰衣服都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
`
;
@ -317,7 +316,11 @@ const getImageTask = async (taskId,taskQueue)=>{
giminiServer.getTaskGinimi(taskId,taskQueue,(imgurls)=>{
let imgItem = imgurls.splice(0,1)[0]
formData.value.internalImageUrl=imgItem.url;
createRemainingImageData(imgurls);
saveProject({
taskId:taskId,
taskQueue:taskQueue,
});
// createRemainingImageData(imgurls);
},()=>{
ElMessage.error('Failed to generate image, please try again later.');
emit('delete');
@ -402,13 +405,13 @@ const handleImageError = (event) => {
};
//
const handleImageLoad = (event) => {
console.log(event,'eventeventevent');
// console.log(event,'eventeventevent');
const img = event.target;
const width = img.naturalWidth;
const height = img.naturalHeight;
if (width > 0 && height > 0) {
imageAspectRatio.value = width / height;
console.log(`图片比例: ${width}:${height} (${imageAspectRatio.value.toFixed(2)})`);
// console.log(`: ${width}:${height} (${imageAspectRatio.value.toFixed(2)})`);
}
};
</script>
@ -719,6 +722,17 @@ const handleImageLoad = (event) => {
pointer-events: auto;
}
/* 移动端适配:点击卡片后显示功能按钮 */
@media (max-width: 768px) {
/* 点击卡片容器时显示功能按钮 */
.ip-card-container:active .right-circular-controls,
.ip-card-container.controls-visible .right-circular-controls {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
}
}
.control-button {
width: 48px;
height: 48px;
@ -759,6 +773,14 @@ const handleImageLoad = (event) => {
display: block;
}
/* 确保el-image内部图片正确显示 */
.ip-card-image :deep(img) {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.image-placeholder {
width: 100%;
height: 100%;

View File

@ -0,0 +1,701 @@
<template>
<transition name="fade">
<div v-if="visible" class="image-preview-modal" @click.self="handleClose">
<!-- 顶部工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<div class="image-info">{{ imageIndex + 1 }} / {{ images.length }}</div>
</div>
<div class="toolbar-right">
<button class="tool-btn" @click="handleRotate" title="旋转">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="currentColor"/>
</svg>
</button>
<!-- <button class="tool-btn" @click="handleDownload" title="下载">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" fill="currentColor"/>
</svg>
</button> -->
<button class="tool-btn close-btn" @click="handleClose" title="关闭">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
</svg>
</button>
</div>
</div>
<!-- 主内容区域 -->
<div class="preview-container" ref="containerRef">
<!-- 左侧切换按钮 -->
<button
v-if="images.length > 1"
class="nav-btn prev-btn"
@click="prevImage"
:disabled="isFirstImage"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
</svg>
</button>
<!-- 右侧切换按钮 -->
<button
v-if="images.length > 1"
class="nav-btn next-btn"
@click="nextImage"
:disabled="isLastImage"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="currentColor"/>
</svg>
</button>
<!-- 图片容器 -->
<div
class="image-wrapper"
ref="imageWrapperRef"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
@wheel="handleWheel"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
:style="{ cursor: isDragging ? 'grabbing' : 'grab' }"
>
<img
ref="imageRef"
:src="currentImage"
:style="imageStyle"
@load="handleImageLoad"
@error="handleImageError"
draggable="false"
alt="预览图片"
/>
</div>
</div>
<!-- 底部缩放控制 -->
<div class="zoom-controls">
<button class="zoom-btn" @click="zoomOut" :disabled="scale <= minScale">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13H5v-2h14v2z" fill="currentColor"/>
</svg>
</button>
<div class="zoom-level">{{ Math.round(scale * 100) }}%</div>
<button class="zoom-btn" @click="zoomIn" :disabled="scale >= maxScale">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
</svg>
</button>
<button class="zoom-btn" @click="resetZoom">Reset</button>
</div>
</div>
</transition>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false
},
images: {
type: Array,
default: () => []
},
initialIndex: {
type: Number,
default: 0
}
});
// Emits
const emit = defineEmits(['close']);
//
const imageIndex = ref(0);
const scale = ref(1);
const rotation = ref(0);
const position = ref({ x: 0, y: 0 });
const isDragging = ref(false);
const dragStart = ref({ x: 0, y: 0 });
const imageLoaded = ref(false);
//
const minScale = 0.1;
const maxScale = 5;
const zoomStep = 0.2;
//
const containerRef = ref(null);
const imageWrapperRef = ref(null);
const imageRef = ref(null);
//
const currentImage = computed(() => {
return props.images[imageIndex.value] || '';
});
const isFirstImage = computed(() => {
return imageIndex.value === 0;
});
const isLastImage = computed(() => {
return imageIndex.value === props.images.length - 1;
});
const imageStyle = computed(() => {
return {
transform: `scale(${scale.value}) rotate(${rotation.value}deg)`,
transition: isDragging.value ? 'none' : 'transform 0.3s ease',
maxWidth: 'none',
maxHeight: 'none'
};
});
// visible
watch(() => props.visible, (newVal) => {
if (newVal) {
imageIndex.value = props.initialIndex;
resetTransform();
document.body.style.overflow = 'hidden';
nextTick(() => {
fitToScreen();
});
} else {
document.body.style.overflow = '';
}
});
//
watch(imageIndex, () => {
resetTransform();
imageLoaded.value = false;
});
//
const resetTransform = () => {
scale.value = 1;
rotation.value = 0;
position.value = { x: 0, y: 0 };
};
//
const fitToScreen = () => {
if (!imageRef.value || !containerRef.value) return;
nextTick(() => {
const imgWidth = imageRef.value.naturalWidth;
const imgHeight = imageRef.value.naturalHeight;
const containerWidth = containerRef.value.clientWidth;
const containerHeight = containerRef.value.clientHeight;
const imgRatio = imgWidth / imgHeight;
const containerRatio = containerWidth / containerHeight;
if (imgRatio > containerRatio) {
//
scale.value = containerWidth / imgWidth * 0.9;
} else {
//
scale.value = containerHeight / imgHeight * 0.9;
}
//
scale.value = Math.min(scale.value, 1);
});
};
//
const handleImageLoad = () => {
imageLoaded.value = true;
if (scale.value === 1) {
fitToScreen();
}
};
//
const handleImageError = () => {
console.error('图片加载失败');
};
//
const handleClose = () => {
emit('close');
};
//
const prevImage = () => {
if (!isFirstImage.value) {
imageIndex.value--;
}
};
const nextImage = () => {
if (!isLastImage.value) {
imageIndex.value++;
}
};
//
const handleRotate = () => {
rotation.value = (rotation.value + 90) % 360;
};
//
const handleDownload = () => {
const link = document.createElement('a');
link.href = currentImage.value;
link.download = `image-${Date.now()}.jpg`;
link.click();
};
//
const zoomIn = () => {
if (scale.value < maxScale) {
scale.value = Math.min(scale.value + zoomStep, maxScale);
}
};
const zoomOut = () => {
if (scale.value > minScale) {
scale.value = Math.max(scale.value - zoomStep, minScale);
}
};
const resetZoom = () => {
resetTransform();
fitToScreen();
};
//
const handleWheel = (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
const newScale = Math.min(Math.max(scale.value + delta, minScale), maxScale);
if (newScale !== scale.value) {
//
const rect = imageWrapperRef.value.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
//
const scaleRatio = newScale / scale.value;
// 使
position.value.x = x - (x - position.value.x) * scaleRatio;
position.value.y = y - (y - position.value.y) * scaleRatio;
scale.value = newScale;
}
};
//
const handleMouseDown = (e) => {
if (e.button === 0) { //
isDragging.value = true;
dragStart.value = {
x: e.clientX - position.value.x,
y: e.clientY - position.value.y
};
}
};
const handleMouseMove = (e) => {
if (isDragging.value) {
position.value = {
x: e.clientX - dragStart.value.x,
y: e.clientY - dragStart.value.y
};
}
};
const handleMouseUp = () => {
isDragging.value = false;
};
//
const touchStartDistance = ref(0);
const touchStartScale = ref(1);
const lastTouchTime = ref(0);
const handleTouchStart = (e) => {
if (e.touches.length === 1) {
//
isDragging.value = true;
dragStart.value = {
x: e.touches[0].clientX - position.value.x,
y: e.touches[0].clientY - position.value.y
};
lastTouchTime.value = Date.now();
} else if (e.touches.length === 2) {
//
isDragging.value = false;
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
touchStartDistance.value = Math.sqrt(dx * dx + dy * dy);
touchStartScale.value = scale.value;
}
};
const handleTouchMove = (e) => {
e.preventDefault();
if (e.touches.length === 1 && isDragging.value) {
//
position.value = {
x: e.touches[0].clientX - dragStart.value.x,
y: e.touches[0].clientY - dragStart.value.y
};
} else if (e.touches.length === 2) {
//
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (touchStartDistance.value > 0) {
const newScale = Math.min(
Math.max(touchStartScale.value * (distance / touchStartDistance.value), minScale),
maxScale
);
if (newScale !== scale.value) {
//
const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
const rect = imageWrapperRef.value.getBoundingClientRect();
const x = centerX - rect.left;
const y = centerY - rect.top;
const scaleRatio = newScale / scale.value;
position.value.x = x - (x - position.value.x) * scaleRatio;
position.value.y = y - (y - position.value.y) * scaleRatio;
scale.value = newScale;
}
}
}
};
const handleTouchEnd = (e) => {
if (e.touches.length === 0) {
isDragging.value = false;
//
const currentTime = Date.now();
if (currentTime - lastTouchTime.value < 300) {
//
if (scale.value === 1) {
fitToScreen();
} else {
resetZoom();
}
}
}
};
//
const handleKeyDown = (e) => {
if (!props.visible) return;
switch (e.key) {
case 'Escape':
handleClose();
break;
case 'ArrowLeft':
prevImage();
break;
case 'ArrowRight':
nextImage();
break;
case '+':
case '=':
zoomIn();
break;
case '-':
case '_':
zoomOut();
break;
}
};
//
onMounted(() => {
document.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
});
</script>
<style scoped>
.image-preview-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
z-index: 9999;
display: flex;
flex-direction: column;
color: #fff;
user-select: none;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
}
.toolbar-left {
display: flex;
align-items: center;
}
.image-info {
font-size: 16px;
font-weight: 500;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 16px;
}
.tool-btn {
background: none;
border: none;
color: #fff;
cursor: pointer;
padding: 8px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.tool-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.tool-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.close-btn:hover {
background-color: rgba(255, 59, 48, 0.8);
}
.preview-container {
flex: 1;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
background-color: rgba(0, 0, 0, 0.5);
border: none;
border-radius: 50%;
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.2s;
}
.nav-btn:hover:not(:disabled) {
background-color: rgba(255, 255, 255, 0.2);
}
.nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.prev-btn {
left: 20px;
}
.next-btn {
right: 20px;
}
.image-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.image-wrapper img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transform-origin: center;
}
.zoom-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 16px;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
}
.zoom-btn {
background: none;
border: none;
color: #fff;
cursor: pointer;
padding: 8px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
min-width: 40px;
height: 40px;
}
.zoom-btn:hover:not(:disabled) {
background-color: rgba(255, 255, 255, 0.1);
}
.zoom-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.zoom-level {
min-width: 60px;
text-align: center;
font-size: 14px;
font-weight: 500;
}
/* 过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.toolbar {
padding: 12px 16px;
}
.toolbar-right {
gap: 12px;
}
.nav-btn {
width: 40px;
height: 40px;
}
.prev-btn {
left: 10px;
}
.next-btn {
right: 10px;
}
.zoom-controls {
padding: 12px;
gap: 12px;
}
.zoom-btn {
min-width: 36px;
height: 36px;
}
.zoom-level {
min-width: 50px;
font-size: 12px;
}
}
@media (max-width: 480px) {
.toolbar {
padding: 8px 12px;
}
.image-info {
font-size: 14px;
}
.tool-btn {
padding: 6px;
}
.nav-btn {
width: 36px;
height: 36px;
}
.prev-btn {
left: 8px;
}
.next-btn {
right: 8px;
}
.zoom-controls {
padding: 8px;
gap: 8px;
}
.zoom-btn {
min-width: 32px;
height: 32px;
}
.zoom-level {
min-width: 45px;
font-size: 11px;
}
}
</style>

View File

@ -15,7 +15,7 @@
/* background: red; */
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
z-index: 9999;
z-index: 99999;
opacity: 0;
visibility: hidden;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);

View File

@ -9,7 +9,7 @@
<el-timeline-item
v-for="(event, index) in timelineEvents"
:key="index"
:timestamp="formatDate(event.timestamp)"
:timestamp="dateUtils.formatDate(event.timestamp)"
:color="getEventColor(event.type)"
:icon="getEventIcon(event.type)"
:type="getEventType(event.type)"
@ -53,7 +53,7 @@
</div>
<div class="info-row">
<span class="label">{{ $t('logistics.estimatedDelivery') }}:</span>
<span class="value">{{ formatDate(logisticsInfo.estimatedDelivery) }}</span>
<span class="value">{{ dateUtils.formatDate(logisticsInfo.estimatedDelivery) }}</span>
</div>
</div>
</div>
@ -68,7 +68,7 @@
<p class="current-address">{{ logisticsInfo.currentLocation || '北京市朝阳区分拣中心' }}</p>
<div class="status-update">
<el-icon><Clock /></el-icon>
<span>{{ $t('logistics.lastUpdate') }}: {{ formatDate(logisticsInfo.lastUpdate) }}</span>
<span>{{ $t('logistics.lastUpdate') }}: {{ dateUtils.formatDate(logisticsInfo.lastUpdate) }}</span>
</div>
</div>
</div>
@ -92,7 +92,8 @@ import {
CircleCheck
} from '@element-plus/icons-vue'
import { ORDER_STATUS } from '@/stores/orders'
import dayjs from 'dayjs'
import {dateUtils} from '@deotaland/utils';
// import dayjs from 'dayjs'
const props = defineProps({
orderId: {
@ -127,11 +128,7 @@ const EVENT_TYPES = {
EXCEPTION: 'exception'
}
//
const formatDate = (dateString) => {
if (!dateString) return ''
return dayjs(dateString).format('YYYY-MM-DD HH:mm')
}
//
const getEventColor = (eventType) => {

View File

@ -232,6 +232,7 @@ onBeforeUnmount(() => {
transform: translateX(-50%);
display: flex;
justify-content: center;
z-index: 20;
}
.toolbar-content {

View File

@ -3,8 +3,6 @@
<div ref="containerRef" class="three-model-container" :style="containerStyle">
<!-- 加载动画覆盖层 -->
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-text">{{ 'loadingModel' }}</div>
<div class="loading-progress">{{ Math.round(loadingProgress) }}%</div>
<div class="loading-bar">
<div class="loading-progress-bar" :style="{ width: loadingProgress + '%' }"></div>
@ -750,13 +748,16 @@ defineExpose({
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
width: 50px;
height: 50px;
border: 4px solid rgba(107, 70, 193, 0.3);
border-radius: 50%;
border-top-color: #4287f5;
animation: spin 1s ease-in-out infinite;
border-top-color: #6B46C1;
border-right-color: #A78BFA;
border-bottom-color: #D6BCFA;
animation: spin 1s linear infinite, pulse 1.5s ease-in-out infinite alternate;
margin-bottom: 16px;
box-shadow: 0 0 20px rgba(107, 70, 193, 0.5);
}
.loading-text {
@ -764,28 +765,54 @@ defineExpose({
font-size: 14px;
margin-bottom: 8px;
text-align: center;
background: linear-gradient(90deg, #6B46C1, #A78BFA);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: fadeIn 0.5s ease-in-out;
}
.loading-progress {
color: #4287f5;
font-size: 16px;
background: linear-gradient(90deg, #6B46C1, #A78BFA);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-size: 20px;
font-weight: bold;
margin-bottom: 12px;
text-shadow: 0 0 10px rgba(107, 70, 193, 0.5);
animation: glow 1.5s ease-in-out infinite alternate;
}
.loading-bar {
width: 80%;
height: 4px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 2px;
height: 6px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 3px;
overflow: hidden;
box-shadow: 0 0 10px rgba(107, 70, 193, 0.3);
position: relative;
}
.loading-progress-bar {
height: 100%;
background-color: #4287f5;
background: linear-gradient(90deg, #6B46C1, #A78BFA, #D6BCFA, #A78BFA, #6B46C1);
background-size: 300% 100%;
transition: width 0.3s ease;
border-radius: 2px;
border-radius: 3px;
animation: gradientFlow 2s linear infinite;
position: relative;
}
.loading-progress-bar::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent);
animation: shimmer 1.5s ease-in-out infinite;
}
/* 错误提示样式 */
@ -809,6 +836,10 @@ defineExpose({
.error-icon {
font-size: 32px;
margin-bottom: 12px;
background: linear-gradient(90deg, #6B46C1, #A78BFA);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.error-text {
@ -833,6 +864,60 @@ defineExpose({
}
}
/* 脉冲动画 */
@keyframes pulse {
from {
transform: scale(1);
box-shadow: 0 0 20px rgba(107, 70, 193, 0.5);
}
to {
transform: scale(1.1);
box-shadow: 0 0 30px rgba(107, 70, 193, 0.8);
}
}
/* 渐变流动动画 */
@keyframes gradientFlow {
0% {
background-position: 0% 50%;
}
100% {
background-position: 100% 50%;
}
}
/* 闪光动画 */
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* 发光动画 */
@keyframes glow {
from {
text-shadow: 0 0 10px rgba(107, 70, 193, 0.5);
}
to {
text-shadow: 0 0 20px rgba(107, 70, 193, 0.8), 0 0 30px rgba(167, 139, 250, 0.6);
}
}
/* 淡入动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.export-controls {
display: flex;
gap: 8px;

View File

@ -1,10 +1,10 @@
<template>
<div class="forgot-password-form">
<div class="form-container">
<div class="form-header">
<!-- <div class="form-header">
<h2 class="form-title">{{ t('forgotPassword.title') }}</h2>
<p class="form-subtitle">{{ t('forgotPassword.subtitle') }}</p>
</div>
</div> -->
<form v-if="!showSuccess" @submit.prevent="handleSubmit" class="form-content">
<!-- 邮箱输入框 -->

View File

@ -1,7 +1,7 @@
<template>
<button
class="google-oauth-button"
:disabled="loading"
:disabled="loading || disabled"
@click="handleGoogleLogin"
>
<!-- Google Logo -->
@ -38,7 +38,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useI18n } from 'vue-i18n'
import { requestUtils,clientApi } from '@deotaland/utils'
@ -49,6 +49,10 @@ const props = defineProps({
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
})

View File

@ -0,0 +1,200 @@
<template>
<div class="invite-code-section">
<!-- 邀请码输入 -->
<div class="form-group">
<label class="form-label" for="inviteCode">{{ t('login.invite_code_label') }}</label>
<div class="input-wrapper" :class="{ 'focused': isInviteCodeFocused }">
<input
id="inviteCode"
v-model="localValue"
type="text"
class="form-input"
:class="{ 'error': error }"
:placeholder="t('login.invite_code_placeholder')"
@focus="isInviteCodeFocused = true"
@blur="isInviteCodeFocused = false"
:disabled="disabled"
autocomplete="off"
/>
<div v-if="error" class="input-error-icon">
<el-icon><WarningFilled /></el-icon>
</div>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { WarningFilled } from '@element-plus/icons-vue'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
error: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'validate'])
const { t } = useI18n()
//
const isInviteCodeFocused = ref(false)
const localValue = ref(props.modelValue)
// modelValue
watch(() => props.modelValue, (newValue) => {
localValue.value = newValue
})
//
watch(localValue, (newValue) => {
emit('update:modelValue', newValue)
emit('validate', newValue)
})
</script>
<style scoped>
/* 邀请码区域 */
.invite-code-section {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid rgba(139, 92, 246, 0.1);
}
/* 表单组 */
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
/* 表单标签 */
.form-label {
font-size: 14px;
font-weight: 600;
color: #374151;
margin-left: 4px;
}
/* 输入框容器 */
.input-wrapper {
position: relative;
display: flex;
align-items: center;
transition: all 0.2s ease;
}
.input-wrapper.focused {
transform: translateY(-1px);
}
/* 输入框样式 */
.form-input {
width: 100%;
height: 48px;
padding: 0 16px;
border: 2px solid #E5E7EB;
border-radius: 12px;
background: white;
font-size: 16px;
color: #1F2937;
transition: all 0.2s ease;
outline: none;
}
.form-input:focus {
border-color: #7C3AED;
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.15);
background: #FAFBFF;
}
.form-input.error {
border-color: #EF4444;
background: #FEF2F2;
}
.form-input:disabled {
background: #F9FAFB;
color: #9CA3AF;
cursor: not-allowed;
}
.form-input::placeholder {
color: #9CA3AF;
font-weight: 400;
}
/* 错误图标 */
.input-error-icon {
position: absolute;
right: 12px;
color: #EF4444;
font-size: 16px;
}
/* 错误消息 */
.error-message {
font-size: 13px;
color: #EF4444;
margin-left: 4px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.form-input {
height: 44px;
font-size: 15px;
}
}
@media (max-width: 480px) {
.form-input {
height: 42px;
font-size: 14px;
padding: 0 14px;
}
}
/* 暗色主题适配 */
html.dark .form-label {
color: #F3F4F6;
}
html.dark .form-input {
background: rgba(31, 41, 55, 0.8);
border-color: rgba(156, 163, 175, 0.3);
color: #F3F4F6;
}
html.dark .form-input:focus {
border-color: #7C3AED;
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.15);
background: rgba(31, 41, 55, 0.9);
}
html.dark .form-input.error {
border-color: #EF4444;
background: rgba(127, 29, 29, 0.8);
}
html.dark .form-input:disabled {
background: rgba(31, 41, 55, 0.6);
color: #9CA3AF;
}
html.dark .invite-code-section {
border-top-color: rgba(139, 92, 246, 0.2);
}
</style>

View File

@ -52,6 +52,14 @@
<div v-if="passwordError" class="error-message">{{ passwordError }}</div>
</div>
<!-- 邀请码输入 -->
<InviteCodeInput
v-model="form.inviteCode"
:error="inviteCodeError"
:disabled="loading"
@validate="validateInviteCode"
/>
<!-- 登录按钮 -->
<button
type="submit"
@ -74,13 +82,14 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { WarningFilled, View, Hide, InfoFilled } from '@element-plus/icons-vue'
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength } from '@vuelidate/validators'
import InviteCodeInput from './InviteCodeInput.vue'
const { t } = useI18n()
const emit = defineEmits(['login', 'error'])
const emit = defineEmits(['login', 'error', 'update:inviteCode'])
const props = defineProps({
loading: {
type: Boolean,
@ -90,7 +99,8 @@ const props = defineProps({
//
const form = ref({
email: '',
password: ''
password: '',
inviteCode: ''
})
//
@ -101,18 +111,21 @@ const showPassword = ref(false)
//
const emailError = ref('')
const passwordError = ref('')
const inviteCodeError = ref('')
//
const rules = {
email: { required, email },
password: { required, minLength: minLength(6) }
password: { required, minLength: minLength(6) },
inviteCode: { required }
}
const v$ = useVuelidate(rules, form)
//
const isFormValid = computed(() => {
return form.value.email && form.value.password && !emailError.value && !passwordError.value
return form.value.email && form.value.password && form.value.inviteCode &&
!emailError.value && !passwordError.value && !inviteCodeError.value
})
//
@ -136,9 +149,19 @@ const validatePassword = () => {
}
}
const validateInviteCode = () => {
if (!form.value.inviteCode) {
inviteCodeError.value = t('login.invite_code_empty_error')
} else {
inviteCodeError.value = ''
}
emit('update:inviteCode', form.value.inviteCode)
}
//
watch(() => form.value.email, validateEmail)
watch(() => form.value.password, validatePassword)
watch(() => form.value.inviteCode, validateInviteCode)
//
const handleLogin = async () => {
@ -147,6 +170,7 @@ const handleLogin = async () => {
if (v$.value.$invalid) {
validateEmail()
validatePassword()
validateInviteCode()
return
}
emit('login', form.value)

View File

@ -268,8 +268,8 @@
</template>
<script setup>
import lts from '../../assets/sketches/lts.png'
import mk2dy from '../../assets/sketches/mk2dy.png'
// import lts from '../../assets/sketches/lts.png'
// import mk2dy from '../../assets/sketches/mk2dy.png'
import { ref, onMounted, watch, nextTick, computed, getCurrentInstance, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import cz1 from '../../assets/material/cz1.jpg'
@ -431,76 +431,76 @@ const electronicModules = ref([
]);
// -
const sketchDatabase = ref({
arduino_uno: [
{
id: 'humanoid_basic',
name: 'Basic Humanoid',
proportions: 'Head:Body = 1:1',
pose: 'standing',
imageUrl: lts,
description: 'Classic humanoid proportions suitable for Arduino-based robots',
compatibility: ['arduino_uno', 'servo_motor'],
headToBodyRatio: '5:5'
}
],
raspberry_pi: [
{
id: 'advanced_humanoid',
name: 'Advanced Humanoid',
proportions: 'Head:Body = 1:2.5',
pose: 'dynamic',
imageUrl: mk2dy,
description: 'Sophisticated design leveraging Raspberry Pi capabilities',
compatibility: ['raspberry_pi', 'camera_module'],
headToBodyRatio: '6:4'
},
],
esp32: [
{
id: 'iot_character',
name: 'IoT Character',
proportions: 'Head:Body = 1:6',
pose: 'connected',
imageUrl: '/src/assets/sketches/esp32_compact.svg',
description: 'IoT-enabled character design for ESP32',
compatibility: ['esp32']
}
],
servo_motor: [
{
id: 'articulated_figure',
name: 'Articulated Figure',
proportions: 'Head:Body = 1:7',
pose: 'articulated',
imageUrl: '/src/assets/sketches/arduino_standing.svg',
description: 'Design emphasizing joint movement and servo integration',
compatibility: ['servo_motor', 'arduino_uno']
}
],
led_matrix: [
{
id: 'display_character',
name: 'Display Character',
proportions: 'Head:Body = 1:6',
pose: 'expressive',
imageUrl: '/src/assets/sketches/arduino_sitting.svg',
description: 'Character design featuring LED matrix display integration',
compatibility: ['led_matrix', 'arduino_uno']
}
],
camera_module: [
{
id: 'vision_robot',
name: 'Vision Robot',
proportions: 'Head:Body = 1:6',
pose: 'observing',
imageUrl: '/src/assets/sketches/raspberry_action.svg',
description: 'Design optimized for camera module and computer vision',
compatibility: ['camera_module', 'raspberry_pi']
}
]
});
// const sketchDatabase = ref({
// arduino_uno: [
// {
// id: 'humanoid_basic',
// name: 'Basic Humanoid',
// proportions: 'Head:Body = 1:1',
// pose: 'standing',
// imageUrl: lts,
// description: 'Classic humanoid proportions suitable for Arduino-based robots',
// compatibility: ['arduino_uno', 'servo_motor'],
// headToBodyRatio: '5:5'
// }
// ],
// raspberry_pi: [
// {
// id: 'advanced_humanoid',
// name: 'Advanced Humanoid',
// proportions: 'Head:Body = 1:2.5',
// pose: 'dynamic',
// imageUrl: mk2dy,
// description: 'Sophisticated design leveraging Raspberry Pi capabilities',
// compatibility: ['raspberry_pi', 'camera_module'],
// headToBodyRatio: '6:4'
// },
// ],
// esp32: [
// {
// id: 'iot_character',
// name: 'IoT Character',
// proportions: 'Head:Body = 1:6',
// pose: 'connected',
// imageUrl: '/src/assets/sketches/esp32_compact.svg',
// description: 'IoT-enabled character design for ESP32',
// compatibility: ['esp32']
// }
// ],
// servo_motor: [
// {
// id: 'articulated_figure',
// name: 'Articulated Figure',
// proportions: 'Head:Body = 1:7',
// pose: 'articulated',
// imageUrl: '/src/assets/sketches/arduino_standing.svg',
// description: 'Design emphasizing joint movement and servo integration',
// compatibility: ['servo_motor', 'arduino_uno']
// }
// ],
// led_matrix: [
// {
// id: 'display_character',
// name: 'Display Character',
// proportions: 'Head:Body = 1:6',
// pose: 'expressive',
// imageUrl: '/src/assets/sketches/arduino_sitting.svg',
// description: 'Character design featuring LED matrix display integration',
// compatibility: ['led_matrix', 'arduino_uno']
// }
// ],
// camera_module: [
// {
// id: 'vision_robot',
// name: 'Vision Robot',
// proportions: 'Head:Body = 1:6',
// pose: 'observing',
// imageUrl: '/src/assets/sketches/raspberry_action.svg',
// description: 'Design optimized for camera module and computer vision',
// compatibility: ['camera_module', 'raspberry_pi']
// }
// ]
// });
//
const availableSketches = computed(() => {

View File

@ -270,11 +270,11 @@
</template>
<script setup>
import lts from '../../assets/sketches/lts.png'
import mk2dy from '../../assets/sketches/mk2dy.png'
// import lts from '../../assets/sketches/lts.png'
// import mk2dy from '../../assets/sketches/mk2dy.png'
import { ref, onMounted, watch, nextTick, computed, getCurrentInstance, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import cz1 from '../../assets/material/cz1.jpg'
// import cz1 from '../../assets/material/cz1.jpg'
import { FileServer } from '@deotaland/utils';
const filePlug = new FileServer();
//
@ -381,35 +381,35 @@ const colorDatabase = ref({
});
//
const materials = ref([
{
id: 'metal_brushed',
name: '白毛绒',
type: 'Metal',
icon: '🔩',
imageUrl: cz1, // URL
description: 'Industrial brushed metal finish',
properties: {
reflectivity: 0.8,
roughness: 0.3,
metallic: 1.0
}
}
// ,
// const materials = ref([
// {
// id: 'plastic_matte',
// name: '2',
// type: 'Plastic',
// icon: '🧱',
// imageUrl: '',
// description: 'Non-reflective plastic surface',
// id: 'metal_brushed',
// name: '',
// type: 'Metal',
// icon: '🔩',
// imageUrl: cz1, // URL
// description: 'Industrial brushed metal finish',
// properties: {
// reflectivity: 0.1,
// roughness: 0.9,
// metallic: 0.0
// reflectivity: 0.8,
// roughness: 0.3,
// metallic: 1.0
// }
// }
]);
// // ,
// // {
// // id: 'plastic_matte',
// // name: '2',
// // type: 'Plastic',
// // icon: '🧱',
// // imageUrl: '',
// // description: 'Non-reflective plastic surface',
// // properties: {
// // reflectivity: 0.1,
// // roughness: 0.9,
// // metallic: 0.0
// // }
// // }
// ]);
//
const electronicModules = ref([
@ -430,76 +430,76 @@ const electronicModules = ref([
]);
// -
const sketchDatabase = ref({
arduino_uno: [
{
id: 'humanoid_basic',
name: 'Basic Humanoid',
proportions: 'Head:Body = 1:1',
pose: 'standing',
imageUrl: lts,
description: 'Classic humanoid proportions suitable for Arduino-based robots',
compatibility: ['arduino_uno', 'servo_motor'],
headToBodyRatio: '5:5'
}
],
raspberry_pi: [
{
id: 'advanced_humanoid',
name: 'Advanced Humanoid',
proportions: 'Head:Body = 1:2.5',
pose: 'dynamic',
imageUrl: mk2dy,
description: 'Sophisticated design leveraging Raspberry Pi capabilities',
compatibility: ['raspberry_pi', 'camera_module'],
headToBodyRatio: '6:4'
},
],
esp32: [
{
id: 'iot_character',
name: 'IoT Character',
proportions: 'Head:Body = 1:6',
pose: 'connected',
imageUrl: '/src/assets/sketches/esp32_compact.svg',
description: 'IoT-enabled character design for ESP32',
compatibility: ['esp32']
}
],
servo_motor: [
{
id: 'articulated_figure',
name: 'Articulated Figure',
proportions: 'Head:Body = 1:7',
pose: 'articulated',
imageUrl: '/src/assets/sketches/arduino_standing.svg',
description: 'Design emphasizing joint movement and servo integration',
compatibility: ['servo_motor', 'arduino_uno']
}
],
led_matrix: [
{
id: 'display_character',
name: 'Display Character',
proportions: 'Head:Body = 1:6',
pose: 'expressive',
imageUrl: '/src/assets/sketches/arduino_sitting.svg',
description: 'Character design featuring LED matrix display integration',
compatibility: ['led_matrix', 'arduino_uno']
}
],
camera_module: [
{
id: 'vision_robot',
name: 'Vision Robot',
proportions: 'Head:Body = 1:6',
pose: 'observing',
imageUrl: '/src/assets/sketches/raspberry_action.svg',
description: 'Design optimized for camera module and computer vision',
compatibility: ['camera_module', 'raspberry_pi']
}
]
});
// const sketchDatabase = ref({
// arduino_uno: [
// {
// id: 'humanoid_basic',
// name: 'Basic Humanoid',
// proportions: 'Head:Body = 1:1',
// pose: 'standing',
// imageUrl: lts,
// description: 'Classic humanoid proportions suitable for Arduino-based robots',
// compatibility: ['arduino_uno', 'servo_motor'],
// headToBodyRatio: '5:5'
// }
// ],
// raspberry_pi: [
// {
// id: 'advanced_humanoid',
// name: 'Advanced Humanoid',
// proportions: 'Head:Body = 1:2.5',
// pose: 'dynamic',
// imageUrl: mk2dy,
// description: 'Sophisticated design leveraging Raspberry Pi capabilities',
// compatibility: ['raspberry_pi', 'camera_module'],
// headToBodyRatio: '6:4'
// },
// ],
// esp32: [
// {
// id: 'iot_character',
// name: 'IoT Character',
// proportions: 'Head:Body = 1:6',
// pose: 'connected',
// imageUrl: '/src/assets/sketches/esp32_compact.svg',
// description: 'IoT-enabled character design for ESP32',
// compatibility: ['esp32']
// }
// ],
// servo_motor: [
// {
// id: 'articulated_figure',
// name: 'Articulated Figure',
// proportions: 'Head:Body = 1:7',
// pose: 'articulated',
// imageUrl: '/src/assets/sketches/arduino_standing.svg',
// description: 'Design emphasizing joint movement and servo integration',
// compatibility: ['servo_motor', 'arduino_uno']
// }
// ],
// led_matrix: [
// {
// id: 'display_character',
// name: 'Display Character',
// proportions: 'Head:Body = 1:6',
// pose: 'expressive',
// imageUrl: '/src/assets/sketches/arduino_sitting.svg',
// description: 'Character design featuring LED matrix display integration',
// compatibility: ['led_matrix', 'arduino_uno']
// }
// ],
// camera_module: [
// {
// id: 'vision_robot',
// name: 'Vision Robot',
// proportions: 'Head:Body = 1:6',
// pose: 'observing',
// imageUrl: '/src/assets/sketches/raspberry_action.svg',
// description: 'Design optimized for camera module and computer vision',
// compatibility: ['camera_module', 'raspberry_pi']
// }
// ]
// });
//
const availableSketches = computed(() => {
@ -984,7 +984,7 @@ onMounted(() => {
cursor: pointer;
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
background-color: var(--bg-color, rgba(255, 255, 255, 0.05));
height: 60px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;

View File

@ -14,7 +14,7 @@
<router-link to="/" class="brand-link">
<div class="brand-logo">
<div class="logo-icon">
<img src="@/assets/logo.png" alt="Logo" class="logo-image" />
<img src="@/assets/logo.webp" alt="Logo" class="logo-image" />
</div>
<span class="brand-name">{{ t('app.title') }}</span>
</div>

View File

@ -153,7 +153,7 @@ watch(() => window.location.pathname, () => {
.main-layout {
display: flex;
flex-direction: column;
min-height: 98vh;
min-height: 100vh;
/* 移除 overflow: hidden 以允许页面滚动 */
position: relative;
}
@ -171,8 +171,9 @@ watch(() => window.location.pathname, () => {
/* 第二行:内容区域容器(侧边栏 + 主内容) */
.content-row {
display: flex;
flex: 1;
overflow: hidden;
/* flex: 1; */
height: calc(100vh - 100px);
/* overflow: hidden; */
}
/* 侧边栏容器 */
@ -180,7 +181,7 @@ watch(() => window.location.pathname, () => {
position: relative;
flex-shrink: 0;
width: 120px;
/* height: 100%; */
height: 100%;
background: var(--sidebar-bg, #ffffff);
border-right: 1px solid var(--border-color, #e5e7eb);
z-index: 1000;
@ -201,10 +202,13 @@ watch(() => window.location.pathname, () => {
flex: 1;
display: flex;
flex-direction: column;
width: 100%;
/* width: 100%; */
/* 移除 overflow: hidden 以允许页面滚动 */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
height: 100%;
overflow-y: auto;
background-color: red;
}
.breadcrumb-container {
@ -220,7 +224,7 @@ watch(() => window.location.pathname, () => {
height: 64px;
background: var(--header-bg, #ffffff);
border-bottom: 1px solid var(--border-color, #e5e7eb);
z-index: 999;
z-index: 9999;
position: sticky;
top: 0;
}
@ -230,7 +234,7 @@ watch(() => window.location.pathname, () => {
/* overflow-y: auto; */
background: var(--content-bg, #f8fafc);
width: 100%;
height:90vh;
height:100%;
}
/* 移动端样式 */

View File

@ -3,6 +3,9 @@
class="ip-card-container"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
:class="{ 'controls-visible': isControlsVisible }"
>
<!-- 主卡片区域 - 替换为3D模型展示容器 -->
<div class="ip-card-wrapper" >
@ -12,29 +15,20 @@
:style="cardStyle"
>
<!-- 如果没有模型URL但有图片URL显示图片 -->
<!-- !currentModelUrl && props.cardData.imageUrl -->
<div v-if="!currentModelUrl && props.cardData.imageUrl" class="image-preview">
<img
:src="props.cardData.imageUrl"
:alt="altText"
class="preview-image"
@load="handleImageLoad"
@error="handleImageError"
/>
<!-- 生成模型按钮 -->
<button
v-if="!isGenerating"
class="generate-model-btn"
@click.stop="handleGenerateModel"
>
{{ t('modelCard.generateModelButton') }}
</button>
<!-- 生成进度指示器 -->
<div v-else class="generating-indicator">
<div class="spinner"></div>
<span>{{ t('modelCard.generatingIndicator') }}</span>
<div class="generating-indicator">
<!-- <div class="spinner"></div> -->
<!-- <span>{{ t('modelCard.generatingIndicator') }}</span> -->
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercentage + '%' }">
</div>
<div
class="progress-background-text"
@ -69,7 +63,7 @@
<!-- 右侧控件区域 -->
<div class="right-controls-container" @click.stop>
<!-- 右侧圆形按钮控件 -->
<div class="right-circular-controls" v-if="generatedModelUrl">
<div class="right-circular-controls" v-if="currentModelUrl">
<button class="control-button rotate-btn" title="detail" @click="handleCardClick">
<span class="btn-icon">🔍</span>
</button>
@ -119,9 +113,12 @@ const Meshy = new MeshyServer();
const showRightControls = ref(false);
const threeModelViewer = ref(null);
const isRotating = ref(true);
const isGenerating = ref(false);//
const isGenerating = ref(true);//
const isInitializing = ref(true); //
const progressPercentage = ref(0); //
//
const isTouching = ref(false);
const isControlsVisible = ref(false);
// -
const handleCardClick = () => {
// URL
@ -167,6 +164,7 @@ const TaskStatus = (result,resultTask)=>{
resultTask:resultTask,
},(modelUrl)=>{
if(modelUrl){
console.log(modelUrl,'modelUrlmodelUrlmodelUrl');
//
generatedModelUrl.value = modelUrl;
isGenerating.value = false;
@ -177,9 +175,10 @@ const TaskStatus = (result,resultTask)=>{
taskId:result,
status:'success'
});
}else{
emit('delete');
}
},(error)=>{
console.error('模型生成失败:', error);
emit('delete');
},(progress)=>{
if (progress !== undefined) {
@ -190,7 +189,6 @@ const TaskStatus = (result,resultTask)=>{
// URLURL
onMounted(() => {
// handleGenerateModel();
// return
switch (props.cardData.status) {
case 'loading':
handleGenerateModel();
@ -215,6 +213,24 @@ const handleMouseLeave = () => {
//
showRightControls.value = false;
};
//
const handleTouchStart = () => {
isTouching.value = true;
setTimeout(() => {
if (isTouching.value) {
isControlsVisible.value = true;
}
}, 100); //
};
//
const handleTouchEnd = () => {
isTouching.value = false;
setTimeout(() => {
isControlsVisible.value = false;
}, 3000); // 3
};
//
const handleImageLoad = (event) => {
const img = event.target;
@ -406,14 +422,14 @@ const cardStyle = computed(() => {
align-items: center;
gap: 10px;
padding: 15px 25px;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 20px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 10px;
color: white;
font-size: 14px;
z-index: 10;
min-width: 200px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(5px);
width: 90%;
}
/* 进度条样式 */
@ -586,6 +602,17 @@ justify-content: center;
pointer-events: auto;
}
/* 移动端适配:点击卡片后显示功能按钮 */
@media (max-width: 768px) {
/* 点击卡片容器时显示功能按钮 */
.ip-card-container:active .right-circular-controls,
.ip-card-container.controls-visible .right-circular-controls {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
}
}
.control-button {
width: 48px;
height: 48px;

View File

@ -123,6 +123,11 @@ export default {
modelModal: {
customizeToHome: '定制到家'
},
modelCard: {
generateModelButton: '生成模型',
generatingIndicator: '正在生成模型...',
progressText: '{percentage}%'
},
orderProcess: {
title: '定制到家流程',
subtitle: '了解您的订单从支付到发货的全过程',
@ -514,7 +519,11 @@ export default {
remaining: '剩余支付时间',
expired: '已超时'
},
expiredNotice: '订单已超时,无法支付,请重新下单'
expiredNotice: '订单已超时,无法支付,请重新下单',
confirm: {
title: '确认收货',
message: '是否确定收货'
}
},
logistics: {
title: '物流状态',
@ -524,7 +533,9 @@ export default {
service: '服务类型',
estimatedDelivery: '预计送达',
currentLocation: '当前位置',
lastUpdate: '最后更新时间'
lastUpdate: '最后更新时间',
status: '物流状态',
timeline: '物流时间线'
},
login: {
divider_text: '或',
@ -563,6 +574,9 @@ export default {
theme_toggle_dark: '切换到深色主题',
forgot_password: '忘记密码?',
register_account: '注册账号',
invite_code_label: '邀请码',
invite_code_placeholder: '请输入邀请码',
invite_code_empty_error: '请输入邀请码',
},
payment: {
methods: '支付方式',
@ -1229,6 +1243,11 @@ export default {
modelModal: {
customizeToHome: 'Customize to Home'
},
modelCard: {
generateModelButton: 'Generate Model',
generatingIndicator: 'Generating model...',
progressText: '{percentage}%'
},
orderProcess: {
title: 'Customize to Home Process',
subtitle: 'Understand the complete process of your order from payment to delivery',
@ -1619,17 +1638,23 @@ export default {
remaining: 'Remaining Payment Time',
expired: 'Expired'
},
expiredNotice: 'Order has expired and cannot be paid, please place a new order'
expiredNotice: 'Order has expired and cannot be paid, please place a new order',
confirm: {
title: 'Confirm Receipt',
message: 'Are you sure you have received the order? This action cannot be undone.'
}
},
logistics: {
title: 'Logistics Status',
trackingNumber: 'Tracking Number',
carrierInfo: 'Carrier Information',
carrier: 'Courier Company',
carrier: 'Carrier',
service: 'Service Type',
estimatedDelivery: 'Estimated Delivery',
currentLocation: 'Current Location',
lastUpdate: 'Last Update Time'
lastUpdate: 'Last Updated',
status: 'Status',
timeline: 'Tracking Timeline'
},
login: {
divider_text: 'Or',
@ -1668,6 +1693,9 @@ export default {
theme_toggle_dark: 'Switch to dark theme',
forgot_password: 'Forgot Password?',
register_account: 'Register Account',
invite_code_label: 'Invite Code',
invite_code_placeholder: 'Please enter invite code',
invite_code_empty_error: 'Please enter invite code',
},
payment: {
methods: 'Payment Methods',

View File

@ -16,7 +16,7 @@ import i18nConfig from './locales/index.js'
import router from './router'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import VueLazyload from 'vue3-lazyload'
// import VueLazyload from 'vue3-lazyload'
import 'element-plus/dist/index.css'
import 'nprogress/nprogress.css'
import {ElMessage,ElLoading } from 'element-plus'
@ -47,10 +47,10 @@ app.use(router)
app.use(ElementPlus)
// Lazyload
app.use(VueLazyload, {
loading: '/logo.png',
error: '/logo.png',
})
// app.use(VueLazyload, {
// loading: '/logo.png',
// error: '/logo.png',
// })
// Configure Element Plus ElMessage component position via individual message calls
// Using default ElMessage configuration - no global config needed

View File

@ -139,8 +139,8 @@ const router = createRouter({
router.beforeEach(async (to, from, next) => {
NProgress.start()
// if(window.location.hostname=='localhost'){
// window.localStorage.setItem('token','123')
// return next()
window.localStorage.setItem('token','123')
return next()
// }
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token')

View File

@ -279,113 +279,7 @@ export const useOrderStore = defineStore('orders', () => {
// 初始化示例数据(用于测试)
const initSampleData = () => {
const now = Date.now()
const sampleOrders = [
{
id: '1001',
orderId: 'ORD-2025-001',
customerName: '张三',
customerEmail: 'zhangsan@example.com',
customerPhone: '13800138000',
status: 'pending',
amount: 1499.0,
total: 1499.0,
currency: 'CNY',
products: [
{ id: 'p1', name: '玩偶机器人 A', price: 1499.0, quantity: 1, image: '/src/assets/demo.png' }
],
shipping: { recipientName: '张三', phone: '13800138000', address: '北京市朝阳区科技路88号' },
payment: { method: 'stripe', status: 'pending' },
expiresAt: new Date(now + 15 * 60 * 1000).toISOString()
},
{
id: '1002',
orderId: 'ORD-2025-002',
customerName: '李四',
customerEmail: 'lisi@example.com',
customerPhone: '13900139000',
status: 'paid',
amount: 2999.0,
total: 2999.0,
currency: 'CNY',
products: [
{ id: 'p2', name: '玩偶机器人 B', price: 2999.0, quantity: 1, image: '/src/assets/demo1.png' }
],
shipping: { recipientName: '李四', phone: '13900139000', address: '上海市浦东新区创新路66号' },
payment: { method: 'stripe', status: 'paid', paidAt: new Date(now - 2 * 60 * 60 * 1000).toISOString() },
tracking: { courier: '顺丰速运', number: 'SF1234567890' },
timelineEvents: [
{ type: 'order_created', timestamp: new Date(now - 3 * 60 * 60 * 1000).toISOString(), description: '订单已创建', location: '上海' },
{ type: 'payment_confirmed', timestamp: new Date(now - 2 * 60 * 60 * 1000).toISOString(), description: '支付已确认', location: '线上' }
]
},
{
id: '1003',
orderId: 'ORD-2025-003',
customerName: '王五',
customerEmail: 'wangwu@example.com',
customerPhone: '13700137000',
status: 'shipped',
amount: 1999.0,
total: 1999.0,
currency: 'CNY',
products: [
{ id: 'p3', name: '玩偶机器人 C', price: 1999.0, quantity: 1, image: '/src/assets/demo3.png' }
],
shipping: { recipientName: '王五', phone: '13700137000', address: '北京市海淀区学院路1号' },
payment: { method: 'stripe', status: 'paid', paidAt: new Date(now - 24 * 60 * 60 * 1000).toISOString() },
tracking: { courier: '顺丰速运', number: 'SF9876543210' },
timelineEvents: [
{ type: 'order_created', timestamp: new Date(now - 26 * 60 * 60 * 1000).toISOString(), description: '订单已创建' },
{ type: 'payment_confirmed', timestamp: new Date(now - 24 * 60 * 60 * 1000).toISOString(), description: '支付确认' },
{ type: 'shipped', timestamp: new Date(now - 20 * 60 * 60 * 1000).toISOString(), description: '包裹已发出', location: '北京' },
{ type: 'in_transit', timestamp: new Date(now - 10 * 60 * 60 * 1000).toISOString(), description: '运输中', location: '廊坊中转站' }
]
},
{
id: '1004',
orderId: 'ORD-2025-004',
customerName: '赵六',
customerEmail: 'zhaoliu@example.com',
customerPhone: '13600136000',
status: 'completed',
amount: 2499.0,
total: 2499.0,
currency: 'CNY',
products: [
{ id: 'p4', name: '玩偶机器人 D', price: 2499.0, quantity: 1, image: '/src/assets/demo4.png' }
],
shipping: { recipientName: '赵六', phone: '13600136000', address: '广州市天河区科华街9号' },
payment: { method: 'stripe', status: 'paid', paidAt: new Date(now - 72 * 60 * 60 * 1000).toISOString() },
tracking: { courier: '顺丰速运', number: 'SF2468135790' },
timelineEvents: [
{ type: 'order_created', timestamp: new Date(now - 80 * 60 * 60 * 1000).toISOString(), description: '订单已创建' },
{ type: 'payment_confirmed', timestamp: new Date(now - 72 * 60 * 60 * 1000).toISOString(), description: '支付确认' },
{ type: 'shipped', timestamp: new Date(now - 60 * 60 * 60 * 1000).toISOString(), description: '已发货' },
{ type: 'in_transit', timestamp: new Date(now - 48 * 60 * 60 * 1000).toISOString(), description: '运输中' },
{ type: 'out_for_delivery', timestamp: new Date(now - 24 * 60 * 60 * 1000).toISOString(), description: '派送中' },
{ type: 'delivered', timestamp: new Date(now - 12 * 60 * 60 * 1000).toISOString(), description: '已送达', location: '广州天河区' }
]
},
{
id: '1005',
orderId: 'ORD-2025-005',
customerName: '钱七',
customerEmail: 'qianqi@example.com',
customerPhone: '13500135000',
status: 'expired',
amount: 1299.0,
total: 1299.0,
currency: 'CNY',
products: [
{ id: 'p5', name: '玩偶机器人 E', price: 1299.0, quantity: 1, image: '/src/assets/demo5.png' }
],
shipping: { recipientName: '钱七', phone: '13500135000', address: '成都市高新区创业路12号' },
payment: { method: 'stripe', status: 'pending' },
expiresAt: new Date(now - 10 * 60 * 1000).toISOString()
}
]
const sampleOrders = []
setOrders(sampleOrders)
}

View File

@ -3,7 +3,9 @@
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<h1 class="page-title">{{ t('agentManagement.pageTitle') }}</h1>
<h1 class="page-title">
<!-- {{ t('agentManagement.pageTitle') }} -->
</h1>
<div class="header-actions">
<div class="search-container">
<el-input
@ -23,7 +25,7 @@
</div>
</div>
<div style="height: 10px;width: 100%"></div>
<el-scrollbar height="83vh" @end-reached="loadMore">
<el-scrollbar v-if="false" height="100%" @end-reached="loadMore">
<div class="agents-section">
<!-- 智能体列表 -->
<div class="agents-grid">
@ -332,7 +334,7 @@ onMounted(() => {
<style scoped>
.agent-management {
min-height: 100vh;
height: 100%;
background: var(--bg-color, #F3F4F6);
}
@ -515,7 +517,7 @@ onMounted(() => {
/* 暗色主题样式 */
:root.dark .agent-management {
background: linear-gradient(135deg, #1F2937 0%, #111827 50%, #030712 100%);
min-height: 100vh;
height: 100%;
}
:root.dark .page-title {

View File

@ -8,7 +8,7 @@
style="display: none"
@change="handleFileSelect"
/>
<el-scrollbar height="88vh" @end-reached="loadMore">
<el-scrollbar height="100%" @end-reached="loadMore">
<!-- 项目卡片网格 -->
<div class="projects-grid">
@ -459,6 +459,7 @@ export default {
padding: 24px;
margin: 0 auto;
position: relative;
height: 100%;
}
.page-header {
margin-bottom: 32px;

View File

@ -23,46 +23,22 @@
<div class="google-login-section">
<GoogleOAuthButton
@success="handleLoginSuccess"
:disabled="!isInviteCodeValid"
/>
</div>
<!-- 分割线 -->
<div class="divider">
<div class="divider-line"></div>
<span class="divider-text">{{ t('login.divider_text') }}</span>
<div class="divider-line"></div>
</div>
<!-- 邮箱登录表单 -->
<div class="email-login-section">
<LoginForm
@login="handleLogin"
@update:inviteCode="updateInviteCode"
/>
</div>
<!-- 角色信息展示 -->
<div class="role-info-section" v-if="false">
<div class="role-info-card">
<div class="role-info-header">
<el-icon class="role-icon"><InfoFilled /></el-icon>
<h3 class="role-info-title">{{ t('login.role_system') }}</h3>
</div>
<div class="role-list">
<div class="role-item">
<div class="role-badge creator">{{ t('login.creator_role') }}</div>
<p class="role-description">{{ t('login.creator_desc') }}</p>
</div>
<div class="role-item">
<div class="role-badge admin">{{ t('login.admin_role') }}</div>
<p class="role-description">{{ t('login.admin_desc') }}</p>
</div>
<div class="role-item">
<div class="role-badge viewer">{{ t('login.viewer_role') }}</div>
<p class="role-description">{{ t('login.viewer_desc') }}</p>
</div>
</div>
</div>
</div>
<!-- 错误提示 -->
<div v-if="authStore.error" class="error-message">
<el-icon class="error-icon"><WarningFilled /></el-icon>
@ -96,7 +72,7 @@
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { onMounted, reactive, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
@ -111,6 +87,18 @@ const router = useRouter()
const authStore = useAuthStore()
const { t } = useI18n()
const plugin = reactive(new LOGIN());
//
const inviteCode = ref('')
const isInviteCodeValid = computed(() => {
return inviteCode.value.trim() !== ''
})
//
const updateInviteCode = (value) => {
inviteCode.value = value
}
const handleLogin = async (data) => {
plugin.login(data)
}

View File

@ -6,7 +6,7 @@
<div class="content">
<section class="left-panel">
<div class="hero">
<img :src="heroImage" alt="Custom Models" />
<!-- <img :src="heroImage" alt="Custom Models" /> -->
</div>
<div class="info-block">
<h3 class="block-title">说明</h3>
@ -58,7 +58,6 @@ import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElIcon } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import heroImage from '@/assets/demo4.png'
const route = useRoute()
const router = useRouter()

View File

@ -5,7 +5,7 @@
<div class="welcome-content">
<div class="welcome-left">
<div class="welcome-logo">
<img src="@/assets/logo.png" alt="Logo" class="welcome-logo-image" />
<img src="@/assets/logo.webp" alt="Logo" class="welcome-logo-image" />
<div class="logo-glow"></div>
</div>
<h1 class="welcome-title">

View File

@ -5,7 +5,7 @@
<div class="welcome-content">
<div class="welcome-left">
<div class="welcome-logo">
<img src="@/assets/logo.png" alt="Logo" class="welcome-logo-image" />
<img src="@/assets/logo.webp" alt="Logo" class="welcome-logo-image" />
<div class="logo-glow"></div>
</div>
<h1 class="welcome-title">

View File

@ -71,6 +71,9 @@
<span class="tracking-number">{{ $t('logistics.trackingNumber') }}: {{ logisticsData.trackingNo }}</span>
</div>
<!-- 左右并排布局容器 -->
<div class="logistics-content-wrapper">
<!-- 左侧承运信息卡片 -->
<div class="logistics-info">
<div class="info-card">
<div class="card-header">
@ -96,12 +99,12 @@
</div>
</div>
<el-divider>{{ $t('logistics.timeline') }}</el-divider>
<!-- 右侧物流时间线 -->
<div class="logistics-timeline-container">
<div class="timeline-title">{{ $t('logistics.timeline') }}</div>
<el-timeline>
<el-timeline-item
v-for="(trace, index) in logisticsData.traces"
v-for="(trace, index) in logisticsData.traces.slice().reverse()"
:key="index"
:timestamp="trace.time"
:type="index === 0 ? 'success' : 'primary'"
@ -115,6 +118,7 @@
</div>
</div>
</div>
</div>
<div v-else-if="isLoading" class="loading-container">
<el-skeleton :rows="10" animated />
</div>
@ -217,13 +221,13 @@ onMounted(() => {
</script>
<style scoped>
.order-detail { padding: 24px; }
.order-detail { padding: 24px; height: 100vh; display: flex; flex-direction: column; }
.detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.header-left { display: flex; align-items: center; gap: 8px; }
.back-btn { padding: 0 8px; }
.detail-title { margin: 0; font-size: 20px; font-weight: 700; }
.detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
.detail-card { background: var(--card-bg, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 12px; padding: 16px; }
.detail-card { background: var(--card-bg, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 12px; padding: 16px; flex-shrink: 0; }
.card-title { margin: 0 0 8px 0; font-size: 16px; font-weight: 600; }
.products-list { display: flex; flex-direction: column; gap: 8px; }
.product-item { display: flex; align-items: center; gap: 8px; padding: 8px; background: var(--bg-secondary, #f9fafb); border-radius: 8px; }
@ -237,7 +241,7 @@ onMounted(() => {
.payment-info p strong { display: inline-block; min-width: 66px; margin-right: 8px; }
.countdown { font-size: 14px; color: var(--text-secondary, #6b7280); display: flex; align-items: center; }
.detail-card p { display: flex; align-items: center; }
.timeline-section { margin-top: 16px; }
.timeline-section { margin-top: 16px; flex: 1; display: flex; flex-direction: column; }
.expired-notice { display: flex; align-items: center; gap: 8px; padding: 10px; background: var(--el-color-danger-light, #fee2e2); border: 1px solid var(--el-color-danger, #ef4444); border-radius: 8px; color: var(--el-color-danger-dark, #7f1d1d); }
/* 物流信息区域样式 */
@ -247,6 +251,9 @@ onMounted(() => {
border-radius: 12px;
padding: 24px;
margin-top: 16px;
flex: 1;
display: flex;
flex-direction: column;
}
.timeline-header {
@ -271,11 +278,19 @@ onMounted(() => {
font-weight: 500;
}
.logistics-info {
/* 左右并排布局容器 */
.logistics-content-wrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
margin-bottom: 24px;
grid-template-columns: 300px 1fr;
gap: 24px;
margin-top: 16px;
flex: 1;
overflow: hidden;
}
/* 左侧:承运信息卡片 */
.logistics-info {
margin: 0;
}
.info-card {
@ -283,6 +298,7 @@ onMounted(() => {
border-radius: 8px;
padding: 16px;
border: 1px solid var(--border-color, #e5e7eb);
height: fit-content;
}
.card-header {
@ -317,12 +333,23 @@ onMounted(() => {
font-size: 14px;
}
/* 物流时间线样式 */
/* 时间线标题 */
.timeline-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #1F2937);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
/* 右侧:物流时间线样式 */
.logistics-timeline-container {
max-height: 400px;
max-height: none;
height: 38%;
overflow-y: auto;
padding-right: 8px;
margin-top: 16px;
border-radius: 8px;
scrollbar-width: thin;
scrollbar-color: #d1d5db #f3f4f6;
@ -492,8 +519,10 @@ onMounted(() => {
@media (max-width: 768px) {
.order-detail { padding: 16px; }
.logistics-info {
/* 小屏幕下切换为垂直布局 */
.logistics-content-wrapper {
grid-template-columns: 1fr;
gap: 16px;
}
.timeline-header {

View File

@ -15,7 +15,10 @@ export class OrderManagement {
return requestUtils.common(clientApi.default.orderCancel, params);
}
//确认收货
receiveAddress(params){
receiveAddress(order_id){
let params = {
id: order_id
}
return requestUtils.common(clientApi.default.receiveAddress, params);
}
//退款订单

View File

@ -53,6 +53,7 @@
@pay-order="handlePayOrder"
@cancel-order="handleCancelOrder"
@expired-order="handleExpiredOrder"
@confirm-order="handleConfirmOrder"
/>
</div>
</el-scrollbar>
@ -67,32 +68,20 @@
</div>
</template>
<script>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { DocumentDelete, Search } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import OrderCard from '@/components/OrderCard.vue'
import StripePaymentForm from '@/components/StripePaymentForm.vue'
import { useOrderStore } from '@/stores/orders'
import {OrderManagement} from './OrderManagement';
import {OrderManagement} from './OrderManagement'
import {orderStatus} from '@deotaland/utils'
const orderPlug = new OrderManagement()
export default {
name: 'OrderManagement',
components: {
DocumentDelete,
Search,
OrderCard,
StripePaymentForm
},
setup() {
const { t } = useI18n()
const router = useRouter()
const orderStore = useOrderStore()
const paymentDialogVisible = ref(false)
const currentPaymentOrder = ref(null)
const ordersToShow = computed(() => orderStore.paginatedOrders || [])
const selectedStatus = ref('all')
const statusFilters = orderStatus.selectList('1');
const sortOptions = ref([
@ -106,30 +95,22 @@ export default {
const handlePayOrder = (orderData) => {
window.location.href = orderData.stripe_url
}
const onPaymentSuccess = ({ orderId }) => {
orderStore.updateOrder(orderId, {
status: 'paid',
payment: {
...(orderStore.getOrder(orderId)?.payment || {}),
status: 'paid',
paidAt: new Date().toISOString(),
method: 'stripe'
//
const handleConfirmOrder = (id)=>{
ElMessageBox.confirm(
t('orderManagement.confirm.message'),
t('orderManagement.confirm.title'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
).then(() => {
orderPlug.receiveAddress(id).then(res => {
init()
})
})
paymentDialogVisible.value = false
currentPaymentOrder.value = null
}
const onPaymentError = () => {
paymentDialogVisible.value = false
currentPaymentOrder.value = null
}
const onPaymentCancel = () => {
paymentDialogVisible.value = false
currentPaymentOrder.value = null
}
const handleCancelOrder = (orderData) => {
ElMessageBox.confirm(
t('orderManagement.cancelConfirm.message'),
@ -159,7 +140,6 @@ export default {
orderStore.updateOrder(orderId, { status: 'expired' })
}
const toCents = (amount) => Math.round((amount || 0) * 100)
const page = ref(1);
const page_size = ref(10);
//
@ -227,35 +207,9 @@ export default {
init()
}
watch(sort_by, () => {
init();
})
return {
t,
paymentDialogVisible,
currentPaymentOrder,
ordersToShow,
selectedStatus,
statusFilters,
sortOptions,
viewOrderDetails,
handlePayOrder,
handleCancelOrder,
handleExpiredOrder,
onPaymentSuccess,
onPaymentError,
onPaymentCancel,
toCents,
selectStatus,
order_list,
order_no,
sort_by,
loadMore
}
}
}
</script>
<style scoped>

View File

@ -1,5 +1,5 @@
<template>
<div class="creative-zone" :style="{ '--grid-size': `${gridSize}px` }">
<div class="creative-zone" @contextmenu.prevent>
<!-- 顶部固定头部组件 -->
<div class="header-wrapper">
<HeaderComponent :freeImageCount="Limits.generateCount" :freeModelCount="Limits.modelCount" :projectName="projectInfo.title" @updateProjectInfo="projectInfo = {...projectInfo, ...$event}" @openGuideModal="showGuideModal = true" />
@ -40,9 +40,15 @@
class="draggable-element"
:style="getElementStyle(index)"
@mousedown="startElementDrag($event, index)"
@touchstart="startElementDrag($event, index)"
@mousemove="dragElement"
@touchmove="dragElement"
@mouseup="stopElementDrag"
@touchend="stopElementDrag"
@mouseenter="bringToFront(index)"
@mouseleave="sendToBack(index)"
>
<!-- @mouseleave="stopElementDrag" -->
<!-- 删除按钮 -->
<button
v-if="(card.imageUrl&&card.type==='image')||(card.type==='model'&&card.modelUrl)"
@ -58,6 +64,7 @@
<!-- 根据卡片类型显示不同组件 -->
<IPCard
@preview-image="handlePreviewImage"
@generate-smooth-white-model="(imageUrl)=>handleGenerateSmoothWhiteModel(index,imageUrl)"
@create-new-card="(data)=>handleCreateFourViewCard(index,data)"
@delete="handleDeleteCard(index)"
@ -103,6 +110,14 @@
@complete="completeGuide"
/>
<!-- 图片预览弹窗 -->
<ImagePreviewModal
:visible="showImagePreview"
:images="previewImages"
:initialIndex="currentImageIndex"
@close="showImagePreview = false"
/>
<!-- 测试侧边栏动画的按钮 -->
<!-- <button
class="test-animation-btn"
@ -124,6 +139,7 @@ import ModelModal from '../../components/ModelModal/index.vue';
import CharacterImportModal from '../../components/CharacterImportModal/index.vue';
import HeaderComponent from '../../components/HeaderComponent/HeaderComponent.vue';
import GuideModal from '../../components/GuideModal/index.vue';
import ImagePreviewModal from '../../components/ImagePreviewModal/index.vue';
import {useRoute,useRouter} from 'vue-router';
import {MeshyServer,GiminiServer} from '@deotaland/utils';
import { ElMessage } from 'element-plus';
@ -142,6 +158,11 @@ const selectedModel = ref(null);
const showImportModal = ref(false);
const importUrl = ref('https://xiaozhi.me/console/agents');
const showGuideModal = ref(false);
//
const showImagePreview = ref(false);
const previewImages = ref([]);
const currentImageIndex = ref(0);
//
const cleanupFunctions = ref({});
const projectId = ref(null);
@ -162,7 +183,20 @@ watch(()=>[projectInfo.value,cards.value], () => {
newProjectInfo.details.node_card = cards.value;
updateProjectInfo(newProjectInfo)
}, {deep: true});
const handlePreviewImage = (imageUrl) => {
console.log('点击了图片', imageUrl);
//
if (typeof imageUrl === 'string') {
previewImages.value = [imageUrl];
} else if (Array.isArray(imageUrl)) {
previewImages.value = imageUrl;
} else {
console.error('无效的图片URL:', imageUrl);
return;
}
currentImageIndex.value = 0;
showImagePreview.value = true;
}
//
const handleSaveProject = (index,item,type='image')=>{
let cardItem = cards.value[index];
@ -520,6 +554,7 @@ const handleModelGenerated = (modelData) => {
//
const isElementDragging = ref(false);
const isSceneDragging = ref(false);
const isPinchZooming = ref(false); //
const draggedElementIndex = ref(null);
const elementStartX = ref(0);
const elementStartY = ref(0);
@ -531,6 +566,14 @@ const scale = ref(1); // 缩放比例
const zIndexCounter = ref(100); // z-index
const originalZIndexes = ref(new Map()); // z-index
//
const initialPinchDistance = ref(0); //
const initialPinchCenterX = ref(0); // X
const initialPinchCenterY = ref(0); // Y
const initialScale = ref(1); //
const initialOffsetX = ref(0); // X
const initialOffsetY = ref(0); // Y
//
const elementVelocityX = ref(0);
const elementVelocityY = ref(0);
@ -543,22 +586,9 @@ const animationFrameId = ref(null);
const sceneContainerStyle = computed(() => ({
transform: `translate(${sceneOffsetX.value}px, ${sceneOffsetY.value}px) scale(${scale.value})`,
transformOrigin: 'center center', //
transition: isSceneDragging.value ? 'none' : 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)' //
transition: 'none' //
}));
//
const gridSize = computed(() => {
// 20px
// 120px
//
//
const baseSize = 20;
// 使
//
const adjustedSize = baseSize * scale.value;
return Math.max(5, Math.min(50, adjustedSize));
});
//
const getElementStyle = (index) => {
const card = cards.value[index];
@ -594,10 +624,19 @@ const contentCursor = computed(() => {
return isSceneDragging.value ? 'grabbing' : 'grab';
});
//
const isMobile = computed(() => {
return window.innerWidth <= 768;
});
//
const startElementDrag = (e, index) => {
e.stopPropagation(); //
e.preventDefault(); //
//
if (!isMobile.value) {
e.preventDefault(); //
}
//
if (e.target.closest('.control-button')) {
@ -608,12 +647,15 @@ const startElementDrag = (e, index) => {
isSceneDragging.value = false;
draggedElementIndex.value = index;
cards.value[index].zIndex =cards.value.length+1;
//
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
//
const currentOffsetX = parseFloat(cards.value[index].offsetX) || 0;
const currentOffsetY = parseFloat(cards.value[index].offsetY) || 0;
//
elementStartX.value = clientX - (currentOffsetX * scale.value);
elementStartY.value = clientY - (currentOffsetY * scale.value);
@ -624,7 +666,10 @@ const startElementDrag = (e, index) => {
//
const dragElement = (e) => {
if (isElementDragging.value && draggedElementIndex.value !== null) {
//
if (!isMobile.value) {
e.preventDefault(); //
}
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
@ -700,8 +745,39 @@ const startSceneDrag = (e) => {
//
if (e.target.closest('.main-content') && !e.target.closest('.floating-sidebar')) {
//
if (e.touches && e.touches.length === 2) {
//
if (!isMobile.value) {
e.preventDefault();
}
isPinchZooming.value = true;
isSceneDragging.value = false;
//
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
initialPinchDistance.value = distance;
//
const centerX = (touch1.clientX + touch2.clientX) / 2;
const centerY = (touch1.clientY + touch2.clientY) / 2;
initialPinchCenterX.value = centerX;
initialPinchCenterY.value = centerY;
//
initialScale.value = scale.value;
initialOffsetX.value = sceneOffsetX.value;
initialOffsetY.value = sceneOffsetY.value;
} else {
//
//
if (!isMobile.value) {
e.preventDefault();
}
isSceneDragging.value = true;
isPinchZooming.value = false;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
@ -709,12 +785,73 @@ const startSceneDrag = (e) => {
sceneStartX.value = clientX - sceneOffsetX.value;
sceneStartY.value = clientY - sceneOffsetY.value;
}
}
};
//
const calculateDistance = (touch1, touch2) => {
return Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
};
//
const dragScene = (e) => {
if (isSceneDragging.value) {
if (isPinchZooming.value) {
//
if (e.touches && e.touches.length === 2) {
//
if (!isMobile.value) {
e.preventDefault();
}
//
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = calculateDistance(touch1, touch2);
//
const scaleFactor = currentDistance / initialPinchDistance.value;
const newScale = Math.min(Math.max(initialScale.value * scaleFactor, 0.1), 5);
if (newScale !== scale.value) {
//
const currentCenterX = (touch1.clientX + touch2.clientX) / 2;
const currentCenterY = (touch1.clientY + touch2.clientY) / 2;
//
const container = e.currentTarget;
//
const rect = container.getBoundingClientRect();
const mouseX = currentCenterX - rect.left;
const mouseY = currentCenterY - rect.top;
// 600400
// 600400
//
const adjustedMouseX = isMobile.value ? mouseX : mouseX - 900;
const adjustedMouseY = isMobile.value ? mouseY : mouseY - 400;
//
const currentOffsetFromMouseX = adjustedMouseX - initialOffsetX.value;
const currentOffsetFromMouseY = adjustedMouseY - initialOffsetY.value;
// 使
const newOffsetFromMouseX = currentOffsetFromMouseX * (newScale / initialScale.value);
const newOffsetFromMouseY = currentOffsetFromMouseY * (newScale / initialScale.value);
//
sceneOffsetX.value = adjustedMouseX - newOffsetFromMouseX;
sceneOffsetY.value = adjustedMouseY - newOffsetFromMouseY;
//
scale.value = newScale;
}
}
} else if (isSceneDragging.value) {
//
//
if (!isMobile.value) {
e.preventDefault();
}
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
@ -729,12 +866,16 @@ const dragScene = (e) => {
//
const stopSceneDrag = () => {
isSceneDragging.value = false;
isPinchZooming.value = false; //
isElementDragging.value = false;
draggedElementIndex.value = null;
};
//
const handleWheel = (e) => {
//
if (!isMobile.value) {
e.preventDefault(); //
}
// CtrlWindows/LinuxCmdMac
if (e.ctrlKey || e.metaKey) {
@ -782,16 +923,22 @@ const handleWheel = (e) => {
const preventZoom = (e) => {
//
if ((e.ctrlKey || e.metaKey) && e.target.closest('.floating-sidebar')) {
//
if (!isMobile.value) {
e.preventDefault();
}
}
};
//
const preventPinchZoom = (e) => {
// 使
if (e.touches && e.touches.length > 1) {
//
if (e.touches && e.touches.length > 1 && e.target.closest('.floating-sidebar')) {
//
if (!isMobile.value) {
e.preventDefault();
}
}
};
//
import { addPassiveEventListener } from '@/utils/passiveEventListeners'
@ -879,6 +1026,15 @@ html.dark .header-wrapper {
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 50%, #e5e7eb 100%);
}
/* 移动端适配 */
@media (max-width: 768px) {
.creative-zone {
height: 100vh; /* 移动端使用视口高度 */
min-height: -webkit-fill-available; /* iOS Safari 支持 */
overflow: hidden;
}
}
/* 暗色主题适配 */
html.dark .creative-zone {
color: #ffffff; /* 暗色主题文字颜色 */
@ -992,13 +1148,25 @@ html {
-ms-text-size-adjust: 100%;
}
/* 防止用户选择文本和右键菜单 */
/* 移动端适配 - 移除全局用户选择限制 */
@media (max-width: 768px) {
body {
user-select: auto;
-webkit-user-select: auto;
-moz-user-select: auto;
-ms-user-select: auto;
}
}
/* 桌面端防止用户选择文本和右键菜单 */
@media (min-width: 769px) {
body {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
}
@ -1011,39 +1179,51 @@ body {
touch-action: none; /* 阻止触摸设备上的默认行为 */
}
/* 移动端主内容区域适配 */
@media (max-width: 768px) {
.main-content {
touch-action: pan-x pan-y; /* 移动端允许平移 */
user-select: auto; /* 移动端允许选择文本 */
}
}
.scene-container {
width: 100%;
height: 100%;
transition: transform 0.1s ease-out;
position: relative;
z-index: 1;
background:
/* 网格背景 */
linear-gradient(rgba(107, 70, 193, 0.2) 1px, transparent 1px),
linear-gradient(90deg, rgba(107, 70, 193, 0.2) 1px, transparent 1px),
radial-gradient(ellipse at center, rgba(255, 255, 255, 0.8) 0%, rgba(243, 244, 246, 0.8) 100%);
background-size: var(--grid-size) var(--grid-size), var(--grid-size) var(--grid-size), auto;
background-position: center center, center center, center;
/* 简约画布背景 */
radial-gradient(ellipse at center, rgba(255, 255, 255, 0.9) 0%, rgba(243, 244, 246, 0.9) 100%),
/* 方格子四个角点 */
radial-gradient(circle at 0 0, rgba(107, 70, 193, 0.3) 2px, transparent 3px),
radial-gradient(circle at 100% 0, rgba(107, 70, 193, 0.3) 2px, transparent 3px),
radial-gradient(circle at 0 100%, rgba(107, 70, 193, 0.3) 2px, transparent 3px),
radial-gradient(circle at 100% 100%, rgba(107, 70, 193, 0.3) 2px, transparent 3px);
background-size: 100% 100%, 30px 30px, 30px 30px, 30px 30px, 30px 30px;
background-position: center center, 0 0, 0 0, 0 0, 0 0;
border-radius: 8px;
overflow: hidden;
box-shadow:
inset 0 2px 8px rgba(0, 0, 0, 0.05),
0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
inset 0 1px 3px rgba(0, 0, 0, 0.03),
0 1px 3px rgba(0, 0, 0, 0.03);
}
/* 暗色主题下的场景容器 */
html.dark .scene-container {
background:
/* 网格背景 */
linear-gradient(rgba(255, 255, 255, 0.15) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.15) 1px, transparent 1px),
radial-gradient(ellipse at center, rgba(31, 41, 55, 0.8) 0%, rgba(17, 24, 39, 0.8) 100%);
background-size: var(--grid-size) var(--grid-size), var(--grid-size) var(--grid-size), auto;
background-position: center center, center center, center;
/* 简约画布背景 - 暗色主题 */
radial-gradient(ellipse at center, rgba(31, 41, 55, 0.9) 0%, rgba(17, 24, 39, 0.9) 100%),
/* 方格子四个角点 - 暗色主题 */
radial-gradient(circle at 0 0, rgba(167, 139, 250, 0.4) 2px, transparent 3px),
radial-gradient(circle at 100% 0, rgba(167, 139, 250, 0.4) 2px, transparent 3px),
radial-gradient(circle at 0 100%, rgba(167, 139, 250, 0.4) 2px, transparent 3px),
radial-gradient(circle at 100% 100%, rgba(167, 139, 250, 0.4) 2px, transparent 3px);
background-size: 100% 100%, 30px 30px, 30px 30px, 30px 30px, 30px 30px;
background-position: center center, 0 0, 0 0, 0 0, 0 0;
box-shadow:
inset 0 2px 8px rgba(0, 0, 0, 0.3),
0 2px 8px rgba(0, 0, 0, 0.2);
inset 0 1px 3px rgba(0, 0, 0, 0.2),
0 1px 3px rgba(0, 0, 0, 0.15);
}
.scene-content {
@ -1276,5 +1456,21 @@ p {
p {
font-size: 1rem;
}
/* 移动端场景容器适配 */
.scene-container {
touch-action: pan-x pan-y pinch-zoom; /* 允许平移和缩放 */
}
/* 移动端可拖动元素适配 */
.draggable-element {
touch-action: none; /* 拖动元素时阻止默认触摸行为 */
}
/* 移动端头部适配 */
.header-wrapper {
width: 100%;
border-radius: 0;
}
}
</style>

View File

@ -55,7 +55,7 @@ export default defineConfig({
manualChunks: {
vendor: ['vue', 'vue-router', 'vue-i18n'],
elementPlus: ['element-plus'],
utils: ['axios', 'dayjs']
// utils: ['axios', 'dayjs']
}
}
}

28
package-lock.json generated
View File

@ -38,7 +38,6 @@
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^14.1.0",
"axios": "^1.13.2",
"country-state-city": "^3.2.1",
"dayjs": "^1.11.13",
"element-plus": "^2.11.7",
@ -2612,7 +2611,8 @@
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/autoprefixer": {
"version": "10.4.22",
@ -2657,6 +2657,7 @@
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@ -2787,6 +2788,7 @@
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@ -2973,6 +2975,7 @@
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@ -3156,6 +3159,7 @@
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.4.0"
}
@ -3252,6 +3256,7 @@
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
@ -3345,6 +3350,7 @@
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@ -3354,6 +3360,7 @@
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@ -3363,6 +3370,7 @@
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0"
},
@ -3375,6 +3383,7 @@
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
@ -3823,6 +3832,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4.0"
},
@ -3853,6 +3863,7 @@
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@ -3952,6 +3963,7 @@
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -4000,6 +4012,7 @@
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
@ -4024,6 +4037,7 @@
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"peer": true,
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
@ -4137,6 +4151,7 @@
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
},
@ -4186,6 +4201,7 @@
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
},
@ -4198,6 +4214,7 @@
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"peer": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@ -4213,6 +4230,7 @@
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@ -4881,6 +4899,7 @@
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@ -4902,6 +4921,7 @@
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@ -4911,6 +4931,7 @@
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@ -5429,7 +5450,8 @@
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/punycode": {
"version": "2.3.1",

View File

@ -186,7 +186,7 @@ export class GiminiServer extends FileServer {
// "aspectRatio": "9:16"
// },
"aspect_ratio": "16:9",
"model": "gemini-3-pro-image-preview",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image",
"model": "gemini-2.5-flash-image",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image",
"location": "global",
"vertexai": true,
...config,