222
|
|
@ -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. 确保与现有功能兼容
|
||||
|
|
@ -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`
|
||||
|
||||
|
|
@ -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卡片中的图片时,会弹出一个全屏的图片预览窗口
|
||||
- 预览窗口支持缩放、旋转等操作
|
||||
- 点击预览窗口外部或关闭按钮可以关闭预览
|
||||
- 原有卡片样式和布局保持不变
|
||||
|
|
@ -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. 支持明暗主题切换
|
||||
|
|
@ -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
|
||||
- 点的颜色适配当前主题
|
||||
- 缩放和拖动画布时,点网格保持正确的位置关系
|
||||
- 不影响现有卡片元素的交互
|
||||
|
|
@ -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模块将呈现参考图所示的设计,支持中英文切换功能。当切换语言时,按钮文字会自动更新为对应语言,同时保持参考图的视觉设计。
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 759 KiB |
|
Before Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 475 KiB |
|
Before Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 343 KiB |
|
Before Width: | Height: | Size: 361 KiB |
|
Before Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
|
@ -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 |
|
|
@ -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 |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 375 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 371 KiB |
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ onBeforeUnmount(() => {
|
|||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.toolbar-content {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
<div class="model-viewer-wrapper" :class="{ 'fullscreen': fullScreen }">
|
||||
<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 v-if="isLoading" class="loading-overlay">
|
||||
<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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<!-- 邮箱输入框 -->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
// ,
|
||||
// {
|
||||
// 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 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
|
||||
// }
|
||||
// }
|
||||
// // ,
|
||||
// // {
|
||||
// // 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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
|
||||
/* 移动端样式 */
|
||||
|
|
|
|||
|
|
@ -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)=>{
|
|||
// 组件挂载时,如果有图片URL但没有模型URL,自动生成模型
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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,46 +533,51 @@ export default {
|
|||
service: '服务类型',
|
||||
estimatedDelivery: '预计送达',
|
||||
currentLocation: '当前位置',
|
||||
lastUpdate: '最后更新时间'
|
||||
lastUpdate: '最后更新时间',
|
||||
status: '物流状态',
|
||||
timeline: '物流时间线'
|
||||
},
|
||||
login: {
|
||||
divider_text: '或',
|
||||
role_system: '角色系统',
|
||||
creator_role: '创作者',
|
||||
admin_role: '管理员',
|
||||
viewer_role: '访客',
|
||||
creator_desc: '完整系统访问权限,包括用户管理和系统配置',
|
||||
admin_desc: '内容管理和用户管理权限',
|
||||
viewer_desc: '基础功能访问权限',
|
||||
theme_toggle_tooltip: '切换到深色主题',
|
||||
theme_toggle_tooltip_light: '切换到浅色主题',
|
||||
language_toggle_tooltip: '切换到英文',
|
||||
login_success: '登录成功',
|
||||
login_error: '登录失败',
|
||||
google_login: '使用 Google 登录',
|
||||
google_logging: '正在登录...',
|
||||
email_login: '登录',
|
||||
email_logging: '正在登录...',
|
||||
email_placeholder: '请输入您的邮箱',
|
||||
password_placeholder: '请输入您的密码',
|
||||
email_label: '邮箱地址',
|
||||
password_label: '密码',
|
||||
email_empty_error: '请输入邮箱地址',
|
||||
email_invalid_error: '请输入有效的邮箱地址',
|
||||
password_empty_error: '请输入密码',
|
||||
password_min_error: '密码至少需要6位字符',
|
||||
login_success_message: '登录成功!',
|
||||
login_error_message: '登录失败',
|
||||
google_login_success: 'Google 登录成功!',
|
||||
google_login_error: 'Google 登录失败',
|
||||
login_processing_error: '登录过程中发生错误',
|
||||
google_login_processing_error: 'Google 登录过程中发生错误',
|
||||
email_login_notice: '邮箱登录功能预留中,敬请期待',
|
||||
theme_toggle_light: '切换到浅色主题',
|
||||
theme_toggle_dark: '切换到深色主题',
|
||||
forgot_password: '忘记密码?',
|
||||
register_account: '注册账号',
|
||||
},
|
||||
divider_text: '或',
|
||||
role_system: '角色系统',
|
||||
creator_role: '创作者',
|
||||
admin_role: '管理员',
|
||||
viewer_role: '访客',
|
||||
creator_desc: '完整系统访问权限,包括用户管理和系统配置',
|
||||
admin_desc: '内容管理和用户管理权限',
|
||||
viewer_desc: '基础功能访问权限',
|
||||
theme_toggle_tooltip: '切换到深色主题',
|
||||
theme_toggle_tooltip_light: '切换到浅色主题',
|
||||
language_toggle_tooltip: '切换到英文',
|
||||
login_success: '登录成功',
|
||||
login_error: '登录失败',
|
||||
google_login: '使用 Google 登录',
|
||||
google_logging: '正在登录...',
|
||||
email_login: '登录',
|
||||
email_logging: '正在登录...',
|
||||
email_placeholder: '请输入您的邮箱',
|
||||
password_placeholder: '请输入您的密码',
|
||||
email_label: '邮箱地址',
|
||||
password_label: '密码',
|
||||
email_empty_error: '请输入邮箱地址',
|
||||
email_invalid_error: '请输入有效的邮箱地址',
|
||||
password_empty_error: '请输入密码',
|
||||
password_min_error: '密码至少需要6位字符',
|
||||
login_success_message: '登录成功!',
|
||||
login_error_message: '登录失败',
|
||||
google_login_success: 'Google 登录成功!',
|
||||
google_login_error: 'Google 登录失败',
|
||||
login_processing_error: '登录过程中发生错误',
|
||||
google_login_processing_error: 'Google 登录过程中发生错误',
|
||||
email_login_notice: '邮箱登录功能预留中,敬请期待',
|
||||
theme_toggle_light: '切换到浅色主题',
|
||||
theme_toggle_dark: '切换到深色主题',
|
||||
forgot_password: '忘记密码?',
|
||||
register_account: '注册账号',
|
||||
invite_code_label: '邀请码',
|
||||
invite_code_placeholder: '请输入邀请码',
|
||||
invite_code_empty_error: '请输入邀请码',
|
||||
},
|
||||
payment: {
|
||||
methods: '支付方式',
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -71,47 +71,51 @@
|
|||
<span class="tracking-number">{{ $t('logistics.trackingNumber') }}: {{ logisticsData.trackingNo }}</span>
|
||||
</div>
|
||||
|
||||
<div class="logistics-info">
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<el-icon><Van /></el-icon>
|
||||
<span>{{ $t('logistics.carrierInfo') }}</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="info-row">
|
||||
<span class="label">{{ $t('logistics.carrier') }}:</span>
|
||||
<span class="value">{{ logisticsData.logisticsCompany }}</span>
|
||||
<!-- 左右并排布局容器 -->
|
||||
<div class="logistics-content-wrapper">
|
||||
<!-- 左侧:承运信息卡片 -->
|
||||
<div class="logistics-info">
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<el-icon><Van /></el-icon>
|
||||
<span>{{ $t('logistics.carrierInfo') }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">{{ $t('logistics.status') }}:</span>
|
||||
<el-tag :type="logisticsData.logisticsStatus === 4 ? 'success' : 'info'">
|
||||
{{ logisticsData.logisticsStatusText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">{{ $t('logistics.trackingNumber') }}:</span>
|
||||
<span class="value">{{ logisticsData.trackingNo }}</span>
|
||||
<div class="card-content">
|
||||
<div class="info-row">
|
||||
<span class="label">{{ $t('logistics.carrier') }}:</span>
|
||||
<span class="value">{{ logisticsData.logisticsCompany }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">{{ $t('logistics.status') }}:</span>
|
||||
<el-tag :type="logisticsData.logisticsStatus === 4 ? 'success' : 'info'">
|
||||
{{ logisticsData.logisticsStatusText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">{{ $t('logistics.trackingNumber') }}:</span>
|
||||
<span class="value">{{ logisticsData.trackingNo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider>{{ $t('logistics.timeline') }}</el-divider>
|
||||
|
||||
<div class="logistics-timeline-container">
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(trace, index) in logisticsData.traces"
|
||||
:key="index"
|
||||
:timestamp="trace.time"
|
||||
:type="index === 0 ? 'success' : 'primary'"
|
||||
>
|
||||
<div class="logistics-item">
|
||||
<div class="logistics-content">{{ trace.description }}</div>
|
||||
<div v-if="trace.location" class="logistics-location">{{ trace.location }}</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<!-- 右侧:物流时间线 -->
|
||||
<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.slice().reverse()"
|
||||
:key="index"
|
||||
:timestamp="trace.time"
|
||||
:type="index === 0 ? 'success' : 'primary'"
|
||||
>
|
||||
<div class="logistics-item">
|
||||
<div class="logistics-content">{{ trace.description }}</div>
|
||||
<div v-if="trace.location" class="logistics-location">{{ trace.location }}</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
//退款订单
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
@pay-order="handlePayOrder"
|
||||
@cancel-order="handleCancelOrder"
|
||||
@expired-order="handleExpiredOrder"
|
||||
@confirm-order="handleConfirmOrder"
|
||||
/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
|
@ -67,195 +68,148 @@
|
|||
</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([
|
||||
{ label: t('orderManagement.sort.created_at'), value: 'created_at' },
|
||||
{ label: t('orderManagement.sort.total'), value: 'amount' },
|
||||
])
|
||||
const viewOrderDetails = (orderData) => {
|
||||
console.log(orderData);
|
||||
router.push({ name: 'order-detail', params: { orderId:orderData.id } })
|
||||
}
|
||||
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'
|
||||
}
|
||||
})
|
||||
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'),
|
||||
t('orderManagement.cancelConfirm.title'),
|
||||
{
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
orderPlug.orderCancel({
|
||||
id: orderData.id
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(t('orderManagement.cancelSuccess'))
|
||||
init()
|
||||
} else {
|
||||
ElMessage.error(res.message || t('orderManagement.cancelFail'))
|
||||
}
|
||||
})
|
||||
}).catch(() => {
|
||||
// 用户点击取消,无需处理
|
||||
})
|
||||
}
|
||||
|
||||
const handleExpiredOrder = (orderId) => {
|
||||
orderStore.updateOrder(orderId, { status: 'expired' })
|
||||
}
|
||||
|
||||
const toCents = (amount) => Math.round((amount || 0) * 100)
|
||||
const page = ref(1);
|
||||
const page_size = ref(10);
|
||||
//订单筛选
|
||||
const order_no = ref('');
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
//排序方式
|
||||
const sort_by = ref('created_at')
|
||||
const sort_order = ref('desc');//asc/desc
|
||||
const order_list = ref([]);
|
||||
const total = ref(0);
|
||||
//剩余筛选条件
|
||||
const sxtjjson = ref({});
|
||||
const loadMore = ()=>{
|
||||
getOrderList();
|
||||
}
|
||||
//获取订单列表
|
||||
const getOrderList = ()=>{
|
||||
if (loading.value || finished.value) return;
|
||||
loading.value = true;
|
||||
orderPlug.getOrderList({
|
||||
page: page.value,
|
||||
page_size: page_size.value,
|
||||
order_no: order_no.value,
|
||||
sort_by: sort_by.value,
|
||||
sort_order: sort_order.value,
|
||||
...sxtjjson.value
|
||||
}).then(res=>{
|
||||
if(res.code==0){
|
||||
let data = res.data;
|
||||
if (page.value === 1) {
|
||||
order_list.value = data.items;
|
||||
} else {
|
||||
order_list.value = [...order_list.value, ...data.items];
|
||||
}
|
||||
total.value = data.total;
|
||||
finished.value = order_list.value.length >= total.value;
|
||||
page.value++;
|
||||
}
|
||||
})
|
||||
}
|
||||
const init = ()=>{
|
||||
loading.value = false;
|
||||
finished.value = false;
|
||||
order_list.value = [];
|
||||
page.value = 1;
|
||||
page_size.value = 10;
|
||||
getOrderList();
|
||||
}
|
||||
watch(order_no, () => {
|
||||
// 防抖:300ms 内无新输入再触发
|
||||
clearTimeout(window._orderNoDebounce)
|
||||
window._orderNoDebounce = setTimeout(() => {
|
||||
init()
|
||||
}, 300)
|
||||
})
|
||||
onMounted(() => {
|
||||
init();
|
||||
})
|
||||
|
||||
const selectStatus = (status) => {
|
||||
const json = orderStatus.selectOrderStatusOptions(status);
|
||||
sxtjjson.value = json;
|
||||
selectedStatus.value = status;
|
||||
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
|
||||
}
|
||||
}
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
const selectedStatus = ref('all')
|
||||
const statusFilters = orderStatus.selectList('1');
|
||||
const sortOptions = ref([
|
||||
{ label: t('orderManagement.sort.created_at'), value: 'created_at' },
|
||||
{ label: t('orderManagement.sort.total'), value: 'amount' },
|
||||
])
|
||||
const viewOrderDetails = (orderData) => {
|
||||
console.log(orderData);
|
||||
router.push({ name: 'order-detail', params: { orderId:orderData.id } })
|
||||
}
|
||||
const handlePayOrder = (orderData) => {
|
||||
window.location.href = orderData.stripe_url
|
||||
}
|
||||
//确认收货
|
||||
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()
|
||||
})
|
||||
})
|
||||
}
|
||||
const handleCancelOrder = (orderData) => {
|
||||
ElMessageBox.confirm(
|
||||
t('orderManagement.cancelConfirm.message'),
|
||||
t('orderManagement.cancelConfirm.title'),
|
||||
{
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
orderPlug.orderCancel({
|
||||
id: orderData.id
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(t('orderManagement.cancelSuccess'))
|
||||
init()
|
||||
} else {
|
||||
ElMessage.error(res.message || t('orderManagement.cancelFail'))
|
||||
}
|
||||
})
|
||||
}).catch(() => {
|
||||
// 用户点击取消,无需处理
|
||||
})
|
||||
}
|
||||
|
||||
const handleExpiredOrder = (orderId) => {
|
||||
orderStore.updateOrder(orderId, { status: 'expired' })
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const page_size = ref(10);
|
||||
//订单筛选
|
||||
const order_no = ref('');
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
//排序方式
|
||||
const sort_by = ref('created_at')
|
||||
const sort_order = ref('desc');//asc/desc
|
||||
const order_list = ref([]);
|
||||
const total = ref(0);
|
||||
//剩余筛选条件
|
||||
const sxtjjson = ref({});
|
||||
const loadMore = ()=>{
|
||||
getOrderList();
|
||||
}
|
||||
//获取订单列表
|
||||
const getOrderList = ()=>{
|
||||
if (loading.value || finished.value) return;
|
||||
loading.value = true;
|
||||
orderPlug.getOrderList({
|
||||
page: page.value,
|
||||
page_size: page_size.value,
|
||||
order_no: order_no.value,
|
||||
sort_by: sort_by.value,
|
||||
sort_order: sort_order.value,
|
||||
...sxtjjson.value
|
||||
}).then(res=>{
|
||||
if(res.code==0){
|
||||
let data = res.data;
|
||||
if (page.value === 1) {
|
||||
order_list.value = data.items;
|
||||
} else {
|
||||
order_list.value = [...order_list.value, ...data.items];
|
||||
}
|
||||
total.value = data.total;
|
||||
finished.value = order_list.value.length >= total.value;
|
||||
page.value++;
|
||||
}
|
||||
})
|
||||
}
|
||||
const init = ()=>{
|
||||
loading.value = false;
|
||||
finished.value = false;
|
||||
order_list.value = [];
|
||||
page.value = 1;
|
||||
page_size.value = 10;
|
||||
getOrderList();
|
||||
}
|
||||
watch(order_no, () => {
|
||||
// 防抖:300ms 内无新输入再触发
|
||||
clearTimeout(window._orderNoDebounce)
|
||||
window._orderNoDebounce = setTimeout(() => {
|
||||
init()
|
||||
}, 300)
|
||||
})
|
||||
onMounted(() => {
|
||||
init();
|
||||
})
|
||||
|
||||
const selectStatus = (status) => {
|
||||
const json = orderStatus.selectOrderStatusOptions(status);
|
||||
sxtjjson.value = json;
|
||||
selectedStatus.value = status;
|
||||
init()
|
||||
}
|
||||
|
||||
watch(sort_by, () => {
|
||||
init();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -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,根据缩放比例调整
|
||||
// 当缩放比例为1时,网格大小为20px
|
||||
// 当缩放比例变大时,网格大小相应变小,保持视觉上的一致性
|
||||
// 当缩放比例变小时,网格大小相应变大,避免网格过于密集
|
||||
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) {
|
||||
e.preventDefault(); // 阻止默认行为
|
||||
// 移动端不阻止默认行为,避免影响触摸事件
|
||||
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,21 +745,113 @@ const startSceneDrag = (e) => {
|
|||
|
||||
// 只有在主内容区域(不是侧边栏)才允许拖动场景
|
||||
if (e.target.closest('.main-content') && !e.target.closest('.floating-sidebar')) {
|
||||
e.preventDefault();
|
||||
isSceneDragging.value = true;
|
||||
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
sceneStartX.value = clientX - sceneOffsetX.value;
|
||||
sceneStartY.value = clientY - sceneOffsetY.value;
|
||||
// 检测是否为双指触摸
|
||||
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;
|
||||
|
||||
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) {
|
||||
e.preventDefault();
|
||||
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;
|
||||
|
||||
// 补偿偏移量:向右偏移600像素,向上偏移400像素
|
||||
// 这意味着实际鼠标位置需要向左偏移600,向下偏移400
|
||||
// 移动端不需要调整偏差
|
||||
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) => {
|
||||
e.preventDefault(); // 阻止默认的滚轮行为(页面滚动)
|
||||
// 移动端不阻止默认行为,避免影响触摸事件
|
||||
if (!isMobile.value) {
|
||||
e.preventDefault(); // 阻止默认的滚轮行为(页面滚动)
|
||||
}
|
||||
|
||||
// 如果按下了Ctrl键(Windows/Linux)或Cmd键(Mac),执行缩放操作
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
|
|
@ -782,15 +923,21 @@ const handleWheel = (e) => {
|
|||
const preventZoom = (e) => {
|
||||
// 检查事件是否来自侧边栏区域
|
||||
if ((e.ctrlKey || e.metaKey) && e.target.closest('.floating-sidebar')) {
|
||||
e.preventDefault();
|
||||
// 移动端不阻止默认行为,避免影响触摸事件
|
||||
if (!isMobile.value) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 监听触摸事件以防止缩放手势
|
||||
const preventPinchZoom = (e) => {
|
||||
// 检测多点触控(缩放手势通常使用两个手指)
|
||||
if (e.touches && e.touches.length > 1) {
|
||||
e.preventDefault();
|
||||
// 仅在侧边栏区域阻止缩放手势,主内容区域允许缩放手势
|
||||
if (e.touches && e.touches.length > 1 && e.target.closest('.floating-sidebar')) {
|
||||
// 移动端不阻止默认行为,避免影响触摸事件
|
||||
if (!isMobile.value) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
// 导入被动事件监听器工具
|
||||
|
|
@ -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,12 +1148,24 @@ html {
|
|||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/* 防止用户选择文本和右键菜单 */
|
||||
body {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
/* 移动端适配 - 移除全局用户选择限制 */
|
||||
@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>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export default defineConfig({
|
|||
manualChunks: {
|
||||
vendor: ['vue', 'vue-router', 'vue-i18n'],
|
||||
elementPlus: ['element-plus'],
|
||||
utils: ['axios', 'dayjs']
|
||||
// utils: ['axios', 'dayjs']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||