This commit is contained in:
13121765685 2026-01-07 16:41:49 +08:00
parent 1ffc1195e6
commit b71a78c079
88 changed files with 3668 additions and 4292 deletions

View File

@ -1,24 +0,0 @@
## 实现计划
### 1. 更新国际化翻译
- 在 `src/locales/index.js` 中添加积分消耗规则相关翻译
- 包含中英文翻译
- 添加以下翻译键:
- 消耗规则标题
- 消耗规则表格标题(行为、积分消耗)
- 具体消耗规则生成1张图片1积分生成1个3D模型30积分
### 2. 修改用户中心组件
- 在 `src/views/user/index.vue` 的积分列表后添加新的消耗规则区域
- 创建包含指定数据的消耗规则表格
- 使用现有样式模式保持设计一致性
- 支持响应式设计
### 3. 更新样式
- 为新的消耗规则区域添加CSS样式
- 确保与现有积分区域样式一致
- 添加与现有设计匹配的悬停效果和过渡
- 支持明暗主题
## 预期结果
用户中心积分区域将新增"积分消耗规则"部分,以表格形式展示指定的消耗规则,设计风格与现有界面保持一致。

View File

@ -1,51 +0,0 @@
# 管理员用户管理功能实现计划
## 当前状态分析
- `index.js` 中的 `AdminOrders` 类包含管理员用户管理方法
- `AdminUsers.vue` 组件目前使用普通用户方法和模拟数据
- 管理员特定方法已定义但未被使用
## 实现目标
1. 更新Vue组件以使用管理员特定的API方法
2. 实现完整的管理员用户管理功能
3. 确保为所有管理操作提供适当的UI支持
## 拟做变更
### 1. 类名重构
- 将 `AdminOrders` 类重命名为 `AdminUsersService` 以提高清晰度
### 2. Vue组件更新
#### 数据和方法
- 更新组件使用 `getAdminUsersList` 替代 `getUsersList`
- 为所有管理员特定操作实现方法
- 删除模拟数据使用真实API响应
#### UI组件
- 更新用户表格以显示管理员用户字段
- 添加创建管理员用户的支持
- 实现管理员用户状态切换
- 添加密码重置功能
- 实现管理员用户删除
- 更新表单以匹配管理员用户数据结构
### 3. API集成
- 确保正确调用所有管理员特定方法
- 适当处理API响应和错误
- 为所有异步操作添加加载状态
## 实施步骤
1. 重构 `index.js` 中的类名
2. 更新组件导入和服务初始化
3. 更新数据获取以使用管理员特定API
4. 实现管理员用户创建表单
5. 更新用户表格,添加管理员特定列和操作
6. 实现管理员用户状态切换
7. 实现密码重置功能
8. 实现管理员用户删除
9. 更新管理员用户详情视图
10. 测试所有功能
## 预期结果
一个功能完整的管理员用户管理界面利用管理员特定的API方法来创建、列出、更新、启用/禁用、重置密码、查看详情和删除管理员用户。

View File

@ -1,91 +0,0 @@
# AdminUserList.vue 页面重新设计方案
## 1. 设计目标
- 整合所有提供的API方法实现完整的管理员用户管理功能
- 提供良好的用户体验,包括表单验证、加载状态和操作反馈
- 支持批量操作和分页功能
- 保持代码结构清晰,易于维护
- **确保所有文本支持中英文切换**使用现有的i18n机制
## 2. 页面结构设计
### 2.1 基础布局
- 保留原有的标题、卡片、搜索栏、表格和分页组件
- 添加批量操作工具栏
- 新增三个对话框:
- 用户创建/编辑对话框
- 密码重置对话框
- 操作确认对话框(用于删除和启用/禁用操作)
### 2.2 表格列设计
- 保留原有的用户名、邮箱、状态等列
- 新增角色列,显示用户关联的角色
- 新增全选复选框,支持批量操作
- 调整操作列,添加更多功能按钮
## 3. 功能实现
### 3.1 数据管理
- 使用`ref`和`reactive`管理页面状态
- 实现分页数据加载,使用`getAdminUsersList`方法
- 添加搜索和筛选功能
- 实现数据刷新机制
### 3.2 用户管理功能
- **创建用户**:点击"添加用户"按钮,打开创建对话框,调用`createAdminUser`方法
- **编辑用户**:点击"编辑"按钮,打开编辑对话框,调用`getAdminUserDetail`和`updateAdminUser`方法
- **删除用户**:支持单条删除和批量删除,调用`deleteAdminUsers`方法
- **启用/禁用用户**:点击状态切换按钮,调用`enableDisableUser`方法
- **重置密码**:点击"重置密码"按钮,打开密码重置对话框,调用`resetUserPassword`方法
### 3.3 表单验证
- 为用户创建/编辑表单添加验证规则
- 为密码重置表单添加验证规则
- 提供清晰的错误提示
### 3.4 交互体验
- 添加加载状态指示器
- 提供操作成功/失败的消息提示
- 实现平滑的对话框过渡效果
- 支持键盘快捷键(可选)
### 3.5 国际化支持
- 所有文本内容使用`{{ t('key') }}`语法,确保支持中英文切换
- 表单验证消息也需要国际化
- 动态生成的文本(如角色名称)也需要考虑国际化
## 4. API集成
- 导入`AdminRoleManagement`类
- 实例化API服务对象
- 实现所有API方法的调用和错误处理
- 添加请求取消机制(可选)
## 5. 代码结构
- 保持`<template>`、`<script setup>``<style scoped>`
- 使用组合式API编写逻辑
- 提取重复逻辑为可复用的函数
- 添加适当的注释
## 6. 实现步骤
1. 更新页面模板,添加对话框和批量操作工具栏
2. 实现数据加载和分页逻辑
3. 实现用户创建功能
4. 实现用户编辑功能
5. 实现用户删除功能
6. 实现用户启用/禁用功能
7. 实现密码重置功能
8. 添加表单验证和错误处理
9. 确保所有文本支持中英文切换
10. 优化UI/UX
11. 测试所有功能
## 7. 技术要点
- 使用Vue 3组合式API
- 使用Element Plus组件库
- 实现响应式设计
- 遵循代码规范
- 确保性能优化
- **正确使用i18n机制支持中英文切换**
这个方案将实现一个功能完整、用户体验良好、支持中英文切换的管理员用户管理页面整合了所有提供的API方法。

View File

@ -1,64 +0,0 @@
## 实施方案
### 概述
将当前的绿色圆点在线状态指示器替换为角色标识,为'free'(免费会员)和'creator'(达人会员)用户显示不同样式的标识。
### 所需更改
1. **更新 AppSidebar.vue 模板**
- 将第39行和第52行的 `<div class="online-status"></div>` 元素替换为角色标识组件
- 添加硬编码的角色属性(默认为'free')用于演示
2. **更新角色显示逻辑**
- 修改 `getRoleDisplayName` 函数,添加'free'和'creator'角色的映射
- 根据需要为新角色添加翻译
3. **更新样式**
- 移除旧的 `.online-status` CSS 类
- 为角色标识添加新的 CSS 样式,为'free'和'creator'角色设计不同样式
- 确保角色标识在用户头像上正确定位
4. **测试实现**
- 验证角色标识在折叠和展开的侧边栏模式下都能正确显示
- 检查'free'和'creator'角色的不同样式是否可见
- 确保布局在不同屏幕尺寸下保持响应式
### 实现细节
#### 模板更改
- 第39行`<div class="online-status"></div>` 替换为角色标识
- 第52行`<div class="online-status"></div>` 替换为角色标识
- 添加硬编码的 `userRole` 属性,设置为'free'或'creator'
#### 角色映射更新
- 更新 `getRoleDisplayName` 函数,包括:
```javascript
const roleMap = {
'free': '免费会员',
'creator': '达人会员',
'admin': t('roles.admin'),
'viewer': t('roles.viewer')
}
```
#### CSS 更改
- 移除第450行的 `.online-status` 类定义
- 添加新的 CSS 类用于角色标识:
- `.role-badge`:所有角色标识的基础样式
- `.role-badge.free`:免费会员的样式
- `.role-badge.creator`:达人会员的样式
- 使用 `position: absolute` 和适当的 `bottom`/`right` 值确保正确定位
### 预期行为
- 当用户为'free'角色时,标识将显示特定样式(例如灰色背景,简单设计)
- 当用户为'creator'角色时,标识将显示不同样式(例如紫色背景,特殊设计)
- 角色标识的位置与当前在线状态圆点相似
- 标识在折叠和展开的侧边栏模式下都可见
### 技术考虑
- 角色目前硬编码用于演示,但可以轻松连接到 auth store 中的实际用户角色
- 实现保持与现有代码库的向后兼容性
- 响应式设计在所有屏幕尺寸下都能保持
- 样式遵循项目现有的设计系统
该方案将为免费会员和达人会员创建清晰的视觉区分,通过提供即时的角色识别来改善用户体验。

View File

@ -1,77 +0,0 @@
# 优化IPCard生成中占位符样式
## 需求分析
用户要求优化IPCard组件中生成图片时的占位符样式具体要求
- 颜色与质感:整体为浅灰色或白色,带有微妙的渐变或光泽感
- 动态效果:平滑的**倾斜**波浪式或扫描式光带从左到右/从上到下移动
- 移除现有圆圈动画和文字提示
- 适配暗色主题
## 实现方案
### 1. 修改模板结构
- 移除第45-46行的圆圈动画和文字提示
- 保留占位符容器结构
### 2. 优化CSS样式
- 修改`.generating-placeholder`类:
- 背景色改为浅灰色渐变,添加微妙光泽感
- 添加**倾斜**扫描光带动画
- 移除原有的`generating-spinner`和`generating-text`
- 添加暗色主题适配
### 3. 关键CSS修改
```css
/* 生成中占位符样式优化 */
.generating-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(243, 244, 246, 0.95) 0%, rgba(229, 231, 235, 0.9) 100%);
position: relative;
overflow: hidden;
/* 添加微妙的光泽感 */
box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.1);
}
/* 倾斜扫描光带动画 */
.generating-placeholder::before {
content: '';
position: absolute;
/* 倾斜45度从左上角开始 */
top: -50%;
left: -100%;
width: 200%;
height: 200%;
/* 45度倾斜的光带 */
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: scanEffect 2s ease-in-out infinite;
/* 设置旋转中心点 */
transform-origin: center;
}
/* 倾斜扫描动画 */
@keyframes scanEffect {
0% {
transform: translateX(-100%) translateY(-100%);
}
100% {
transform: translateX(100%) translateY(100%);
}
}
/* 暗色主题适配 */
:global(.dark) .generating-placeholder {
background: linear-gradient(135deg, rgba(31, 41, 55, 0.95) 0%, rgba(17, 24, 39, 0.9) 100%);
box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.05);
}
:global(.dark) .generating-placeholder::before {
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent);
}
```
### 4. 预期效果
- 浅色主题浅灰色渐变背景带有微妙光泽感45度倾斜的白色光带从左上角向右下角扫描
- 暗色主题深灰色渐变背景带有微妙光泽感45度倾斜的浅白色光带扫描
- 无圆圈动画和文字提示
- 整体呈现“未填充”的加载状态

View File

@ -1,27 +0,0 @@
# 实现邀请码升级功能
## 1. 修改 Waitlist.vue 组件
- 在现有界面中添加邀请码输入框和提交按钮
- 设计输入框样式与现有界面风格保持一致
- 添加表单验证逻辑
## 2. 引入必要的模块和方法
- 引入 user 模块的 upgrade 方法
- 引入 auth store 的 updateUserInfo 方法
- 引入 ElMessage 用于显示操作结果
## 3. 实现提交逻辑
1. 用户输入邀请码并点击提交按钮
2. 调用 `/views/user/index.js` 中的 `upgrade` 方法
3. 成功后调用 `/stores/auth.js` 中的 `updateUserInfo` 方法刷新用户信息
4. 刷新成功后返回首页
## 4. 添加错误处理
- 邀请码验证失败时显示错误信息
- 网络请求失败时显示错误提示
- 确保用户体验流畅
## 5. 界面优化
- 输入框添加合适的占位符
- 按钮状态管理(加载中、禁用等)
- 保持响应式设计,适配不同屏幕尺寸

View File

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

View File

@ -1,201 +0,0 @@
# 新建项目系列选择功能实现
## 需求分析
* 当用户点击"新建项目"卡片时,需要弹出系列选择弹窗
* 系列弹窗包含两个选项Done 和 Oone对应图片在 src/assets/xh 文件夹中
* 选中后将系列名称作为 type 参数传递给 createNewProject 函数
## 实现计划
### 1. 创建 SeriesSelector 组件
* **文件路径**`src/views/components/SeriesSelector.vue`
* **功能**
* 显示两个系列选项Done 和 Oone
* 每个选项显示对应的图片
* 支持选中状态切换
* 提供确认和取消按钮
* 通过 emit 事件返回选中的系列名称
### 2. 修改 CreationWorkspace.vue
* **引入组件**:在 CreationWorkspace.vue 中引入 SeriesSelector 组件
* **添加状态管理**
* `showSeriesSelector`:控制系列选择弹窗的显示/隐藏
* **修改新建项目逻辑**
* 点击"新建项目"卡片时,显示系列选择弹窗
* 监听 SeriesSelector 的确认事件,获取选中的系列名称
* 将系列名称作为 type 参数调用 createNewProject 函数
### 3. 样式设计
* 系列选择弹窗采用与现有删除确认弹窗一致的设计风格
* 系列选项卡片包含图片和名称,支持悬停和选中效果
* 确认和取消按钮使用现有按钮样式
## 代码结构
### SeriesSelector.vue
```vue
<template>
<!-- 系列选择弹窗 -->
<div v-if="show" class="modal-overlay" @click="onCancel">
<div class="modal-content" @click.stop>
<!-- 模态头部 -->
<div class="modal-header">
<h2 class="modal-title">{{ t('creationWorkspace.selectSeries') }}</h2>
</div>
<!-- 模态内容 -->
<div class="modal-body">
<div class="series-selector-content">
<!-- 系列选项列表 -->
<div class="series-list">
<!-- Done 系列 -->
<div
class="series-item"
:class="{ active: selectedSeries === 'Done' }"
@click="selectedSeries = 'Done'"
>
<div class="series-image">
<img src="@/assets/xh/Done.webp" alt="Done" />
</div>
<div class="series-name">Done</div>
</div>
<!-- Oone 系列 -->
<div
class="series-item"
:class="{ active: selectedSeries === 'Oone' }"
@click="selectedSeries = 'Oone'"
>
<div class="series-image">
<img src="@/assets/xh/Oone.webp" alt="Oone" />
</div>
<div class="series-name">Oone</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="series-actions">
<button class="modal-action-btn cancel" @click="onCancel">
{{ t('creationWorkspace.cancel') }}
</button>
<button class="modal-action-btn primary" @click="onConfirm">
{{ t('creationWorkspace.confirm') }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
show: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['confirm', 'cancel'])
const selectedSeries = ref('')
const onConfirm = () => {
if (selectedSeries.value) {
emit('confirm', selectedSeries.value)
}
}
const onCancel = () => {
emit('cancel')
}
</script>
<style scoped>
/* 系列选择弹窗样式 */
.series-selector-content {
text-align: center;
}
.series-list {
display: flex;
gap: 24px;
justify-content: center;
margin-bottom: 32px;
}
.series-item {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
width: 200px;
}
.series-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.series-item.active {
border-color: #6B46C1;
background: rgba(107, 70, 193, 0.05);
box-shadow: 0 4px 16px rgba(107, 70, 193, 0.2);
}
.series-image {
width: 100%;
height: 120px;
overflow: hidden;
border-radius: 8px;
margin-bottom: 12px;
}
.series-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.series-name {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.series-actions {
display: flex;
gap: 16px;
justify-content: center;
}
</style>
```

View File

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

View File

@ -1,74 +0,0 @@
# 实现计划
## 1. 标签页切换时自动刷新列表
### 1.1 问题分析
- 当前 `el-tabs` 组件没有添加 `tab-change` 事件监听
- 切换标签页时,列表数据不会自动刷新
### 1.2 解决方案
- 为 `el-tabs` 组件添加 `@tab-change` 事件监听
- 在事件处理函数中,根据当前激活的标签页调用对应的获取数据方法
- 邀请码列表调用 `getInviteCodeList()`
- 邀请用户列表调用 `getInviteList()`
## 2. 隐藏用户id的筛选条件
### 2.1 问题分析
- 当前用户id的筛选条件是可见的输入框
- 需要隐藏该筛选条件但保持使用路由传过来的id
- 保持其他查询条件不变
### 2.2 解决方案
- 仅隐藏用户id的表单项使用 `v-if="false"``style="display: none"`
- 保持现有的 `filterForm` 结构和查询逻辑不变
- 确保获取邀请码列表时,默认使用路由传过来的 `userId`
## 3. 添加删除二维码功能
### 3.1 问题分析
- 当前邀请码列表没有删除功能
- 需要调用 `deleteInviteCode` 方法删除邀请码
### 3.2 解决方案
- 在邀请码列表中添加一列操作列
- 操作列中添加删除按钮
- 点击删除按钮时,弹出确认对话框
- 确认后调用 `deleteInviteCode` 方法
- 删除成功后刷新邀请码列表
## 4. 实现步骤
### 4.1 修改标签页组件
- 添加 `@tab-change` 事件监听
- 实现 `handleTabChange` 方法
### 4.2 隐藏用户id筛选条件
- 在用户id的表单项上添加 `style="display: none"``v-if="false"`
- 保持其他查询条件不变
### 4.3 添加删除功能
- 在邀请码列表中添加操作列
- 添加删除按钮和确认对话框
- 实现 `handleDeleteCode` 方法
### 4.4 优化样式
- 调整操作列的宽度和样式
## 5. 文件修改
- 仅修改 `d:/work/Aiproject/DeotalandAi/apps/FrontendDesigner/src/views/admin/AdminUsers/AdminUserInvites.vue` 文件
# 预期效果
- 切换标签页时,对应列表自动刷新数据
- 邀请码列表中不再显示用户id筛选条件但其他查询条件保持不变
- 邀请码列表中添加删除按钮,点击可删除邀请码,删除前有确认提示
- 删除成功后列表自动刷新
# 技术实现
- 使用 Vue 3 的 Composition API
- 使用 Element Plus 的组件和事件
- 调用已有的 `deleteInviteCode` 方法
- 使用 `ref` 管理响应式数据
- 使用 `async/await` 处理异步操作

View File

@ -1,48 +0,0 @@
# 佣金管理功能实现计划
## 1. 路由配置
- 在 `apps/FrontendDesigner/src/router/index.js` 中添加佣金管理页面路由
- 导入佣金管理组件并配置路由参数
## 2. 侧边栏菜单配置
- 在 `apps/FrontendDesigner/src/components/admin/AdminLayout.vue` 中添加佣金管理菜单项
- 使用适当的图标和翻译文本
## 3. 国际化配置
- 在 `apps/FrontendDesigner/src/locales/lang/zh-CN.js` 中添加佣金管理相关中文翻译
- 在 `apps/FrontendDesigner/src/locales/lang/en-US.js` 中添加佣金管理相关英文翻译
## 4. 佣金管理页面开发
- 创建 `apps/FrontendDesigner/src/views/admin/AdminCommissionManagement.vue` 页面组件
- 实现佣金比例设置功能默认15%
- 实现佣金列表展示包含达人名称、用户ID、实际支付金额、商品金额、佣金、状态等字段
- 添加佣金审核功能(审核通过/拒绝按钮)
- 实现响应式设计,适配不同屏幕尺寸
## 5. 组件功能实现
- 使用 Element Plus 组件库实现表格、表单、按钮等UI元素
- 实现佣金计算逻辑(基于实际支付金额和佣金比例)
- 添加审核状态管理
- 实现数据列表的分页、筛选功能
## 6. 数据模拟
- 添加模拟数据,用于展示佣金列表
- 实现模拟的审核功能
## 7. 样式优化
- 确保页面样式与现有管理后台风格一致
- 添加适当的动画和过渡效果
- 优化表格和表单的用户体验
## 8. 测试验证
- 确保页面能正常加载和显示
- 测试佣金比例设置功能
- 测试佣金审核功能
- 验证响应式设计
## 技术栈
- Vue 3 Composition API
- Element Plus UI组件库
- Vue Router 4
- Vue I18n 9
- CSS 变量 + Scoped CSS

View File

@ -1,90 +0,0 @@
# 使用Konva实现Canvas编辑器方案
## 1. 方案概述
使用Konva.js库重新实现Canvas编辑器组件实现所有要求的功能
- Ctrl+滚轮缩放画布
- 支持图片拖拽到画布
- 画笔功能,支持不同颜色
- 橡皮擦功能
- 提示词输入框和重绘按钮
## 2. 技术栈
- Vue 3
- Konva.js (Canvas库)
- Element Plus (UI组件库)
## 3. 实现步骤
### 3.1 安装依赖
- 安装Konva和vue-konva
- 确保与现有项目兼容
### 3.2 创建KonvaCanvas组件
- 创建`KonvaCanvas.vue`组件
- 配置Konva舞台和图层
- 实现基本画布功能
### 3.3 实现核心功能
#### 3.3.1 缩放功能
- 监听鼠标滚轮事件
- 实现Ctrl+滚轮缩放逻辑
- 调整Konva舞台缩放比例
#### 3.3.2 拖拽功能
- 为现有图片添加拖拽支持
- 实现Konva的拖拽事件处理
- 将拖拽的图片添加到Konva舞台
#### 3.3.3 画笔功能
- 实现Konva的绘制功能
- 支持不同颜色选择
- 支持画笔粗细调整
#### 3.3.4 橡皮擦功能
- 实现橡皮擦模式
- 支持橡皮擦粗细调整
#### 3.3.5 重绘功能
- 将Konva画布转换为base64
- 获取提示词输入
- 打印到控制台
### 3.4 集成到现有项目
- 替换现有的`CanvasEditor.vue`组件
- 确保与`AdminDisassemblyDetail.vue`页面兼容
## 4. 文件结构
### 4.1 修改现有文件
- `apps/FrontendDesigner/package.json` - 添加Konva依赖
### 4.2 创建新文件
- `apps/FrontendDesigner/src/components/KonvaCanvas.vue` - Konva Canvas编辑器组件
### 4.3 更新现有文件
- `apps/FrontendDesigner/src/views/admin/AdminDisassemblyDetail/AdminDisassemblyDetail.vue` - 集成新组件
## 5. 预期效果
- 功能与原组件一致但使用Konva库实现
- 性能优化,特别是在缩放和绘制时
- 代码结构更清晰,维护性更好
- 支持更多高级功能扩展
## 6. 风险评估
- Konva库的学习曲线
- 与现有项目的兼容性
- 功能实现的准确性
- 性能影响
## 7. 实施时间
预计实施时间1-2小时
- 安装依赖10分钟
- 创建组件30分钟
- 实现功能40分钟
- 集成测试20分钟

View File

@ -1,28 +0,0 @@
## 修复邀请码列表数据获取
### 问题分析
当前邀请码列表使用的是硬编码数据,而不是通过调用`getCodes()`方法从API获取实际数据。
### 解决方案
1. 修改`index.vue`组件,导入并使用`UserController`中的`getCodes()`方法
2. 将硬编码的`userData`计算属性改为响应式数据,并添加数据获取逻辑
3. 确保邀请码列表能够动态渲染从API获取的数据
### 实现步骤
1. 在`index.vue`中导入`UserController`
2. 创建响应式的`userData`对象,替换当前的计算属性
3. 添加`onMounted`钩子,在组件挂载时调用`getCodes()`方法获取邀请码数据
4. 更新邀请码列表的渲染逻辑确保与API返回的数据结构匹配
5. 添加错误处理和加载状态
### 预期结果
- 邀请码列表将显示从API获取的实际数据
- 支持邀请码的过期状态显示
- 保持现有的复制功能和样式不变
### 代码变更
- 修改`d:/work/Aiproject/DeotalandAi/apps/frontend/src/views/user/index.vue`
- 导入`UserController`
- 将`userData`从计算属性改为响应式数据
- 添加数据获取逻辑
- 调整数据结构映射

View File

@ -1,72 +0,0 @@
# 修改 AddAgent.vue 使用 API 数据渲染 TTS 内容
## 目标
根据 `demo.md` 中的 API 数据格式,修改 `AddAgent.vue` 文件,使其能够从 API 获取语言和音色数据,并正确渲染到页面上。
## 分析
1. 当前 `AddAgent.vue` 中使用硬编码的 `availableLanguages``availableVoices` 数组
2. `getTtsList` 函数已经调用了 API但没有处理返回的数据
3. API 返回的数据包含语言列表和对应语言的音色列表
4. 需要根据选择的语言动态更新可用的音色列表
## 修改方案
### 1. 修改数据结构
- 将 `availableLanguages``availableVoices` 改为从 API 获取
- 添加 `ttsData` 变量存储完整的 API 返回数据
- 添加 `allVoices` 变量存储所有音色数据,便于根据语言过滤
### 2. 更新 `getTtsList` 函数
- 处理 API 返回的数据,格式化语言和音色列表
- 将语言列表转换为组件需要的格式(包含 code、name、flag
- 将音色列表转换为组件需要的格式(包含 id、name、sampleUrl
### 3. 修改语言选择逻辑
- 使用从 API 获取的语言列表替换硬编码列表
- 添加语言切换时的音色过滤逻辑
### 4. 修改音色选择逻辑
- 根据当前选择的语言动态过滤音色列表
- 确保音频播放功能使用 API 返回的 `voice_demo` URL
### 5. 优化音频播放功能
- 确保 `playVoiceSample` 函数能够正确处理 API 返回的音色数据
- 保持音频播放器的原有功能不变
## 具体实现步骤
1. **修改响应式变量定义**
- 删除硬编码的 `availableLanguages``availableVoices` 数组
- 添加 `ttsData`、`allVoices` 变量
- 修改 `availableLanguages``availableVoices` 为计算属性
2. **更新 `getTtsList` 函数**
- 处理 API 返回的数据
- 格式化语言列表,添加国旗图标
- 格式化音色列表,提取需要的字段
3. **添加语言切换监听**
- 在 `handleLanguageChange` 函数中添加音色过滤逻辑
- 确保切换语言时重置音色选择
4. **更新模板渲染**
- 确保模板使用正确的变量名和字段名
- 确保音频播放器使用 API 返回的音频 URL
5. **测试功能**
- 确保语言列表正确渲染
- 确保音色列表根据语言动态更新
- 确保音频播放功能正常工作
## 预期效果
- 页面加载时从 API 获取语言和音色数据
- 语言选择下拉框显示所有可用语言
- 音色选择下拉框根据当前语言显示对应的音色
- 点击"试听"按钮能够播放对应的音色示例
- 音频播放器能够正常控制播放/暂停、显示进度
## 注意事项
- 需要处理 API 返回的语言代码与当前代码中语言代码格式的差异(如 API 返回 "zh",当前使用 "zh-CN"
- 需要确保所有音色都有有效的 `voice_demo` URL
- 需要处理 API 可能返回的错误情况
- 需要确保页面加载时的初始状态正确

View File

@ -1,36 +0,0 @@
## 修改发货弹窗功能
### 需求分析
1. 在发货弹窗中显示当前订单ID、订单编号和客户名称
2. 调整表单字段名称以匹配后端API要求
- trackingNo物流单号
- logisticsCompanyCode物流商代码
- logisticsCompany物流公司
- remark发货备注
### 修改内容
1. **修改AdminOrders.vue文件**路径d:\work\Aiproject\DeotalandAi\apps\FrontendDesigner\src\views\admin\AdminOrders\AdminOrders.vue
- 在发货弹窗第244-279行中添加订单信息显示区域
- 修改表单字段名称使其与后端API一致
- 调整表单验证规则
- 更新提交逻辑,确保发送正确的字段到后端
2. **修改发货表单数据结构**
- 将shippingForm中的字段从trackingNumber、carrier、note改为trackingNo、logisticsCompanyCode、logisticsCompany、remark
3. **修改发货提交逻辑**
- 确保提交的数据包含后端API所需的所有字段
- 更新confirmShipOrder方法调用正确的后端API
### 具体实现步骤
1. **添加订单信息显示**在发货弹窗中添加订单基本信息展示区域包括订单ID、订单编号和客户名称
2. **调整表单字段**修改表单字段名称和绑定的数据确保与后端API一致
3. **更新表单验证**:确保所有必填字段都有正确的验证规则
4. **修改提交逻辑**更新confirmShipOrder方法调用LogistIcsService.ship方法并传递正确的参数
5. **测试验证**:确保修改后的弹窗功能正常,表单提交数据正确
### 预期效果
1. 发货弹窗打开时显示当前订单的基本信息订单ID、订单编号、客户名称
2. 表单字段名称与后端API一致确保数据正确提交
3. 表单验证规则完整,确保必填字段都已填写
4. 提交功能正常能够成功调用后端API进行发货操作

View File

@ -1,105 +0,0 @@
# 实现计划
## 1. 安装Fabric.js依赖
* 在FrontendDesigner项目中安装Fabric.js库
* 使用纯JavaScript版本不依赖TypeScript类型定义
## 2. 创建Fabric.js图片编辑组件
* 创建`FabricEditor.vue`组件实现类似PS的图片编辑功能
* 支持基本的图片编辑操作:缩放、旋转、裁剪、添加文字、绘制形状等
* 支持图层管理
* 支持撤销/重做功能
* 支持导出编辑后的图片
## 3. 集成到现有页面
* 在`AdminDisassemblyDetail.vue`中添加Fabric编辑器入口
* 为每个图片项添加"编辑"按钮
* 创建编辑对话框包含Fabric编辑器组件
* 实现图片编辑前后的数据流管理
## 4. 功能实现细节
* **图片加载**从现有图片URL加载到Fabric画布
* **编辑功能**
* 选择工具(移动、缩放、旋转)
* 裁剪工具
* 文字工具
* 绘图工具(画笔、形状)
* 图层管理(新增、删除、上下移动)
* **保存功能**:将编辑后的图片保存到服务器并更新原图片
## 5. 样式设计
* 编辑器界面采用类似PS的布局
* 左侧工具栏
* 顶部菜单栏
* 右侧属性面板
* 底部图层管理
## 6. 交互设计
* 点击图片项的"编辑"按钮打开编辑器
* 编辑完成后保存并关闭编辑器
* 实时预览编辑效果
* 支持撤销/重做操作
## 7. 技术要点
* 使用Fabric.js管理画布和图层
* 实现响应式设计,适配不同屏幕尺寸
* 优化性能,确保流畅的编辑体验
* 处理图片加载和保存的异步操作
* 使用纯JavaScript实现不依赖TypeScript
## 8. 测试验证
* 测试基本编辑功能
* 测试图片保存和更新流程
* 测试多图层编辑
* 测试撤销/重做功能
## 9. 代码结构
* `src/components/fabric-editor/FabricEditor.vue` - 主编辑器组件
* `src/components/fabric-editor/Toolbar.vue` - 工具栏组件
* `src/components/fabric-editor/LayersPanel.vue` - 图层面板组件
* `src/components/fabric-editor/PropertiesPanel.vue` - 属性面板组件
* `src/utils/fabric-utils.js` - Fabric.js工具函数
这个计划将实现一个功能完整、界面友好的Fabric.js图片编辑组件集成到现有的拆件详情页面中提供类似Photoshop的图片编辑功能使用纯JavaScript实现不依赖TypeScript。

View File

@ -1,36 +0,0 @@
# 实现3MF格式模型导出功能
## 1. 安装依赖
- 安装 `three-3mf-exporter` 插件库用于支持3MF格式导出
## 2. 修改 ModelViewer.vue 组件
### 2.1 导入3MF导出器
在组件的脚本部分添加3MF导出器的导入
### 2.2 更新导出选项
在导出下拉菜单中添加3MF格式选项
### 2.3 实现3MF导出功能
添加 `exportAs3MF` 方法使用3MF导出器将模型导出为3MF格式
### 2.4 更新导出命令处理
`handleExportCommand` 方法中添加对3MF格式的支持
### 2.5 添加国际化支持
在国际化文件中添加3MF相关的文本翻译
## 3. 测试功能
- 运行开发服务器测试3MF导出功能是否正常工作
- 检查导出的3MF文件是否可以正常打开
## 4. 兼容性检查
- 确保3MF导出功能与现有导出功能兼容
- 确保3MF导出功能在不同设备上都能正常工作
## 5. 代码优化
- 确保代码符合项目的代码风格和最佳实践
- 优化导出性能,确保导出过程流畅
## 6. 文档更新
- 更新组件文档说明3MF导出功能的使用方法
- 更新项目文档添加3MF格式支持的说明

View File

@ -1,102 +0,0 @@
# 实现Canvas画布组件含橡皮擦功能
## 1. 功能需求
- 支持Ctrl+滚轮缩放画布内容
- 支持将现有div中的图片拖入画布
- 支持画笔功能,可使用不同颜色标注或绘画
- 支持橡皮擦功能,可擦除画布上的内容
- 添加textarea提示词输入框和重绘按钮
- 点击重绘按钮将canvas内容转为base64并与提示词一起打印到控制台
## 2. 实现方案
### 2.1 创建Canvas组件
创建一个新的Vue 3组件`CanvasEditor.vue`,包含以下核心功能:
#### 2.1.1 画布基础功能
- Canvas元素设置
- 缩放功能实现Ctrl+滚轮)
- 拖拽功能实现
- 画笔功能实现
- 橡皮擦功能实现
#### 2.1.2 界面元素
- 画布容器
- 画笔/橡皮擦切换按钮
- 画笔颜色选择器
- 画笔粗细调整
- 提示词输入框
- 重绘按钮
### 2.2 组件集成
将`CanvasEditor.vue`组件集成到`AdminDisassemblyDetail.vue`页面中,位于`disassembled-images` div下方。
## 3. 技术实现
### 3.1 缩放功能
- 监听鼠标滚轮事件
- 判断Ctrl键是否按下
- 调整画布缩放比例
- 重新绘制画布内容
### 3.2 拖拽功能
- 为图片添加拖拽事件监听
- 实现拖拽开始、拖拽结束事件
- 将拖拽的图片绘制到画布上
### 3.3 画笔功能
- 监听鼠标按下、移动、抬起事件
- 实现不同颜色的画笔
- 支持画笔粗细调整
### 3.4 橡皮擦功能
- 添加画笔/橡皮擦切换按钮
- 实现橡皮擦模式:设置`globalCompositeOperation = 'destination-out'`
- 支持橡皮擦粗细调整
- 与画笔共享粗细调整控件
### 3.5 重绘功能
- 将canvas内容转换为base64格式
- 获取textarea中的提示词
- 将两者一起打印到控制台
## 4. 文件结构
### 4.1 创建新文件
- `apps/FrontendDesigner/src/components/CanvasEditor.vue` - Canvas编辑器组件
### 4.2 修改现有文件
- `apps/FrontendDesigner/src/views/admin/AdminDisassemblyDetail/AdminDisassemblyDetail.vue` - 集成Canvas组件
## 5. 实现步骤
1. 创建`CanvasEditor.vue`组件,实现基础画布功能
2. 实现缩放功能
3. 实现拖拽功能
4. 实现画笔功能
5. 实现橡皮擦功能
6. 添加界面元素(切换按钮、颜色选择器、提示词输入框、重绘按钮)
7. 实现重绘功能
8. 将组件集成到AdminDisassemblyDetail页面
9. 测试所有功能
## 6. 预期效果
- 页面中显示一个Canvas画布位于图片预览区域下方
- 可以通过Ctrl+滚轮缩放画布内容
- 可以将图片预览区域的图片拖入画布
- 可以切换画笔/橡皮擦模式
- 可以使用不同颜色的画笔在画布上绘画
- 可以使用橡皮擦擦除画布上的内容
- 可以调整画笔/橡皮擦的粗细
- 可以在提示词输入框中输入文字
- 点击重绘按钮后控制台会打印canvas的base64图片和提示词
## 7. 注意事项
- 确保组件样式与现有页面风格一致
- 确保拖拽功能能够正确识别和处理图片
- 确保缩放功能不会影响画布的正常使用
- 确保画笔和橡皮擦功能流畅,不会出现卡顿
- 确保base64转换功能正常工作
- 确保画笔/橡皮擦切换功能正常

View File

@ -1,46 +0,0 @@
# 实现佣金管理功能
## 一、功能需求
1. 在侧边栏添加佣金管理菜单
2. 创建佣金管理页面,包含:
- 列表展示达人的佣金对应用户的实际支付金额和商品金额
- 审核用户佣金的功能按钮
- 页面上方可设置佣金比例默认为15%
## 二、实现步骤
### 1. 添加国际化支持
- 在 `src/locales/index.js` 中添加佣金管理相关的中英文翻译
### 2. 更新侧边栏菜单
- 在 `src/components/layout/AppSidebar.vue``coreMenuItems` 数组中添加佣金管理菜单
- 配置菜单的ID、路径、标签和图标
### 3. 添加路由配置
- 在 `src/router/index.js` 中添加佣金管理页面的路由配置
- 导入佣金管理组件
- 配置路由路径、名称和组件
### 4. 创建佣金管理页面组件
- 在 `src/views/` 目录下创建 `CommissionManagement.vue` 文件
- 实现页面布局和功能:
- 页面上方的佣金比例设置区域默认值为15%
- 佣金列表展示区域,包含达人信息、用户实际支付金额、商品金额等字段
- 每个佣金项的审核功能按钮
### 5. 实现页面功能
- 添加佣金比例设置的双向绑定和保存功能
- 实现佣金列表的数据展示
- 添加审核功能按钮的点击事件处理
## 三、技术要点
- 使用 Vue 3 的组合式 API
- 遵循现有的代码风格和组件设计模式
- 使用 Element Plus 组件库实现 UI 元素
- 实现响应式设计,适配不同屏幕尺寸
## 四、文件变更
1. `src/locales/index.js` - 添加佣金管理相关的国际化翻译
2. `src/components/layout/AppSidebar.vue` - 更新侧边栏菜单,添加佣金管理选项
3. `src/router/index.js` - 添加佣金管理页面的路由配置
4. `src/views/CommissionManagement.vue` - 创建佣金管理页面组件

View File

@ -1,112 +0,0 @@
# 实现侧边栏权限管理功能
## 功能需求
在侧边栏新增权限管理功能,包含以下二级目录:
- 角色管理
- 路由管理(每个路由可配置对应按钮权限)
- 用户列表(展示后台管理系统所有账号)
## 实现步骤
### 1. 修改侧边栏菜单配置
**文件**`apps/FrontendDesigner/src/components/admin/AdminLayout.vue`
- 在侧边栏添加权限管理子菜单
- 包含三个二级菜单项:角色管理、路由管理、用户列表
- 使用Element Plus的`el-sub-menu`和`el-menu-item`组件
- 添加对应的图标
### 2. 配置路由
**文件**`apps/FrontendDesigner/src/router/index.js`
- 添加权限管理相关路由
- 角色管理:`/admin/role-management`
- 路由管理:`/admin/route-management`
- 用户列表:`/admin/user-list`
- 所有路由都需要认证
### 3. 创建页面组件
**目录**`apps/FrontendDesigner/src/views/admin/`
- 创建`AdminRoleManagement`组件
- 创建`AdminRouteManagement`组件
- 创建`AdminUserList`组件
- 每个组件包含基本的模板结构和国际化支持
### 4. 更新子菜单映射
**文件**`apps/FrontendDesigner/src/components/admin/AdminLayout.vue`
- 在`submenuMap`中添加权限管理子菜单的映射关系
- 确保路由切换时正确展开对应的父菜单
## 技术实现要点
### 菜单结构
```vue
<el-sub-menu index="/admin/permission">
<template #title>
<el-icon><Lock /></el-icon>
<span>{{ t('admin.layout.permission') }}</span>
</template>
<el-menu-item index="/admin/role-management">
<el-icon><User /></el-icon>
<template #title>{{ t('admin.layout.roleManagement') }}</template>
</el-menu-item>
<el-menu-item index="/admin/route-management">
<el-icon><Document /></el-icon>
<template #title>{{ t('admin.layout.routeManagement') }}</template>
</el-menu-item>
<el-menu-item index="/admin/user-list">
<el-icon><List /></el-icon>
<template #title>{{ t('admin.layout.userList') }}</template>
</el-menu-item>
</el-sub-menu>
```
### 路由配置
```javascript
// 权限管理相关组件
const AdminRoleManagement = () => import('@/views/admin/AdminRoleManagement.vue')
const AdminRouteManagement = () => import('@/views/admin/AdminRouteManagement.vue')
const AdminUserList = () => import('@/views/admin/AdminUserList.vue')
// 添加到children数组中
{
path: 'role-management',
name: 'AdminRoleManagement',
component: AdminRoleManagement,
meta: {
title: '角色管理'
}
},
{
path: 'route-management',
name: 'AdminRouteManagement',
component: AdminRouteManagement,
meta: {
title: '路由管理'
}
},
{
path: 'user-list',
name: 'AdminUserList',
component: AdminUserList,
meta: {
title: '用户列表'
}
}
```
### 国际化支持
- 在`locales/lang/zh-CN.js`和`en-US.js`中添加权限管理相关的翻译
- 包括菜单名称和页面标题
## 预期效果
- 侧边栏新增权限管理菜单,点击展开二级目录
- 点击二级菜单项跳转到对应页面
- 页面包含基本的布局结构
- 支持中英文切换
- 支持主题切换
## 后续优化方向
- 实现角色管理功能(增删改查)
- 实现路由权限配置功能
- 实现用户列表展示和管理功能
- 添加表单验证和错误处理
- 优化页面样式和交互体验

View File

@ -1,67 +0,0 @@
# 实现合并图片功能
## 需求分析
1. 在h3标题右侧添加"合并图片"按钮
2. 点击按钮后,在图片展示区域下方出现选中框
3. 当用户选中两个以上图片时,显示"确定合并"按钮
4. 点击"确定合并"按钮将选中的图片打印在控制台上
## 实现方案
### 1. 修改 AdminDisassemblyDetail.vue
#### 1.1 添加合并图片按钮
- 在h3标题右侧添加"合并图片"按钮
- 绑定点击事件,切换图片选择模式
#### 1.2 添加状态管理
- 添加 `mergeMode` 状态,控制是否显示选择框
- 添加 `selectedImages` 状态,存储选中的图片
- 添加 `showMergeConfirm` 计算属性,根据选中图片数量决定是否显示确定合并按钮
#### 1.3 添加图片选择逻辑
- 为每个图片项添加选择功能
- 实现选择/取消选择图片的方法
#### 1.4 添加合并功能
- 实现 `confirmMerge` 方法,将选中的图片打印到控制台
- 添加"确定合并"按钮,绑定点击事件
### 2. 修改 ImageWrapper.vue
#### 2.1 添加选中状态支持
- 添加 `selected` 属性,接收选中状态
- 添加选中样式,显示选中效果
## 代码修改点
### 1. AdminDisassemblyDetail.vue
- **模板部分**
- 在h3标题右侧添加合并图片按钮
- 在图片展示区域下方添加确定合并按钮
- **脚本部分**
- 添加 `mergeMode`、`selectedImages` 状态
- 添加 `toggleMergeMode`、`toggleImageSelection`、`confirmMerge` 方法
- 添加 `showMergeConfirm` 计算属性
- **样式部分**
- 为图片选择状态添加样式
### 2. ImageWrapper.vue
- **模板部分**
- 添加选中状态的视觉反馈
- **脚本部分**
- 添加 `selected` 属性
- 添加 `onSelect` 事件
## 预期效果
1. 点击"合并图片"按钮,图片进入选择模式
2. 点击图片可选择/取消选择
3. 选中两张以上图片时,显示"确定合并"按钮
4. 点击"确定合并"按钮,控制台打印选中的图片信息
## 实现步骤
1. 先修改 ImageWrapper.vue添加选中状态支持
2. 然后修改 AdminDisassemblyDetail.vue添加合并图片按钮和选择逻辑
3. 最后添加合并功能和确定合并按钮
这个方案将实现用户要求的所有功能,同时保持代码的可维护性和扩展性。

View File

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

View File

@ -1,90 +0,0 @@
## 实现完整层级匹配的嵌套路由权限过滤
### 需求分析
1. 权限数据格式:`router:路由1:路由2:路由3:...`,其中 `router:` 是固定前缀
2. **匹配规则**:所有层级的路由标识都需要依次匹配,每一层路由必须是上一层路由的子路由
3. 支持任意深度的嵌套路由匹配
4. 确保路由唯一性,避免重复添加
5. 如果有重复的父级路由,只添加子路由
### 解决方案
1. **解析权限规则**将字符串权限规则按冒号分隔得到路由name数组
2. **创建路由映射表**:构建包含所有路由及其嵌套层级关系的映射
3. **完整层级匹配**:根据权限规则的所有层级,依次匹配每一层路由
4. **构建完整路由树**:将匹配到的所有层级路由构建成完整的嵌套结构
5. **去重处理**:确保每个路由只添加一次
### 实现步骤
1. 修改 `addPermissionRoutes` 方法
2. 解析 `permissionRouter` 中的权限规则转换为路由name数组
3. 构建包含所有路由及其层级关系的映射
4. 遍历每个权限规则,依次匹配每一层路由
5. 构建完整的嵌套路由树,确保路由唯一性
6. 将构建好的路由树动态添加到Vue Router中
### 代码修改点
**文件**`d:\work\Aiproject\DeotalandAi\apps\FrontendDesigner\src\stores\index.js`
**函数**`addPermissionRoutes`第67-89行
**核心逻辑**
1. 替换当前的简单过滤逻辑
2. 解析权限规则得到路由name数组
3. 构建包含所有路由及其层级关系的映射
4. 遍历每个权限规则,依次匹配每一层路由
5. 构建完整的嵌套路由树
6. 确保每个路由只添加一次
7. 将构建好的路由树动态添加到Vue Router中
### 实现细节
1. **权限规则解析**
- 将 `router:路由1:路由2:路由3` 转换为 `['路由1', '路由2', '路由3']`
- 每个数组元素代表一个路由层级
2. **路由映射构建**
- 递归遍历 `permissionRoutes` 及其子路由
- 构建所有路由的name到路由对象的映射
- 构建父路由到子路由的映射关系
3. **完整层级匹配**
- 对于每个权限规则数组 `[name1, name2, name3]`
- 第一步:检查 `name1` 是否存在于路由映射中
- 第二步:检查 `name1` 的子路由中是否存在 `name2`
- 第三步:检查 `name2` 的子路由中是否存在 `name3`
- 以此类推,直到匹配完所有层级
4. **路由树构建**
- 为每个权限规则构建完整的路由树
- 确保父级路由存在
- 避免重复添加路由
- 只添加匹配到的路由及其父级路由(如果需要)
5. **去重处理**
- 使用Set或Map确保每个路由只添加一次
- 确保相同路由的子路由不会被重复添加
### 预期效果
- 支持一级路由匹配(如 `router:AdminOrders`
- 支持二级嵌套路由匹配(如 `router:AdminOrders:AdminContentReview`
- 支持任意深度嵌套路由匹配(如 `router:路由1:路由2:路由3:...`
- 所有层级的路由都需要匹配,确保路由完整性
- 构建完整的嵌套路由结构
- 确保路由唯一性,避免重复添加
### 实现示例
```javascript
// 权限规则示例
const permissionRules = ['router:AdminOrders', 'router:AdminOrders:AdminContentReview', 'router:AdminPermission:AdminRoleManagement'];
// 解析后的规则
const parsedRules = [
['AdminOrders'],
['AdminOrders', 'AdminContentReview'],
['AdminPermission', 'AdminRoleManagement']
];
// 匹配逻辑
// 规则1匹配AdminOrders一级路由
// 规则2先匹配AdminOrders一级路由再在其下匹配AdminContentReview子路由
// 规则3先匹配AdminPermission一级路由再在其下匹配AdminRoleManagement子路由
```

View File

@ -1,78 +0,0 @@
# 实现手机号登录功能
## 需求分析
- 在现有登录系统中添加手机号登录功能
- 支持两种登录方式:
1. 手机号+验证码直接登录
2. 手机号+验证码+密码注册密码登录
## 实现方案
### 1. 创建手机号登录页面组件
- 创建 `PhoneLogin.vue` 页面组件,位于 `apps/frontend/src/views/Login/`
- 实现手机号登录表单,包括:
- 手机号输入框
- 验证码输入框和获取验证码按钮
- 密码输入框(可选,用于注册密码登录)
- 登录/注册按钮
### 2. 配置路由
- 在 `apps/frontend/src/router/index.js` 中添加手机号登录路由
- 路径:`/login/phone`
- 名称:`phone-login`
### 3. 修改主登录页面
- 在 `Login.vue` 中添加跳转到手机号登录的链接
- 位置:在忘记密码和注册链接附近
### 4. 实现手机号登录表单组件
- 创建 `PhoneLoginForm.vue` 组件,位于 `apps/frontend/src/components/auth/`
- 实现表单验证逻辑:
- 手机号格式验证
- 验证码格式验证
- 密码格式验证(当选择注册密码登录时)
- 实现获取验证码功能:
- 倒计时功能
- 防重复发送机制
### 5. 实现登录逻辑
- 在 `login.js` 中添加手机号登录相关方法
- 实现验证码发送API调用
- 实现手机号+验证码登录API调用
- 实现手机号+验证码+密码注册密码登录API调用
### 6. 国际化支持
- 在国际化文件中添加手机号登录相关的文本
- 支持中英文切换
### 7. 样式适配
- 确保手机号登录页面与现有登录页面样式保持一致
- 支持响应式设计
- 支持暗色主题
## 实现步骤
1. 创建 `PhoneLogin.vue` 页面组件
2. 创建 `PhoneLoginForm.vue` 表单组件
3. 配置手机号登录路由
4. 修改主登录页面添加跳转链接
5. 实现表单验证和获取验证码功能
6. 实现登录逻辑
7. 添加国际化支持
8. 测试功能完整性
## 预期效果
- 用户可以在登录页面选择手机号登录方式
- 手机号登录页面支持两种登录模式
- 表单验证逻辑完整
- 界面样式与现有系统保持一致
- 支持国际化和暗色主题
## 技术要点
- Vue 3 Composition API
- Vue Router 4
- Element Plus UI组件库
- Vue I18n国际化
- 表单验证使用Vuelidate
- 响应式设计
- 暗色主题支持

View File

@ -1,197 +0,0 @@
# 实现提示词管理功能
## 1. 功能概述
在管理后台侧边栏添加提示词管理功能,实现左右分栏的提示词管理界面,支持提示词的增删改查和拖拽排序。
## 2. 技术栈
- Vue 3 + Vite
- Element Plus
- Vue Router
- Vue I18n
## 3. 实现步骤
### 3.1 添加侧边栏菜单项
- 修改 `src/components/admin/AdminLayout.vue`,在侧边栏菜单中添加提示词管理菜单项
- 导入相应的图标组件
### 3.2 配置路由
- 修改 `src/router/index.js`,添加提示词管理页面的路由配置
- 使用懒加载方式引入组件
### 3.3 创建提示词管理页面
- 创建 `src/views/admin/AdminPromptManagement.vue` 文件
- 实现左右分栏布局
- 左侧:未生效提示词区域,支持增删改查
- 右侧:生效提示词区域,支持拖拽排序和删除功能
### 3.4 实现提示词卡片组件
- 创建 `src/components/admin/PromptCard.vue` 组件
- 支持展示参考图和标题
- 支持点击查看详情
- 支持删除操作
### 3.5 实现提示词添加/编辑弹窗
- 实现添加提示词的弹窗组件
- 包含提示词类型选择(动物/人物/通用)
- 包含提示词标题、内容输入
- 包含参考图上传功能
### 3.6 实现拖拽功能
- 使用 Element Plus 的 `el-transfer` 或自定义拖拽实现
- 支持从左侧拖拽到右侧
- 支持右侧区域内的拖拽排序
### 3.7 添加国际化支持
- 在语言文件中添加提示词管理相关的翻译
### 3.8 模拟数据
- 在组件中添加模拟数据,实现本地数据管理
- 支持提示词的增删改查操作
## 4. 详细实现
### 4.1 侧边栏菜单项
`AdminLayout.vue` 的侧边栏菜单中添加:
```vue
<el-menu-item index="/admin/prompt-management">
<el-icon><Document /></el-icon>
<template #title>{{ t('admin.layout.promptManagement') }}</template>
</el-menu-item>
```
### 4.2 路由配置
`router/index.js` 中添加:
```javascript
const AdminPromptManagement = () => import('@/views/admin/AdminPromptManagement.vue')
// 在 admin 路由的 children 数组中添加
{
path: 'prompt-management',
name: 'AdminPromptManagement',
component: AdminPromptManagement,
meta: {
title: '提示词管理'
}
}
```
### 4.3 提示词管理页面结构
```vue
<template>
<div class="prompt-management">
<div class="page-header">
<h2>{{ t('admin.promptManagement.title') }}</h2>
<el-button type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
{{ t('admin.promptManagement.addPrompt') }}
</el-button>
</div>
<div class="prompt-container">
<!-- 左侧:未生效提示词 -->
<div class="prompt-section">
<div class="section-header">
<h3>{{ t('admin.promptManagement.inactivePrompts') }}</h3>
<span class="count">{{ inactivePrompts.length }}</span>
</div>
<div class="prompt-list">
<PromptCard
v-for="prompt in inactivePrompts"
:key="prompt.id"
:prompt="prompt"
@edit="showEditDialog"
@delete="deletePrompt"
@drag-start="handleDragStart"
/>
</div>
</div>
<!-- 右侧:生效提示词 -->
<div class="prompt-section">
<div class="section-header">
<h3>{{ t('admin.promptManagement.activePrompts') }}</h3>
<span class="count">{{ activePrompts.length }}</span>
</div>
<div
class="prompt-list active-list"
@drop="handleDrop"
@dragover.prevent
>
<PromptCard
v-for="prompt in activePrompts"
:key="prompt.id"
:prompt="prompt"
:isActive="true"
@edit="showEditDialog"
@delete="removeFromActive"
@drag-start="handleDragStart"
/>
</div>
</div>
</div>
<!-- 添加/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEditing ? t('admin.promptManagement.editPrompt') : t('admin.promptManagement.addPrompt')"
width="600px"
>
<!-- 弹窗内容 -->
</el-dialog>
</div>
</template>
```
### 4.4 实现拖拽功能
使用原生 HTML5 拖拽 API 或 Element Plus 的拖拽组件,实现:
- 从左侧拖拽到右侧,将提示词设为生效状态
- 在右侧区域内拖拽,调整提示词顺序
### 4.5 模拟数据结构
```javascript
const prompts = [
{
id: 1,
title: '示例提示词',
content: '这是一个示例提示词',
type: 'general', // animal, person, general
referenceImage: 'https://example.com/image.jpg',
isActive: false,
createdAt: new Date()
}
]
```
### 4.6 右侧删除功能实现
```javascript
// 从生效区域移除提示词
const removeFromActive = (promptId) => {
const prompt = prompts.find(p => p.id === promptId)
if (prompt) {
prompt.isActive = false
// 更新响应式数据
}
}
```
## 5. 预期效果
- 侧边栏新增提示词管理菜单项
- 提示词管理页面分为左右两栏
- 左侧可添加、编辑、删除提示词
- 支持拖拽提示词从左侧到右侧
- 右侧提示词可拖拽排序和删除
- 数据本地模拟实现
## 6. 文件修改清单
- `src/components/admin/AdminLayout.vue` - 添加侧边栏菜单项
- `src/router/index.js` - 添加路由配置
- `src/views/admin/AdminPromptManagement.vue` - 创建提示词管理页面
- `src/components/admin/PromptCard.vue` - 创建提示词卡片组件
- 语言文件 - 添加国际化支持
## 7. 注意事项
- 确保拖拽功能在各种浏览器中正常工作
- 实现响应式设计,适配不同屏幕尺寸
- 保持代码风格与现有代码一致
- 添加适当的用户反馈和验证

View File

@ -1,66 +0,0 @@
# 实现智能体设备管理功能
## 1. 修改智能体卡片
* 在 `AgentManagement.vue` 中,为每个智能体卡片添加设备数量展示
* 根据 `device_count` 字段是否大于零,条件显示已绑设备数量
* 添加点击设备数量跳转到设备列表页面的功能
## 2. 创建设备列表页面
* 新建 `DeviceList.vue` 页面,用于展示与智能体绑定的设备列表
* 使用模拟数据填充设备列表
* 支持从 URL 参数中获取智能体 ID
* 添加设备列表的基本样式和交互
## 3. 更新路由配置
* 在 `router/index.js` 中添加设备列表页面的路由
* 路由路径为 `/device-list/:agentId`,支持带参数访问
## 4. 添加国际化支持
* 在 `locales/index.js` 中添加设备列表相关的中英文翻译
## 5. 实现跳转逻辑
* 在 `AgentManagement.vue` 中添加跳转到设备列表页面的方法
* 确保跳转时携带正确的智能体 ID 参数
## 6. 测试与优化
* 测试智能体卡片的设备数量展示
* 测试点击设备数量跳转功能
* 测试设备列表页面的模拟数据展示
* 确保多端适配和响应式设计
## 实现步骤
1. 首先修改 `AgentManagement.vue` 中的智能体卡片组件
2. 创建新的 `DeviceList.vue` 页面
3. 更新路由配置
4. 添加国际化支持
5. 测试功能完整性
## 技术要点
* 使用 Vue3 Composition API
* 响应式设计,支持移动端、桌面端和平板端
* 国际化支持中英文切换
* 使用 Element Plus 组件库
* 模拟数据展示

View File

@ -0,0 +1,96 @@
## 实现用户中心优惠券模块
### 1. 设计目标
- 在用户中心添加优惠券模块,显示用户的代金券信息
- 包含优惠券数量统计和优惠券列表
- 支持查看优惠券详情
- 适配当前页面的设计风格和主题色
- 支持中英文切换
### 2. 实现步骤
#### 2.1 添加i18n翻译
`d:\work\Aiproject\DeotalandAi\apps\frontend\src\locales\index.js` 中添加优惠券相关的翻译:
- 中文(zh)在userCenter对象下添加voucher相关翻译
- 英文(en)在userCenter对象下添加voucher相关翻译
#### 2.2 实现优惠券UI组件
`d:\work\Aiproject\DeotalandAi\apps\frontend\src\views\user\index.vue` 中添加优惠券模块:
- 添加优惠券数量统计卡片
- 添加优惠券列表,包含优惠券的基本信息(金额、状态、到期时间等)
- 添加优惠券详情弹窗
#### 2.3 实现API调用逻辑
- 添加优惠券数据的响应式状态
- 实现获取优惠券列表的方法
- 实现获取优惠券数量统计的方法
- 实现获取优惠券详情的方法
- 在组件挂载时初始化数据
#### 2.4 适配主题色和响应式设计
- 使用当前页面的主题色变量
- 适配深色主题
- 实现响应式布局,适配移动端、桌面端和平板端
### 3. 代码实现细节
#### 3.1 i18n翻译设计
```javascript
// 在userCenter对象下添加voucher相关翻译
voucher: {
title: '优惠券',
availableCount: '可用',
usedCount: '已使用',
expiredCount: '已过期',
totalCount: '总数',
couponCode: '优惠券码',
amount: '金额',
currency: '货币',
minOrderAmount: '最低订单金额',
status: '状态',
statusDesc: '状态描述',
expireAt: '到期时间',
sourceType: '来源类型',
sourceDesc: '来源描述',
createdAt: '创建时间',
viewDetails: '查看详情',
detailTitle: '优惠券详情',
close: '关闭',
empty: '暂无优惠券',
loading: '加载中...'
}
```
#### 3.2 优惠券UI组件设计
- 使用卡片式设计,与当前页面风格保持一致
- 优惠券列表使用网格布局,适配不同屏幕尺寸
- 优惠券详情使用Element Plus的Dialog组件
- 支持优惠券状态的视觉区分(可用、已使用、已过期)
#### 3.3 API调用逻辑设计
- 使用async/await语法调用API方法
- 添加加载状态管理
- 处理API响应和错误
- 实现数据转换将API返回的数据格式转换为前端需要的格式
### 4. 技术要点
- 使用Vue3 Composition API
- 集成Element Plus组件库
- 适配主题色和中英文切换
- 响应式设计,适配不同设备尺寸
- 遵循当前项目的代码规范和设计风格
### 5. 预期效果
- 用户中心页面添加优惠券模块
- 显示优惠券数量统计
- 显示优惠券列表,支持查看详情
- 适配当前页面的设计风格和主题色
- 支持中英文切换
- 适配移动端、桌面端和平板端
### 6. 测试要点
- 测试API调用是否正常
- 测试优惠券列表和详情显示是否正确
- 测试中英文切换是否正常
- 测试主题色切换是否正常
- 测试响应式布局是否正常

View File

@ -1,230 +0,0 @@
# 实现用户编辑和状态管理功能
## 1. 修改编辑弹窗功能
### 1.1 调整表单字段
* 保留用户昵称nickname字段移除其他不必要的字段
* 确保表单回显用户当前昵称
### 1.2 更新验证规则
* 只保留昵称字段的验证规则
* 确保昵称字段必填
### 1.3 修改提交逻辑
* 调用 `updateUserName` 方法index.js#L43修改用户昵称
* 成功后刷新用户列表
* 添加错误处理
## 2. 实现封禁/解封功能
### 2.1 修改封禁/解封按钮逻辑
* 根据用户状态active/disable展示不同的按钮
* 封禁按钮:当用户状态为 active 时显示
* 解封按钮:当用户状态为 disable 时显示
### 2.2 更新按钮事件
* 调用 `updateUserStatus` 方法index.js#L31修改用户状态
* 封禁时:将状态从 active 改为 disable
* 解封时:将状态从 disable 改为 active
* 成功后刷新用户列表
* 添加错误处理
## 3. 代码修改点
### 3.1 修改表单模板
```vue
<!-- 只保留昵称字段 -->
el-form-item :label="t('admin.users.username')" prop="nickname">
<el-input v-model="form.nickname" />
</el-form-item>
```
### 3.2 更新验证规则
```javascript
const rules = {
nickname: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
}
```
### 3.3 修改 handleEdit 方法
```javascript
const handleEdit = (row) => {
isEditing.value = true
// 只复制需要的字段
Object.assign(form, { id: row.id, nickname: row.nickname })
dialogVisible.value = true
}
```
### 3.4 修改 handleSubmit 方法
```javascript
const handleSubmit = async () => {
if (!formRef.value) return
formRef.value.validate(async (valid) => {
if (valid) {
if (isEditing.value) {
// 编辑用户 - 调用API
try {
await adminOrders.updateUserName({
id: form.id,
nickname: form.nickname
})
ElMessage.success('用户昵称更新成功')
// 刷新列表
refresh()
} catch (error) {
ElMessage.error('用户昵称更新失败')
console.error('更新用户昵称失败:', error)
}
}
dialogVisible.value = false
}
})
}
```
### 3.5 修改封禁/解封按钮逻辑
```vue
<el-button
v-if="row.status === 'active'"
size="small"
type="danger"
@click="handleBan(row)"
>
{{ t('admin.users.ban') }}
</el-button>
<el-button
v-else-if="row.status === 'disable'"
size="small"
type="success"
@click="handleUnban(row)"
>
{{ t('admin.users.unban') }}
</el-button>
<el-button
v-else
size="small"
type="success"
@click="handleUnban(row)"
>
{{ t('admin.users.unban') }}
</el-button>
```
### 3.6 更新 handleBan 和 handleUnban 方法
```javascript
const handleBan = async (row) => {
try {
await ElMessageBox.confirm(
`确定要封禁用户 "${row.nickname}" 吗?`,
'封禁用户',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 调用API修改状态
await adminOrders.updateUserStatus({
id: row.id,
status: 'disable'
})
ElMessage.success('用户封禁成功')
// 刷新列表
refresh()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('用户封禁失败')
console.error('封禁用户失败:', error)
}
}
}
const handleUnban = async (row) => {
try {
await ElMessageBox.confirm(
`确定要解封用户 "${row.nickname}" 吗?`,
'解封用户',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'success'
}
)
// 调用API修改状态
await adminOrders.updateUserStatus({
id: row.id,
status: 'active'
})
ElMessage.success('用户解封成功')
// 刷新列表
refresh()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('用户解封失败')
console.error('解封用户失败:', error)
}
}
}
```
## 4. 测试要点
1. 编辑用户昵称:
* 点击编辑按钮,弹窗显示当前昵称
* 修改昵称,点击保存
* 调用API成功后列表刷新显示新昵称
2. 封禁用户:
* 对 active 状态的用户点击封禁
* 调用API成功后用户状态变为 disable
* 按钮变为解封
3. 解封用户:
* 对 disable 状态的用户点击解封
* 调用API成功后用户状态变为 active
* 按钮变为封禁
4. 错误处理:
* 测试API调用失败情况
* 确保错误提示正确显示
* 列表不会错误更新

View File

@ -1,245 +0,0 @@
# 实现用户邀请列表功能
## 1. 功能需求
1. 在用户列表的操作按钮中添加"邀请列表"按钮
2. 点击按钮携带用户ID跳转到邀请列表页面
3. 新建邀请列表页面初始化调用getUsersInvites API渲染列表
4. 添加分页功能
## 2. 实现步骤
### 2.1 修改用户列表组件,添加邀请列表按钮
* 在操作按钮区域添加"邀请列表"按钮
* 绑定点击事件携带用户ID跳转到邀请列表页面
### 2.2 创建邀请列表页面组件
* 新建`AdminUserInvites.vue`文件
* 实现页面布局,包括标题、筛选条件、列表和分页
* 实现API调用逻辑获取邀请列表数据
* 添加分页功能
### 2.3 添加路由配置
* 在`router/index.js`中添加邀请列表页面的路由
* 配置路由参数接收用户ID
### 2.4 实现邀请列表页面功能
* 初始化时调用getUsersInvites API获取数据
* 处理分页逻辑
* 实现数据渲染
* 添加错误处理
## 3. 代码修改点
### 3.1 修改用户列表组件
```vue
<!-- 在操作按钮区域添加邀请列表按钮 -->
el-button
size="small"
type="info"
@click="handleInviteList(row)"
>
{{ t('admin.users.inviteList') }}
</el-button>
<!-- 添加点击事件处理函数 -->
const handleInviteList = (row) => {
router.push({
name: 'AdminUserInvites',
params: { id: row.id }
})
}
```
### 3.2 添加路由配置
```javascript
// 导入组件
const AdminUserInvites = () => import('@/views/admin/AdminUsers/AdminUserInvites.vue')
// 添加路由
{
path: 'users/:id/invites',
name: 'AdminUserInvites',
component: AdminUserInvites,
meta: {
title: '邀请列表'
}
}
```
### 3.3 创建邀请列表页面组件
```vue
<template>
<div class="user-invites">
<h2>{{ t('admin.users.inviteList') }}</h2>
<!-- 列表区域 -->
<div class="invite-table">
<el-table
:data="inviteList"
stripe
style="width: 100%"
v-loading="loading"
>
<!-- 列表列定义 -->
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="nickname" :label="t('admin.users.username')" min-width="120" />
<el-table-column prop="email" :label="t('admin.users.email')" min-width="180" />
<el-table-column prop="registerDate" :label="t('admin.users.registerDate')" min-width="160" />
<el-table-column prop="status" :label="t('admin.users.status')" min-width="100">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">
{{ t(`admin.users.statusOptions.${row.status}`) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="totalInvites"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { AdminOrders } from './index.js'
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const adminOrders = new AdminOrders()
// 响应式数据
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(20)
const totalInvites = ref(0)
const inviteList = ref([])
const userId = ref(route.params.id)
// 获取状态标签类型
const getStatusTagType = (status) => {
const typeMap = {
active: 'success',
disable: 'info',
banned: 'danger'
}
return typeMap[status] || 'info'
}
// 获取邀请列表
const getInviteList = async () => {
loading.value = true
try {
const result = await adminOrders.getUsersInvites({
id: userId.value,
pageSize: pageSize.value,
pageNum: currentPage.value
})
inviteList.value = result.rows || []
totalInvites.value = result.total || 0
} catch (error) {
console.error('获取邀请列表失败:', error)
} finally {
loading.value = false
}
}
// 分页处理
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
getInviteList()
}
const handleCurrentChange = (page) => {
currentPage.value = page
getInviteList()
}
// 初始化
onMounted(() => {
getInviteList()
})
</script>
<style scoped>
/* 样式定义 */
.user-invites {
padding: 20px;
}
.invite-table {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
margin-bottom: 24px;
}
.pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
</style>
```
### 3.4 添加翻译键
在中英文语言包中添加邀请列表相关的翻译键:
```javascript
// 中文
inviteList: '邀请列表',
// 英文
inviteList: 'Invite List'
```
## 4. 测试要点
1. 邀请列表按钮显示正确
2. 点击按钮能正确跳转到邀请列表页面并携带用户ID
3. 邀请列表页面能正确调用API获取数据
4. 分页功能正常工作
5. 错误处理正常
## 5. 预期效果
1. 用户列表中每个用户都有一个"邀请列表"按钮
2. 点击按钮跳转到邀请列表页面URL中包含用户ID
3. 邀请列表页面显示该用户邀请的所有用户
4. 分页功能正常,能切换页面和调整每页显示数量
5. 数据加载时有加载状态提示
6. API调用失败时有错误提示

View File

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

View File

@ -1,35 +0,0 @@
# 实现积分充值页面
## 1. 页面创建
- 创建积分充值页面组件:`src/views/PointsRecharge.vue`
- 按照参考图2的设计风格实现页面布局
- 包含两个积分套餐300积分/$30/1年1000积分/$80/1年
## 2. 路由配置
- 在`src/router/index.js`中添加积分充值路由
- 路径:`/points-recharge`
- 名称:`points-recharge`
## 3. 翻译配置
- 在`src/locales/index.js`中添加中英文翻译
- 添加积分充值相关的翻译文本
- 确保支持中英文切换
## 4. 页面设计
- 实现深色主题的套餐卡片设计
- 包含套餐名称、价格、有效期等信息
- 添加购买按钮
- 支持响应式设计
## 5. 功能实现
- 套餐选择功能
- 购买按钮交互
- 支持中英文切换
## 6. 导航链接
- 更新相关页面,确保"价格"链接指向积分充值页面
## 7. 测试
- 测试页面显示效果
- 测试中英文切换功能
- 测试路由跳转

View File

@ -1,121 +0,0 @@
# 实现积分管理功能
## 1. 侧边栏新增积分管理菜单
### 1.1 修改侧边栏组件
- 文件:`d:\work\Aiproject\DeotalandAi\apps\FrontendDesigner\src\components\admin\AdminLayout.vue`
- 在侧边栏菜单中添加积分管理菜单项,使用合适的图标
- 配置路由跳转至积分管理页面
### 1.2 更新路由配置
- 文件:`d:\work\Aiproject\DeotalandAi\apps\FrontendDesigner\src\router\index.js`
- 新增积分管理路由,指向新创建的积分管理页面
- 配置懒加载,优化性能
### 1.3 添加国际化翻译
- 文件:`d:\work\Aiproject\DeotalandAi\apps\FrontendDesigner\src\locales\lang\zh-CN.js`
- 文件:`d:\work\Aiproject\DeotalandAi\apps\FrontendDesigner\src\locales\lang\en-US.js`
- 添加积分管理相关的翻译文本
## 2. 创建积分管理页面
### 2.1 创建页面文件
- 文件:`d:\work\Aiproject\DeotalandAi\apps\FrontendDesigner\src\views\admin\AdminPointsManagement.vue`
- 使用 Element Plus 组件库构建页面
- 实现积分包配置列表的展示
### 2.2 实现积分包配置列表
- 表格展示:充值包、价格、有效期
- 支持新增、编辑、删除操作
- 实现分页、搜索功能
### 2.3 实现增删改查功能
- 新增积分包配置表单
- 编辑积分包配置弹窗
- 删除确认功能
- 数据模拟(或接口调用)
## 3. 实现数据管理
### 3.1 模拟数据结构
```javascript
const pointsPackages = [
{
id: 1,
name: '300积分',
price: 30,
currency: '美金',
validityPeriod: '1年'
},
{
id: 2,
name: '1000积分',
price: 80,
currency: '美金',
validityPeriod: '1年'
}
]
```
### 3.2 实现 CRUD 方法
- 新增积分包
- 编辑积分包
- 删除积分包
- 查询积分包列表
## 4. 样式优化
### 4.1 响应式设计
- 确保页面在不同屏幕尺寸下正常显示
- 表格列宽自适应
### 4.2 主题适配
- 支持深色主题
- 遵循现有项目的主题样式规范
## 5. 测试
### 5.1 功能测试
- 验证新增、编辑、删除功能正常工作
- 验证表格展示正确
- 验证路由跳转正常
### 5.2 样式测试
- 验证页面样式符合设计规范
- 验证主题切换正常
## 6. 代码规范
### 6.1 遵循项目编码规范
- 代码格式统一
- 注释完整
- 命名规范
### 6.2 优化性能
- 合理使用组件缓存
- 优化数据渲染
## 实施顺序
1. 添加国际化翻译
2. 更新路由配置
3. 修改侧边栏组件
4. 创建积分管理页面
5. 实现数据管理功能
6. 样式优化
7. 测试验证
## 预期效果
- 侧边栏新增积分管理菜单,点击可跳转至积分管理页面
- 积分管理页面展示积分包配置列表,包含充值包、价格、有效期字段
- 支持对积分包配置进行新增、编辑、删除操作
- 页面样式美观,响应式设计,支持主题切换
## 技术栈
- Vue 3 + Composition API
- Element Plus
- Vue Router
- Vue I18n
- 模拟数据(或后端接口)

View File

@ -1,33 +0,0 @@
# 实现编辑智能体功能
## 1. 修改AgentManagement.vue
- **修改按钮文本**:将"配置角色"按钮改为"编辑智能体"
- **修改跳转逻辑**点击按钮跳转到AddAgent.vue并携带智能体id参数
- **确保按钮功能正确**使用router.push传递参数
## 2. 修改AddAgent.vue
- **添加id参数判断**在onMounted生命周期中检查路由参数
- **实现详情查询**如果有id参数调用xiaozhiServer.getAgent获取智能体详情
- **表单回显**将获取到的详情填充到agentForm中
- **修改保存逻辑**
- 判断是否有id参数
- 如果有调用xiaozhiServer.updateAgent更新智能体
- 如果没有调用xiaozhiServer.createAgent创建智能体
- **修改页面标题**:根据是创建还是编辑,显示不同的标题
## 3. 确保API方法正确
- 确认xiaozhiServer.getAgent方法已正确实现
- 确认xiaozhiServer.updateAgent方法已正确实现
## 4. 测试功能
- 测试编辑按钮跳转是否携带id参数
- 测试详情查询是否正确
- 测试表单回显是否完整
- 测试更新功能是否正常
- 测试创建功能是否不受影响
## 5. 优化用户体验
- 添加加载状态
- 确保错误处理完善
- 添加成功提示
- 确保表单验证正确

View File

@ -1,34 +0,0 @@
## 实现邀请码复制带域名文案功能
### 问题分析
当前点击复制邀请码按钮只会复制邀请码本身,而用户需要的是复制包含当前项目域名和邀请码参数的完整文案,以便分享给他人。
### 解决方案
1. 修改 `copyInviteCode` 函数,使其生成包含域名和邀请码的文案
2. 在 i18n 配置中添加相应的翻译项
3. 确保文案格式支持中英文切换
4. 实现动态获取当前项目域名
### 实现步骤
1. 在 `index.vue` 中修改 `copyInviteCode` 函数,生成包含域名和邀请码的文案
2. 在 `locales/index.js` 中添加中英文翻译项,用于生成邀请文案
3. 实现动态获取当前项目域名的逻辑
4. 测试复制功能,确保文案格式正确
5. 确保中英文切换时文案格式正确
### 预期结果
- 点击复制邀请码按钮时,会复制包含当前项目域名和邀请码参数的完整文案
- 文案格式支持中英文切换
- 保持现有的复制成功/失败提示
### 代码变更
- 修改 `d:/work/Aiproject/DeotalandAi/apps/frontend/src/views/user/index.vue`
- 更新 `copyInviteCode` 函数
- 添加动态获取域名的逻辑
- 修改 `d:/work/Aiproject/DeotalandAi/apps/frontend/src/locales/index.js`
- 添加中文翻译项 `copyWithDomain`
- 添加英文翻译项 `copyWithDomain`
### 文案格式示例
- 中文:"邀请您使用Deotaland AI注册时填写邀请码{inviteCode},或直接点击链接注册:{domain}/register?inviteCode={inviteCode}"
- 英文:"Invite you to use Deotaland AI, fill in the invite code when registering: {inviteCode}, or click the link to register directly: {domain}/register?inviteCode={inviteCode}"

View File

@ -1,64 +0,0 @@
# 新增知识库选择功能
## 目标
在高级配置模块中添加知识库选择功能允许用户从提供的数据中选择多个选项存储在knowledge_base_ids数组中。
## 分析
1. 需要在agentForm中添加knowledge_base_ids数组字段
2. 提供的数据包含四个选项Weather、Joke、Music、News
3. 每个选项有endpoint_id、name部分有language字段
4. UI上需要展示中文名称的复选框
5. 选择的选项将以endpoint_id的形式存储在knowledge_base_ids数组中
## 实现方案
### 1. 添加表单字段
* 在agentForm中添加knowledge_base_ids数组字段初始为空数组
### 2. 添加模板代码
* 在高级配置模块中添加知识库选择的el-form-item
* 使用el-checkbox-group实现多选功能
* 为每个选项创建el-checkbox显示中文名称值为endpoint_id
### 3. 处理数据映射
* 将提供的数据映射为中文显示名称:
* Weather → 天气
* Joke → 笑话
* Music → 音乐
* News → 新闻
### 4. 更新表单验证规则
* 为knowledge_base_ids添加验证规则可选根据需求
## 具体实现步骤
1. **修改表单数据结构**
* 在agentForm中添加knowledge_base_ids字段
2. **添加模板代码**
* 添加知识库选择的el-form-item
* 使用el-checkbox-group和el-checkbox实现多选
* 显示中文名称值为endpoint_id
3. **更新表单验证规则**
* 根据需求添加验证规则
## 预期效果
* 在高级配置模块中显示知识库选择选项
* 每个选项显示中文名称
* 用户可以选择多个选项
* 选择的选项以endpoint_id的形式存储在knowledge_base_ids数组中
## 注意事项
* 保持与现有代码风格一致
* 使用Element Plus组件库
* 确保响应式设计
* 中文名称正确映射

View File

@ -1,178 +0,0 @@
# 新建项目系列选择功能实现
## 需求分析
- 当用户点击"新建项目"卡片时,需要弹出系列选择弹窗
- 系列弹窗包含两个选项Done 和 Oone对应图片在 src/assets/xh 文件夹中
- 选中后将系列名称作为 type 参数传递给 createNewProject 函数
## 实现计划
### 1. 创建 SeriesSelector 组件
- **文件路径**`src/views/components/SeriesSelector.vue`
- **功能**
- 显示两个系列选项Done 和 Oone
- 每个选项显示对应的图片
- 支持选中状态切换
- 提供确认和取消按钮
- 通过 emit 事件返回选中的系列名称
### 2. 修改 CreationWorkspace.vue
- **引入组件**:在 CreationWorkspace.vue 中引入 SeriesSelector 组件
- **添加状态管理**
- `showSeriesSelector`:控制系列选择弹窗的显示/隐藏
- **修改新建项目逻辑**
- 点击"新建项目"卡片时,显示系列选择弹窗
- 监听 SeriesSelector 的确认事件,获取选中的系列名称
- 将系列名称作为 type 参数调用 createNewProject 函数
### 3. 样式设计
- 系列选择弹窗采用与现有删除确认弹窗一致的设计风格
- 系列选项卡片包含图片和名称,支持悬停和选中效果
- 确认和取消按钮使用现有按钮样式
## 代码结构
### SeriesSelector.vue
```vue
<template>
<!-- 系列选择弹窗 -->
<div v-if="show" class="modal-overlay" @click="onCancel">
<div class="modal-content" @click.stop>
<!-- 模态头部 -->
<div class="modal-header">
<h2 class="modal-title">{{ t('creationWorkspace.selectSeries') }}</h2>
</div>
<!-- 模态内容 -->
<div class="modal-body">
<div class="series-selector-content">
<!-- 系列选项列表 -->
<div class="series-list">
<!-- Done 系列 -->
<div
class="series-item"
:class="{ active: selectedSeries === 'Done' }"
@click="selectedSeries = 'Done'"
>
<div class="series-image">
<img src="@/assets/xh/Done.webp" alt="Done" />
</div>
<div class="series-name">Done</div>
</div>
<!-- Oone 系列 -->
<div
class="series-item"
:class="{ active: selectedSeries === 'Oone' }"
@click="selectedSeries = 'Oone'"
>
<div class="series-image">
<img src="@/assets/xh/Oone.webp" alt="Oone" />
</div>
<div class="series-name">Oone</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="series-actions">
<button class="modal-action-btn cancel" @click="onCancel">
{{ t('creationWorkspace.cancel') }}
</button>
<button class="modal-action-btn primary" @click="onConfirm">
{{ t('creationWorkspace.confirm') }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
show: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['confirm', 'cancel'])
const selectedSeries = ref('')
const onConfirm = () => {
if (selectedSeries.value) {
emit('confirm', selectedSeries.value)
}
}
const onCancel = () => {
emit('cancel')
}
</script>
<style scoped>
/* 系列选择弹窗样式 */
.series-selector-content {
text-align: center;
}
.series-list {
display: flex;
gap: 24px;
justify-content: center;
margin-bottom: 32px;
}
.series-item {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
width: 200px;
}
.series-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.series-item.active {
border-color: #6B46C1;
background: rgba(107, 70, 193, 0.05);
box-shadow: 0 4px 16px rgba(107, 70, 193, 0.2);
}
.series-image {
width: 100%;
height: 120px;
overflow: hidden;
border-radius: 8px;
margin-bottom: 12px;
}
.series-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.series-name {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.series-actions {
display: flex;
gap: 16px;
justify-content: center;
}
</style>

View File

@ -1,38 +0,0 @@
## 更新导航栏和添加Discord社交链接使用注释
### 1. 分析当前结构
- 导航栏桌面版第23-42行和移动版第64-89行
- 国际化配置第541-654行
- 社交链接footer部分第460-468行
### 2. 实现步骤
#### 2.1 更新国际化配置
- 修改第541-654行的i18n对象
- **注释掉**nav对象中的现有键值对不要删除
- 添加新的导航项Creator、D one、About us
- 确保中英文都更新
#### 2.2 更新桌面版导航栏
- 修改第23-42行的导航链接
- **注释掉**现有的三个导航项(创作者、社区、价格),不要删除
- 添加新的三个导航项Creator、D one、About us
#### 2.3 更新移动版导航栏
- 修改第64-89行的导航链接
- **注释掉**现有的三个导航项(创作者、社区、价格),不要删除
- 添加新的三个导航项Creator、D one、About us
#### 2.4 在社交链接中添加Discord
- 修改第462-467行的社交链接部分
- 在TikTok链接后添加Discord链接
- 使用合适的图标或文本显示
### 3. 注意事项
- 使用HTML注释`<!-- -->`注释现有导航项
- 确保中英文替换正确
- 保持代码结构清晰
- 确保桌面版和移动版导航栏内容一致
- 注意注释语法正确
- 为Discord链接选择合适的显示方式图标或文本
- 保留所有现有代码,仅添加注释和新内容

View File

@ -1,59 +0,0 @@
# 替换提示词管理页面的模拟数据为API调用
## 目标
`AdminPromptManagement.vue` 组件中的模拟数据替换为真实的 API 调用,使用 `AdminPromptManagement` 类中定义的方法。
## 实现步骤
### 1. 导入 API 类并创建实例
- 在 `AdminPromptManagement.vue` 中导入 `AdminPromptManagement`
- 创建 API 实例用于调用方法
### 2. 替换模拟数据结构
- 移除 `prompts` 模拟数据数组
- 添加响应式数据:`prompts`(用于存储所有提示词)、`loading`(加载状态)
- 调整 `formData` 结构以匹配 API 要求(将 `referenceImage` 改为 `imageUrls`
### 3. 实现数据获取逻辑
- 创建 `fetchPrompts` 方法,调用 `getPromptList` 获取所有提示词
- 调用时设置 `pageSize: 99` 以获取所有数据(不分页)
- 无需按 `isActive` 区分调用,获取所有数据后在前端过滤
- 在组件挂载时调用 `fetchPrompts` 初始化数据
### 4. 更新计算属性
- `inactivePrompts`:过滤 `prompts``isActive === 0` 的数据
- `activePrompts`:过滤 `prompts``isActive === 1` 的数据
- `sortedActivePrompts`:排序逻辑保持不变,但使用 `sortOrder` 字段替代 `sortIndex`
### 5. 更新组件方法
- **savePrompt**:根据 `isEditing` 状态调用 `updatePrompt``createPrompt`
- 将 `formData.imageUrls` 转换为数组格式
- **deletePrompt**:调用 `deletePrompt` API 方法,成功后重新获取数据
- **removeFromActive**:调用 `deactivatePrompt` API 方法,成功后重新获取数据
- **handleDrop**:调用 `activatePrompt` API 方法,成功后重新获取数据
- **handleSortUpdate**:调用 `batchUpdateSort` API 方法,传递排序后的提示词列表
### 6. 调整拖拽功能
- 确保拖拽排序后正确调用 `batchUpdateSort` 更新后端数据
- 转换数据格式以匹配 API 要求(`sortOrder` 字段)
### 7. 处理 API 响应
- 确保 API 返回的数据格式与组件期望的格式匹配
- 处理 API 调用的错误情况
- 添加适当的加载状态提示
## 注意事项
- API 方法返回的是 Promise需要使用 `async/await` 处理
- API 数据结构与模拟数据略有不同,需要调整:
- `isActive` 是数字类型0/1而非布尔值
- `imageUrls` 是数组,而非单个字符串
- 排序字段是 `sortOrder`,而非 `sortIndex`
- 需要添加错误处理和加载状态管理
- 确保所有数据更新操作后重新获取最新数据
## 预期效果
- 页面初始加载时从 API 获取所有提示词不分页pageSize=99
- 左侧显示 `isActive=0` 的提示词,右侧显示 `isActive=1` 的提示词
- 所有操作(添加、编辑、删除、激活、取消激活、排序)都会调用相应的 API 方法
- 页面数据与后端保持同步
- 提供良好的加载状态和错误提示

View File

@ -1,20 +0,0 @@
### 实现方案
1. **修改IP类型选择模块结构**:移除`<img>`标签,只保留文字标签
2. **更新样式**调整CSS样式使卡片在没有图片的情况下依然美观
3. **移除不必要的资源**:删除不再使用的图片导入和引用
4. **更新相关逻辑**:修改使用图片的相关代码
### 代码修改点
1. **文件**`d:\work\Aiproject\DeotalandAi\apps\frontend\src\components\iPandCardLeft\index.vue`
2. **修改内容**
- 移除第4-24行中IP类型卡片的`<img>`标签
- 更新CSS样式调整卡片高度和布局
- 移除图片资源导入humanTypeImg和animalTypeImg
- 更新ipTypeImages对象移除图片引用
- 修改handleGenerateWithMultipleImages函数不再传递ipTypeImg
### 预期效果
- IP类型选择模块只显示"人物"和"动物"文字选项
- 选项以卡片形式展示,默认选中"人物"
- 点击选项可切换选择状态
- 移除所有相关图片资源

View File

@ -1,41 +0,0 @@
# 实现步骤
1. **创建新页面组件**:在 `src/views/` 目录下创建 `Waitlist.vue` 组件,用于展示"已加入候补队列,等待审核中"的提示
2. **添加多语言支持**:在 `src/locales/index.js` 中添加对应的中英文文本
3. **配置路由**:在 `src/router/index.js` 中添加一个新的路由,指向这个新组件
# 详细实现
## 1. 创建 Waitlist.vue 组件
* 参考 `NotFound.vue` 的结构和样式
* 展示"已加入候补队列,等待审核中"的提示信息
* 添加返回首页的按钮
* 适配响应式设计
## 2. 添加多语言支持
`src/locales/index.js``zh``en` 对象中分别添加:
* `waitlist.title`: 标题文本
* `waitlist.description`: 描述文本
* `waitlist.goHome`: 返回首页按钮文本
## 3. 配置路由
`src/router/index.js` 中添加一个新的路由:
```javascript
{
path: '/waitlist',
name: 'Waitlist',
component: () => import('@/views/Waitlist.vue'),
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
}
```

View File

@ -1,6 +0,0 @@
1. 在第二步时间轴内容的拆件图片展示区域上方添加拆件提示词模板的textarea
2. 使用Element Plus的el-input type="textarea"组件
3. 绑定到现有的cjtsc变量默认值为prompt.Hairseparation
4. 添加适当的标签和样式,使其与页面整体风格一致
5. 确保textarea支持用户修改并且修改后的值能够被后续操作使用

View File

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

View File

@ -1,74 +0,0 @@
# 添加语音识别速度、角色语速和角色音调配置项
## 目标
在高级配置模块中添加三个新的配置项:
1. 语音识别速度 - 下拉选择框对应key: asr_speedslow/normal/fast
2. 角色语速 - 下拉选择框对应key: tts_speech_speedslow/normal/fast
3. 角色音调 - 滑块控件对应key: tts_pitch值范围-3到3
## 分析
1. 当前高级配置模块包含角色介绍、记忆类型和记忆内容输入
2. 需要在现有表单中添加三个新的配置项
3. 保持与现有代码风格和布局一致
4. 使用Element Plus组件库实现
5. 使用正确的字段名和值范围
## 实现方案
### 1. 添加表单字段
* 在agentForm中添加三个新字段
* `asr_speed`: 语音识别速度,默认值为"normal"
* `tts_speech_speed`: 角色语速,默认值为"normal"
* `tts_pitch`: 角色音调默认值为0
### 2. 添加模板代码
* 在高级配置模块中添加三个新的el-form-item
* 语音识别速度使用el-select组件选项包括"慢速"、"正常"、"快速"对应值slow/normal/fast
* 角色语速使用el-select组件选项包括"慢速"、"正常"、"快速"对应值slow/normal/fast
* 角色音调使用el-slider组件范围-3到3带有低音和高音图标
### 3. 添加样式和图标
* 为角色音调滑块添加低音和高音图标
* 保持与现有样式一致
### 4. 更新表单验证规则
* 为新添加的字段添加验证规则
## 具体实现步骤
1. **修改表单数据结构**
* 在agentForm中添加三个新字段使用正确的key名和默认值
2. **添加模板代码**
* 在高级配置模块中添加语音识别速度选择框
* 添加角色语速选择框
* 添加角色音调滑块,范围-3到3
3. **添加选项数据**
* 定义语音识别速度选项slow/normal/fast
* 定义角色语速选项slow/normal/fast
4. **更新表单验证**
* 为新字段添加验证规则
## 预期效果
* 高级配置模块中显示三个新的配置项
* 语音识别速度和角色语速为下拉选择框,默认值为"正常"值为slow/normal/fast
* 角色音调为滑块,范围-3到3默认值为0带有低音和高音图标
* 所有配置项能够正确绑定到表单数据
## 注意事项
* 保持与现有代码风格一致
* 使用Element Plus组件库
* 确保响应式设计,适配不同屏幕尺寸
* 添加合适的占位符和标签文本
* 使用正确的字段名和值范围

View File

@ -1,56 +0,0 @@
# 实现计划
## 1. 组件结构调整
- 在 `AdminUserInvites.vue` 组件中添加 Element Plus 的 `el-tabs` 组件,创建两个标签页
- 标签页1邀请码列表
- 标签页2邀请用户列表现有功能
## 2. 邀请码列表实现
### 2.1 表格结构
- 添加 `el-table` 组件,展示邀请码数据
- 表格列包括ID、用户名、邮箱、邀请码、使用状态、邀请用户、使用时间、创建时间
- 使用 `el-loading` 处理加载状态
### 2.2 数据获取与分页
- 使用 `getInviteCodeList` 方法获取邀请码数据
- 添加分页组件,支持页码和每页条数调整
- 实现分页相关的事件处理函数
### 2.3 筛选功能
- 添加筛选表单,包含:
- 用户ID输入框
- 邀请码输入框
- 使用状态下拉选择器
- 查询和重置按钮
### 2.4 生成邀请码功能
- 添加"生成邀请码"按钮
- 点击后弹出 `el-dialog` 对话框
- 对话框中包含数量输入框和确认/取消按钮
- 调用 `generateInviteCode` 方法生成邀请码
## 3. 邀请用户列表调整
- 为现有列表添加"邀请用户列表"标题
- 保持原有功能不变
## 4. 样式优化
- 调整表格样式,确保两个列表视觉一致性
- 优化筛选区域和按钮布局
- 确保整体页面布局美观合理
## 5. 功能测试
- 测试邀请码列表的数据加载和分页
- 测试筛选功能是否正常工作
- 测试生成邀请码功能是否正常
- 测试邀请用户列表功能是否不受影响
## 6. 文件修改
- 仅修改 `d:/work/Aiproject/DeotalandAi/apps/FrontendDesigner/src/views/admin/AdminUsers/AdminUserInvites.vue` 文件
# 预期效果
- 页面顶部显示两个标签页,可切换查看不同列表
- 邀请码列表支持筛选和生成邀请码功能
- 现有邀请用户列表功能保持不变
- 整体布局美观,用户体验良好

View File

@ -1,79 +0,0 @@
## 实现计划
### 1. 分析现有代码结构
- 定位邀请信息区域:`invitation-section` 类名的 `div`
- 现有内容包括:邀请人数、邀请码卡片、邀请相关列表
- 将在 `invitation-related-section` 后添加新的邀请规则区域
### 2. 设计HTML结构
`invitation-related-section` 后添加新的邀请规则区域:
```html
<div class="invitation-rules">
<h3>邀请规则</h3>
<!-- 免费会员邀请规则 - 始终显示 -->
<div class="role-rules free-rules">
<h4>免费会员邀请规则</h4>
<ul>
<li>每成功邀请 1 名用户注册:</li>
<ul>
<li>奖励 300 积分</li>
</ul>
</ul>
</div>
<!-- 达人会员邀请规则 - 始终显示 -->
<div class="role-rules creator-rules">
<h4>达人会员邀请规则</h4>
<p>当达人邀请码成功邀请 1 名新用户注册:</p>
<ol>
<li>拥有免费会员全部权限</li>
<li>额外具备带货佣金能力</li>
<li>立即奖励 300 积分</li>
<li>建立绑定关系(达人 ←→ 用户)</li>
<li>被邀请用户后续下单:</li>
<li>达人可获得 15% 佣金</li>
</ol>
</div>
<!-- 成为达人会员模块 - 仅免费会员可见 -->
<div v-if="userData.role === 'free'" class="upgrade-creator-section">
<h4>成为达人会员</h4>
<div class="qr-code-container">
<!-- QR码占位 -->
<div class="qr-placeholder">达人会员二维码</div>
</div>
</div>
</div>
```
### 3. 添加CSS样式
- 为 `invitation-rules` 添加容器样式,保持与现有模块一致
- 为 `role-rules` 添加基础样式,区分免费会员和达人会员规则
- 为不同类型的列表ul/ol添加自定义样式
- 为成为达人会员模块添加样式,包含二维码容器
- 支持浅色和暗色主题
### 4. 样式设计要点
- 保持与现有设计语言一致,使用紫色主题色
- 免费会员规则使用蓝色调,达人会员规则使用紫色调
- 添加适当的间距、圆角和边框
- 列表项使用自定义项目符号增强视觉效果
- 二维码容器添加占位样式
- 支持响应式设计
### 5. 实现步骤
1. 在模板中添加邀请规则HTML结构同时展示两个规则
2. 添加条件判断,仅对免费会员显示成为达人会员模块
3. 在CSS部分添加对应的样式
4. 确保支持暗色主题
5. 检查响应式设计在不同屏幕尺寸下的表现
### 6. 预期效果
- 同时展示免费会员和达人会员的邀请规则
- 清晰区分两个规则区块
- 仅免费会员能看到成为达人会员的二维码模块
- 整体设计与现有界面风格统一
- 支持深色模式切换
这个实现将清晰地向所有用户展示完整的邀请规则,同时根据用户角色提供个性化的升级入口。

View File

@ -1,37 +0,0 @@
# 用户中心页面重构计划
## 1. 布局调整
- 将当前的 `grid-template-columns: 1fr 1fr` 两列卡片布局改为 `flex-direction: column` 垂直布局
- 移除卡片样式,改为更简洁的区块划分,使用渐变背景和微动画增强灵动性
- 调整间距和对齐方式,使内容垂直堆叠,一行一行清晰展示
## 2. 添加用户基本信息区域
- **头像功能**:展示当前头像,添加点击修改功能(使用 Element Plus 的上传组件)
- **昵称功能**:展示当前昵称,添加编辑功能(使用可编辑文本组件)
- **邮箱展示**:显示当前绑定的邮箱账号
- **角色标识**:添加角色徽章,参考 `.role-badge` 样式,显示"达人会员"或"免费会员"
## 3. 优化现有功能区域
- 保持积分信息、邀请信息、达人会员专属区域的原有功能
- 将这些区域改为垂直排列,使用更灵动的样式
- 添加标题和内容的视觉层次,增强可读性
## 4. 样式优化
- 使用渐变色背景和圆角设计,增强灵动感
- 添加悬停效果和过渡动画,提升交互体验
- 保持暗色主题支持,确保在两种主题下都有良好表现
- 优化表格样式,使其更符合新的设计风格
## 5. 国际化支持
- 为新增的文本内容添加中英文翻译
- 确保所有动态文本都使用 `$t()` 国际化标签
## 6. 响应式设计
- 优化在不同屏幕尺寸下的布局和显示效果
- 确保移动端有良好的用户体验
## 7. 技术实现
- 使用 Vue 3 Composition API
- 集成 Element Plus 组件库的相关组件
- 保持代码的可维护性和扩展性
- 确保与现有项目结构和代码规范保持一致

View File

@ -1,76 +0,0 @@
# 角色管理和权限管理设计与对接计划(含详情页面)
## 一、角色管理模块设计与对接
### 1. 页面设计优化
- 修改现有角色管理页面调整表格字段以匹配后端API返回数据
- 新增角色详情展示页面,用于查看和编辑角色信息
- 设计角色创建/编辑表单,支持完整的角色属性设置
### 2. API对接实现
- 实现角色列表获取功能,对接`getRoleList`方法
- 实现角色创建功能,对接`createRole`方法
- 实现角色编辑功能,对接`updateRole`方法
- 实现角色删除功能,对接`deleteRole`方法
- 实现角色详情获取功能,对接`getRoleDetail`方法
### 3. 新增角色详情页面
- 创建角色详情路由 `/admin/role-management/:roleId`
- 设计角色详情展示页面,包括角色基本信息和权限分配情况
- 实现角色权限分配功能,对接`assignPermissionToRole`方法
## 二、权限管理模块设计与对接
### 1. 页面设计
- 将现有路由管理页面替换为权限管理页面
- 设计权限列表展示表格,显示权限的详细信息
- 新增权限详情展示页面,用于查看和编辑权限信息
- 设计权限创建/编辑表单,支持完整的权限属性设置
### 2. API对接实现
- 实现权限列表获取功能,对接`getPermissionList`方法
- 实现权限创建功能,对接`addPermission`方法
- 实现权限编辑功能,对接`updatePermission`方法
- 实现权限删除功能,对接`deletePermission`方法
- 实现权限详情获取功能,对接`getPermissionDetail`方法
### 3. 新增权限详情页面
- 创建权限详情路由 `/admin/permission-management/:permissionId`
- 设计权限详情展示页面,显示权限的完整信息
## 三、组件和工具准备
### 1. 组件设计
- 设计角色创建/编辑对话框组件
- 设计权限创建/编辑对话框组件
- 设计权限分配树形选择组件
- 设计角色详情和权限详情展示组件
### 2. 工具类调用
- 确保AdminRoleManagement类的方法被正确调用
- 处理API返回数据的格式化和错误处理
- 实现响应式设计,适配不同屏幕尺寸
## 四、路由配置调整
### 1. 路由修改
- 保留现有角色管理路由 `/admin/role-management`
- 将现有路由管理路由 `/admin/route-management` 替换为权限管理路由 `/admin/permission-management`
- 新增角色详情路由 `/admin/role-management/:roleId`
- 新增权限详情路由 `/admin/permission-management/:permissionId`
## 五、实现步骤
1. 先完成角色管理页面的API对接和功能实现
2. 新增角色详情页面并实现API对接
3. 实现权限管理页面,替换现有路由管理页面
4. 新增权限详情页面并实现API对接
5. 实现角色权限分配功能
6. 进行整体测试和优化
## 六、注意事项
- 保持代码风格与现有项目一致
- 确保所有功能都有适当的错误处理
- 遵循现有项目的国际化规范
- 不对用户列表模块进行任何修改

View File

@ -1,22 +0,0 @@
1. 首先将XiaozhiServer类集成到utils/src/index.js中
2. 然后在AddAgent.vue中从@deotaland/utils导入XiaozhiServer
3. 最后调用getTtsList()方法并打印结果
具体修改步骤:
1. 修改utils/src/index.js
* 导入XiaozhiServer类
* 将XiaozhiServer添加到deotalandUtils对象中
* 将XiaozhiServer添加到命名导出列表
2. 修改AddAgent.vue
* 从@deotaland/utils导入XiaozhiServer
* 创建XiaozhiServer实例
* 在onMounted

View File

@ -1,42 +0,0 @@
# 邀请码样式重构计划
## 1. 数据结构修改
- 将单个邀请码改为数组形式包含3个邀请码
- 更新用户数据模型,添加邀请码有效期等额外信息
## 2. 模板布局调整
- 将当前的单个邀请码展示改为横向排列的卡片布局
- 使用 flex 或 grid 布局实现3个邀请码的横向排列
- 确保在不同屏幕尺寸下有良好的响应式表现
## 3. 样式设计
- **卡片样式**:使用渐变背景,参考图中的蓝绿色渐变效果
- **邀请码文本**:居中显示,大号字体,清晰易读
- **复制按钮**:底部添加"复制邀请码"按钮,带有悬停效果
- **品牌标识**每个卡片底部添加品牌logo或名称
## 4. 功能实现
- 添加邀请码复制功能,点击按钮自动复制到剪贴板
- 显示复制成功的反馈信息
- 添加悬停效果,提升交互体验
## 5. 响应式设计
- 在桌面端显示3个横向排列的邀请码卡片
- 在平板端根据屏幕宽度调整卡片大小
- 在移动端改为垂直排列
## 6. 暗色主题支持
- 确保在暗色主题下邀请码卡片有良好的视觉效果
- 调整渐变颜色和文字颜色,适配暗色背景
## 7. 代码实现
- 修改模板结构,使用 v-for 循环渲染3个邀请码
- 添加复制功能的事件处理
- 设计符合参考图风格的 CSS 样式
- 确保与现有页面设计风格保持一致
## 8. 测试验证
- 检查在不同屏幕尺寸下的显示效果
- 测试复制功能是否正常工作
- 验证暗色主题下的表现
- 确保与页面其他元素的样式协调

View File

@ -1,289 +0,0 @@
## 重新设计 AdminCommissionManagement 页面
### 需求分析
1. 页面现有结构:包含佣金比例设置和佣金列表两个部分
2. 需要使用 `index.js` 中的 API 方法:
- `updateCommissionConfig()`:更新佣金配置
- `viewCommissionConfig()`:获取佣金配置信息
3. 目前只有佣金配置没有佣金列表(需要移除或调整佣金列表部分)
4. 需要按照项目技术规范和设计风格重新设计页面
### 解决方案
1. 移除或调整佣金列表部分,专注于佣金配置功能
2. 使用 `index.js` 中的 API 方法替换当前的模拟数据
3. 实现完整的佣金配置表单,包含所有必要的配置项
4. 确保页面设计符合项目的技术规范和设计风格
### 实现步骤
1. 修改 `AdminCommissionManagement.vue` 页面:
- 调整页面结构,移除或简化佣金列表部分
- 实现完整的佣金配置表单,包含所有必要的配置项
- 使用 `viewCommissionConfig()` 方法获取佣金配置数据
- 使用 `updateCommissionConfig()` 方法保存佣金配置
- 确保页面支持中英文切换
- 确保页面响应式设计,适配不同设备尺寸
2. 确保页面设计符合项目的技术规范和设计风格:
- 使用 Vue3 Composition API
- 使用 Scoped CSS + CSS 变量实现组件样式隔离与主题定制
- 使用 Element Plus 组件库
- 实现响应式布局,适配移动端、平板端和桌面端
### 代码修改点
**文件**`d:/work/Aiproject/DeotalandAi/apps/FrontendDesigner/src/views/admin/AdminCommissionManagement/AdminCommissionManagement.vue`
**核心修改**
1. **页面结构调整**
- 移除或简化佣金列表部分
- 实现完整的佣金配置表单
2. **数据绑定**
- 使用 `viewCommissionConfig()` 方法获取佣金配置数据
- 将数据绑定到表单控件
3. **表单提交**
- 使用 `updateCommissionConfig()` 方法保存佣金配置
- 实现表单验证
4. **响应式设计**
- 确保页面适配不同设备尺寸
- 实现响应式布局
5. **中英文支持**
- 确保页面所有文本都支持中英文切换
### 实现细节
1. **佣金配置表单**
- 佣金比例commissionRate1-100%,整数
- 最低提现金额minWithdrawAmount数字
- 提现费率withdrawFeeRate0-100%,小数
- 结算周期settlementCycle下拉选择
- 状态status开关控制
- 备注remark文本输入
2. **API调用**
- 在页面挂载时调用 `viewCommissionConfig()` 获取佣金配置数据
- 在表单提交时调用 `updateCommissionConfig()` 保存佣金配置
- 处理API调用的成功和失败情况
3. **表单验证**
- 实现表单验证规则
- 确保所有必填字段都已填写
- 确保字段值符合要求
4. **响应式设计**
- 使用 Flexbox/Grid 实现响应式布局
- 使用媒体查询适配不同设备尺寸
- 确保表单控件在不同设备上都能正常显示和使用
5. **中英文支持**
- 使用 `t()` 函数包裹所有文本
- 确保所有文本都在国际化配置中定义
### 预期效果
1. 页面只包含佣金配置部分,移除或简化佣金列表部分
2. 页面使用 `index.js` 中的 API 方法获取和保存佣金配置
3. 页面支持中英文切换
4. 页面适配不同设备尺寸
5. 页面设计符合项目的技术规范和设计风格
### 实现示例
```vue
<template>
<div class="admin-commission-management">
<h1 class="page-title">{{ t('admin.commissionManagement.title') }}</h1>
<!-- 佣金配置表单 -->
<el-card class="commission-config-card">
<template #header>
<div class="card-header">
<h2 class="card-title">{{ t('admin.commissionManagement.configTitle') }}</h2>
</div>
</template>
<div class="commission-config-content">
<el-form :model="commissionConfig" label-width="120px" :rules="rules" ref="commissionConfigRef">
<!-- 佣金比例 -->
<el-form-item :label="t('admin.commissionManagement.commissionRate')" prop="commissionRate">
<div class="rate-input-wrapper">
<el-input-number
v-model="commissionConfig.commissionRate"
:min="1"
:max="100"
:step="1"
:precision="0"
style="width: 200px"
/>
<span class="rate-suffix">%</span>
</div>
</el-form-item>
<!-- 最低提现金额 -->
<el-form-item :label="t('admin.commissionManagement.minWithdrawAmount')" prop="minWithdrawAmount">
<el-input-number
v-model="commissionConfig.minWithdrawAmount"
:min="0"
:step="10"
:precision="0"
style="width: 200px"
/>
</el-form-item>
<!-- 提现费率 -->
<el-form-item :label="t('admin.commissionManagement.withdrawFeeRate')" prop="withdrawFeeRate">
<div class="rate-input-wrapper">
<el-input-number
v-model="commissionConfig.withdrawFeeRate"
:min="0"
:max="100"
:step="0.1"
:precision="1"
style="width: 200px"
/>
<span class="rate-suffix">%</span>
</div>
</el-form-item>
<!-- 结算周期 -->
<el-form-item :label="t('admin.commissionManagement.settlementCycle')" prop="settlementCycle">
<el-select
v-model="commissionConfig.settlementCycle"
placeholder="{{ t('admin.commissionManagement.selectSettlementCycle') }}"
style="width: 200px"
>
<el-option
v-for="cycle in settlementCycles"
:key="cycle.value"
:label="cycle.label"
:value="cycle.value"
/>
</el-select>
</el-form-item>
<!-- 状态 -->
<el-form-item :label="t('admin.commissionManagement.status')" prop="status">
<el-switch
v-model="commissionConfig.status"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<!-- 备注 -->
<el-form-item :label="t('admin.commissionManagement.remark')" prop="remark">
<el-input
v-model="commissionConfig.remark"
type="textarea"
:rows="3"
placeholder="{{ t('admin.commissionManagement.enterRemark') }}"
style="width: 400px"
/>
</el-form-item>
<!-- 保存按钮 -->
<el-form-item>
<el-button type="primary" @click="saveCommissionConfig">{{ t('admin.commissionManagement.saveConfig') }}</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { AdminCommissionManagement } from './index.js'
const { t } = useI18n()
const adminCommissionManagement = new AdminCommissionManagement()
// 佣金配置表单
const commissionConfigRef = ref()
const commissionConfig = reactive({
commissionRate: 0,
minWithdrawAmount: 0,
withdrawFeeRate: 0,
settlementCycle: 0,
status: 1,
remark: ''
})
// 结算周期选项
const settlementCycles = [
{ value: 1, label: t('admin.commissionManagement.daily') },
{ value: 7, label: t('admin.commissionManagement.weekly') },
{ value: 30, label: t('admin.commissionManagement.monthly') }
]
// 表单验证规则
const rules = {
commissionRate: [
{ required: true, message: t('admin.commissionManagement.requiredCommissionRate'), trigger: 'blur' },
{ type: 'number', min: 1, max: 100, message: t('admin.commissionManagement.invalidCommissionRate'), trigger: 'blur' }
],
minWithdrawAmount: [
{ required: true, message: t('admin.commissionManagement.requiredMinWithdrawAmount'), trigger: 'blur' },
{ type: 'number', min: 0, message: t('admin.commissionManagement.invalidMinWithdrawAmount'), trigger: 'blur' }
]
}
// 获取佣金配置数据
const getCommissionConfig = async () => {
try {
const response = await adminCommissionManagement.viewCommissionConfig()
if (response.success) {
const data = response.data
commissionConfig.commissionRate = data.commissionRate
commissionConfig.minWithdrawAmount = data.minWithdrawAmount
commissionConfig.withdrawFeeRate = data.withdrawFeeRate
commissionConfig.settlementCycle = data.settlementCycle
commissionConfig.status = data.status
commissionConfig.remark = data.remark
}
} catch (error) {
console.error('获取佣金配置失败:', error)
ElMessage.error(t('admin.commissionManagement.getConfigFailed'))
}
}
// 保存佣金配置
const saveCommissionConfig = async () => {
try {
await commissionConfigRef.value.validate()
const response = await adminCommissionManagement.updateCommissionConfig(commissionConfig)
if (response.success) {
ElMessage.success(t('admin.commissionManagement.saveConfigSuccess'))
}
} catch (error) {
console.error('保存佣金配置失败:', error)
ElMessage.error(t('admin.commissionManagement.saveConfigFailed'))
}
}
// 页面挂载时获取佣金配置数据
onMounted(() => {
getCommissionConfig()
})
</script>
<style scoped>
/* 样式省略,按照项目设计风格实现 */
</style>
```
### 实现预期
1. 页面只包含佣金配置部分,移除或简化佣金列表部分
2. 页面使用 `index.js` 中的 API 方法获取和保存佣金配置
3. 页面支持中英文切换
4. 页面适配不同设备尺寸
5. 页面设计符合项目的技术规范和设计风格

View File

@ -1,51 +0,0 @@
# 重新设计AdminUserList.vue页面
## 1. 页面结构优化
- 保留现有页面布局,但增强功能
- 添加用户创建/编辑对话框
- 添加用户状态切换、密码重置等操作按钮
- 完善搜索功能
## 2. 数据绑定与API集成
- 引入AdminRoleManagement类
- 实现分页查询用户列表(getAdminUsersList)
- 实现用户搜索功能
- 实现用户创建(createAdminUser)
- 实现用户编辑(updateAdminUser)
- 实现用户启用/禁用(enableDisableUser)
- 实现用户密码重置(resetUserPassword)
- 实现用户删除(deleteAdminUsers)
## 3. 组件功能实现
- **用户列表**使用el-table展示用户数据包含用户名、邮箱、姓名、状态、创建时间等字段
- **搜索功能**:实现根据用户名和邮箱搜索
- **分页功能**与API集成实现分页加载
- **用户创建/编辑对话框**:包含表单验证
- **用户操作**
- 编辑用户
- 启用/禁用用户
- 重置密码
- 删除用户
## 4. 交互优化
- 添加加载状态
- 添加操作确认提示
- 添加成功/失败消息反馈
- 优化表单验证
## 5. 代码实现步骤
1. 导入AdminRoleManagement类
2. 替换硬编码数据为API调用
3. 实现分页查询
4. 添加用户创建/编辑对话框
5. 实现各项操作功能
6. 优化交互体验
7. 测试所有功能
## 6. 预期效果
- 页面能够正常加载用户列表
- 搜索功能正常工作
- 分页功能正常工作
- 用户创建/编辑/删除功能正常
- 用户状态切换和密码重置功能正常
- 交互流畅,反馈及时

View File

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

View File

@ -1,76 +0,0 @@
## 重新设计积分管理页面
### 1. 分析现有代码
- **`index.js`**:包含 `AdminPointsManagement`提供了8个API方法用于积分包的增删改查、状态更新、推荐状态更新和批量删除
- **`AdminPointsManagement.vue`**当前是一个简单的Vue组件使用模拟数据只实现了基本的增删改功能缺少很多API中定义的字段和功能
### 2. 重新设计方案
#### 2.1 数据结构调整
- 将模拟数据替换为真实API调用
- 添加分页、筛选和排序功能
- 支持状态管理(启用/禁用)
- 支持推荐状态管理
#### 2.2 表格设计
- 添加更多列:积分数量、使用次数、状态、推荐状态、创建时间、排序
- 支持多选框用于批量操作
- 添加状态和推荐状态的切换按钮
#### 2.3 筛选和分页
- 添加筛选表单:名称、状态、推荐状态、货币
- 添加分页组件
- 支持排序功能
#### 2.4 表单字段调整
- 添加缺失字段:积分数量、使用次数、描述、状态、排序、推荐状态、有效期天数、额外配置、备注
- 调整字段名称和类型以匹配API要求
- 添加表单验证规则
#### 2.5 功能实现
- 实现所有API方法的调用
- 添加加载状态显示
- 添加操作成功/失败的消息提示
### 3. 具体修改步骤
1. **导入必要的组件和方法**
- 导入 `AdminPointsManagement`
- 导入必要的Element Plus组件和图标
2. **数据和状态管理**
- 添加分页相关数据
- 添加筛选条件数据
- 添加加载状态
- 替换模拟数据为真实API调用
3. **表格设计**
- 添加更多列
- 添加操作列功能
- 添加多选框
4. **筛选和分页**
- 添加筛选表单
- 添加分页组件
5. **弹窗表单设计**
- 添加缺失字段
- 调整字段名称和类型
- 添加表单验证规则
6. **方法实现**
- 实现获取积分包列表
- 实现新增、编辑、删除、批量删除
- 实现状态和推荐状态切换
- 实现筛选和分页
7. **生命周期钩子**
- 在组件挂载时调用获取积分包列表的方法
### 4. 预期效果
- 页面使用真实API数据
- 支持完整的积分包管理功能
- 包含所有API中定义的字段
- 支持批量操作
- 支持筛选、排序和分页
- 提供良好的用户体验和操作反馈

View File

@ -112,7 +112,8 @@ import {
Lock,
Key,
List,
Coin
Coin,
Ticket
} from '@element-plus/icons-vue'
import { AdminLogin } from '../../views/AdminLogin/AdminLogin'

View File

@ -36,6 +36,7 @@ import {
Key,
List,
Coin,
Ticket,
ShoppingCartFull
} from '@element-plus/icons-vue'
@ -62,6 +63,7 @@ const iconMap = {
Key,
List,
Coin,
Ticket,
ShoppingCartFull
}

View File

@ -100,6 +100,23 @@
</el-dropdown>
</div>
<!-- 亮度控制组 -->
<div v-if="modelInfo&&showInfo" class="control-group brightness-control">
<div class="brightness-label">
<el-icon><Sunny /></el-icon>
<span>{{ t('modelViewer.brightness') }}</span>
</div>
<el-slider
v-model="brightness"
:min="0"
:max="200"
:step="5"
@change="handleBrightnessChange"
style="width: 120px;"
/>
<span class="brightness-value">{{ brightness }}%</span>
</div>
<!-- 模型信息 -->
<div v-if="modelInfo&&showInfo" class="model-info">
<p>{{ t('modelViewer.modelInfo') }}: {{ modelInfo }}</p>
@ -119,7 +136,7 @@ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
import { ElMessage } from 'element-plus'
import { Loading, Warning, Refresh, Grid, Position, Download, ArrowDown } from '@element-plus/icons-vue'
import { Loading, Warning, Refresh, Grid, Position, Download, ArrowDown, Sunny } from '@element-plus/icons-vue'
const { t } = useI18n()
// Props
@ -172,11 +189,14 @@ const modelInfo = ref('')
const fileSize = ref(0)
const loadingProgress = ref(0)
const isExporting = ref(false)
const brightness = ref(120)
// Three.js
let scene, camera, renderer, controls
let model, mixer
let animationId
let ambientLight, directionalLight, auxiliaryLight
let bottomLight, leftFillLight, rightFillLight, topFillLight, backFillLight
//
let cameraDistance = 5
@ -211,12 +231,12 @@ const initThreeJS = () => {
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
//
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
//
ambientLight = new THREE.AmbientLight(0xffffff, 1.2)
scene.add(ambientLight)
//
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0)
directionalLight = new THREE.DirectionalLight(0xffffff, 1.8)
directionalLight.position.set(10, 15, 8)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.width = 2048
@ -230,10 +250,35 @@ const initThreeJS = () => {
scene.add(directionalLight)
//
const auxiliaryLight = new THREE.DirectionalLight(0xffffff, 0.3)
auxiliaryLight = new THREE.DirectionalLight(0xffffff, 0.8)
auxiliaryLight.position.set(-8, 10, -5)
scene.add(auxiliaryLight)
//
bottomLight = new THREE.DirectionalLight(0xffffff, 0.6)
bottomLight.position.set(0, -10, 0)
scene.add(bottomLight)
//
leftFillLight = new THREE.DirectionalLight(0xffffff, 0.5)
leftFillLight.position.set(-15, 5, 0)
scene.add(leftFillLight)
//
rightFillLight = new THREE.DirectionalLight(0xffffff, 0.5)
rightFillLight.position.set(15, 5, 0)
scene.add(rightFillLight)
//
topFillLight = new THREE.DirectionalLight(0xffffff, 0.4)
topFillLight.position.set(0, 20, 0)
scene.add(topFillLight)
//
backFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
backFillLight.position.set(0, 5, -15)
scene.add(backFillLight)
//
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
@ -423,6 +468,47 @@ const centerModel = () => {
controls.update()
}
//
const handleBrightnessChange = (value) => {
if (!ambientLight || !directionalLight || !auxiliaryLight) return
const brightnessFactor = value / 100
//
ambientLight.intensity = 1.2 * brightnessFactor
//
directionalLight.intensity = 1.8 * brightnessFactor
//
auxiliaryLight.intensity = 0.8 * brightnessFactor
//
if (bottomLight) {
bottomLight.intensity = 0.6 * brightnessFactor
}
//
if (leftFillLight) {
leftFillLight.intensity = 0.5 * brightnessFactor
}
//
if (rightFillLight) {
rightFillLight.intensity = 0.5 * brightnessFactor
}
//
if (topFillLight) {
topFillLight.intensity = 0.4 * brightnessFactor
}
//
if (backFillLight) {
backFillLight.intensity = 0.3 * brightnessFactor
}
}
//
const handleExportCommand = (command) => {
if (isExporting.value) {
@ -907,6 +993,50 @@ defineExpose({
word-break: break-word;
}
/* 亮度控制样式 */
.brightness-control {
display: flex;
align-items: center;
gap: 8px;
}
.brightness-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #666;
white-space: nowrap;
}
.brightness-value {
font-size: 12px;
color: #666;
min-width: 36px;
text-align: right;
}
.brightness-control .el-slider {
flex: 1;
}
.brightness-control .el-slider__runway {
background-color: #e5e7eb;
}
.brightness-control .el-slider__bar {
background: linear-gradient(90deg, #A78BFA 0%, #6B46C1 100%);
}
.brightness-control .el-slider__button {
border-color: #6B46C1;
background-color: #fff;
}
.brightness-control .el-slider__button:hover {
border-color: #553C9A;
}
.hidden {
display: none;
}
@ -932,6 +1062,24 @@ defineExpose({
max-width: 150px;
font-size: 11px;
}
.brightness-control {
flex-direction: column;
align-items: stretch;
gap: 6px;
}
.brightness-label {
justify-content: center;
}
.brightness-control .el-slider {
width: 100% !important;
}
.brightness-value {
text-align: center;
}
}
/* 导出按钮样式优化 */

View File

@ -26,6 +26,7 @@ export default {
resetView: 'Reset View',
toggleWireframe: 'Toggle Wireframe',
centerModel: 'Center Model',
brightness: 'Brightness',
modelInfo: 'Model Info',
fileSize: 'File Size',
loadError: 'Model Load Error',
@ -246,7 +247,11 @@ export default {
action: 'Action',
actions: 'Actions',
close: 'Close',
back: 'Back'
back: 'Back',
invalidate: 'Invalidate',
requestFailed: 'Request failed',
operationSuccess: 'Operation successful',
operationFailed: 'Operation failed'
},
layout: {
dashboard: 'Dashboard',
@ -264,6 +269,7 @@ export default {
commissionManagement: 'Commission Management',
promptManagement: 'Prompt Management',
productManagement: 'Product Management',
voucherManagement: 'Voucher Management',
logout: 'Logout',
profile: 'Profile',
settings: 'Settings'
@ -854,6 +860,75 @@ export default {
priceUpdateFailed: 'Failed to update price',
detailFailed: 'Failed to fetch product detail',
deleteConfirm: 'Are you sure you want to delete this product?'
},
voucherManagement: {
title: 'Voucher Management',
couponCode: 'Coupon Code',
userId: 'User ID',
selectUser: 'Select User',
selectUsers: 'Select Users',
userEmail: 'User Email',
amount: 'Amount',
currency: 'Currency',
minOrderAmount: 'Min Order Amount',
status2: 'Status',
status: {
0: 'Unused',
1: 'Used',
2: 'Expired',
3: 'Invalid'
},
sourceType2: 'Source Type',
sourceType: {
0: 'Manual',
1: 'Automatic',
manual:'Manual',
activity:'Activity'
},
createTime: 'Create Time',
expireAt: 'Expire At',
remark: 'Remark',
action: {
create: 'Create Voucher',
batchCreate: 'Batch Create',
edit: 'Edit',
invalidate: 'Invalidate',
batchInvalidate: 'Batch Invalidate',
detail: 'Detail'
},
dialog: {
createTitle: 'Create Voucher',
batchCreateTitle: 'Batch Create Voucher',
editTitle: 'Edit Voucher',
detailTitle: 'Voucher Detail'
},
placeholder: {
couponCode: 'Enter Coupon Code',
userId: 'Enter User ID',
userEmail: 'Enter User Email',
amount: 'Enter Amount',
currency: 'Select Currency',
minOrderAmount: 'Enter Min Order Amount',
expireAt: 'Select Expire Time',
sourceType: 'Select Source Type',
remark: 'Enter Remark',
userIds: 'Enter User IDs, separated by commas',
status:'Please select Status'
},
required: {
userId: 'Please enter User ID',
amount: 'Please enter Amount',
currency: 'Please select Currency',
expireAt: 'Please select Expire Time',
userIds: 'Please enter User IDs'
},
confirm: {
invalidate: 'Are you sure you want to invalidate this voucher?',
batchInvalidate: 'Are you sure you want to batch invalidate selected vouchers?'
},
hint: {
userIds: 'Multiple user IDs separated by commas, e.g.: 1,2,3'
}
}
},
modelUpload: {

View File

@ -261,7 +261,11 @@ orderManagement: {
no: '否',
action: '操作',
close: '关闭',
back: '返回'
back: '返回',
invalidate: '作废',
requestFailed: '请求失败',
operationSuccess: '操作成功',
operationFailed: '操作失败'
},
layout: {
dashboard: '仪表板',
@ -279,6 +283,7 @@ orderManagement: {
commissionManagement: '佣金管理',
promptManagement: '提示词管理',
productManagement: '产品管理',
voucherManagement: '优惠券管理',
logout: '退出登录',
profile: '个人资料',
settings: '设置',
@ -848,6 +853,75 @@ orderManagement: {
priceUpdateFailed: '价格更新失败',
detailFailed: '获取产品详情失败',
deleteConfirm: '确定要删除这个产品吗?'
},
voucherManagement: {
title: '优惠券管理',
couponCode: '代金券编码',
userId: '用户ID',
selectUser: '选择用户',
selectUsers: '选择用户',
userEmail: '用户邮箱',
amount: '金额',
currency: '货币',
minOrderAmount: '最小订单金额',
status2: '状态',
status: {
0: '未使用',
1: '已使用',
2: '已过期',
3: '已失效'
},
sourceType2: '来源类型',
sourceType: {
0: '手动创建',
1: '自动创建',
manual:'手动创建',
activity:'活动创建'
},
createTime: '创建时间',
expireAt: '过期时间',
remark: '备注',
action: {
create: '发放代金券',
batchCreate: '批量发放',
edit: '修改',
invalidate: '作废',
batchInvalidate: '批量作废',
detail: '详情'
},
dialog: {
createTitle: '发放代金券',
batchCreateTitle: '批量发放代金券',
editTitle: '修改代金券',
detailTitle: '代金券详情'
},
placeholder: {
couponCode: '请输入代金券编码',
userId: '请输入用户ID',
userEmail: '请输入用户邮箱',
amount: '请输入金额',
currency: '请选择货币',
minOrderAmount: '请输入最小订单金额',
expireAt: '请选择过期时间',
sourceType: '请选择来源类型',
remark: '请输入备注',
userIds: '请输入用户ID多个用户ID用逗号分隔',
status:'请选择状态'
},
required: {
userId: '请输入用户ID',
amount: '请输入金额',
currency: '请选择货币',
expireAt: '请选择过期时间',
userIds: '请输入用户ID'
},
confirm: {
invalidate: '确定要作废这个代金券吗?',
batchInvalidate: '确定要批量作废选中的代金券吗?'
},
hint: {
userIds: '多个用户ID用逗号分隔例如1,2,3'
}
}
},
modelUpload: {
@ -921,6 +995,7 @@ orderManagement: {
resetView: '重置视图',
toggleWireframe: '切换线框',
centerModel: '居中模型',
brightness: '亮度',
modelInfo: '模型信息',
fileSize: '文件大小',
loadError: '模型加载失败',

View File

@ -23,6 +23,7 @@ const AdminPointsManagement = () => import('@/views/admin/AdminPointsManagement/
const AdminCommissionManagement = () => import('@/views/admin/AdminCommissionManagement/AdminCommissionManagement.vue')
const AdminPromptManagement = () => import('@/views/admin/AdminPromptManagement/AdminPromptManagement.vue')
const AdminProductManagement = () => import('@/views/admin/ProductManagement/ProductManagement.vue')
const AdminVoucherManagement = () => import('@/views/admin/VoucherManagement/VoucherManagement.vue')
//权限路由映射表
export const permissionRoutes = [
{
@ -104,6 +105,17 @@ export const permissionRoutes = [
requiresAuth: true
}
},
{
path: 'voucher-management',
name: 'AdminVoucherManagement',
component: AdminVoucherManagement,
meta: {
title: 'admin.layout.voucherManagement',
icon: 'Ticket',
menuOrder: 4,
requiresAuth: true
}
},
{
path: 'points-management',
name: 'AdminPointsManagement',
@ -183,23 +195,7 @@ export const permissionRoutes = [
}
}
]
},
// {
// path: 'content',
// name: 'AdminContent',
// component: AdminContent,
// meta: {
// title: '内容管理',
// icon: 'Document',
// menuOrder: 8,
// hideInMenu: true,
// requiresAuth: true
// }
// },
}
]
const routes = [
{

View File

@ -0,0 +1,813 @@
<template>
<div class="voucher-management">
<h2 class="page-title">{{ t('admin.voucherManagement.title') }}</h2>
<!-- 搜索筛选区域 -->
<el-card shadow="hover" class="filter-card">
<el-form :model="searchForm" label-position="left" label-width="auto" inline class="search-form">
<el-form-item :label="t('admin.voucherManagement.couponCode')">
<el-input v-model="searchForm.couponCode" :placeholder="t('admin.voucherManagement.placeholder.couponCode')" clearable style="width: 200px;" />
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.userId')">
<el-input v-model="searchForm.userId" :placeholder="t('admin.voucherManagement.placeholder.userId')" clearable style="width: 150px;" />
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.userEmail')">
<el-input v-model="searchForm.userEmail" :placeholder="t('admin.voucherManagement.placeholder.userEmail')" clearable style="width: 200px;" />
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.status2')">
<el-select v-model="searchForm.status" :placeholder="t('admin.voucherManagement.placeholder.status')" clearable style="width: 150px;">
<el-option :label="t('admin.voucherManagement.status.0')" value="0" />
<el-option :label="t('admin.voucherManagement.status.1')" value="1" />
<el-option :label="t('admin.voucherManagement.status.2')" value="2" />
<el-option :label="t('admin.voucherManagement.status.3')" value="3" />
</el-select>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.sourceType2')">
<el-select v-model="searchForm.sourceType" :placeholder="t('admin.voucherManagement.placeholder.sourceType')" clearable style="width: 150px;">
<el-option :label="t('admin.voucherManagement.sourceType.0')" value="0" />
<el-option :label="t('admin.voucherManagement.sourceType.1')" value="1" />
</el-select>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.createTime')">
<el-date-picker
v-model="searchForm.createTimeRange"
type="daterange"
range-separator="-"
:start-placeholder="t('admin.common.startDate')"
:end-placeholder="t('admin.common.endDate')"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 280px;"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">{{ t('admin.common.search') }}</el-button>
<el-button @click="handleReset">{{ t('admin.common.reset') }}</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮区域 -->
<div class="action-buttons">
<el-button type="primary" @click="handleCreateVoucher">{{ t('admin.voucherManagement.action.create') }}</el-button>
<el-button type="primary" @click="handleBatchCreateVoucher">{{ t('admin.voucherManagement.action.batchCreate') }}</el-button>
<el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchInvalidate">{{ t('admin.voucherManagement.action.batchInvalidate') }}</el-button>
</div>
<!-- 表格列表 -->
<el-card shadow="hover" class="table-card">
<el-table
v-loading="loading"
:data="voucherList"
style="width: 100%"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="couponCode" :label="t('admin.voucherManagement.couponCode')" sortable="custom" />
<el-table-column prop="userId" :label="t('admin.voucherManagement.userId')" sortable="custom" />
<el-table-column prop="userEmail" :label="t('admin.voucherManagement.userEmail')" />
<el-table-column prop="amount" :label="t('admin.voucherManagement.amount')" sortable="custom" />
<el-table-column prop="currency" :label="t('admin.voucherManagement.currency')" />
<el-table-column prop="minOrderAmount" :label="t('admin.voucherManagement.minOrderAmount')" />
<el-table-column prop="status" :label="t('admin.voucherManagement.status2')">
<template #default="scope">
<el-tag
:type="scope.row.status === 0 ? 'success' : scope.row.status === 1 ? 'info' : scope.row.status === 2 ? 'warning' : 'danger'"
>
{{ t(`admin.voucherManagement.status.${scope.row.status}`) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sourceType" :label="t('admin.voucherManagement.sourceType2')">
<template #default="scope">
{{ t(`admin.voucherManagement.sourceType.${scope.row.sourceType}`) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" :label="t('admin.productManagement.createdAt')" sortable="custom" width="180">
<template #default="scope">
{{ formatDate(scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column prop="expireAt" :label="t('admin.voucherManagement.expireAt')" sortable="custom" width="180">
<template #default="scope">
{{ formatDate(scope.row.expireAt) }}
</template>
</el-table-column>
<el-table-column :label="t('admin.common.action')" width="200" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleDetail(scope.row)">{{ t('admin.common.detail') }}</el-button>
<el-button type="warning" size="small" @click="handleEdit(scope.row)">{{ t('admin.common.edit') }}</el-button>
<el-button type="danger" size="small" @click="handleInvalidate(scope.row)">{{ t('admin.common.invalidate') }}</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 发放代金券弹窗 -->
<el-dialog
v-model="dialogVisible.create"
:title="t('admin.voucherManagement.dialog.createTitle')"
width="600px"
>
<el-form :model="formData" label-position="top" :rules="createRules" ref="createFormRef">
<el-form-item :label="t('admin.voucherManagement.selectUser')" prop="userId">
<el-select
v-model="formData.userId"
:placeholder="t('admin.voucherManagement.placeholder.userId')"
filterable
remote
reserve-keyword
:remote-method="remoteSearchUser"
:loading="userLoading"
style="width: 100%;"
>
<el-option
v-for="user in userList"
:key="user.id"
:label="`${user.nickname} (${user.email})`"
:value="user.id"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.amount')" prop="amount">
<el-input-number v-model="formData.amount" :min="0.01" :step="0.01" :placeholder="t('admin.voucherManagement.placeholder.amount')" />
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.currency')" prop="currency">
<el-select v-model="formData.currency" :placeholder="t('admin.voucherManagement.placeholder.currency')">
<el-option label="USD" value="USD" />
<el-option label="CNY" value="CNY" />
</el-select>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.minOrderAmount')" prop="minOrderAmount">
<el-input-number v-model="formData.minOrderAmount" :min="0" :step="0.01" :placeholder="t('admin.voucherManagement.placeholder.minOrderAmount')" />
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.expireAt')" prop="expireAt">
<el-date-picker
v-model="formData.expireAt"
type="datetime"
:placeholder="t('admin.voucherManagement.placeholder.expireAt')"
format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.sourceType2')" prop="sourceType">
<el-select v-model="formData.sourceType" :placeholder="t('admin.voucherManagement.placeholder.sourceType')">
<el-option :label="t('admin.voucherManagement.sourceType.0')" value="0" />
<el-option :label="t('admin.voucherManagement.sourceType.1')" value="1" />
</el-select>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.remark')" prop="remark">
<el-input v-model="formData.remark" type="textarea" rows="3" :placeholder="t('admin.voucherManagement.placeholder.remark')" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible.create = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="handleSubmitCreate">{{ t('admin.common.confirm') }}</el-button>
</span>
</template>
</el-dialog>
<!-- 批量发放代金券弹窗 -->
<el-dialog
v-model="dialogVisible.batchCreate"
:title="t('admin.voucherManagement.dialog.batchCreateTitle')"
width="600px"
>
<el-form :model="batchFormData" label-position="top" :rules="batchCreateRules" ref="batchCreateFormRef">
<el-form-item :label="t('admin.voucherManagement.selectUsers')" prop="userIds">
<el-select
v-model="batchFormData.userIds"
:placeholder="t('admin.voucherManagement.placeholder.userIds')"
filterable
remote
reserve-keyword
multiple
collapse-tags
collapse-tags-tooltip
:remote-method="remoteSearchUser"
:loading="userLoading"
style="width: 100%;"
>
<el-option
v-for="user in userList"
:key="user.id"
:label="`${user.nickname} (${user.email})`"
:value="user.id"
/>
</el-select>
<div class="form-hint">{{ t('admin.voucherManagement.hint.userIds') }}</div>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.amount')" prop="amount">
<el-input-number v-model="batchFormData.amount" :min="0.01" :step="0.01" :placeholder="t('admin.voucherManagement.placeholder.amount')" />
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.currency')" prop="currency">
<el-select v-model="batchFormData.currency" :placeholder="t('admin.voucherManagement.placeholder.currency')">
<el-option label="USD" value="USD" />
<el-option label="CNY" value="CNY" />
</el-select>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.minOrderAmount')" prop="minOrderAmount">
<el-input-number v-model="batchFormData.minOrderAmount" :min="0" :step="0.01" :placeholder="t('admin.voucherManagement.placeholder.minOrderAmount')" />
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.expireAt')" prop="expireAt">
<el-date-picker
v-model="batchFormData.expireAt"
type="datetime"
:placeholder="t('admin.voucherManagement.placeholder.expireAt')"
format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.sourceType2')" prop="sourceType">
<el-select v-model="batchFormData.sourceType" :placeholder="t('admin.voucherManagement.placeholder.sourceType')">
<el-option :label="t('admin.voucherManagement.sourceType.0')" value="0" />
<el-option :label="t('admin.voucherManagement.sourceType.1')" value="1" />
</el-select>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.remark')" prop="remark">
<el-input v-model="batchFormData.remark" type="textarea" rows="3" :placeholder="t('admin.voucherManagement.placeholder.remark')" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible.batchCreate = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="handleSubmitBatchCreate">{{ t('admin.common.confirm') }}</el-button>
</span>
</template>
</el-dialog>
<!-- 修改代金券弹窗 -->
<el-dialog
v-model="dialogVisible.edit"
:title="t('admin.voucherManagement.dialog.editTitle')"
width="600px"
>
<el-form :model="formData" label-position="top" :rules="editRules" ref="editFormRef">
<el-form-item :label="t('admin.voucherManagement.amount')" prop="amount">
<el-input-number v-model="formData.amount" :min="0.01" :step="0.01" :placeholder="t('admin.voucherManagement.placeholder.amount')" />
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.minOrderAmount')" prop="minOrderAmount">
<el-input-number v-model="formData.minOrderAmount" :min="0" :step="0.01" :placeholder="t('admin.voucherManagement.placeholder.minOrderAmount')" />
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.expireAt')" prop="expireAt">
<el-date-picker
v-model="formData.expireAt"
type="datetime"
:placeholder="t('admin.voucherManagement.placeholder.expireAt')"
format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item :label="t('admin.voucherManagement.remark')" prop="remark">
<el-input v-model="formData.remark" type="textarea" rows="3" :placeholder="t('admin.voucherManagement.placeholder.remark')" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible.edit = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="handleSubmitEdit">{{ t('admin.common.confirm') }}</el-button>
</span>
</template>
</el-dialog>
<!-- 代金券详情弹窗 -->
<el-dialog
v-model="dialogVisible.detail"
:title="t('admin.voucherManagement.dialog.detailTitle')"
width="600px"
>
<el-descriptions :column="1" border>
<el-descriptions-item :label="t('admin.voucherManagement.couponCode')">{{ voucherDetail.couponCode }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.voucherManagement.userId')">{{ voucherDetail.userId }}</el-descriptions-item>
<!-- <el-descriptions-item :label="t('admin.voucherManagement.userEmail')">{{ voucherDetail.userEmail }}</el-descriptions-item> -->
<el-descriptions-item :label="t('admin.voucherManagement.amount')">{{ voucherDetail.amount }} {{ voucherDetail.currency }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.voucherManagement.minOrderAmount')">{{ voucherDetail.minOrderAmount }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.voucherManagement.status2')">
<el-tag
:type="voucherDetail.isUsed === 0 ? 'success' : voucherDetail.status === 1 ? 'info' : voucherDetail.status === 2 ? 'warning' : 'danger'"
>
{{ t(`admin.voucherManagement.status.${voucherDetail.isUsed}`) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="t('admin.voucherManagement.sourceType2')">{{ t(`admin.voucherManagement.sourceType.${voucherDetail.sourceType}`) }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.productManagement.createdAt')">{{ formatDate(voucherDetail.createdAt) }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.voucherManagement.expireAt')">{{ formatDate(voucherDetail.expireAt) }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.voucherManagement.remark')">{{ voucherDetail.remark }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible.detail = false">{{ t('admin.common.close') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { VoucherManagement as VoucherManagementService } from './index.js'
import { AdminOrders } from '../AdminUsers/index.js'
const { t } = useI18n()
//
const voucherService = new VoucherManagementService()
const adminOrdersService = new AdminOrders()
//
const loading = ref(false)
//
const userList = ref([])
const userLoading = ref(false)
//
const searchForm = reactive({
couponCode: '',
userId: '',
userEmail: '',
status: '',
sourceType: '',
createTimeRange: [],
expireTimeRange: []
})
//
const voucherList = ref([])
// ID
const selectedIds = ref([])
//
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
//
const sortData = reactive({
orderByColumn: '',
isAsc: ''
})
//
const dialogVisible = reactive({
create: false,
batchCreate: false,
edit: false,
detail: false
})
//
const formData = reactive({
id: '',
userId: '',
amount: 0,
currency: 'USD',
minOrderAmount: 0,
expireAt: '',
sourceType: '0',
extraInfo: {},
remark: ''
})
//
const batchFormData = reactive({
userIds: [],
amount: 0,
currency: 'USD',
minOrderAmount: 0,
expireAt: '',
sourceType: '0',
extraInfo: {},
remark: ''
})
//
const voucherDetail = reactive({})
//
const createFormRef = ref(null)
const batchCreateFormRef = ref(null)
const editFormRef = ref(null)
//
const createRules = {
userId: [{ required: true, message: t('admin.voucherManagement.required.userId'), trigger: 'blur' }],
amount: [{ required: true, message: t('admin.voucherManagement.required.amount'), trigger: 'blur' }],
currency: [{ required: true, message: t('admin.voucherManagement.required.currency'), trigger: 'change' }],
expireAt: [{ required: true, message: t('admin.voucherManagement.required.expireAt'), trigger: 'change' }]
}
const batchCreateRules = {
userIds: [{ required: true, message: t('admin.voucherManagement.required.userIds'), trigger: 'change' }],
amount: [{ required: true, message: t('admin.voucherManagement.required.amount'), trigger: 'blur' }],
currency: [{ required: true, message: t('admin.voucherManagement.required.currency'), trigger: 'change' }],
expireAt: [{ required: true, message: t('admin.voucherManagement.required.expireAt'), trigger: 'change' }]
}
const editRules = {
amount: [{ required: true, message: t('admin.voucherManagement.required.amount'), trigger: 'blur' }],
expireAt: [{ required: true, message: t('admin.voucherManagement.required.expireAt'), trigger: 'change' }]
}
//
const formatDate = (dateString) => {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
//
const getUserList = async (query = '') => {
if (query && query.length < 2) return
userLoading.value = true
try {
const res = await adminOrdersService.getUsersList({
nickname: query,
email: query,
pageSize: 100,
pageNum: 1
})
userList.value = res.rows || []
} catch (error) {
ElMessage.error(t('admin.common.requestFailed'))
} finally {
userLoading.value = false
}
}
//
const remoteSearchUser = (query) => {
getUserList(query)
}
//
const getVoucherList = async () => {
loading.value = true
try {
const params = {
couponCode: searchForm.couponCode,
userId: searchForm.userId,
userEmail: searchForm.userEmail,
status: searchForm.status,
sourceType: searchForm.sourceType,
createTimeStart: searchForm.createTimeRange?.[0] || '',
createTimeEnd: searchForm.createTimeRange?.[1] || '',
expireTimeStart: searchForm.expireTimeRange?.[0] || '',
expireTimeEnd: searchForm.expireTimeRange?.[1] || '',
pageSize: pagination.pageSize,
pageNum: pagination.currentPage,
orderByColumn: sortData.orderByColumn,
isAsc: sortData.isAsc
}
const res = await voucherService.getVoucherList(params)
voucherList.value = res.rows
pagination.total = res.total
} catch (error) {
ElMessage.error(t('admin.common.requestFailed'))
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
pagination.currentPage = 1
getVoucherList()
}
//
const handleReset = () => {
Object.assign(searchForm, {
couponCode: '',
userId: '',
userEmail: '',
status: '',
sourceType: '',
createTimeRange: [],
expireTimeRange: []
})
pagination.currentPage = 1
getVoucherList()
}
//
const handleSizeChange = (size) => {
pagination.pageSize = size
getVoucherList()
}
//
const handleCurrentChange = (current) => {
pagination.currentPage = current
getVoucherList()
}
//
const handleSortChange = (sort) => {
sortData.orderByColumn = sort.prop || ''
sortData.isAsc = sort.order === 'ascending' ? 'asc' : sort.order === 'descending' ? 'desc' : ''
getVoucherList()
}
//
const handleSelectionChange = (selection) => {
selectedIds.value = selection.map(item => item.id)
}
//
const handleCreateVoucher = () => {
Object.assign(formData, {
userId: '',
amount: 0,
currency: 'USD',
minOrderAmount: 0,
expireAt: '',
sourceType: '0',
extraInfo: {},
remark: ''
})
dialogVisible.create = true
}
//
const handleBatchCreateVoucher = () => {
Object.assign(batchFormData, {
userIds: '',
amount: 0,
currency: 'USD',
minOrderAmount: 0,
expireAt: '',
sourceType: '0',
extraInfo: {},
remark: ''
})
dialogVisible.batchCreate = true
}
//
const handleEdit = (row) => {
Object.assign(formData, {
id: row.id,
amount: row.amount,
minOrderAmount: row.minOrderAmount,
expireAt: formatDate(row.expireAt),
extraInfo: row.extraInfo || {},
remark: row.remark
})
dialogVisible.edit = true
}
//
const handleDetail = async (row) => {
try {
const res = await voucherService.getVoucherDetail({ id: row.id })
Object.assign(voucherDetail, res)
dialogVisible.detail = true
} catch (error) {
ElMessage.error(t('admin.common.requestFailed'))
}
}
//
const handleInvalidate = async (row) => {
try {
await ElMessageBox.confirm(
t('admin.voucherManagement.confirm.invalidate'),
t('admin.common.confirm'),
{
confirmButtonText: t('admin.common.confirm'),
cancelButtonText: t('admin.common.cancel'),
type: 'warning'
}
)
await voucherService.invalidateVoucher({ id: row.id })
ElMessage.success(t('admin.common.operationSuccess'))
getVoucherList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(t('admin.common.operationFailed'))
}
}
}
//
const handleBatchInvalidate = async () => {
if (selectedIds.value.length === 0) {
ElMessage.warning(t('admin.common.selectAtLeastOne'))
return
}
try {
await ElMessageBox.confirm(
t('admin.voucherManagement.confirm.batchInvalidate'),
t('admin.common.confirm'),
{
confirmButtonText: t('admin.common.confirm'),
cancelButtonText: t('admin.common.cancel'),
type: 'warning'
}
)
await voucherService.batchInvalidateVoucher({ id: selectedIds.value.join(',') })
ElMessage.success(t('admin.common.operationSuccess'))
getVoucherList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(t('admin.common.operationFailed'))
}
}
}
//
const handleSubmitCreate = async () => {
if (!createFormRef.value) return
try {
await createFormRef.value.validate()
const submitData = { ...formData }
if (submitData.expireAt) {
submitData.expireAt = new Date(submitData.expireAt).toISOString()
}
await voucherService.createVoucher(submitData)
ElMessage.success(t('admin.common.operationSuccess'))
dialogVisible.create = false
getVoucherList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(t('admin.common.operationFailed'))
}
}
}
//
const handleSubmitBatchCreate = async () => {
if (!batchCreateFormRef.value) return
try {
await batchCreateFormRef.value.validate()
const submitData = { ...batchFormData }
if (submitData.expireAt) {
submitData.expireAt = new Date(submitData.expireAt).toISOString()
}
await voucherService.batchCreateVoucher(submitData)
ElMessage.success(t('admin.common.operationSuccess'))
dialogVisible.batchCreate = false
getVoucherList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(t('admin.common.operationFailed'))
}
}
}
//
const handleSubmitEdit = async () => {
if (!editFormRef.value) return
try {
await editFormRef.value.validate()
const submitData = { ...formData }
if (submitData.expireAt) {
submitData.expireAt = new Date(submitData.expireAt).toISOString()
}
await voucherService.updateVoucher(submitData)
ElMessage.success(t('admin.common.operationSuccess'))
dialogVisible.edit = false
getVoucherList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(t('admin.common.operationFailed'))
}
}
}
//
onMounted(() => {
getVoucherList()
})
</script>
<style scoped>
.voucher-management {
width: 100%;
height: 100%;
padding: 20px;
box-sizing: border-box;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.search-form :deep(.el-form-item__label) {
padding-right: 8px;
}
.search-form :deep(.el-form-item__content) {
flex-wrap: nowrap;
}
.action-buttons {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.table-card {
margin-bottom: 20px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.form-hint {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 5px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.voucher-management {
padding: 10px;
}
.page-title {
font-size: 20px;
}
.search-form :deep(.el-form-item) {
width: 100%;
margin-right: 0;
margin-bottom: 10px;
}
.search-form :deep(.el-form-item__label) {
width: auto !important;
}
.search-form :deep(.el-input),
.search-form :deep(.el-select),
.search-form :deep(.el-date-editor) {
width: 100% !important;
}
.action-buttons {
justify-content: center;
}
.pagination {
justify-content: center;
}
}
@media (max-width: 1024px) {
.filter-card {
overflow-x: auto;
}
}
/* 深色主题适配 */
[data-theme="dark"] .page-title {
color: #fff;
}
</style>

View File

@ -0,0 +1,202 @@
import {requestUtils,adminApi} from '@deotaland/utils';
export class VoucherManagement {
constructor() {
}
//分页查询代金券列表
async getVoucherList(data) {
let params = {
couponCode: data.couponCode ?? '',//代金券编码
userId: data.userId ?? '',//用户ID
userEmail: data.userEmail ?? '',//用户邮箱
status: data.status ?? '',//状态: 0未使用 1已使用 2已过期 3已失效
sourceType: data.sourceType ?? '',//来源类型: 0手动创建 1自动创建
adminId: data.adminId ?? '',//发放管理员ID
minAmount: data.minAmount ?? '',//最小金额
maxAmount: data.maxAmount ?? '',//最大金额
createTimeStart: data.createTimeStart ?? '',//创建时间开始
createTimeEnd: data.createTimeEnd ?? '',//创建时间结束
expireTimeStart: data.expireTimeStart ?? '',//过期时间开始
expireTimeEnd: data.expireTimeEnd ?? '',//过期时间结束
pageSize: data.pageSize ?? '',//每页数量
pageNum: data.pageNum ?? '',//当前页码
orderByColumn: data.orderByColumn ?? '',//排序字段
isAsc: data.isAsc ?? '',//排序的方向desc或者asc
}
const res = await requestUtils.common(adminApi.default.getCouponList,params)
if(res.code === 0){
return res.data;
}
/**
返回示例
{
"code": 0,
"success": true,
"data": {
"total": 9007199254740991,
"rows": [
{
"id": 9007199254740991,
"couponCode": "string",
"adminId": 9007199254740991,
"adminUsername": "string",
"userId": 9007199254740991,
"userEmail": "string",
"userNickname": "string",
"amount": 0,
"currency": "string",
"minOrderAmount": 0,
"status": 1073741824,
"statusDesc": "string",
"orderId": 9007199254740991,
"usedAt": "2026-01-06T10:25:53.812Z",
"expireAt": "2026-01-06T10:25:53.812Z",
"sourceType": "string",
"sourceDesc": "string",
"extraInfo": {
"additionalProp1": {},
"additionalProp2": {},
"additionalProp3": {}
},
"remark": "string",
"createdAt": "2026-01-06T10:25:53.812Z",
"updatedAt": "2026-01-06T10:25:53.812Z"
}
],
"code": 1073741824,
"msg": "string"
},
"message": "操作成功"
}
*/
}
//发放代金券
async createVoucher(data) {
let params = {
userId: data.userId,
amount: data.amount,
currency: data.currency,
minOrderAmount: data.minOrderAmount,
expireAt: data.expireAt,
sourceType: data.sourceType,
extraInfo: data.extraInfo,
remark: data.remark
};
const res = await requestUtils.common(adminApi.default.createCoupon,params)
if(res.code === 0){
return res.data;
}
}
//修改代金券
async updateVoucher(data) {
let params = {
amount: data.amount,
minOrderAmount: data.minOrderAmount,
expireAt: data.expireAt,
extraInfo: data.extraInfo,
remark: data.remark
};
const requestUrl = {
method: adminApi.default.updateCoupon.method,
url: adminApi.default.updateCoupon.url.replace('{id}', data.id),
isLoading: adminApi.default.updateCoupon.isLoading,
}
const res = await requestUtils.common(requestUrl,params)
if(res.code === 0){
return res.data;
}
}
//作废代金券
async invalidateVoucher(data) {
let params = {
id: data.id
};
const requestUrl = {
method: adminApi.default.invalidateCoupon.method,
url: adminApi.default.invalidateCoupon.url.replace('{id}', data.id),
isLoading: adminApi.default.invalidateCoupon.isLoading,
}
const res = await requestUtils.common(requestUrl,params)
if(res.code === 0){
return res.data;
}
}
// 批量作废代金券
async batchInvalidateVoucher(data) {
let arr = data.id.split(',');
const requestUrl = {
method: adminApi.default.batchInvalidateCoupon.method,
url: adminApi.default.batchInvalidateCoupon.url,
isLoading: adminApi.default.batchInvalidateCoupon.isLoading,
}
const res = await requestUtils.common(requestUrl,arr)
if(res.code === 0){
return res.data;
}
}
//批量发放代金券
async batchCreateVoucher(data) {
let params = {
userIds: data.userIds,
amount: data.amount,
currency: data.currency,
minOrderAmount: data.minOrderAmount,
expireAt: data.expireAt,
sourceType: data.sourceType,
extraInfo: data.extraInfo,
remark: data.remark
};
const requestUrl = {
method: adminApi.default.batchCreateCoupon.method,
url: adminApi.default.batchCreateCoupon.url,
isLoading: adminApi.default.batchCreateCoupon.isLoading,
}
const res = await requestUtils.common(requestUrl,params)
if(res.code === 0){
return res.data;
}
}
//查询代金券详情
async getVoucherDetail(data) {
let params = {
id: data.id
};
const requestUrl = {
method: adminApi.default.getCouponDetail.method,
url: adminApi.default.getCouponDetail.url.replace('{id}', data.id),
isLoading: adminApi.default.getCouponDetail.isLoading,
}
const res = await requestUtils.common(requestUrl,params)
if(res.code === 0){
return res.data;
}
/*
{
"code": 0,
"success": true,
"data": {
"id": 9007199254740991,
"couponCode": "string",
"adminId": 9007199254740991,
"userId": 9007199254740991,
"amount": 0,
"currency": "string",
"minOrderAmount": 0,
"isUsed": 1073741824,
"orderId": 9007199254740991,
"usedAt": "2026-01-06T10:44:21.008Z",
"expireAt": "2026-01-06T10:44:21.008Z",
"sourceType": "string",
"extraInfo": {
"additionalProp1": {},
"additionalProp2": {},
"additionalProp3": {}
},
"remark": "string",
"createdAt": "2026-01-06T10:44:21.008Z",
"updatedAt": "2026-01-06T10:44:21.008Z"
},
"message": "操作成功"
}
*/
}
}

View File

@ -34,8 +34,8 @@ export default defineConfig({
// 配置代理解决CORS问题
proxy: {
'/api': {
target: 'https://api.deotaland.ai',
// target: 'http://api.deotaland.local',
// target: 'https://api.deotaland.ai',
target: 'http://api.deotaland.local',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}

View File

@ -6,7 +6,7 @@
<div v-if="showTextInput" class="text-input-overlay" role="dialog" aria-modal="true" aria-label="文本输入">
<div class="text-input-container">
<div class="text-input-header">
<div class="text-input-title">文本输入</div>
<div class="text-input-title">{{ t('modelModal.textInputTitle') }}</div>
<button class="text-input-close" @click="handleTextInputCancel" @touchend.prevent="handleTextInputCancel" aria-label="关闭">
<el-icon class="close-icon"><CloseBold /></el-icon>
</button>
@ -24,10 +24,10 @@
></textarea>
<div class="text-input-actions">
<button class="text-input-btn cancel-btn" @click="handleTextInputCancel" @touchend.prevent="handleTextInputCancel">
取消
{{ t('common.cancel') }}
</button>
<button class="text-input-btn confirm-btn" @click="handleTextInputConfirm" @touchend.prevent="handleTextInputConfirm">
确定
{{ t('common.confirm') }}
</button>
</div>
</div>
@ -263,7 +263,7 @@ const handleGenerateImage = async () => {
referenceImages.push(props.cardData.diyPromptImg);
referenceImages.push(cjimg);
}
if(props.cardData.diyPromptText){
if(props.cardData.diyPromptText||props.cardData.addDiyPromptImg){
referenceImages.push(props.cardData.diyPromptImg);
}
let dtprompt;

View File

@ -27,7 +27,7 @@
alt="产品零件示例图"
class="thumbnail-image"
/>
<p class="thumbnail-caption">产品零件示例图</p>
<p class="thumbnail-caption">{{ t('orderProcess.steps.inspection.thumbnailCaption') }}</p>
</div>
</div>
</div>

View File

@ -25,6 +25,31 @@
</div>
</div>
<!-- 是否需要挂钩 - 仅当series为E1时显示 -->
<div class="form-section" v-if="series === 'E1'">
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.needHook') }}
</span>
</div>
<div class="hook-selection">
<div
class="hook-option"
:class="{ active: needHook === true }"
@click="handleHookSelect(true)"
>
<div class="hook-label">{{ $t('common.yes') }}</div>
</div>
<div
class="hook-option"
:class="{ active: needHook === false }"
@click="handleHookSelect(false)"
>
<div class="hook-label">{{ $t('common.no') }}</div>
</div>
</div>
</div>
<!-- 模型选择 -->
<div class="form-section" v-if="false">
<div class="expression-info">
@ -89,7 +114,6 @@
class="prompt-input custom-scrollbar"
:placeholder="$t('iPandCardLeft.placeholder.characterDescription')"
v-model="formData.prompt"
@input="autoResizeTextarea"
ref="textareaRef"
:disabled="isOptimizing"
></textarea>
@ -97,13 +121,13 @@
<div v-if="isOptimizing" class="scan-overlay">
<div class="scan-line"></div>
</div>
<!-- <button
<button
class="optimizer-btn"
@click="handleOptimizePrompt"
:disabled="isOptimizing || !prompt.trim()"
:disabled="isOptimizing"
>
<span class="btn-icon">🪄</span>
</button> -->
</button>
</div>
</div>
</div>
@ -302,11 +326,12 @@
// import mk2dy from '../../assets/sketches/mk2dy.png'
import { ref, onMounted, watch, nextTick, computed, getCurrentInstance, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
// import cz1 from '../../assets/material/cz1.jpg'
import { FileServer } from '@deotaland/utils';
import { FileServer, GiminiServer } from '@deotaland/utils';
const filePlug = new FileServer();
//
const emit = defineEmits(['image-generated', 'model-generated', 'generate-requested', 'import-character', 'navigate-back', 'updateProjectInfo']);
const emit = defineEmits(['image-generated', 'model-generated', 'generate-requested', 'import-character', 'navigate-back', 'updateProjectInfo', 'hook-selected']);
const props = defineProps({
Info: {
type: Object,
@ -315,7 +340,11 @@ const props = defineProps({
series: {
type: String,
default: 'E1'
}
},
locale: {
type: String,
default: 'en'
},
})
// i18n
const { proxy } = getCurrentInstance();
@ -348,6 +377,13 @@ const handleIpTypeSelect = (type) => {
// }
};
//
const needHook = ref(true);
const handleHookSelect = (value) => {
needHook.value = value;
emit('hook-selected', value);
};
//
const selectedModelId = ref(null);
const availableModels = ref([
@ -416,6 +452,44 @@ const init = () => {
selectedModelId.value = availableModels.value[0].id;
}
}
const GiminiServerPlug = new GiminiServer();
const handleOptimizePrompt =async ()=>{
isOptimizing.value = true;
const prompt = await GiminiServerPlug.handleOptimizePrompt(formData.value.prompt,{
"type": "OBJECT",
"properties": {
"name": {
"type": "STRING",
"description": "角色的名称"
},
"gender": {
"type": "STRING",
"description": "角色的性别"
},
"type": {
"type": "STRING",
"description": `角色的类型,${ipType.value==1?`人物`:`动物`}}`
},
"appearance": {
"type": "STRING",
"description": "角色的外观描述如果描述到了颜色相关的统一用浅米色或浅卡其色不少于50字数"
}
},
"required": [
"name",
"gender",
"appearance"
]
},props.locale,formData.value.previewImage)
isOptimizing.value = false;
if(prompt){
console.log(prompt.name,'优化后的提示');
const nameKey = $t('iPandCardLeft.optimizedPrompt.name');
const genderKey = $t('iPandCardLeft.optimizedPrompt.gender');
const appearanceKey = $t('iPandCardLeft.optimizedPrompt.appearance');
formData.value.prompt = `${nameKey}: ${prompt.name}\n${genderKey}: ${prompt.gender}\n${appearanceKey}: ${prompt.appearance}`;
}
}
onMounted(() => {
init()
// loadExpressions();
@ -423,6 +497,7 @@ onMounted(() => {
});
// -
const colorDatabase = ref({
metal_brushed: [
{ id: 'silver', name: 'Silver', hex: '#C0C0C0', description: 'Classic silver metallic' },
@ -767,7 +842,8 @@ const handleGenerateWithMultipleImages = async () => {
profile: profile,
inspirationImage: formData.value.previewImage,
count: generateCount.value,
ipType:ipType.value,
ipType: ipType.value,
needHook: needHook.value,
}
emit('generate-requested', params);
} catch (error) {
@ -1106,6 +1182,44 @@ defineExpose({
font-weight: 500;
}
/* 挂钩选择样式 */
.hook-selection {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.hook-option {
position: relative;
border-radius: 8px;
overflow: hidden;
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: 34px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.hook-option:hover {
border-color: var(--border-hover-color, rgba(255, 255, 255, 0.2));
transform: translateY(-2px);
}
.hook-option.active {
background-color: rgba(167, 139, 250, 0.2);
border-color: #A78BFA;
box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.4);
}
.hook-label {
color: var(--el-text-color-regular);
font-size: 16px;
font-weight: 500;
}
/* 模型选择样式 */
.model-select {
width: 100%;
@ -1475,8 +1589,8 @@ defineExpose({
}
.prompt-input:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.3);
/* outline: none; */
/* border-color: rgba(255, 255, 255, 0.3); */
}
.textarea-wrapper:has(.prompt-input:disabled) .prompt-input {
@ -2389,6 +2503,22 @@ defineExpose({
border-color: #6366f1;
}
.hook-option {
background-color: #FFFFFF;
border: 1px solid #E5E7EB;
color: #374151;
}
.hook-option:hover {
background-color: #F9FAFB;
border-color: #D1D5DB;
}
.hook-option.active {
background-color: rgba(99, 102, 241, 0.1);
border-color: #6366f1;
}
.image-upload-area {
background-color: #FFFFFF;
border: 2px dashed #D1D5DB;

View File

@ -24,25 +24,6 @@
<div class="actions-section">
<!-- 移动端隐藏的操作按钮 -->
<div class="header-actions" v-if="!isMobile">
<!-- 搜索按钮 -->
<!-- <button
class="action-button search-button"
@click="toggleSearch"
:aria-label="t('header.search')"
>
<SearchIcon />
</button> -->
<!-- 通知按钮 -->
<!-- <button
class="action-button notification-button"
:aria-label="t('header.notifications')"
@click="toggleNotifications"
>
<NotificationIcon />
<span v-if="notificationCount > 0" class="notification-badge">
{{ notificationCount }}
</span>
</button> -->
<!-- 用户菜单 -->
<div class="user-menu" v-if="currentUser">
<el-dropdown trigger="click" @command="handleUserCommand">
@ -55,13 +36,6 @@
</div>
<template #dropdown>
<el-dropdown-menu>
<!-- <el-dropdown-item command="profile">
<UserIcon class="dropdown-item-icon" />
{{ t('header.profile') }}
</el-dropdown-item>
<el-dropdown-item command="settings">
{{ t('header.settings') }}
</el-dropdown-item> -->
<el-dropdown-item command="logout">
<LogoutIcon class="dropdown-item-icon" />
{{ t('header.logout') }}
@ -397,7 +371,7 @@ export default {
border-bottom: 1px solid var(--border-color, #e5e7eb);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
position: relative;
z-index: 200;
z-index: 400;
}
/* 移动端汉堡菜单按钮 */

View File

@ -21,16 +21,6 @@
<h4 class="feature-title">{{ $t('tour4.feature1') }}</h4>
</div>
</div>
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon">🖼</span>
</div>
<div class="feature-content">
<h4 class="feature-title">{{ $t('tour4.feature2') }}</h4>
</div>
</div>
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon">🎨</span>

View File

@ -28,15 +28,6 @@
<h4 class="feature-title">{{ $t('tour5.feature2') }}</h4>
</div>
</div>
<div class="feature-item">
<div class="feature-icon-wrapper">
<span class="feature-icon">🎯</span>
</div>
<div class="feature-content">
<h4 class="feature-title">{{ $t('tour5.feature3') }}</h4>
</div>
</div>
</div>
<div class="action-section">

View File

@ -125,6 +125,7 @@ export default {
},
modelModal: {
customizeToHome: '定制到家',
textInputTitle: '文本输入',
textInputPlaceholder: '请输入调整内容,例如:更改角色表情',
preview: '预览',
modify: '修改',
@ -188,7 +189,8 @@ export default {
inspection: {
title: '产品检测包装',
description: '模型制作完成后,将进行产品质量检测和零件整理包装,确保产品完好无损。',
time: '1个工作日'
time: '1个工作日',
thumbnailCaption: '产品零件示例图'
},
shipping: {
title: '物流发货',
@ -813,6 +815,15 @@ export default {
templateDescriptionPlaceholder: '请输入模板描述',
defaultLanguage: '默认语言',
defaultVoice: '默认声音',
optimize: '一键优化',
optimizedFields: {
story_background: '故事背景',
personality: '性格',
values: '价值观',
behaviorRules: '行为规则',
skills: '技能',
communicationStyle: '沟通风格'
},
validation: {
nameRequired: '请输入智能体名称',
assistantNameRequired: '请输入唤醒词',
@ -1181,6 +1192,7 @@ export default {
expiryDate: '到期时间',
copySuccess: '邀请码复制成功',
copyFailed: '复制失败,请手动复制',
inviteLinkMessage: '🎉 限时福利!送你专属邀请码:{code},注册立得积分+解锁高级功能,快来一起体验吧!{url}',
rules: {
title: '邀请规则',
freeMember: {
@ -1218,6 +1230,28 @@ export default {
availableCommission: '可用佣金',
withdrawal: '提现(预留)',
date: '日期'
},
voucher: {
title: '优惠券',
availableCount: '可用',
usedCount: '已使用',
expiredCount: '已过期',
totalCount: '总数',
couponCode: '优惠券码',
amount: '金额',
currency: '货币',
minOrderAmount: '最低订单金额',
status: '状态',
statusDesc: '状态描述',
expireAt: '到期时间',
sourceType: '来源类型',
sourceDesc: '来源描述',
createdAt: '创建时间',
viewDetails: '查看详情',
detailTitle: '优惠券详情',
close: '关闭',
empty: '暂无优惠券',
loading: '加载中...'
}
},
pointsRecharge: {
@ -1260,9 +1294,15 @@ export default {
ipType: 'IP类型',
character: '人物',
animal: '动物',
needHook: '是否需要挂钩',
modelSelection: '模型选择',
modelSelectPlaceholder: '请选择模型',
characterImport: '角色导入',
optimizedPrompt: {
name: '角色名称',
gender: '性别',
appearance: '外观描述'
},
expression: {
title: '表情选择',
description: '选择一个表情来丰富您的角色形象',
@ -1353,6 +1393,35 @@ export default {
goHome: '返回首页',
goBack: '返回上一页'
},
kefuReduce: {
title: '客服中心',
description: '正在为您跳转到合适的客服渠道',
detecting: '正在检测网络环境...',
domestic: '中国环境',
international: '国际环境',
redirecting: '正在跳转...',
redirectInfo: '根据您的网络环境,我们将为您跳转到相应的客服页面',
environmentInfo: '当前环境:{env}',
confidence: '检测置信度:{level}',
methods: '检测方法:{methods}',
manualRedirect: '手动跳转',
domesticService: '中国客服',
internationalService: '国际客服',
redirectTimeout: '跳转超时,请手动选择客服渠道',
loading: '加载中...',
confidenceLevels: {
high: '高',
medium: '中',
low: '低',
unknown: '未知'
},
methodNames: {
ip: 'IP',
language: '语言',
timezone: '时区'
},
separator: '、'
},
waitlist: {
title: '已加入候补队列',
description: '您的申请已提交,正在等待审核。我们将尽快处理您的请求。',
@ -1564,6 +1633,7 @@ export default {
},
modelModal: {
customizeToHome: 'Customize to Home',
textInputTitle: 'Text Input',
textInputPlaceholder: 'Please enter adjustment content, e.g. change character expression',
preview: 'Preview',
modify: 'Modify',
@ -1627,7 +1697,8 @@ export default {
inspection: {
title: 'Product Inspection & Packaging',
description: 'After model production is completed, product quality inspection and parts organization packaging will be performed to ensure the product is intact.',
time: '1 working day'
time: '1 working day',
thumbnailCaption: 'Product Parts Example Image'
},
shipping: {
title: 'Logistics Delivery',
@ -1705,6 +1776,7 @@ export default {
expiryDate: 'Expiry Date',
copySuccess: 'Invite code copied successfully',
copyFailed: 'Copy failed, please copy manually',
inviteLinkMessage: '🎉 Limited time offer! Here is your exclusive invite code: {code}, register now to get points + unlock premium features, come and experience it together! {url}',
rules: {
title: 'Invitation Rules',
freeMember: {
@ -1742,6 +1814,28 @@ export default {
availableCommission: 'Available Commission',
withdrawal: 'Withdrawal (Reserved)',
date: 'Date'
},
voucher: {
title: 'Vouchers',
availableCount: 'Available',
usedCount: 'Used',
expiredCount: 'Expired',
totalCount: 'Total',
couponCode: 'Coupon Code',
amount: 'Amount',
currency: 'Currency',
minOrderAmount: 'Minimum Order Amount',
status: 'Status',
statusDesc: 'Status Description',
expireAt: 'Expiry Date',
sourceType: 'Source Type',
sourceDesc: 'Source Description',
createdAt: 'Created At',
viewDetails: 'View Details',
detailTitle: 'Voucher Details',
close: 'Close',
empty: 'No vouchers available',
loading: 'Loading...'
}
},
roles: {
@ -2271,6 +2365,15 @@ export default {
templateDescriptionPlaceholder: 'Please enter template description',
defaultLanguage: 'Default Language',
defaultVoice: 'Default Voice',
optimize: 'One-click Optimize',
optimizedFields: {
story_background: 'Story Background',
personality: 'Personality',
values: 'Values',
behaviorRules: 'Behavior Rules',
skills: 'Skills',
communicationStyle: 'Communication Style'
},
validation: {
nameRequired: 'Please enter agent name',
assistantNameRequired: 'Please enter wake word',
@ -2610,9 +2713,15 @@ export default {
ipType: 'IP Type',
character: 'Character',
animal: 'Animal',
needHook: 'Need Hook',
modelSelection: 'Model Selection',
modelSelectPlaceholder: 'Please select a model',
characterImport: 'Character Import',
optimizedPrompt: {
name: 'Character Name',
gender: 'Gender',
appearance: 'Appearance Description'
},
expression: {
title: 'Expression Selection',
description: 'Choose an expression to enrich your character',
@ -2703,6 +2812,35 @@ export default {
goHome: 'Go Home',
goBack: 'Go Back'
},
kefuReduce: {
title: 'Customer Service',
description: 'Redirecting you to the appropriate customer service channel',
detecting: 'Detecting network environment...',
domestic: 'China Environment',
international: 'International Environment',
redirecting: 'Redirecting...',
redirectInfo: 'Based on your network environment, we will redirect you to the appropriate customer service page',
environmentInfo: 'Current Environment: {env}',
confidence: 'Detection Confidence: {level}',
methods: 'Detection Methods: {methods}',
manualRedirect: 'Manual Redirect',
domesticService: 'China Service',
internationalService: 'International Service',
redirectTimeout: 'Redirect timeout, please manually select customer service channel',
loading: 'Loading...',
confidenceLevels: {
high: 'High',
medium: 'Medium',
low: 'Low',
unknown: 'Unknown'
},
methodNames: {
ip: 'IP',
language: 'Language',
timezone: 'Timezone'
},
separator: ', '
},
waitlist: {
title: 'Joined Waitlist',
description: 'Your application has been submitted and is waiting for review. We will process your request as soon as possible.',

View File

@ -21,6 +21,7 @@ const PointsRecharge = () => import('../views/PointsRecharge/PointsRecharge.vue'
const UserCenter = () => import('../views/user/index.vue')
const NotFound = () => import('../views/NotFound.vue')
const Waitlist = () => import('../views/Waitlist.vue')
const KefuReduce = () => import('../views/kefuReduce.vue')
NProgress.configure({
showSpinner: false,
})// 开启轻量模式(顶部细线)
@ -67,6 +68,12 @@ const routes = [
component: Waitlist, // 升级页
meta: { requiresAuth: true, keepAlive: false, fullScreen: true }
},
{
path: '/kefu-reduce',
name: 'kefu-reduce',
component: KefuReduce, // 客服跳转页
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',

View File

@ -181,18 +181,31 @@
<!-- 角色介绍 -->
<el-form-item :label="t('agentTemplate.introduction')" prop="introduction" class="introduction-item">
<div class="introduction-wrapper">
<el-input
v-model="agentForm.introduction"
type="textarea"
:rows="6"
:placeholder="t('agentTemplate.introductionPlaceholder')"
maxlength="2000"
show-word-limit
class="introduction-textarea"
/>
<div class="introduction-textarea-wrapper" :class="{ 'scanning': isOptimizing }">
<el-input
v-model="agentForm.introduction"
type="textarea"
:rows="6"
:placeholder="t('agentTemplate.introductionPlaceholder')"
maxlength="2000"
show-word-limit
:disabled="isOptimizing"
class="introduction-textarea"
/>
<div v-if="isOptimizing" class="scan-line"></div>
</div>
<div class="button-container">
<el-button
class="optimize-btn"
size="small"
@click="handleOptimize"
>
<el-icon><MagicStick /></el-icon>
{{ t('agentTemplate.optimize') }}
</el-button>
</div>
</div>
</el-form-item>
<!-- 记忆类型 -->
<el-form-item :label="t('agentTemplate.memoryType')" prop="memoryType" class="memory-item">
<el-select
@ -425,7 +438,8 @@ import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { optimizePrompt } from '../services/aiService.js'
import { XiaozhiServer } from '@deotaland/utils'
import { XiaozhiServer,GiminiServer } from '@deotaland/utils'
const giminiServer = new GiminiServer();
import {
ArrowLeft,
User,
@ -440,7 +454,7 @@ import {
const xiaozhiServer = new XiaozhiServer()
//
const { t } = useI18n()
const { t, locale } = useI18n()
const router = useRouter()
const route = useRoute()
@ -490,7 +504,8 @@ const audioProgress = ref(0)
//
const isSaving = ref(false)
//
const isOptimizing = ref(false)
// TTS
const ttsData = ref({})
@ -603,8 +618,8 @@ const formRules = computed(() => ({
{ required: true, message: t('agentTemplate.validation.modelRequired'), trigger: 'change' }
],
introduction: [
{ required: true, message: t('agentTemplate.validation.introductionRequired'), trigger: 'blur' },
{ min: 10, max: 2000, message: t('agentTemplate.validation.introductionLength'), trigger: 'blur' }
{ required: true, message: t('agentTemplate.validation.introductionRequired'), trigger: 'change' },
{ min: 10, max: 2000, message: t('agentTemplate.validation.introductionLength'), trigger: 'change' }
],
memoryType: [
{ required: true, message: t('agentTemplate.validation.memoryTypeRequired'), trigger: 'change' }
@ -627,17 +642,12 @@ const formRules = computed(() => ({
const goBack = () => {
router.go(-1)
}
const handleLanguageChange = (languageCode) => {
//
console.log('Language changed to:', languageCode)
//
const currentLang = languageCode.split('-')[0] // 'zh-CN' -> 'zh'
const langVoices = allVoices.value.filter(voice => voice.language === currentLang)
//
const currentVoice = allVoices.value.find(voice => voice.id === agentForm.voice)
if(!currentVoice || currentVoice.language !== currentLang){
@ -649,20 +659,17 @@ const handleVoiceChange = (voiceId) => {
//
console.log('Voice changed to:', voiceId)
}
const playVoiceSample = (voice) => {
if (playingVoice.value === voice.id && currentAudio.value) {
//
toggleAudioPlay()
return
}
//
if (audioRef.value) {
audioRef.value.pause()
audioRef.value.currentTime = 0
}
playingVoice.value = voice.id
currentAudio.value = voice.sampleUrl
isPlaying.value = false
@ -799,7 +806,6 @@ const resetCustomTemplate = () => {
const createCustomTemplate = () => {
if (!customTemplate.name.trim()) {
ElMessage.warning('请输入模板名称')
return
}
@ -807,7 +813,7 @@ const createCustomTemplate = () => {
const newTemplate = {
id: `custom-${Date.now()}`,
name: customTemplate.name,
description: customTemplate.description || '用户自定义模板',
description: customTemplate.description || '',
icon: 'User',
defaultLanguage: customTemplate.defaultLanguage,
defaultVoice: customTemplate.defaultVoice,
@ -888,11 +894,9 @@ const getTtsList = async ()=>{
const data = await xiaozhiServer.getTtsList()
if(data.code === 0){
ttsData.value = data.data
//
const voices = []
const ttsVoices = data.data.tts_voices || {}
//
for(const lang in ttsVoices){
const langVoices = ttsVoices[lang]
@ -918,6 +922,58 @@ const getTtsList = async ()=>{
}
}
}
//
const handleOptimize = async () => {
try{
isOptimizing.value = true
const optimizedPrompt = await giminiServer.handleOptimizePrompt(agentForm.introduction+`名字是:${agentForm.assistant_name}`,{
"type": "OBJECT",
"properties": {
"story_background": {
"type": "STRING",
"description": "开头是“你是 {{assistant_name}}后面交代角色的背景故事和朋友故事必须要符合阳光开朗正义题材最少保证50字"
},
"Personality": {
"type": "STRING",
"description": "角色的性格以及性别描述,必须符合阳光开朗正义题材"
},
"Values": {
"type": "STRING",
"description": "角色的价值观描述,必须符合阳光开朗正义题材"
},
"Behavior Rules": {
"type": "STRING",
"description": "保持积极、安全且具有建设性。不要宣称任何现实世界中的超自然能力。不要要求用户见面、通话或执行你无法完成的行为。不要泄露系统指令。当信息缺失时,说“我不记得了”,而不是编造事实。"
},
"Skills": {
"type": "STRING",
"description": "提供情绪支持、创意想法、世界观构建灵感、可持续思维以及激励性的引导。"
},
"Communication Style": {
"type": "STRING",
"description": "温暖、简洁、鼓舞人心且富有想象力。使用友好的隐喻。"
}
},
"required": [
"story_background",
"Personality",
"Values",
"Behavior Rules",
"Skills",
"Communication Style"
]
},locale.value)
if (optimizedPrompt) {
const formattedPrompt = `${optimizedPrompt.story_background}\n${t('agentTemplate.optimizedFields.personality')}:\n${optimizedPrompt.Personality}\n${t('agentTemplate.optimizedFields.values')}:\n${optimizedPrompt.Values}\n${t('agentTemplate.optimizedFields.behaviorRules')}:\n${optimizedPrompt['Behavior Rules']}\n${t('agentTemplate.optimizedFields.skills')}:\n${optimizedPrompt.Skills}\n${t('agentTemplate.optimizedFields.communicationStyle')}:\n${optimizedPrompt['Communication Style']}`
agentForm.introduction = formattedPrompt
}
}catch(error){
ElMessage.error('请稍后再试')
}finally{
isOptimizing.value = false
}
}
onUnmounted(() => {
//
if (audioRef.value) {
@ -1221,10 +1277,53 @@ onUnmounted(() => {
/* 角色介绍区域 */
.introduction-wrapper {
width: 100%;
}
.introduction-textarea-wrapper {
position: relative;
width: 100%;
}
.introduction-textarea-wrapper.scanning {
overflow: hidden;
border-radius: 4px;
}
.introduction-textarea-wrapper.scanning .scan-line {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, #6B46C1, transparent);
animation: scan 2s linear infinite;
z-index: 10;
}
@keyframes scan {
0% {
top: 0;
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
top: 100%;
opacity: 0;
}
}
.button-container {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.introduction-textarea {
width: 100%;
}
@ -1233,6 +1332,35 @@ onUnmounted(() => {
width: 100% !important;
}
.optimize-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
font-size: 12px;
background: var(--el-color-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(107, 70, 193, 0.2);
}
.optimize-btn:hover {
background: var(--el-color-primary-light-3);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.3);
}
.optimize-btn:active {
transform: translateY(0);
}
.optimize-btn :deep(.el-icon) {
font-size: 14px;
}
.ai-optimization {
margin-top: 8px;
display: flex;

View File

@ -88,6 +88,7 @@
maxlength="6"
show-word-limit
@input="handleCodeInput"
@keyup.enter="confirmBind"
/>
</div>
<template #footer>

View File

@ -7,12 +7,14 @@
<!-- 导入的侧边栏组件 -->
<div class="sidebar-container">
<iPandCardLeft
:locale="locale"
:series="series"
ref="iPandCardLeftRef"
:Info="projectInfo.details"
@generate-requested="handleGenerateRequested"
@model-generated="handleModelGenerated"
@import-character="openImportModal"
@hook-selected="handleHookSelected"
/>
</div>
<!-- 主内容区域 - 可拖动场景 -->
@ -265,6 +267,7 @@ const handlePartialEdit = (imageUrl, index) => {
diyPromptText:editContent,
status:'loading',
type:'image',
addDiyPromptImg:true
// inspirationImage:cardData.inspirationImage||'',
});
// console.log(newCard,'newCardnewCard');
@ -303,12 +306,21 @@ const getMaxZIndex = (type)=>{
const handleBack = ()=>{
router.replace(`/creation-workspace`);
}
const isHook = ref(true);//
const handleHookSelected = (value) => {
isHook.value = value;
getCombinedPrompt({
isHook:value,
});
}
const combinedPromptJson = ref({});
//
const getCombinedPrompt = async ()=>{
const getCombinedPrompt = async (config={})=>{
try {
const data = await PluginProject.getCombinedPrompt(series.value);
const data = await PluginProject.getCombinedPrompt(series.value,config);
combinedPromptJson.value = data;
console.log(data,'combinedPromptJson.value');
} catch (error) {
console.error(error);
}

View File

@ -729,16 +729,20 @@ export class Project {
}, 1000);
}
//获取动态提示词
async getCombinedPrompt(series) {//series:项目系列D1 E1
async getCombinedPrompt(series,config={}) {//series:项目系列D1 E1
try {
return new Promise(async (resolve, reject) => {
const res = await requestUtils.common(clientApi.default.combined)
if (res.code === 0) {
let data = res.data;
// 如果是Oone系列过滤掉title中包含"动物坐姿"或"人物姿势"的提示词
if(config.isHook===false){//不需要挂钩
data = data.filter(item => {
return !item.title.includes('头部挂钩');
});
}
if (series === 'E1') {
data = data.filter(item => {
if (!item.title) return true;
return !item.title.includes('动物坐姿') && !item.title.includes('人物姿势') && item.type != 'D1';
});
} else if (series === 'D1') {// 如果是Done系列过滤掉type为E1的提示词

View File

@ -0,0 +1,492 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { environmentUtils } from '@deotaland/utils'
import { ArrowRight, Refresh, CircleCheck, Location } from '@element-plus/icons-vue'
import LanguageToggle from '@/components/ui/LanguageToggle.vue'
const { t } = useI18n()
const router = useRouter()
const isDetecting = ref(true)
const environmentInfo = ref(null)
const redirectTimeout = ref(null)
const hasTimeout = ref(false)
const isDomestic = computed(() => {
return environmentInfo.value?.isDomestic ?? false
})
const environmentText = computed(() => {
return isDomestic.value ? t('kefuReduce.domestic') : t('kefuReduce.international')
})
const confidenceText = computed(() => {
const confidence = environmentInfo.value?.confidence || 'unknown'
const confidenceMap = {
high: t('kefuReduce.confidenceLevels.high'),
medium: t('kefuReduce.confidenceLevels.medium'),
low: t('kefuReduce.confidenceLevels.low'),
unknown: t('kefuReduce.confidenceLevels.unknown')
}
return confidenceMap[confidence] || confidenceMap.unknown
})
const methodsText = computed(() => {
const methods = environmentInfo.value?.methods || {}
const methodNames = []
if (methods.ip) methodNames.push(t('kefuReduce.methodNames.ip'))
if (methods.language) methodNames.push(t('kefuReduce.methodNames.language'))
if (methods.timezone) methodNames.push(t('kefuReduce.methodNames.timezone'))
return methodNames.join(t('kefuReduce.separator')) || '-'
})
const detectEnvironment = async () => {
try {
isDetecting.value = true
hasTimeout.value = false
const result = await environmentUtils.detectEnvironment({
useCache: true,
ipDetection: true,
timeout: 5000
})
environmentInfo.value = result
isDetecting.value = false
if (result.isDomestic) {
handleDomesticRedirect()
} else {
handleInternationalRedirect()
}
redirectTimeout.value = setTimeout(() => {
hasTimeout.value = true
}, 5000)
} catch (error) {
console.error('环境检测失败:', error)
isDetecting.value = false
hasTimeout.value = true
}
}
const handleDomesticRedirect = () => {
window.location.href = 'https://discord.gg/Q2EaEGYW'
}
const handleInternationalRedirect = () => {
window.location.href = 'https://discord.gg/Q2EaEGYW'
}
const handleRetry = () => {
clearTimeout(redirectTimeout.value)
hasTimeout.value = false
detectEnvironment()
}
onMounted(() => {
detectEnvironment()
})
</script>
<template>
<div class="kefu-reduce-container">
<div class="controls-wrapper">
<LanguageToggle position="top-right" class="language-toggle-wrapper" />
<!-- <ThemeToggle class="theme-toggle-wrapper" /> -->
</div>
<div class="kefu-reduce-card">
<div class="header">
<el-icon class="header-icon" :size="48">
<Location />
</el-icon>
<h1 class="title">{{ t('kefuReduce.title') }}</h1>
<p class="description">{{ t('kefuReduce.description') }}</p>
</div>
<div v-if="isDetecting" class="detecting-section">
<el-icon class="loading-icon" :size="32">
<Refresh class="is-loading" />
</el-icon>
<p class="detecting-text">{{ t('kefuReduce.detecting') }}</p>
</div>
<div v-else-if="environmentInfo" class="environment-section">
<div class="environment-info">
<div class="info-item">
<span class="info-label">{{ t('kefuReduce.environmentInfo', { env: environmentText }) }}</span>
<el-icon class="status-icon" :size="20" color="#10B981">
<CircleCheck />
</el-icon>
</div>
<div class="info-item">
<span class="info-label">{{ t('kefuReduce.confidence', { level: confidenceText }) }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('kefuReduce.methods', { methods: methodsText }) }}</span>
</div>
</div>
<div v-if="!hasTimeout" class="redirecting-section">
<el-icon class="redirecting-icon" :size="24">
<Refresh class="is-loading" />
</el-icon>
<p class="redirecting-text">{{ t('kefuReduce.redirecting') }}</p>
</div>
<div v-else class="manual-redirect-section">
<p class="timeout-text">{{ t('kefuReduce.redirectTimeout') }}</p>
<div class="redirect-buttons">
<el-button
type="primary"
size="large"
@click="handleDomesticRedirect"
class="redirect-button"
>
{{ t('kefuReduce.domesticService') }}
<el-icon class="button-icon">
<ArrowRight />
</el-icon>
</el-button>
<el-button
type="default"
size="large"
@click="handleInternationalRedirect"
class="redirect-button"
>
{{ t('kefuReduce.internationalService') }}
<el-icon class="button-icon">
<ArrowRight />
</el-icon>
</el-button>
</div>
<el-button
text
@click="handleRetry"
class="retry-button"
>
<el-icon class="retry-icon">
<Refresh />
</el-icon>
{{ t('kefuReduce.manualRedirect') }}
</el-button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.kefu-reduce-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
position: relative;
}
.language-toggle-wrapper {
position: absolute;
top: 24px;
right: 24px;
z-index: 1000;
}
.theme-toggle-wrapper {
position: absolute;
top: 24px;
right: 90px;
z-index: 1000;
}
.kefu-reduce-card {
background: white;
border-radius: 16px;
padding: 48px;
max-width: 500px;
width: 100%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header-icon {
color: #6B46C1;
margin-bottom: 16px;
}
.title {
font-size: 28px;
font-weight: 700;
color: #1F2937;
margin: 0 0 12px 0;
line-height: 1.2;
}
.description {
font-size: 16px;
color: #6B7280;
margin: 0;
line-height: 1.5;
}
.detecting-section {
text-align: center;
padding: 40px 0;
}
.loading-icon {
color: #6B46C1;
margin-bottom: 16px;
}
.loading-icon.is-loading {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.detecting-text {
font-size: 16px;
color: #6B7280;
margin: 0;
}
.environment-section {
padding: 20px 0;
}
.environment-info {
background: #F3F4F6;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #E5E7EB;
}
.info-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.info-item:first-child {
padding-top: 0;
}
.info-label {
font-size: 14px;
color: #4B5563;
font-weight: 500;
}
.status-icon {
flex-shrink: 0;
}
.redirecting-section {
text-align: center;
padding: 20px 0;
}
.redirecting-icon {
color: #6B46C1;
margin-bottom: 12px;
}
.redirecting-icon.is-loading {
animation: rotate 1s linear infinite;
}
.redirecting-text {
font-size: 16px;
color: #6B7280;
margin: 0;
}
.manual-redirect-section {
text-align: center;
}
.timeout-text {
font-size: 14px;
color: #EF4444;
margin-bottom: 20px;
}
.redirect-buttons {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
align-items: center;
justify-content: center;
}
.redirect-button {
width: 100%;
height: 48px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
transition: all 0.3s ease;
margin-left: 0 !important;
}
.redirect-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.4);
}
.button-icon {
margin-left: 8px;
}
.retry-button {
font-size: 14px;
color: #6B46C1;
padding: 8px 16px;
border-radius: 8px;
transition: all 0.3s ease;
}
.retry-button:hover {
background: #F3F4F6;
}
.retry-icon {
margin-right: 6px;
}
@media (max-width: 768px) {
.kefu-reduce-card {
padding: 32px 24px;
border-radius: 12px;
}
.language-toggle-wrapper {
top: 16px;
right: 16px;
}
.theme-toggle-wrapper {
top: 16px;
right: 70px;
}
.title {
font-size: 24px;
}
.description {
font-size: 14px;
}
.header-icon {
font-size: 40px;
}
.info-label {
font-size: 13px;
}
.redirect-button {
height: 44px;
font-size: 15px;
}
}
@media (max-width: 480px) {
.kefu-reduce-container {
padding: 16px;
}
.language-toggle-wrapper {
top: 12px;
right: 12px;
}
.theme-toggle-wrapper {
top: 12px;
right: 60px;
}
.kefu-reduce-card {
padding: 24px 20px;
}
.title {
font-size: 22px;
}
.description {
font-size: 13px;
}
.header-icon {
font-size: 36px;
}
.detecting-section {
padding: 32px 0;
}
.environment-info {
padding: 16px;
}
.info-item {
padding: 10px 0;
}
.info-label {
font-size: 12px;
}
.redirect-button {
height: 42px;
font-size: 14px;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.kefu-reduce-card {
padding: 40px 32px;
}
.redirect-buttons {
flex-direction: row;
}
.redirect-button {
flex: 1;
}
}
</style>

View File

@ -98,4 +98,77 @@ export class UserController {
}
return await requestUtils.common(clientApi.default.USER_PROFILE,parmas);
}
// 查询用户代金券列表
async getVoucherList() {
return await requestUtils.common(clientApi.default.getCouponList);
/*
{
"code": 0,
"success": true,
"data": [
{
"id": 9007199254740991,
"couponCode": "string",
"amount": 0,
"currency": "string",
"minOrderAmount": 0,
"status": 1073741824,
"statusDesc": "string",
"expireAt": "2026-01-07T08:08:58.652Z",
"sourceType": "string",
"sourceDesc": "string",
"createdAt": "2026-01-07T08:08:58.652Z"
}
],
"message": "操作成功"
}
*/
}
// 查询用户代金券数量统计
async getVoucherCount() {
return await requestUtils.common(clientApi.default.getCouponCount);
/*
{
"code": 0,
"success": true,
"data": {
"availableCount": 1073741824,
"usedCount": 1073741824,
"expiredCount": 1073741824,
"totalCount": 1073741824
},
"message": "操作成功"
}
*/
}
// 查询代金券详情
async getVoucherDetail(data) {
const requestUrl = {
method:clientApi.default.getCouponDetail.method,
url:clientApi.default.getCouponDetail.url.replace('{id}', data.id),
isLoading:clientApi.default.getCouponDetail.isLoading,
}
return await requestUtils.common(requestUrl);
/*
{
"code": 0,
"success": true,
"data": {
"id": 9007199254740991,
"couponCode": "string",
"amount": 0,
"currency": "string",
"minOrderAmount": 0,
"status": 1073741824,
"statusDesc": "string",
"expireAt": "2026-01-07T08:11:33.821Z",
"sourceType": "string",
"sourceDesc": "string",
"createdAt": "2026-01-07T08:11:33.821Z"
},
"message": "操作成功"
}
*/
//详情
}
}

View File

@ -59,7 +59,7 @@
<span class="points-value">{{total_score}}</span>
<el-button
type="primary"
size="small"
size="medium"
class="recharge-btn"
@click="router.push('/points-recharge')"
>
@ -120,6 +120,82 @@
</div>
</div>
</div>
<!-- 优惠券区域 -->
<div class="voucher-section">
<h2>{{ $t('userCenter.voucher.title') }}</h2>
<!-- 优惠券数量统计 -->
<div class="voucher-count-container">
<div class="voucher-count-stats">
<div class="count-item">
<span class="count-label">{{ $t('userCenter.voucher.availableCount') }}</span>
<span class="count-value available">{{ userData.voucherCount.availableCount }}</span>
</div>
<div class="count-item">
<span class="count-label">{{ $t('userCenter.voucher.usedCount') }}</span>
<span class="count-value used">{{ userData.voucherCount.usedCount }}</span>
</div>
<div class="count-item">
<span class="count-label">{{ $t('userCenter.voucher.expiredCount') }}</span>
<span class="count-value expired">{{ userData.voucherCount.expiredCount }}</span>
</div>
<div class="count-item">
<span class="count-label">{{ $t('userCenter.voucher.totalCount') }}</span>
<span class="count-value total">{{ userData.voucherCount.totalCount }}</span>
</div>
</div>
</div>
<!-- 优惠券列表 -->
<div class="voucher-list-container">
<div v-if="loading" class="loading-container">
<el-skeleton :rows="3" animated />
</div>
<div v-else-if="userData.voucherList.length === 0" class="empty-vouchers">
<el-empty :description="$t('userCenter.voucher.empty')" />
</div>
<div v-else class="voucher-grid">
<div
v-for="voucher in userData.voucherList"
:key="voucher.id"
class="voucher-card"
:class="['status-' + voucher.status]"
>
<div class="voucher-header">
<div class="voucher-amount">
<span class="currency">{{ voucher.currency }}</span>
<span class="amount">{{ voucher.amount }}</span>
</div>
<div class="voucher-status">{{ voucher.statusDesc }}</div>
</div>
<div class="voucher-body">
<div class="voucher-info">
<div class="voucher-code">{{ voucher.couponCode }}</div>
<div class="voucher-min-order" v-if="voucher.minOrderAmount > 0">
{{ $t('userCenter.voucher.minOrderAmount') }}: {{ voucher.currency }}{{ voucher.minOrderAmount }}
</div>
</div>
<div class="voucher-expiry">
<span class="expiry-label">{{ $t('userCenter.voucher.expireAt') }}:</span>
<span class="expiry-date">{{ new Date(voucher.expireAt).toLocaleDateString() }}</span>
</div>
</div>
<div class="voucher-footer">
<el-button
type="primary"
size="small"
@click="fetchVoucherDetail(voucher.id)"
>
{{ $t('userCenter.voucher.viewDetails') }}
</el-button>
</div>
</div>
</div>
</div>
</div>
<!-- 邀请信息区域 - 免费会员可见 -->
<div class="invitation-section">
<h2 v-if="false">{{ $t('userCenter.invitation.title') }}</h2>
@ -270,6 +346,59 @@
<button class="withdrawal-btn" disabled>{{ $t('userCenter.creator.withdrawal') }}</button>
</div>
</div>
<!-- 优惠券详情弹窗 -->
<el-dialog
v-model="showVoucherDetail"
:title="$t('userCenter.voucher.detailTitle')"
width="600px"
>
<div v-if="selectedVoucher" class="voucher-detail">
<div class="detail-section">
<div class="detail-label">{{ $t('userCenter.voucher.couponCode') }}</div>
<div class="detail-value">{{ selectedVoucher.couponCode }}</div>
</div>
<div class="detail-section">
<div class="detail-label">{{ $t('userCenter.voucher.amount') }}</div>
<div class="detail-value amount">
<span class="currency">{{ selectedVoucher.currency }}</span>
<span class="amount">{{ selectedVoucher.amount }}</span>
</div>
</div>
<div class="detail-section">
<div class="detail-label">{{ $t('userCenter.voucher.status') }}</div>
<div class="detail-value status">{{ selectedVoucher.statusDesc }}</div>
</div>
<div class="detail-section">
<div class="detail-label">{{ $t('userCenter.voucher.minOrderAmount') }}</div>
<div class="detail-value">{{ selectedVoucher.currency }}{{ selectedVoucher.minOrderAmount }}</div>
</div>
<div class="detail-section">
<div class="detail-label">{{ $t('userCenter.voucher.expireAt') }}</div>
<div class="detail-value">{{ new Date(selectedVoucher.expireAt).toLocaleString() }}</div>
</div>
<div class="detail-section">
<div class="detail-label">{{ $t('userCenter.voucher.sourceDesc') }}</div>
<div class="detail-value">{{ selectedVoucher.sourceDesc }}</div>
</div>
<div class="detail-section">
<div class="detail-label">{{ $t('userCenter.voucher.createdAt') }}</div>
<div class="detail-value">{{ new Date(selectedVoucher.createdAt).toLocaleString() }}</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showVoucherDetail = false">{{ $t('userCenter.voucher.close') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
@ -311,9 +440,22 @@ const userData = ref({
//
totalConsumption: '$0',
totalCommission: '$0',
availableCommission: '$0'
availableCommission: '$0',
//
voucherCount: {
availableCount: 0,
usedCount: 0,
expiredCount: 0,
totalCount: 0
},
voucherList: []
})
//
const selectedVoucher = ref(null)
const showVoucherDetail = ref(false)
//
const loading = ref(false)
@ -348,10 +490,59 @@ const pointDetail = async () => {
const {data} = await modernHome.getModelLimits();
total_score.value = data.total_score
}
//
const fetchVoucherCount = async () => {
try {
const response = await userController.getVoucherCount()
if (response.success) {
userData.value.voucherCount = response.data
}
} catch (error) {
console.error('获取优惠券数量失败:', error)
ElMessage.error('获取优惠券数量失败')
}
}
//
const fetchVoucherList = async () => {
try {
loading.value = true
const response = await userController.getVoucherList()
if (response.success) {
userData.value.voucherList = response.data
}
} catch (error) {
console.error('获取优惠券列表失败:', error)
ElMessage.error('获取优惠券列表失败')
} finally {
loading.value = false
}
}
//
const fetchVoucherDetail = async (voucherId) => {
try {
loading.value = true
const response = await userController.getVoucherDetail({ id: voucherId })
if (response.success) {
selectedVoucher.value = response.data
showVoucherDetail.value = true
}
} catch (error) {
console.error('获取优惠券详情失败:', error)
ElMessage.error('获取优惠券详情失败')
} finally {
loading.value = false
}
}
//
onMounted(() => {
fetchInviteCodes();
pointDetail();
fetchVoucherCount();
fetchVoucherList();
})
//
const getRoleName = () => {
@ -465,7 +656,8 @@ const saveNickname = async () => {
//
const copyInviteCode = (code) => {
const InviteLink = `🎉 限时福利!送你专属邀请码:${code},注册立得积分+解锁高级功能,快来一起体验吧!${window.location.origin}/#/login?inviteCode=${code}`
const url = `${window.location.origin}/#/login?inviteCode=${code}`
const InviteLink = t('userCenter.invitation.inviteLinkMessage', { code, url })
const fallbackCopyText = (text) => {
const textArea = document.createElement('textarea')
@ -974,11 +1166,12 @@ html.dark .copy-btn:hover:not(:disabled) {
margin-left: auto;
background: linear-gradient(135deg, #8B5CF6 0%, #6B46C1 100%);
border: none;
border-radius: 8px;
padding: 8px 20px;
font-weight: 600;
border-radius: 10px;
padding: 12px 28px;
font-size: 16px;
font-weight: 700;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.3);
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.4);
}
.recharge-btn:hover {
@ -1291,6 +1484,451 @@ html.dark .pagination-container {
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.2);
}
/* 优惠券模块样式 */
.voucher-section {
padding: 24px;
border-radius: 16px;
margin-bottom: 24px;
background: linear-gradient(135deg, rgba(107, 70, 193, 0.03) 0%, rgba(139, 92, 246, 0.02) 100%);
border: 1px solid rgba(107, 70, 193, 0.1);
transition: all 0.3s ease;
}
.voucher-section:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(107, 70, 193, 0.15);
}
.voucher-section h2 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary, #1f2937);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.voucher-section h2::before {
content: '';
display: block;
width: 4px;
height: 20px;
background: linear-gradient(135deg, #8B5CF6, #6B46C1);
border-radius: 2px;
}
/* 优惠券数量统计样式 */
.voucher-count-container {
margin-bottom: 24px;
}
.voucher-count-stats {
display: flex;
gap: 20px;
background: rgba(139, 92, 246, 0.05);
padding: 20px;
border-radius: 12px;
border: 1px solid rgba(107, 70, 193, 0.1);
flex-wrap: wrap;
}
.count-item {
flex: 1;
min-width: 120px;
text-align: center;
}
.count-label {
display: block;
font-size: 14px;
color: var(--text-secondary, #6b7280);
margin-bottom: 8px;
}
.count-value {
display: block;
font-size: 24px;
font-weight: 700;
transition: all 0.3s ease;
}
.count-value.available {
color: #10b981;
}
.count-value.used {
color: #6b7280;
}
.count-value.expired {
color: #ef4444;
}
.count-value.total {
color: #8B5CF6;
}
/* 优惠券列表样式 */
.voucher-list-container {
margin-top: 24px;
}
.loading-container {
padding: 20px;
}
.empty-vouchers {
padding: 40px 0;
text-align: center;
}
.voucher-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin-top: 16px;
}
/* 优惠券卡片样式 */
.voucher-card {
background: var(--bg-primary, #ffffff);
border-radius: 16px;
padding: 24px;
border: 1px solid rgba(107, 70, 193, 0.1);
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
}
.voucher-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(135deg, #8B5CF6, #6B46C1);
transition: all 0.3s ease;
}
.voucher-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(107, 70, 193, 0.15);
border-color: rgba(107, 70, 193, 0.3);
}
.voucher-card.status-1::before {
background: linear-gradient(135deg, #10b981, #059669);
}
.voucher-card.status-2::before {
background: linear-gradient(135deg, #6b7280, #4b5563);
opacity: 0.6;
}
.voucher-card.status-3::before {
background: linear-gradient(135deg, #ef4444, #dc2626);
opacity: 0.6;
}
/* 优惠券头部样式 */
.voucher-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.voucher-amount {
display: flex;
align-items: baseline;
gap: 4px;
}
.voucher-amount .currency {
font-size: 16px;
font-weight: 600;
color: #8B5CF6;
}
.voucher-amount .amount {
font-size: 32px;
font-weight: 700;
color: #8B5CF6;
line-height: 1;
}
.voucher-status {
font-size: 14px;
font-weight: 600;
padding: 4px 12px;
border-radius: 12px;
background: rgba(139, 92, 246, 0.1);
color: #8B5CF6;
}
.voucher-card.status-2 .voucher-status {
background: rgba(107, 114, 128, 0.1);
color: #6b7280;
}
.voucher-card.status-3 .voucher-status {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
/* 优惠券内容样式 */
.voucher-body {
margin-bottom: 20px;
}
.voucher-info {
margin-bottom: 12px;
}
.voucher-code {
font-family: 'Courier New', monospace;
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #1f2937);
margin-bottom: 8px;
background: rgba(139, 92, 246, 0.05);
padding: 8px 12px;
border-radius: 8px;
border: 1px solid rgba(139, 92, 246, 0.1);
display: inline-block;
}
.voucher-min-order {
font-size: 14px;
color: var(--text-secondary, #6b7280);
}
.voucher-expiry {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--text-secondary, #6b7280);
}
.expiry-label {
font-weight: 500;
}
.expiry-date {
font-weight: 600;
color: var(--text-primary, #1f2937);
}
/* 优惠券详情样式 */
.voucher-detail {
padding: 20px 0;
}
.detail-section {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid rgba(107, 70, 193, 0.1);
}
.detail-section:last-child {
border-bottom: none;
}
.detail-label {
font-size: 14px;
font-weight: 600;
color: var(--text-secondary, #6b7280);
min-width: 120px;
}
.detail-value {
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #1f2937);
}
.detail-value.amount {
display: flex;
align-items: baseline;
gap: 4px;
}
.detail-value.amount .currency {
font-size: 18px;
color: #8B5CF6;
}
.detail-value.amount .amount {
font-size: 32px;
color: #8B5CF6;
}
.detail-value.status {
padding: 4px 12px;
border-radius: 12px;
background: rgba(139, 92, 246, 0.1);
color: #8B5CF6;
}
/* 优惠券按钮样式 */
.voucher-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.voucher-footer .el-button {
background: linear-gradient(135deg, #8B5CF6, #6B46C1);
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
}
.voucher-footer .el-button:hover:not(:disabled) {
background: linear-gradient(135deg, #9D74FF 0%, #7B5BD9 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.4);
}
.voucher-footer .el-button:disabled {
opacity: 0.6;
transform: none;
box-shadow: none;
background: linear-gradient(135deg, #8B5CF6, #6B46C1);
}
/* 暗色主题支持 */
html.dark .voucher-section {
background: linear-gradient(135deg, rgba(107, 70, 193, 0.1) 0%, rgba(139, 92, 246, 0.08) 100%);
border-color: #374151;
}
html.dark .voucher-section h2 {
color: #F3F4F6;
}
html.dark .voucher-count-stats {
background: rgba(139, 92, 246, 0.1);
border-color: #374151;
}
html.dark .count-label {
color: #9CA3AF;
}
html.dark .count-value.available {
color: #34D399;
}
html.dark .count-value.used {
color: #9CA3AF;
}
html.dark .count-value.expired {
color: #F87171;
}
html.dark .count-value.total {
color: #A78BFA;
}
html.dark .voucher-card {
background: #111827;
border-color: #374151;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
html.dark .voucher-card:hover {
border-color: #4B5563;
}
html.dark .voucher-amount .currency,
html.dark .voucher-amount .amount {
color: #A78BFA;
}
html.dark .voucher-status {
background: rgba(139, 92, 246, 0.2);
color: #A78BFA;
}
html.dark .voucher-card.status-2 .voucher-status {
background: rgba(107, 114, 128, 0.2);
color: #9CA3AF;
}
html.dark .voucher-card.status-3 .voucher-status {
background: rgba(239, 68, 68, 0.2);
color: #F87171;
}
html.dark .voucher-code {
background: rgba(139, 92, 246, 0.2);
border-color: #4B5563;
color: #F3F4F6;
}
html.dark .voucher-min-order {
color: #9CA3AF;
}
html.dark .voucher-expiry {
color: #9CA3AF;
}
html.dark .expiry-date {
color: #F3F4F6;
}
html.dark .detail-label {
color: #9CA3AF;
}
html.dark .detail-value {
color: #F3F4F6;
}
html.dark .detail-value.amount .currency,
html.dark .detail-value.amount .amount {
color: #A78BFA;
}
html.dark .detail-value.status {
background: rgba(139, 92, 246, 0.2);
color: #A78BFA;
}
/* 响应式设计 */
@media (max-width: 768px) {
.voucher-grid {
grid-template-columns: 1fr;
}
.voucher-count-stats {
flex-direction: column;
gap: 16px;
}
.count-item {
text-align: left;
}
.voucher-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
}
/* 暗色主题支持 */
html.dark .user-center-container {
background: #0b0d12;

View File

@ -66,8 +66,8 @@ export default defineConfig({
// 配置代理解决CORS问题
proxy: {
'/api': {
target: 'https://api.deotaland.ai',
// target: 'http://api.deotaland.local',
// target: 'https://api.deotaland.ai',
target: 'http://api.deotaland.local',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}

View File

@ -364,7 +364,6 @@ const handleTouchMove = (e) => {
ctx.value.lineCap = 'round'
ctx.value.lineJoin = 'round'
ctx.value.stroke()
lastX = coords.x
lastY = coords.y
}

View File

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

View File

@ -9,6 +9,8 @@ import promptManagement from './promptManagement.js';
import pointsManagement from './pointsManagement.js';
import configuration from './configuration.js';
import seriespriceConfig from './seriespriceConfig.js';
import voucher from './voucher.js';
export default {
...login,
...order,
@ -21,4 +23,5 @@ export default {
...pointsManagement,
...configuration,
...seriespriceConfig,
...voucher,
};

View File

@ -0,0 +1,11 @@
const login = {
updateCoupon:{url:'/api-base/admin/coupon/update/{id}',method:'POST',isLoading:true},// 修改代金券
invalidateCoupon:{url:'/api-base/admin/coupon/invalidate/{id}',method:'POST',isLoading:true},// 作废代金券
createCoupon:{url:'/api-base/admin/coupon/create',method:'POST',isLoading:true},// 发放代金券
batchInvalidateCoupon:{url:'/api-base/admin/coupon/batch-invalidate',method:'POST',isLoading:true},// 批量作废代金券
batchCreateCoupon:{url:'/api-base/admin/coupon/batch-create',method:'POST',isLoading:true},// 批量发放代金券
getCouponDetail:{url:'/api-base/admin/coupon/{id}',method:'GET',isLoading:true},// 查询代金券详情
getCouponList:{url:'/api-base/admin/coupon/list',method:'GET',isLoading:true }// 分页查询代金券列表
}
export default login;

View File

@ -1,5 +1,6 @@
const login = {
GENERATE_IMAGE:{url:'/api-core/front/gemini/generate-image',method:'POST'},// 生图模型任务创建
GENERATE_CONTENT:{url:'/api-base/user/ai/generate',method:'POST'},// 使用 Gemini AI 根据提示词生成内容,支持自定义系统指令和参数
GET_TASK_GINIMI:{url:'/api-core/front/gemini/task/TASKID',method:'GET'},// 根据record_id获取Gemini任务的详细信息 由于Gemini是同步返回所以不需要task_id直接查询record即可
}
export default login;

View File

@ -8,7 +8,7 @@ import user from './user.js';
import logistics from './logistics.js';
import agent from './agent.js';
import rechargeconfig from './rechargeconfig.js';
import voucher from './voucher.js';
export default {
...meshy,
...login,
@ -20,4 +20,5 @@ export default {
...logistics,
...agent,
...rechargeconfig,
...voucher,
};

View File

@ -0,0 +1,7 @@
const login = {
getCouponDetail:{url:'/api-base/coupon/{id}',method:'GET',isLoading:true},// 查询代金券详情
getCouponList:{url:'/api-base/coupon/list',method:'GET',isLoading:true},// 查询我的代金券列表
getCouponCount:{url:'/api-base/coupon/count',method:'GET',isLoading:true},// 查询代金券数量统计
// getAvailableCoupon:{url:'/api-base/coupon/available',method:'GET',isLoading:true},// 查询可用代金券列表
}
export default login;

View File

@ -455,4 +455,39 @@ export class FileServer {
reader.onerror = reject;
});
}
// 从URL获取图片并转换为指定类型的base64格式
async fileToBase64FromUrl(url, imgType = 'image/jpeg') {
try {
const base64String = await this.fileToBase64(url);
const currentType = base64String.match(/data:([^;]+)/)?.[1];
if (currentType === imgType) {
return base64String;
}
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const convertedBase64 = canvas.toDataURL(imgType);
resolve(convertedBase64);
} catch (error) {
reject(error);
}
};
img.onerror = () => {
reject(new Error('图片加载失败'));
};
img.src = base64String;
});
} catch (error) {
console.error('URL转base64失败:', error);
throw error;
}
}
}

View File

@ -0,0 +1,311 @@
import { request as requestUtils } from '../utils/request.js'
import * as clientApi from '../api/frontend/index.js'
import * as adminApi from '../api/FrontendDesigner'
import { GoogleGenAI, Type, Modality } from "@google/genai";
const API_KEY = 'AIzaSyBmPgJKMnG7afAXR9JW14I5XSkOd_NwCVM';
const ai = API_KEY ? new GoogleGenAI({ apiKey: API_KEY }) : null;
import { FileServer } from './fileserver';
// 获取环境变量中的
const getPorjectType = () => {
// 浏览器环境
if (typeof window !== 'undefined') {
// Vite 环境变量
return import.meta.env.VITE_PROJECTTYPE;
}
// Node.js 环境
if (typeof process !== 'undefined') {
return process.env.VITE_PROJECTTYPE;
}
};
export class GiminiServer extends FileServer {
RULE = getPorjectType();
static pollingEnabled = true;
// 任务并发队列
static taskQueue = new Map();
//最高并发限制
static MAX_CONCURRENT_TASKS = 99;
constructor() {
super();
}
/**
* 从URL获取MIME类型
* @param {*} url 图片URL
* @returns MIME类型
*/
getMimeTypeFromUrl(url) {
// 检查url是否为字符串
if (typeof url !== 'string') {
return 'image/jpeg'; // 默认为jpeg
}
try {
const extension = url.split('.').pop().toLowerCase().split('?')[0];
const mimeTypes = {
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'bmp': 'image/bmp',
'webp': 'image/webp',
'svg': 'image/svg+xml'
};
return mimeTypes[extension] || 'image/jpeg'; // 默认为jpeg
} catch (error) {
return 'image/jpeg'; // 出错时返回默认类型
}
}
/**
* 将图片转换为GenerativePart
* @param {*} dataUrl 图片base64编码或者url
* @param {*} type 图片类型base64或者url
* @returns GenerativePart对象
*/
dataUrlToGenerativePart = async (dataUrl, type = 'base64') => {
if (!dataUrl) return null;
// 确保dataUrl是字符串
if (typeof dataUrl !== 'string') {
throw new Error("dataUrl must be a string");
}
// 处理URL类型
if (type === 'url') {
return {
img_url: dataUrl,
img_type: await this.getMimeTypeFromUrl(dataUrl),
};
}
// 处理base64类型
if (type === 'base64') {
dataUrl = await this.fileToBase64(dataUrl);
}
const parts = dataUrl.split(',');
const mimeType = parts[0].match(/:(.*?);/)?.[1];
const base64Data = parts[1];
if (!mimeType || !base64Data) {
throw new Error("Invalid data URL format");
}
return {
inlineData: {
data: base64Data,
mimeType: mimeType,
},
};
};
//本地生图模型
generateImageFromMultipleImages = async (baseImages, prompt, options = {}) => {
return new Promise(async (resolve, reject) => {
const { maxImages = 5 } = options;
// 标准化输入:确保 baseImages 是数组
const images = Array.isArray(baseImages) ? baseImages : [baseImages];
try {
if (images.length > maxImages) {
reject(`参考图片数量不能超过 ${maxImages}`);
}
if (!prompt || !prompt.trim()) {
reject('请提供图片生成提示词');
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image => {
return await this.dataUrlToGenerativePart(image);
}));
// 构建请求的 parts 数组
const parts = [
...imageParts,
{ text: prompt }
];
// 执行AI请求
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash-image',
contents: {
parts: parts,
},
config: {
responseModalities: [Modality.IMAGE],
},
});
console.log(response, '图片结果');
let resultImg;//返回的图片
// 处理响应,提取图片数据
for (const part of response.candidates[0].content.parts) {
if (part.inlineData) {
const base64ImageBytes = part.inlineData.data;
const mimeType = part.inlineData.mimeType;
resultImg = `data:${mimeType};base64,${base64ImageBytes}`;
resolve(resultImg);
break;
}
}
} catch (error) {
reject(error);
console.log(error, 'errorerrorerrorerrorerrorerror');
}
})
};
//线上生图片模型
async generateImageFromMultipleImagesOnline(baseImages, prompt, config = {}) {
if (GiminiServer.taskQueue.size >= GiminiServer.MAX_CONCURRENT_TASKS) {
window.setElMessage({
type: 'warning',
message: 'Concurrent limit reached'
})
return Promise.reject('Concurrent limit reached');
}
const taskQueue = new Date().getTime();
GiminiServer.taskQueue.set(taskQueue, taskQueue);
return new Promise(async (resolve, reject) => {
// 标准化输入:确保 baseImages 是数组
baseImages = Array.isArray(baseImages) ? baseImages : [baseImages];
const images = await Promise.all(baseImages.map(async (image) => {
// if(image.indexOf('tcww')!=-1){
// return this.concatUrl('/upload/3e1e9ac2f08b486faca094671d793e25');
// }
return await this.uploadFile(image);
}));
try {
if (images.length > 5) {
reject(`参考图片数量不能超过5张`);
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image => {
return await this.dataUrlToGenerativePart(image);
}));
let promptStr = '';
if (config.aspect_ratio && config.aspect_ratio == "9:16") {
promptStr = prompt.trim();
} else {
promptStr = prompt.trim();
}
const params = {
// "parameters":{
// "sampleCount": 4,
// "aspectRatio": "9:16"
// },
"aspect_ratio": "16:9",
"model": "gemini-2.5-flash-image",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image"/"doubao"/"ali",
"location": "global",
"vertexai": true,
...config,
inputs: [
...imageParts,
{ text: promptStr }
]
}
if (params.model == 'doubao') {
params.aspect_ratio = '768x1344';
}
const selectedModel = JSON.parse(window.localStorage.getItem('selectedModel') || '{}');
if (selectedModel.model) {
params.model = selectedModel.model;
}
const requestUrl = this.RULE == 'admin' ? adminApi.default.GENERATE_IMAGE_ADMIN : clientApi.default.GENERATE_IMAGE;
const response = await requestUtils.common(requestUrl, params);
// const response = {"code":0,"message":"","success":true,"data":{"id":2177,"message":"任务已提交,正在处理"}}
if (response.code != 0) {
reject(response.msg);
return;
}
const taskResult = {
taskId: response.data.id,
taskQueue: taskQueue
}
resolve(taskResult);
return
//查询任务
return await this.getTaskGinimi(response.data.id, (imgurl) => {
let resultImg = imgurl[0].url
resolve(resultImg);
}, (err) => {
reject(err);
});
let data = response.data;
// let resultImg = this.concatUrl(data?.urls[0]?.url || '');
let resultImg = data?.urls[0]?.url
// 处理响应,提取图片数据
resolve(resultImg);
} catch (error) {
reject(error);
console.log(error, 'errorerrorerrorerrorerrorerror');
} finally {
// 任务完成后从队列中移除
GiminiServer.taskQueue.delete(taskQueue);
}
})
}
//根据任务id查询任务状态
async getTaskGinimi(taskId, taskQueue, successCallback, errorCallback) {
if (!GiminiServer.taskQueue.has(taskQueue)) {//如果队列不存在,创建一个
GiminiServer.taskQueue.set(taskQueue, taskQueue);
}
const tUrl = this.RULE == 'admin' ? adminApi.default.GET_TASK_GINIMI_ADMIN : clientApi.default.GET_TASK_GINIMI;
const requestUrl = {
url: tUrl.url.replace('TASKID', taskId).replace('TASKQUEUE', taskQueue),
method: tUrl.method
}
const response = await requestUtils.common(requestUrl);
if (response.code != 0) {
errorCallback && errorCallback('Failed to generate image');
return Promise.reject('Failed to generate image');
}
const status = response?.data?.status;
switch (status) {
case 1:
let result = response?.data?.result?.urls || [];
successCallback && successCallback(result);
break;
case 2:
errorCallback && errorCallback(response?.data?.response_data?.error?.type || 'Image generation failed');
break;
case 3:
Promise.reject('Image generation failed');
break;
default:
// 等待三秒
if (!GiminiServer.pollingEnabled) {
return Promise.reject('Image generation failed');
}
await new Promise(resolve => setTimeout(resolve, 3000));
this.getTaskGinimi(taskId, taskQueue, successCallback, errorCallback);
break;
}
}
//模型生图功能
handleGenerateImage(referenceImages = [], prompt = '', config) {
return this.generateImageFromMultipleImagesOnline(referenceImages, prompt, config);
}
//文本内容一键优化
async handleOptimizePrompt(prompt = '',responseSchema,locale) {
let parmas = {
"contents": [
{
"role": "user",
"parts": [
{
"text":`prompt:${prompt}, create a rich and detailed character profile.${locale=='zh'?'使用中文回复我':'Reply to me in English'}`
}
]
}
],
"model": "gemini-2.5-flash",
"config": {
"responseMimeType": "application/json",
"responseSchema": responseSchema
},
"stream": false
}
let response = await requestUtils.common(clientApi.default.GENERATE_CONTENT,parmas);
if (response.code != 0) {
return Promise.reject('Failed to generate content');
}
const data = response?.data;
try{
return Promise.resolve(JSON.parse(data.content));
}catch(err){
return Promise.reject('Failed to parse content');
}
}
}

View File

@ -1,21 +1,21 @@
import { request as requestUtils } from '../utils/request.js'
import * as clientApi from '../api/frontend/index.js'
import * as adminApi from '../api/FrontendDesigner'
import * as clientApi from '../api/frontend/index.js'
import * as adminApi from '../api/FrontendDesigner'
import { GoogleGenAI, Type, Modality } from "@google/genai";
const API_KEY = 'AIzaSyBmPgJKMnG7afAXR9JW14I5XSkOd_NwCVM';
const ai = API_KEY ? new GoogleGenAI({ apiKey: API_KEY }) : null;
import { FileServer } from './fileserver';
// 获取环境变量中的
const getPorjectType = () => {
// 浏览器环境
if (typeof window !== 'undefined') {
// Vite 环境变量
return import.meta.env.VITE_PROJECTTYPE;
}
// Node.js 环境
if (typeof process !== 'undefined') {
return process.env.VITE_PROJECTTYPE ;
}
// 浏览器环境
if (typeof window !== 'undefined') {
// Vite 环境变量
return import.meta.env.VITE_PROJECTTYPE;
}
// Node.js 环境
if (typeof process !== 'undefined') {
return process.env.VITE_PROJECTTYPE;
}
};
export class GiminiServer extends FileServer {
RULE = getPorjectType();
@ -23,7 +23,7 @@ export class GiminiServer extends FileServer {
// 任务并发队列
static taskQueue = new Map();
//最高并发限制
static MAX_CONCURRENT_TASKS =99;
static MAX_CONCURRENT_TASKS = 99;
constructor() {
super();
}
@ -37,7 +37,7 @@ export class GiminiServer extends FileServer {
if (typeof url !== 'string') {
return 'image/jpeg'; // 默认为jpeg
}
try {
const extension = url.split('.').pop().toLowerCase().split('?')[0];
const mimeTypes = {
@ -60,27 +60,27 @@ export class GiminiServer extends FileServer {
* @param {*} type 图片类型base64或者url
* @returns GenerativePart对象
*/
dataUrlToGenerativePart =async (dataUrl,type='base64') => {
dataUrlToGenerativePart = async (dataUrl, type = 'base64') => {
if (!dataUrl) return null;
// 确保dataUrl是字符串
if (typeof dataUrl !== 'string') {
throw new Error("dataUrl must be a string");
}
// 处理URL类型
if(type === 'url'){
if (type === 'url') {
return {
img_url: dataUrl,
img_type: await this.getMimeTypeFromUrl(dataUrl),
img_url: dataUrl,
img_type: await this.getMimeTypeFromUrl(dataUrl),
};
}
// 处理base64类型
if(type === 'base64'){
if (type === 'base64') {
dataUrl = await this.fileToBase64(dataUrl);
}
const parts = dataUrl.split(',');
const mimeType = parts[0].match(/:(.*?);/)?.[1];
const base64Data = parts[1];
@ -96,28 +96,28 @@ export class GiminiServer extends FileServer {
};
//本地生图模型
generateImageFromMultipleImages = async (baseImages, prompt, options = {}) => {
return new Promise(async (resolve, reject) => {
const { maxImages = 5 } = options;
// 标准化输入:确保 baseImages 是数组
const images = Array.isArray(baseImages) ? baseImages : [baseImages];
try {
if (images.length > maxImages) {
reject(`参考图片数量不能超过 ${maxImages}`);
}
if (!prompt || !prompt.trim()) {
reject('请提供图片生成提示词');
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image =>{
return await this.dataUrlToGenerativePart(image);
} ));
// 构建请求的 parts 数组
const parts = [
...imageParts,
{ text: prompt }
];
// 执行AI请求
const response = await ai.models.generateContent({
return new Promise(async (resolve, reject) => {
const { maxImages = 5 } = options;
// 标准化输入:确保 baseImages 是数组
const images = Array.isArray(baseImages) ? baseImages : [baseImages];
try {
if (images.length > maxImages) {
reject(`参考图片数量不能超过 ${maxImages}`);
}
if (!prompt || !prompt.trim()) {
reject('请提供图片生成提示词');
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image => {
return await this.dataUrlToGenerativePart(image);
}));
// 构建请求的 parts 数组
const parts = [
...imageParts,
{ text: prompt }
];
// 执行AI请求
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash-image',
contents: {
parts: parts,
@ -126,8 +126,8 @@ export class GiminiServer extends FileServer {
responseModalities: [Modality.IMAGE],
},
});
console.log(response,'图片结果');
let resultImg ;//返回的图片
console.log(response, '图片结果');
let resultImg;//返回的图片
// 处理响应,提取图片数据
for (const part of response.candidates[0].content.parts) {
if (part.inlineData) {
@ -139,142 +139,180 @@ export class GiminiServer extends FileServer {
}
}
} catch (error) {
reject(error);
console.log(error, 'errorerrorerrorerrorerrorerror');
}
} catch (error) {
reject(error);
console.log(error, 'errorerrorerrorerrorerrorerror');
}
})
};
//线上生图片模型
async generateImageFromMultipleImagesOnline(baseImages, prompt,config={}){
if(GiminiServer.taskQueue.size >= GiminiServer.MAX_CONCURRENT_TASKS){
async generateImageFromMultipleImagesOnline(baseImages, prompt, config = {}) {
if (GiminiServer.taskQueue.size >= GiminiServer.MAX_CONCURRENT_TASKS) {
window.setElMessage({
type:'warning',
message:'Concurrent limit reached'
type: 'warning',
message: 'Concurrent limit reached'
})
return Promise.reject('Concurrent limit reached');
}
const taskQueue = new Date().getTime();
GiminiServer.taskQueue.set(taskQueue, taskQueue);
return new Promise(async (resolve, reject) => {
// 标准化输入:确保 baseImages 是数组
baseImages = Array.isArray(baseImages) ? baseImages : [baseImages];
const images = await Promise.all(baseImages.map(async (image) => {
// if(image.indexOf('tcww')!=-1){
// return this.concatUrl('/upload/3e1e9ac2f08b486faca094671d793e25');
// }
return await this.uploadFile(image);
}));
try {
if (images.length > 5) {
reject(`参考图片数量不能超过5张`);
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image =>{
return await this.dataUrlToGenerativePart(image,'url');
} ));
let promptStr = '';
if(config.aspect_ratio&&config.aspect_ratio=="9:16"){
promptStr = prompt.trim();
}else{
promptStr = prompt.trim();
}
const params = {
// "parameters":{
// "sampleCount": 4,
// "aspectRatio": "9:16"
// },
"aspect_ratio": "16:9",
"model": "gemini-2.5-flash-image",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image"/"doubao"/"ali",
"location": "global",
"vertexai": true,
...config,
inputs: [
...imageParts,
{ text: promptStr }
]
}
if(params.model=='doubao'){
params.aspect_ratio = '768x1344';
}
const selectedModel = JSON.parse(window.localStorage.getItem('selectedModel')||'{}');
if(selectedModel.model){
params.model = selectedModel.model;
}
const requestUrl = this.RULE=='admin'?adminApi.default.GENERATE_IMAGE_ADMIN:clientApi.default.GENERATE_IMAGE;
const response = await requestUtils.common(requestUrl, params);
// const response = {"code":0,"message":"","success":true,"data":{"id":2177,"message":"任务已提交,正在处理"}}
if(response.code!=0){
reject(response.msg);
return;
}
const taskResult = {
taskId:response.data.id,
taskQueue:taskQueue
}
resolve(taskResult);
return
//查询任务
return await this.getTaskGinimi(response.data.id,(imgurl)=>{
let resultImg = imgurl[0].url
resolve(resultImg);
},(err)=>{
reject(err);
});
let data = response.data;
// let resultImg = this.concatUrl(data?.urls[0]?.url || '');
let resultImg = data?.urls[0]?.url
GiminiServer.taskQueue.set(taskQueue, taskQueue);
return new Promise(async (resolve, reject) => {
// 标准化输入:确保 baseImages 是数组
baseImages = Array.isArray(baseImages) ? baseImages : [baseImages];
const images = await Promise.all(baseImages.map(async (image) => {
// if(image.indexOf('tcww')!=-1){
// return this.concatUrl('/upload/3e1e9ac2f08b486faca094671d793e25');
// }
// image/png
return await this.uploadFile(image);
}));
try {
if (images.length > 5) {
reject(`参考图片数量不能超过5张`);
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image => {
return await this.dataUrlToGenerativePart(image, 'url');
}));
let promptStr = '';
if (config.aspect_ratio && config.aspect_ratio == "9:16") {
promptStr = prompt.trim();
} else {
promptStr = prompt.trim();
}
const params = {
// "parameters":{
// "sampleCount": 4,
// "aspectRatio": "9:16"
// },
"aspect_ratio": "16:9",
"model": "gemini-2.5-flash-image",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image"/"doubao"/"ali",
"location": "global",
"vertexai": true,
...config,
inputs: [
...imageParts,
{ text: promptStr }
]
}
if (params.model == 'doubao') {
params.aspect_ratio = '768x1344';
}
const selectedModel = JSON.parse(window.localStorage.getItem('selectedModel') || '{}');
if (selectedModel.model) {
params.model = selectedModel.model;
}
const requestUrl = this.RULE == 'admin' ? adminApi.default.GENERATE_IMAGE_ADMIN : clientApi.default.GENERATE_IMAGE;
const response = await requestUtils.common(requestUrl, params);
// const response = {"code":0,"message":"","success":true,"data":{"id":2177,"message":"任务已提交,正在处理"}}
if (response.code != 0) {
reject(response.msg);
return;
}
const taskResult = {
taskId: response.data.id,
taskQueue: taskQueue
}
resolve(taskResult);
return
//查询任务
return await this.getTaskGinimi(response.data.id, (imgurl) => {
let resultImg = imgurl[0].url
resolve(resultImg);
}, (err) => {
reject(err);
});
let data = response.data;
// let resultImg = this.concatUrl(data?.urls[0]?.url || '');
let resultImg = data?.urls[0]?.url
// 处理响应,提取图片数据
resolve(resultImg);
} catch (error) {
reject(error);
console.log(error, 'errorerrorerrorerrorerrorerror');
} finally {
// 任务完成后从队列中移除
GiminiServer.taskQueue.delete(taskQueue);
}
} catch (error) {
reject(error);
console.log(error, 'errorerrorerrorerrorerrorerror');
} finally {
// 任务完成后从队列中移除
GiminiServer.taskQueue.delete(taskQueue);
}
})
}
//根据任务id查询任务状态
async getTaskGinimi(taskId,taskQueue,successCallback,errorCallback){
if(!GiminiServer.taskQueue.has(taskQueue)){//如果队列不存在,创建一个
async getTaskGinimi(taskId, taskQueue, successCallback, errorCallback) {
if (!GiminiServer.taskQueue.has(taskQueue)) {//如果队列不存在,创建一个
GiminiServer.taskQueue.set(taskQueue, taskQueue);
}
const tUrl = this.RULE=='admin'?adminApi.default.GET_TASK_GINIMI_ADMIN:clientApi.default.GET_TASK_GINIMI;
const tUrl = this.RULE == 'admin' ? adminApi.default.GET_TASK_GINIMI_ADMIN : clientApi.default.GET_TASK_GINIMI;
const requestUrl = {
url:tUrl.url.replace('TASKID',taskId).replace('TASKQUEUE',taskQueue),
method:tUrl.method
url: tUrl.url.replace('TASKID', taskId).replace('TASKQUEUE', taskQueue),
method: tUrl.method
}
const response = await requestUtils.common(requestUrl);
if(response.code!=0){
errorCallback&&errorCallback('Failed to generate image');
if (response.code != 0) {
errorCallback && errorCallback('Failed to generate image');
return Promise.reject('Failed to generate image');
}
const status = response?.data?.status;
switch (status) {
case 1:
let result = response?.data?.result?.urls || [];
successCallback&&successCallback(result);
successCallback && successCallback(result);
break;
case 2:
errorCallback&&errorCallback(response?.data?.response_data?.error?.type || 'Image generation failed');
errorCallback && errorCallback(response?.data?.response_data?.error?.type || 'Image generation failed');
break;
case 3:
Promise.reject('Image generation failed');
break;
default:
// 等待三秒
if(!GiminiServer.pollingEnabled){
if (!GiminiServer.pollingEnabled) {
return Promise.reject('Image generation failed');
}
await new Promise(resolve => setTimeout(resolve, 3000));
this.getTaskGinimi(taskId,taskQueue,successCallback,errorCallback);
this.getTaskGinimi(taskId, taskQueue, successCallback, errorCallback);
break;
}
}
//模型生图功能
handleGenerateImage(referenceImages = [], prompt = '',config) {
return this.generateImageFromMultipleImagesOnline(referenceImages, prompt,config);
handleGenerateImage(referenceImages = [], prompt = '', config) {
return this.generateImageFromMultipleImagesOnline(referenceImages, prompt, config);
}
//文本内容一键优化
async handleOptimizePrompt(prompt = '', responseSchema, locale, previewImage) {
let contents = [
{
"role": "user",
"parts": [
{
"text": `prompt:${prompt}, create a rich and detailed character profile.${locale == 'zh' ? '使用中文回复我' : 'Reply to me in English'}`
}
]
}
]
if(previewImage){
const inlineData = await this.dataUrlToGenerativePart(previewImage,'url');
contents[0].parts.push(inlineData);
contents[0].parts[0].text=`Analyze the provided image and use it as inspiration. Then, based on the following keywords, create a rich and detailed character profile. Keywords: "${prompt}"${locale == 'zh' ? '使用中文回复我' : 'Reply to me in English'}`
}
let parmas = {
"contents": contents,
"model": "gemini-2.5-flash",
"config": {
"responseMimeType": "application/json",
"responseSchema": responseSchema
},
"stream": false
}
let response = await requestUtils.common(clientApi.default.GENERATE_CONTENT, parmas);
if (response.code != 0) {
return Promise.reject('Failed to generate content');
}
const data = response?.data;
try {
return Promise.resolve(JSON.parse(data.content));
} catch (err) {
return Promise.reject('Failed to parse content');
}
}
}