最新功能

This commit is contained in:
13121765685 2025-12-18 15:53:43 +08:00
parent d0ad3fcad9
commit 0b6ec14230
72 changed files with 7917 additions and 1472 deletions

View File

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

View File

@ -0,0 +1,64 @@
## 实施方案
### 概述
将当前的绿色圆点在线状态指示器替换为角色标识,为'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

@ -0,0 +1,74 @@
# 实现计划
## 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

@ -0,0 +1,48 @@
# 佣金管理功能实现计划
## 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

@ -0,0 +1,28 @@
## 修复邀请码列表数据获取
### 问题分析
当前邀请码列表使用的是硬编码数据,而不是通过调用`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

@ -0,0 +1,46 @@
# 实现佣金管理功能
## 一、功能需求
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

@ -0,0 +1,112 @@
# 实现侧边栏权限管理功能
## 功能需求
在侧边栏新增权限管理功能,包含以下二级目录:
- 角色管理
- 路由管理(每个路由可配置对应按钮权限)
- 用户列表(展示后台管理系统所有账号)
## 实现步骤
### 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

@ -0,0 +1,35 @@
# 实现积分充值页面
## 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

@ -0,0 +1,121 @@
# 实现积分管理功能
## 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

@ -0,0 +1,34 @@
## 实现邀请码复制带域名文案功能
### 问题分析
当前点击复制邀请码按钮只会复制邀请码本身,而用户需要的是复制包含当前项目域名和邀请码参数的完整文案,以便分享给他人。
### 解决方案
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

@ -0,0 +1,38 @@
## 更新导航栏和添加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,27 +1,38 @@
# 添加规则方格子四个角点
## 实现目标
在画布上添加有规则的点,类似方格子但只展示四个角上的点,支持缩放、拖动交互,并适配亮色和暗色主题。
## 实现步骤
### 1. 修改.scene-container的背景样式
- 将当前的随机装饰点替换为规则的方格子四个角点
- 使用CSS radial-gradient创建点效果
- 通过多个渐变层组合实现四个角的点
- 设置合适的background-size控制方格大小
* 将当前的随机装饰点替换为规则的方格子四个角点
* 使用CSS radial-gradient创建点效果
* 通过多个渐变层组合实现四个角的点
* 设置合适的background-size控制方格大小
### 2. 适配亮色主题
- 为亮色主题设置合适的点颜色和大小
- 确保点与背景对比度适中
* 为亮色主题设置合适的点颜色和大小
* 确保点与背景对比度适中
### 3. 适配暗色主题
- 为暗色主题单独设置点颜色
- 保持与暗色背景的良好对比度
* 为暗色主题单独设置点颜色
* 保持与暗色背景的良好对比度
### 4. 确保交互兼容性
- 点网格应随画布缩放和拖动保持正确位置
- 不影响现有卡片元素的交互
* 点网格应随画布缩放和拖动保持正确位置
* 不影响现有卡片元素的交互
## 技术实现
@ -58,8 +69,14 @@ html.dark .scene-container {
```
## 预期效果
- 画布上出现规则排列的方格子,每个方格的四个角上有一个点
- 方格大小为50px x 50px点大小为2px
- 点的颜色适配当前主题
- 缩放和拖动画布时,点网格保持正确的位置关系
- 不影响现有卡片元素的交互
* 画布上出现规则排列的方格子,每个方格的四个角上有一个点
* 方格大小为50px x 50px点大小为2px
* 点的颜色适配当前主题
* 缩放和拖动画布时,点网格保持正确的位置关系
* 不影响现有卡片元素的交互

View File

@ -0,0 +1,56 @@
# 实现计划
## 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

@ -0,0 +1,79 @@
## 实现计划
### 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

@ -0,0 +1,37 @@
# 用户中心页面重构计划
## 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

@ -0,0 +1,76 @@
# 角色管理和权限管理设计与对接计划(含详情页面)
## 一、角色管理模块设计与对接
### 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

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

View File

@ -115,6 +115,22 @@ onUnmounted(() => {
unwatchLocale()
unwatchTheme()
})
const loading = ref(false);
const qmLoading = ref(false);
const closeMethods = {
close: ()=>{
loading.value = false
qmLoading.value = false;
}
}
window.setElLoading = (qp=false)=>{
if(qp){
qmLoading.value = true
}else{
loading.value = true
}
return closeMethods
}
</script>
<template>
@ -132,21 +148,12 @@ onUnmounted(() => {
<!-- 主要内容区域 -->
<main class="app-main">
<div class="main-container">
<DtLoadingCom v-if="loading" />
<router-view v-slot="{ Component, route }">
<component :is="Component" :key="route.path" />
</router-view>
</div>
</main>
<!-- 全局加载指示器 -->
<div
v-if="appStore.isLoading"
class="loading-overlay"
>
<div class="loading-spinner">
<div class="spinner"></div>
<p class="loading-text">{{ t('common.loading') }}</p>
</div>
</div>
</div>
</template>

View File

@ -100,6 +100,38 @@
<el-icon><UserFilled /></el-icon>
<template #title>{{ t('admin.layout.users') }}</template>
</el-menu-item>
<el-menu-item index="/admin/points-management">
<el-icon><Coin /></el-icon>
<template #title>{{ t('admin.layout.pointsManagement') }}</template>
</el-menu-item>
<el-menu-item index="/admin/commission-management">
<el-icon><Coin /></el-icon>
<template #title>{{ t('admin.layout.commissionManagement') }}</template>
</el-menu-item>
<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><Key /></el-icon>
<template #title>{{ t('admin.layout.roleManagement') }}</template>
</el-menu-item>
<el-menu-item index="/admin/permission-management">
<el-icon><Document /></el-icon>
<template #title>{{ t('admin.layout.permissionManagement') }}</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>
</el-menu>
</el-aside>
@ -139,7 +171,11 @@ import {
Document,
ShoppingCart,
UserFilled,
EditPen
EditPen,
Lock,
Key,
List,
Coin
} from '@element-plus/icons-vue'
import { AdminLogin } from '../../views/AdminLogin/AdminLogin'
const { t, locale } = useI18n();
@ -158,7 +194,8 @@ const activeMenu = computed(() => route.path)
// index -> indexes
const submenuMap = {
'/admin/orders': ['/admin/content-review', '/admin/disassembly-orders', '/admin/orders']
'/admin/orders': ['/admin/content-review', '/admin/disassembly-orders', '/admin/orders'],
'/admin/permission': ['/admin/role-management', '/admin/permission-management', '/admin/user-list']
}
// keys

View File

@ -51,8 +51,15 @@ export default {
preview: 'Preview Model',
delete: 'Delete Model'
},
// User Center
userCenter: {
points: {
pointsList: 'Points List'
}
},
// 应用
// App
app: {
title: 'Vue3 Frontend Designer Tool',
description: 'Modern, responsive frontend design tool'
@ -227,7 +234,10 @@ export default {
error: 'Error',
success: 'Success',
warning: 'Warning',
info: 'Info'
info: 'Info',
yes: 'Yes',
no: 'No',
action: 'Action'
},
layout: {
dashboard: 'Dashboard',
@ -236,10 +246,91 @@ export default {
orders: 'Order Management',
users: 'User Management',
disassemblyOrders: 'Order Processing',
permission: 'Permission Management',
roleManagement: 'Role Management',
routeManagement: 'Route Management',
permissionManagement: 'Permission Management',
userList: 'User List',
pointsManagement: 'Points Management',
commissionManagement: 'Commission Management',
logout: 'Logout',
profile: 'Profile',
settings: 'Settings'
},
roleManagement: {
title: 'Role Management',
roleList: 'Role List',
addRole: 'Add Role',
editRole: 'Edit Role',
roleName: 'Role Name',
roleCode: 'Role Code',
description: 'Description',
createTime: 'Create Time',
updateTime: 'Update Time',
isSystem: 'System Role',
isActive: 'Role Status',
roleNameRequired: 'Please enter role name',
roleCodeRequired: 'Please enter role code',
getListFailed: 'Failed to get role list',
getDetailFailed: 'Failed to get role detail',
deleteConfirm: 'Are you sure to delete role {roleName}?',
basicInfo: 'Basic Info'
},
routeManagement: {
title: 'Route Management',
routeList: 'Route List',
path: 'Path',
name: 'Name',
title: 'Title',
requiresAuth: 'Requires Auth',
buttonPermissions: 'Button Permissions',
configure: 'Configure'
},
permissionManagement: {
title: 'Permission Management',
permissionList: 'Permission List',
addPermission: 'Add Permission',
editPermission: 'Edit Permission',
permissionName: 'Permission Name',
permissionCode: 'Permission Code',
module: 'Module',
action: 'Action',
resource: 'Resource',
description: 'Description',
createTime: 'Create Time',
updateTime: 'Update Time',
isActive: 'Permission Status',
permissionNameRequired: 'Please enter permission name',
permissionCodeRequired: 'Please enter permission code',
moduleRequired: 'Please enter module',
actionRequired: 'Please enter action',
resourceRequired: 'Please enter resource',
getListFailed: 'Failed to get permission list',
getDetailFailed: 'Failed to get permission detail',
deleteConfirm: 'Are you sure to delete permission {permissionName}?',
assignPermission: 'Assign Permission',
permissionAssign: 'Permission Assign',
assignSuccess: 'Permission assigned successfully',
assignFailed: 'Failed to assign permission',
basicInfo: 'Basic Info'
},
userList: {
title: 'User List Management',
userList: 'User List',
addUser: 'Add User',
searchPlaceholder: 'Search username, nickname or email',
username: 'Username',
nickname: 'Nickname',
email: 'Email',
phone: 'Phone',
role: 'Role',
status: 'Status',
createTime: 'Create Time',
admin: 'Admin',
user: 'User',
active: 'Active',
inactive: 'Inactive'
},
dashboard: {
title: 'Dashboard',
subtitle: 'System Overview and Key Metrics',
@ -573,6 +664,50 @@ export default {
completeDisassemblySuccess: 'Disassembly completed successfully',
completeDisassemblyError: 'Failed to complete disassembly, please try again'
}
},
pointsManagement: {
title: 'Points Management',
pointsPackageList: 'Points Package List',
addPointsPackage: 'Add Points Package',
editPointsPackage: 'Edit Points Package',
deletePointsPackage: 'Delete Points Package',
confirmDelete: 'Are you sure you want to delete this points package?',
pointsPackage: 'Points Package',
price: 'Price',
validityPeriod: 'Validity Period',
currency: 'Currency',
actions: 'Actions',
save: 'Save',
cancel: 'Cancel',
add: 'Add',
edit: 'Edit',
delete: 'Delete',
name: 'Package Name',
points: 'Points',
usd: 'USD',
year: 'Year'
},
commissionManagement: {
title: 'Commission Management',
commissionRate: 'Commission Rate',
saveRate: 'Save',
defaultRate: '15%',
rateSaved: 'Commission rate saved successfully',
list: {
title: 'Commission List',
creatorName: 'Creator Name',
userId: 'User ID',
actualPayment: 'Actual Payment',
productAmount: 'Product Amount',
commission: 'Commission',
status: 'Status',
action: 'Action',
approve: 'Approve',
reject: 'Reject',
pending: 'Pending',
approved: 'Approved',
rejected: 'Rejected'
}
}
}
}

View File

@ -245,19 +245,103 @@ orderManagement: {
error: '错误',
success: '成功',
warning: '警告',
info: '信息'
info: '信息',
yes: '是',
no: '否',
action: '操作'
},
layout: {
dashboard: '仪表板',
content: '内容审核',
contentReview: '订单审核',
orders: '订单管理',
users: '用户管理',
disassemblyOrders: '订单处理',
logout: '退出登录',
profile: '个人资料',
settings: '设置',
notifications: '通知'
dashboard: '仪表板',
content: '内容审核',
contentReview: '订单审核',
orders: '订单管理',
users: '用户管理',
disassemblyOrders: '订单处理',
permission: '权限管理',
roleManagement: '角色管理',
routeManagement: '路由管理',
permissionManagement: '权限管理',
userList: '用户列表',
pointsManagement: '充值包管理',
commissionManagement: '佣金管理',
logout: '退出登录',
profile: '个人资料',
settings: '设置',
notifications: '通知'
},
roleManagement: {
title: '角色管理',
roleList: '角色列表',
addRole: '添加角色',
editRole: '编辑角色',
roleName: '角色名称',
roleCode: '角色代码',
description: '描述',
createTime: '创建时间',
updateTime: '更新时间',
isSystem: '系统角色',
isActive: '角色状态',
roleNameRequired: '请输入角色名称',
roleCodeRequired: '请输入角色代码',
getListFailed: '获取角色列表失败',
getDetailFailed: '获取角色详情失败',
deleteConfirm: '确定要删除角色{roleName}吗?',
basicInfo: '基本信息'
},
routeManagement: {
title: '路由管理',
routeList: '路由列表',
path: '路径',
name: '名称',
title: '标题',
requiresAuth: '需要认证',
buttonPermissions: '按钮权限',
configure: '配置'
},
permissionManagement: {
title: '权限管理',
permissionList: '权限列表',
addPermission: '添加权限',
editPermission: '编辑权限',
permissionName: '权限名称',
permissionCode: '权限代码',
module: '所属模块',
action: '操作类型',
resource: '资源名称',
description: '描述',
createTime: '创建时间',
updateTime: '更新时间',
isActive: '权限状态',
permissionNameRequired: '请输入权限名称',
permissionCodeRequired: '请输入权限代码',
moduleRequired: '请输入所属模块',
actionRequired: '请输入操作类型',
resourceRequired: '请输入资源名称',
getListFailed: '获取权限列表失败',
getDetailFailed: '获取权限详情失败',
deleteConfirm: '确定要删除权限{permissionName}吗?',
assignPermission: '分配权限',
permissionAssign: '权限分配',
assignSuccess: '权限分配成功',
assignFailed: '权限分配失败',
basicInfo: '基本信息'
},
userList: {
title: '用户列表管理',
userList: '用户列表',
addUser: '添加用户',
searchPlaceholder: '搜索用户名、昵称或邮箱',
username: '用户名',
nickname: '昵称',
email: '邮箱',
phone: '电话',
role: '角色',
status: '状态',
createTime: '创建时间',
admin: '管理员',
user: '普通用户',
active: '活跃',
inactive: '非活跃'
},
content: {
title: '内容管理',
@ -525,6 +609,7 @@ orderManagement: {
username: '用户名',
email: '邮箱',
phone: '手机号',
userRole: '用户角色',
inviteCode: '邀请码',
invitedBy: '邀请人',
inviteList: '邀请列表',
@ -568,6 +653,50 @@ orderManagement: {
master: '大师级创作者'
},
selectCreatorLevel: '选择创作者等级'
},
pointsManagement: {
title: '充值包管理',
pointsPackageList: '充值包配置列表',
addPointsPackage: '新增充值包',
editPointsPackage: '编辑充值包',
deletePointsPackage: '删除充值包',
confirmDelete: '确定要删除该充值包吗?',
pointsPackage: '充值包',
price: '价格',
validityPeriod: '有效期',
currency: '币种',
actions: '操作',
save: '保存',
cancel: '取消',
add: '新增',
edit: '编辑',
delete: '删除',
name: '充值包名称',
points: '积分数量',
usd: '美金',
year: '年'
},
commissionManagement: {
title: '佣金管理',
commissionRate: '佣金比例',
saveRate: '保存',
defaultRate: '15%',
rateSaved: '佣金比例保存成功',
list: {
title: '佣金列表',
creatorName: '达人名称',
userId: '用户ID',
actualPayment: '实际支付金额',
productAmount: '商品金额',
commission: '佣金',
status: '状态',
action: '操作',
approve: '审核通过',
reject: '拒绝',
pending: '待审核',
approved: '已通过',
rejected: '已拒绝'
}
}
},
@ -637,5 +766,11 @@ orderManagement: {
loadError: '模型加载失败',
preview: '预览',
delete: '删除'
},
// 用户中心
userCenter: {
points: {
pointsList: '积分列表'
}
}
}

View File

@ -9,15 +9,16 @@ import 'element-plus/theme-chalk/dark/css-vars.css'
import './assets/styles/global.css'
import './assets/styles/responsive.css'
import './assets/styles/themes.css'
import dtUI from '@deotaland/ui'
import '@deotaland/ui/style.css'
// 导入Element Plus图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 导入i18n配置
import i18n from './locales/i18n'
// 创建应用实例
const app = createApp(App)
app.use(dtUI)
window.setElMessage = (options={})=>{
ElMessage[options.type || 'info'](options.message || '请求失败')
}

View File

@ -13,6 +13,13 @@ const AdminUserInvites = () => import('@/views/admin/AdminUsers/AdminUserInvites
const AdminContentReview = () => import('@/views/admin/AdminContentReview.vue')
const AdminDisassemblyOrders = () => import('@/views/admin/AdminDisassemblyOrders.vue')
const AdminDisassemblyDetail = () => import('@/views/admin/AdminDisassemblyDetail/AdminDisassemblyDetail.vue')
const AdminRoleManagement = () => import('@/views/admin/AdminRoleManagement/AdminRoleManagement.vue')
const AdminRoleDetail = () => import('@/views/admin/AdminRoleManagement/AdminRoleDetail.vue')
const AdminPermissionManagement = () => import('@/views/admin/AdminPermissionManagement/AdminPermissionManagement.vue')
const AdminPermissionDetail = () => import('@/views/admin/AdminPermissionManagement/AdminPermissionDetail.vue')
const AdminUserList = () => import('@/views/admin/AdminUserList.vue')
const AdminPointsManagement = () => import('@/views/admin/AdminPointsManagement.vue')
const AdminCommissionManagement = () => import('@/views/admin/AdminCommissionManagement.vue')
const routes = [
{
@ -115,6 +122,62 @@ const routes = [
meta: {
title: '拆件详情'
}
},
{
path: 'role-management',
name: 'AdminRoleManagement',
component: AdminRoleManagement,
meta: {
title: '角色管理'
}
},
{
path: 'role-management/:roleId',
name: 'AdminRoleDetail',
component: AdminRoleDetail,
meta: {
title: '角色详情'
}
},
{
path: 'permission-management',
name: 'AdminPermissionManagement',
component: AdminPermissionManagement,
meta: {
title: '权限管理'
}
},
{
path: 'permission-management/:permissionId',
name: 'AdminPermissionDetail',
component: AdminPermissionDetail,
meta: {
title: '权限详情'
}
},
{
path: 'user-list',
name: 'AdminUserList',
component: AdminUserList,
meta: {
title: '用户列表'
}
},
{
path: 'points-management',
name: 'AdminPointsManagement',
component: AdminPointsManagement,
meta: {
title: '充值包管理'
}
},
{
path: 'commission-management',
name: 'AdminCommissionManagement',
component: AdminCommissionManagement,
meta: {
title: '佣金管理'
}
}
]
},
@ -140,7 +203,10 @@ const router = createRouter({
}
}
})
window.Redirectlogin = () => {
localStorage.removeItem('token')
router.push('/login')
}
// 路由守卫 - 认证检查
router.beforeEach((to, from, next) => {
// localStorage.setItem('token','123')

View File

@ -0,0 +1,492 @@
<template>
<div class="admin-commission-management">
<h1 class="page-title">{{ t('admin.commissionManagement.title') }}</h1>
<!-- 佣金比例设置 -->
<el-card class="commission-rate-card">
<template #header>
<div class="card-header">
<h2 class="card-title">{{ t('admin.commissionManagement.commissionRate') }}</h2>
</div>
</template>
<div class="commission-rate-content">
<div class="rate-setting">
<el-form :model="commissionRateForm" label-width="120px" :rules="rules" ref="commissionRateFormRef">
<el-form-item :label="t('admin.commissionManagement.commissionRate')" prop="rate">
<div class="rate-input-wrapper">
<el-input-number
v-model="commissionRateForm.rate"
:min="1"
:max="100"
:step="1"
:precision="0"
style="width: 200px"
/>
<span class="rate-suffix">%</span>
</div>
&nbsp;
&nbsp;
<el-button type="primary" @click="saveCommissionRate">{{ t('admin.commissionManagement.saveRate') }}</el-button>
</el-form-item>
</el-form>
</div>
</div>
</el-card>
<!-- 佣金列表 -->
<el-card class="commission-list-card">
<template #header>
<div class="card-header">
<h2 class="card-title">{{ t('admin.commissionManagement.list.title') }}</h2>
</div>
</template>
<div class="commission-list-content">
<!-- 列表搜索和筛选 -->
<div class="list-header">
<el-input
v-model="searchForm.keyword"
:placeholder="t('admin.common.search')"
prefix-icon="Search"
clearable
class="search-input"
/>
<el-button type="primary" @click="handleSearch">{{ t('admin.common.search') }}</el-button>
<el-button @click="handleReset">{{ t('admin.common.reset') }}</el-button>
</div>
<!-- 列表内容 -->
<el-table
v-loading="loading"
:data="commissionList"
style="width: 100%"
border
stripe
>
<el-table-column
prop="creatorName"
:label="t('admin.commissionManagement.list.creatorName')"
min-width="150"
/>
<el-table-column
prop="userId"
:label="t('admin.commissionManagement.list.userId')"
min-width="120"
/>
<el-table-column
prop="actualPayment"
:label="t('admin.commissionManagement.list.actualPayment')"
min-width="120"
align="right"
:formatter="formatCurrency"
/>
<el-table-column
prop="productAmount"
:label="t('admin.commissionManagement.list.productAmount')"
min-width="120"
align="right"
:formatter="formatCurrency"
/>
<el-table-column
prop="commission"
:label="t('admin.commissionManagement.list.commission')"
min-width="100"
align="right"
:formatter="formatCurrency"
/>
<el-table-column
prop="status"
:label="t('admin.commissionManagement.list.status')"
min-width="100"
>
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.status)"
size="small"
>
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
:label="t('admin.commissionManagement.list.action')"
min-width="150"
fixed="right"
>
<template #default="scope">
<el-button
v-if="scope.row.status === 'pending'"
type="primary"
size="small"
@click="handleApprove(scope.row)"
>
{{ t('admin.commissionManagement.list.approve') }}
</el-button>
<el-button
v-if="scope.row.status === 'pending'"
type="danger"
size="small"
@click="handleReject(scope.row)"
>
{{ t('admin.commissionManagement.list.reject') }}
</el-button>
<el-tag
v-else
type="info"
size="small"
>
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
const { t } = useI18n()
//
const commissionRateFormRef = ref()
const commissionRateForm = reactive({
rate: 15 // 15%
})
//
const rules = {
rate: [
{ required: true, message: '请输入佣金比例', trigger: 'blur' },
{ type: 'number', min: 1, max: 100, message: '佣金比例必须在1-100之间', trigger: 'blur' }
]
}
//
const searchForm = reactive({
keyword: ''
})
//
const pagination = reactive({
currentPage: 1,
pageSize: 20
})
const total = ref(0)
const loading = ref(false)
//
const mockCommissionList = [
{
id: 1,
creatorName: '达人A',
userId: 'user123',
actualPayment: 1000,
productAmount: 1200,
commission: 150,
status: 'pending',
createTime: '2025-01-01 10:00:00'
},
{
id: 2,
creatorName: '达人B',
userId: 'user456',
actualPayment: 2000,
productAmount: 2400,
commission: 300,
status: 'pending',
createTime: '2025-01-02 11:00:00'
},
{
id: 3,
creatorName: '达人C',
userId: 'user789',
actualPayment: 1500,
productAmount: 1800,
commission: 225,
status: 'approved',
createTime: '2025-01-03 12:00:00'
},
{
id: 4,
creatorName: '达人D',
userId: 'user101',
actualPayment: 800,
productAmount: 960,
commission: 120,
status: 'rejected',
createTime: '2025-01-04 13:00:00'
},
{
id: 5,
creatorName: '达人E',
userId: 'user102',
actualPayment: 2500,
productAmount: 3000,
commission: 375,
status: 'pending',
createTime: '2025-01-05 14:00:00'
}
]
//
const commissionList = computed(() => {
let filteredList = [...mockCommissionList]
//
if (searchForm.keyword) {
const keyword = searchForm.keyword.toLowerCase()
filteredList = filteredList.filter(item =>
item.creatorName.toLowerCase().includes(keyword) ||
item.userId.toLowerCase().includes(keyword)
)
}
total.value = filteredList.length
//
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
return filteredList.slice(startIndex, endIndex)
})
//
const saveCommissionRate = async () => {
try {
await commissionRateFormRef.value.validate()
// API
ElMessage.success(t('admin.commissionManagement.rateSaved'))
} catch (error) {
console.log('表单验证失败:', error)
}
}
//
const handleSearch = () => {
pagination.currentPage = 1
}
//
const handleReset = () => {
searchForm.keyword = ''
pagination.currentPage = 1
}
//
const handleSizeChange = (size) => {
pagination.pageSize = size
pagination.currentPage = 1
}
//
const handleCurrentChange = (current) => {
pagination.currentPage = current
}
//
const formatCurrency = (row, column, cellValue) => {
return `¥${cellValue.toFixed(2)}`
}
//
const getStatusTagType = (status) => {
switch (status) {
case 'pending':
return 'warning'
case 'approved':
return 'success'
case 'rejected':
return 'danger'
default:
return 'info'
}
}
//
const getStatusText = (status) => {
switch (status) {
case 'pending':
return t('admin.commissionManagement.list.pending')
case 'approved':
return t('admin.commissionManagement.list.approved')
case 'rejected':
return t('admin.commissionManagement.list.rejected')
default:
return status
}
}
//
const handleApprove = (row) => {
ElMessageBox.confirm(`确定要审核通过${row.creatorName}的佣金吗?`, '审核确认', {
confirmButtonText: t('admin.common.confirm'),
cancelButtonText: t('admin.common.cancel'),
type: 'warning'
}).then(() => {
// API
ElMessage.success('佣金审核通过成功')
//
const index = mockCommissionList.findIndex(item => item.id === row.id)
if (index > -1) {
mockCommissionList[index].status = 'approved'
}
}).catch(() => {
ElMessage.info('已取消审核')
})
}
//
const handleReject = (row) => {
ElMessageBox.confirm(`确定要拒绝${row.creatorName}的佣金吗?`, '审核确认', {
confirmButtonText: t('admin.common.confirm'),
cancelButtonText: t('admin.common.cancel'),
type: 'warning'
}).then(() => {
// API
ElMessage.success('佣金已拒绝')
//
const index = mockCommissionList.findIndex(item => item.id === row.id)
if (index > -1) {
mockCommissionList[index].status = 'rejected'
}
}).catch(() => {
ElMessage.info('已取消审核')
})
}
//
onMounted(() => {
// API
// 使
})
</script>
<style scoped>
.admin-commission-management {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.commission-rate-card,
.commission-list-card {
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.commission-rate-content {
padding: 20px 0;
}
.rate-setting {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
}
.rate-setting .el-form-item {
margin-right: 20px;
margin-bottom: 0;
}
.rate-input-wrapper {
display: flex;
align-items: center;
}
.rate-suffix {
margin-left: 10px;
font-size: 16px;
color: #606266;
}
.commission-list-content {
padding: 20px 0;
}
.list-header {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 10px;
}
.search-input {
width: 300px;
}
.list-content {
margin-top: 20px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 响应式设计 */
@media (max-width: 768px) {
.admin-commission-management {
padding: 10px;
}
.page-title {
font-size: 20px;
}
.list-header {
flex-direction: column;
align-items: stretch;
}
.search-input {
width: 100%;
margin-bottom: 10px;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
</style>

View File

@ -0,0 +1,230 @@
<template>
<div class="admin-permission-detail">
<h2 class="page-title">{{ t('admin.permissionManagement.permissionDetail') }}</h2>
<!-- 权限基本信息 -->
<el-card shadow="hover" class="permission-info-card">
<template #header>
<div class="card-header">
<span>{{ t('admin.permissionManagement.basicInfo') }}</span>
</div>
</template>
<div class="card-body">
<el-descriptions :column="2" border>
<el-descriptions-item label="权限名称">{{ permissionDetail.permName }}</el-descriptions-item>
<el-descriptions-item label="权限代码">{{ permissionDetail.permCode }}</el-descriptions-item>
<el-descriptions-item label="所属模块">{{ permissionDetail.module }}</el-descriptions-item>
<el-descriptions-item label="操作类型">{{ permissionDetail.action }}</el-descriptions-item>
<el-descriptions-item label="资源名称">{{ permissionDetail.resource }}</el-descriptions-item>
<el-descriptions-item label="权限状态">
<el-tag type="success" v-if="permissionDetail.isActive">{{ t('admin.common.active') }}</el-tag>
<el-tag type="danger" v-else>{{ t('admin.common.inactive') }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ permissionDetail.createdAt }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ permissionDetail.updatedAt }}</el-descriptions-item>
<el-descriptions-item label="权限描述" :span="2">{{ permissionDetail.description }}</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button type="primary" @click="editPermission">
{{ t('admin.common.edit') }}
</el-button>
<el-button type="info" @click="goBack">
{{ t('admin.common.back') }}
</el-button>
</div>
<!-- 编辑权限对话框 -->
<el-dialog
v-model="editDialogVisible"
:title="t('admin.permissionManagement.editPermission')"
width="500px"
center
>
<el-form :model="permissionForm" label-width="120px" :rules="permissionRules" ref="permissionFormRef">
<el-form-item label="权限名称" prop="permName">
<el-input v-model="permissionForm.permName" placeholder="请输入权限名称" />
</el-form-item>
<el-form-item label="权限代码" prop="permCode">
<el-input v-model="permissionForm.permCode" placeholder="请输入权限代码" />
</el-form-item>
<el-form-item label="所属模块" prop="module">
<el-input v-model="permissionForm.module" placeholder="请输入所属模块" />
</el-form-item>
<el-form-item label="操作类型" prop="action">
<el-input v-model="permissionForm.action" placeholder="请输入操作类型view, add, edit, delete" />
</el-form-item>
<el-form-item label="资源名称" prop="resource">
<el-input v-model="permissionForm.resource" placeholder="请输入资源名称" />
</el-form-item>
<el-form-item label="权限描述" prop="description">
<el-input type="textarea" v-model="permissionForm.description" placeholder="请输入权限描述" rows="3" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="permissionForm.isActive" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="savePermission">{{ t('admin.common.save') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { AdminRoleManagement } from '../AdminRoleManagement/index'
import { useRoute, useRouter } from 'vue-router'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const roleManagement = new AdminRoleManagement()
//
const permissionDetail = ref({
id: '',
permName: '',
permCode: '',
module: '',
action: '',
resource: '',
description: '',
isActive: true,
createdAt: '',
updatedAt: ''
})
//
const editDialogVisible = ref(false)
const permissionFormRef = ref(null)
//
const permissionForm = reactive({
id: '',
permName: '',
permCode: '',
module: '',
action: '',
resource: '',
description: '',
isActive: true
})
//
const permissionRules = {
permName: [
{ required: true, message: t('admin.permissionManagement.permissionNameRequired'), trigger: 'blur' }
],
permCode: [
{ required: true, message: t('admin.permissionManagement.permissionCodeRequired'), trigger: 'blur' }
],
module: [
{ required: true, message: t('admin.permissionManagement.moduleRequired'), trigger: 'blur' }
],
action: [
{ required: true, message: t('admin.permissionManagement.actionRequired'), trigger: 'blur' }
],
resource: [
{ required: true, message: t('admin.permissionManagement.resourceRequired'), trigger: 'blur' }
]
}
//
const getPermissionDetail = async () => {
const permissionId = route.params.permissionId
try {
const response = await roleManagement.getPermissionDetail({ permissionId })
if (response.success) {
permissionDetail.value = response.data
} else {
ElMessage.error(response.message || t('admin.permissionManagement.getDetailFailed'))
}
} catch (error) {
ElMessage.error(t('admin.permissionManagement.getDetailFailed'))
}
}
//
const editPermission = () => {
Object.assign(permissionForm, permissionDetail.value)
editDialogVisible.value = true
}
//
const savePermission = async () => {
if (!permissionFormRef.value) return
await permissionFormRef.value.validate(async (valid) => {
if (valid) {
try {
const response = await roleManagement.updatePermission(permissionForm)
if (response.success) {
ElMessage.success(response.message || t('admin.common.saveSuccess'))
editDialogVisible.value = false
getPermissionDetail()
} else {
ElMessage.error(response.message || t('admin.common.saveFailed'))
}
} catch (error) {
ElMessage.error(t('admin.common.saveFailed'))
}
}
})
}
//
const goBack = () => {
router.push('/admin/permission-management')
}
onMounted(() => {
getPermissionDetail()
})
</script>
<style scoped>
.admin-permission-detail {
width: 100%;
height: 100%;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.permission-info-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
.action-buttons {
margin-top: 20px;
display: flex;
gap: 10px;
}
/* 深色主题适配 */
[data-theme="dark"] .page-title {
color: #fff;
}
</style>

View File

@ -0,0 +1,284 @@
<template>
<div class="admin-permission-management">
<h2 class="page-title">{{ t('admin.permissionManagement.title') }}</h2>
<div class="permission-management-content">
<el-card shadow="hover" class="permission-card">
<template #header>
<div class="card-header">
<span>{{ t('admin.permissionManagement.permissionList') }}</span>
<el-button type="primary" size="small" @click="showAddPermissionDialog">
{{ t('admin.permissionManagement.addPermission') }}
</el-button>
</div>
</template>
<div class="card-body">
<el-table :data="permissionList" stripe style="width: 100%">
<el-table-column prop="permName" :label="t('admin.permissionManagement.permissionName')" width="180" />
<el-table-column prop="permCode" :label="t('admin.permissionManagement.permissionCode')" width="180" />
<el-table-column prop="module" :label="t('admin.permissionManagement.module')" width="120" />
<el-table-column prop="action" :label="t('admin.permissionManagement.action')" width="120" />
<el-table-column prop="resource" :label="t('admin.permissionManagement.resource')" width="120" />
<el-table-column prop="isActive" :label="t('admin.permissionManagement.isActive')" width="120">
<template #default="scope">
<el-tag type="success" v-if="scope.row.isActive">{{ t('admin.common.active') }}</el-tag>
<el-tag type="danger" v-else>{{ t('admin.common.inactive') }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" :label="t('admin.permissionManagement.createTime')" width="180" />
<el-table-column prop="updatedAt" :label="t('admin.permissionManagement.updateTime')" width="180" />
<el-table-column :label="t('admin.common.action')" width="250" fixed="right">
<template #default="scope">
<el-button size="small" type="primary" @click="showPermissionDetail(scope.row)">
{{ t('admin.common.detail') }}
</el-button>
<el-button size="small" type="primary" @click="editPermission(scope.row)">
{{ t('admin.common.edit') }}
</el-button>
<el-button size="small" type="danger" @click="deletePermission(scope.row)">
{{ t('admin.common.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
<!-- 添加/编辑权限对话框 -->
<el-dialog
v-model="permissionDialogVisible"
:title="permissionDialogTitle"
width="500px"
center
>
<el-form :model="permissionForm" label-width="120px" :rules="permissionRules" ref="permissionFormRef">
<el-form-item label="权限名称" prop="permName">
<el-input v-model="permissionForm.permName" placeholder="请输入权限名称" />
</el-form-item>
<el-form-item label="权限代码" prop="permCode">
<el-input v-model="permissionForm.permCode" placeholder="请输入权限代码" />
</el-form-item>
<el-form-item label="所属模块" prop="module">
<el-input v-model="permissionForm.module" placeholder="请输入所属模块" />
</el-form-item>
<el-form-item label="操作类型" prop="action">
<el-input v-model="permissionForm.action" placeholder="请输入操作类型view, add, edit, delete" />
</el-form-item>
<el-form-item label="资源名称" prop="resource">
<el-input v-model="permissionForm.resource" placeholder="请输入资源名称" />
</el-form-item>
<el-form-item label="权限描述" prop="description">
<el-input type="textarea" v-model="permissionForm.description" placeholder="请输入权限描述" rows="3" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="permissionForm.isActive" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="permissionDialogVisible = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="savePermission">{{ t('admin.common.save') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AdminRoleManagement } from '../AdminRoleManagement/index'
import { useRouter } from 'vue-router'
const { t } = useI18n()
const router = useRouter()
const roleManagement = new AdminRoleManagement()
//
const permissionList = ref([])
//
const permissionDialogVisible = ref(false)
const permissionDialogTitle = ref('')
const permissionFormRef = ref(null)
//
const permissionForm = reactive({
id: '',
permName: '',
permCode: '',
module: '',
action: '',
resource: '',
description: '',
isActive: true
})
//
const permissionRules = {
permName: [
{ required: true, message: t('admin.permissionManagement.permissionNameRequired'), trigger: 'blur' }
],
permCode: [
{ required: true, message: t('admin.permissionManagement.permissionCodeRequired'), trigger: 'blur' }
],
module: [
{ required: true, message: t('admin.permissionManagement.moduleRequired'), trigger: 'blur' }
],
action: [
{ required: true, message: t('admin.permissionManagement.actionRequired'), trigger: 'blur' }
],
resource: [
{ required: true, message: t('admin.permissionManagement.resourceRequired'), trigger: 'blur' }
]
}
//
const getPermissionList = async () => {
try {
const response = await roleManagement.getPermissionList()
if (response.success) {
permissionList.value = response.data
} else {
ElMessage.error(response.message || t('admin.permissionManagement.getListFailed'))
}
} catch (error) {
ElMessage.error(t('admin.permissionManagement.getListFailed'))
}
}
//
const showAddPermissionDialog = () => {
permissionDialogTitle.value = t('admin.permissionManagement.addPermission')
resetPermissionForm()
permissionDialogVisible.value = true
}
//
const editPermission = (row) => {
permissionDialogTitle.value = t('admin.permissionManagement.editPermission')
Object.assign(permissionForm, row)
permissionDialogVisible.value = true
}
//
const resetPermissionForm = () => {
Object.assign(permissionForm, {
id: '',
permName: '',
permCode: '',
module: '',
action: '',
resource: '',
description: '',
isActive: true
})
if (permissionFormRef.value) {
permissionFormRef.value.resetFields()
}
}
//
const savePermission = async () => {
if (!permissionFormRef.value) return
await permissionFormRef.value.validate(async (valid) => {
if (valid) {
try {
let response
if (permissionForm.id) {
//
response = await roleManagement.updatePermission(permissionForm)
} else {
//
response = await roleManagement.addPermission(permissionForm)
}
if (response.success) {
ElMessage.success(response.message || t('admin.common.saveSuccess'))
permissionDialogVisible.value = false
getPermissionList()
} else {
ElMessage.error(response.message || t('admin.common.saveFailed'))
}
} catch (error) {
ElMessage.error(t('admin.common.saveFailed'))
}
}
})
}
//
const deletePermission = (row) => {
ElMessageBox.confirm(
t('admin.permissionManagement.deleteConfirm', { permissionName: row.permName }),
t('admin.common.confirm'),
{
confirmButtonText: t('admin.common.confirm'),
cancelButtonText: t('admin.common.cancel'),
type: 'warning'
}
).then(async () => {
try {
const response = await roleManagement.deletePermission({ permissionId: row.id })
if (response.success) {
ElMessage.success(response.message || t('admin.common.deleteSuccess'))
getPermissionList()
} else {
ElMessage.error(response.message || t('admin.common.deleteFailed'))
}
} catch (error) {
ElMessage.error(t('admin.common.deleteFailed'))
}
}).catch(() => {
//
})
}
//
const showPermissionDetail = (row) => {
router.push(`/admin/permission-management/${row.id}`)
}
onMounted(() => {
getPermissionList()
})
</script>
<style scoped>
.admin-permission-management {
width: 100%;
height: 100%;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.permission-management-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.permission-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
/* 深色主题适配 */
[data-theme="dark"] .page-title {
color: #fff;
}
</style>

View File

@ -0,0 +1,288 @@
<template>
<div class="admin-points-management">
<h2 class="page-title">{{ t('admin.pointsManagement.title') }}</h2>
<div class="points-management-content">
<el-card shadow="hover" class="points-card">
<template #header>
<div class="card-header">
<span>{{ t('admin.pointsManagement.pointsPackageList') }}</span>
<el-button type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
{{ t('admin.pointsManagement.add') }}
</el-button>
</div>
</template>
<div class="card-body">
<el-table :data="pointsPackages" stripe style="width: 100%">
<el-table-column prop="name" :label="t('admin.pointsManagement.pointsPackage')" width="200" />
<el-table-column :label="t('admin.pointsManagement.price')" width="180">
<template #default="scope">
{{ scope.row.price }} {{ scope.row.currency }}
</template>
</el-table-column>
<el-table-column prop="validityPeriod" :label="t('admin.pointsManagement.validityPeriod')" />
<el-table-column :label="t('admin.pointsManagement.actions')" width="200">
<template #default="scope">
<el-button size="small" type="primary" @click="showEditDialog(scope.row)">
<el-icon><EditPen /></el-icon>
{{ t('admin.pointsManagement.edit') }}
</el-button>
<el-button size="small" type="danger" @click="confirmDelete(scope.row)">
<el-icon><Delete /></el-icon>
{{ t('admin.pointsManagement.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
<!-- 积分包弹窗新增/编辑共用 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="500px"
>
<el-form :model="formData" label-width="100px" :rules="rules" ref="formRef">
<el-form-item :label="t('admin.pointsManagement.name')" prop="name">
<el-input v-model="formData.name" :placeholder="t('admin.pointsManagement.name')" />
</el-form-item>
<el-form-item :label="t('admin.pointsManagement.points')" prop="points">
<el-input-number v-model="formData.points" :min="1" :precision="0" style="width: 100%" />
</el-form-item>
<el-form-item :label="t('admin.pointsManagement.price')" prop="price">
<el-input-number v-model="formData.price" :min="0.01" :precision="2" style="width: 100%" />
</el-form-item>
<el-form-item :label="t('admin.pointsManagement.currency')" prop="currency">
<el-select v-model="formData.currency" style="width: 100%">
<el-option :label="t('admin.pointsManagement.usd')" :value="t('admin.pointsManagement.usd')" />
</el-select>
</el-form-item>
<el-form-item :label="t('admin.pointsManagement.validityPeriod')" prop="validityPeriod">
<el-input v-model="formData.validityPeriod" :placeholder="t('admin.pointsManagement.validityPeriod')" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ t('admin.pointsManagement.cancel') }}</el-button>
<el-button type="primary" @click="handleSave">{{ t('admin.pointsManagement.save') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, EditPen, Delete } from '@element-plus/icons-vue'
const { t } = useI18n()
//
const pointsPackages = ref([
{
id: 1,
name: '300积分',
points: 300,
price: 30,
currency: t('admin.pointsManagement.usd'),
validityPeriod: '1年'
},
{
id: 2,
name: '1000积分',
points: 1000,
price: 80,
currency: t('admin.pointsManagement.usd'),
validityPeriod: '1年'
}
])
//
const dialogVisible = ref(false)
// 'add' 'edit'
const dialogMode = ref('add')
// ID
const currentEditId = ref(null)
//
const formRef = ref(null)
//
const dialogTitle = computed(() => {
return dialogMode.value === 'add'
? t('admin.pointsManagement.addPointsPackage')
: t('admin.pointsManagement.editPointsPackage')
})
//
const formData = reactive({
name: '',
points: 0,
price: 0,
currency: t('admin.pointsManagement.usd'),
validityPeriod: ''
})
//
const rules = {
name: [
{ required: true, message: t('admin.pointsManagement.name') + ' ' + t('admin.common.required'), trigger: 'blur' }
],
points: [
{ required: true, message: t('admin.pointsManagement.points') + ' ' + t('admin.common.required'), trigger: 'blur' },
{ type: 'number', min: 1, message: t('admin.pointsManagement.points') + ' ' + t('admin.common.min') + ' 1', trigger: 'blur' }
],
price: [
{ required: true, message: t('admin.pointsManagement.price') + ' ' + t('admin.common.required'), trigger: 'blur' },
{ type: 'number', min: 0.01, message: t('admin.pointsManagement.price') + ' ' + t('admin.common.min') + ' 0.01', trigger: 'blur' }
],
currency: [
{ required: true, message: t('admin.pointsManagement.currency') + ' ' + t('admin.common.required'), trigger: 'change' }
],
validityPeriod: [
{ required: true, message: t('admin.pointsManagement.validityPeriod') + ' ' + t('admin.common.required'), trigger: 'blur' }
]
}
//
const showAddDialog = () => {
//
resetForm()
//
dialogMode.value = 'add'
//
dialogVisible.value = true
}
//
const showEditDialog = (row) => {
//
resetForm()
//
Object.assign(formData, row)
currentEditId.value = row.id
//
dialogMode.value = 'edit'
//
dialogVisible.value = true
}
//
const resetForm = () => {
formData.name = ''
formData.points = 0
formData.price = 0
formData.currency = t('admin.pointsManagement.usd')
formData.validityPeriod = ''
currentEditId.value = null
if (formRef.value) {
formRef.value.resetFields()
}
}
//
const handleSave = () => {
if (formRef.value) {
formRef.value.validate((valid) => {
if (valid) {
if (dialogMode.value === 'add') {
//
// ID
const newId = Math.max(...pointsPackages.value.map(item => item.id)) + 1
//
const newPackage = {
id: newId,
...formData
}
//
pointsPackages.value.push(newPackage)
} else {
//
//
const index = pointsPackages.value.findIndex(item => item.id === currentEditId.value)
if (index !== -1) {
//
pointsPackages.value[index] = {
...formData,
id: currentEditId.value
}
}
}
//
dialogVisible.value = false
//
ElMessage.success(t('admin.common.success'))
//
resetForm()
}
})
}
}
//
const confirmDelete = (row) => {
ElMessageBox.confirm(
t('admin.pointsManagement.confirmDelete'),
t('admin.common.warning'),
{
confirmButtonText: t('admin.common.confirm'),
cancelButtonText: t('admin.common.cancel'),
type: 'warning'
}
)
.then(() => {
//
const index = pointsPackages.value.findIndex(item => item.id === row.id)
if (index !== -1) {
pointsPackages.value.splice(index, 1)
ElMessage.success(t('admin.common.success'))
}
})
.catch(() => {
//
ElMessage.info(t('admin.common.cancel'))
})
}
</script>
<style scoped>
.admin-points-management {
width: 100%;
height: 100%;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.points-management-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.points-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
/* 深色主题适配 */
[data-theme="dark"] .page-title {
color: #fff;
}
</style>

View File

@ -0,0 +1,247 @@
<template>
<div class="admin-role-detail">
<h2 class="page-title">{{ t('admin.roleManagement.roleDetail') }}</h2>
<!-- 角色基本信息 -->
<el-card shadow="hover" class="role-info-card">
<template #header>
<div class="card-header">
<span>{{ t('admin.roleManagement.basicInfo') }}</span>
</div>
</template>
<div class="card-body">
<el-descriptions :column="3" border>
<el-descriptions-item label="角色名称">{{ roleDetail.roleName }}</el-descriptions-item>
<el-descriptions-item label="角色代码">{{ roleDetail.roleCode }}</el-descriptions-item>
<el-descriptions-item label="系统角色">
<el-tag type="success" v-if="roleDetail.isSystem">{{ t('admin.common.yes') }}</el-tag>
<el-tag type="info" v-else>{{ t('admin.common.no') }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="角色状态">
<el-tag type="success" v-if="roleDetail.isActive">{{ t('admin.common.active') }}</el-tag>
<el-tag type="danger" v-else>{{ t('admin.common.inactive') }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ roleDetail.createdAt }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ roleDetail.updatedAt }}</el-descriptions-item>
<el-descriptions-item label="角色描述" :span="3">{{ roleDetail.description }}</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<!-- 权限分配 -->
<el-card shadow="hover" class="permission-assign-card">
<template #header>
<div class="card-header">
<span>{{ t('admin.permissionManagement.permissionAssign') }}</span>
<el-button type="primary" size="small" @click="showAssignPermissionDialog">
{{ t('admin.permissionManagement.assignPermission') }}
</el-button>
</div>
</template>
<div class="card-body">
<el-table :data="rolePermissions" stripe style="width: 100%">
<el-table-column prop="permName" :label="t('admin.permissionManagement.permissionName')" width="180" />
<el-table-column prop="permCode" :label="t('admin.permissionManagement.permissionCode')" width="180" />
<el-table-column prop="module" :label="t('admin.permissionManagement.module')" width="150" />
<el-table-column prop="action" :label="t('admin.permissionManagement.action')" width="150" />
<el-table-column prop="resource" :label="t('admin.permissionManagement.resource')" width="150" />
<el-table-column prop="description" :label="t('admin.permissionManagement.description')" />
</el-table>
</div>
</el-card>
<!-- 权限分配对话框 -->
<el-dialog
v-model="permissionDialogVisible"
:title="t('admin.permissionManagement.assignPermission')"
width="800px"
center
>
<el-form :model="permissionForm" label-width="120px">
<el-form-item label="选择权限">
<el-tree
v-model="permissionForm.permissionIds"
:data="permissionTree"
:props="permissionTreeProps"
show-checkbox
node-key="id"
:check-strictly="false"
:default-checked-keys="defaultCheckedPermissionIds"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="permissionDialogVisible = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="assignPermission">
{{ t('admin.common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AdminRoleManagement } from './index'
import { useRoute, useRouter } from 'vue-router'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const roleManagement = new AdminRoleManagement()
//
const roleDetail = ref({
id: '',
roleName: '',
roleCode: '',
description: '',
isSystem: false,
isActive: true,
createdAt: '',
updatedAt: ''
})
//
const rolePermissions = ref([])
//
const permissionTree = ref([])
const permissionForm = reactive({
permissionIds: []
})
const permissionTreeProps = {
label: 'permName',
children: 'children'
}
//
const permissionDialogVisible = ref(false)
const defaultCheckedPermissionIds = ref([])
//
const getRoleDetail = async () => {
const roleId = route.params.roleId
try {
const response = await roleManagement.getRoleDetail({ roleId })
if (response.success) {
roleDetail.value = response.data
} else {
ElMessage.error(response.message || t('admin.roleManagement.getDetailFailed'))
}
} catch (error) {
ElMessage.error(t('admin.roleManagement.getDetailFailed'))
}
}
//
const getRolePermissions = async () => {
// ID
// API使
rolePermissions.value = []
}
//
const getAllPermissions = async () => {
try {
const response = await roleManagement.getPermissionList()
if (response.success) {
//
permissionTree.value = buildPermissionTree(response.data)
} else {
ElMessage.error(response.message || t('admin.permissionManagement.getListFailed'))
}
} catch (error) {
ElMessage.error(t('admin.permissionManagement.getListFailed'))
}
}
//
const buildPermissionTree = (permissions) => {
//
const moduleMap = new Map()
permissions.forEach(permission => {
if (!moduleMap.has(permission.module)) {
moduleMap.set(permission.module, {
id: permission.module,
permName: permission.module,
permCode: permission.module,
module: permission.module,
children: []
})
}
moduleMap.get(permission.module).children.push(permission)
})
return Array.from(moduleMap.values())
}
//
const showAssignPermissionDialog = async () => {
await getAllPermissions()
// ID
defaultCheckedPermissionIds.value = []
permissionDialogVisible.value = true
}
//
const assignPermission = async () => {
try {
const response = await roleManagement.assignPermissionToRole({
roleId: roleDetail.value.id,
permissionIds: permissionForm.permissionIds
})
if (response.success) {
ElMessage.success(response.message || t('admin.permissionManagement.assignSuccess'))
permissionDialogVisible.value = false
getRolePermissions()
} else {
ElMessage.error(response.message || t('admin.permissionManagement.assignFailed'))
}
} catch (error) {
ElMessage.error(t('admin.permissionManagement.assignFailed'))
}
}
onMounted(() => {
getRoleDetail()
getRolePermissions()
})
</script>
<style scoped>
.admin-role-detail {
width: 100%;
height: 100%;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.role-info-card,
.permission-assign-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
/* 深色主题适配 */
[data-theme="dark"] .page-title {
color: #fff;
}
</style>

View File

@ -0,0 +1,271 @@
<template>
<div class="admin-role-management">
<h2 class="page-title">{{ t('admin.roleManagement.title') }}</h2>
<div class="role-management-content">
<el-card shadow="hover" class="role-card">
<template #header>
<div class="card-header">
<span>{{ t('admin.roleManagement.roleList') }}</span>
<el-button type="primary" size="small" @click="showAddRoleDialog">
{{ t('admin.roleManagement.addRole') }}
</el-button>
</div>
</template>
<div class="card-body">
<el-table :data="roleList" stripe style="width: 100%">
<el-table-column prop="roleName" :label="t('admin.roleManagement.roleName')" width="180" />
<el-table-column prop="roleCode" :label="t('admin.roleManagement.roleCode')" width="180" />
<el-table-column prop="description" :label="t('admin.roleManagement.description')" />
<el-table-column prop="isSystem" :label="t('admin.roleManagement.isSystem')" width="120">
<template #default="scope">
<el-tag type="success" v-if="scope.row.isSystem">{{ t('admin.common.yes') }}</el-tag>
<el-tag type="info" v-else>{{ t('admin.common.no') }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isActive" :label="t('admin.roleManagement.isActive')" width="120">
<template #default="scope">
<el-tag type="success" v-if="scope.row.isActive">{{ t('admin.common.active') }}</el-tag>
<el-tag type="danger" v-else>{{ t('admin.common.inactive') }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" :label="t('admin.roleManagement.createTime')" width="180" />
<el-table-column prop="updatedAt" :label="t('admin.roleManagement.updateTime')" width="180" />
<el-table-column :label="t('admin.common.action')" width="250" fixed="right">
<template #default="scope">
<el-button size="small" type="primary" @click="showRoleDetail(scope.row)">
{{ t('admin.common.detail') }}
</el-button>
<el-button size="small" type="primary" @click="editRole(scope.row)"
:disabled="scope.row.isSystem">
{{ t('admin.common.edit') }}
</el-button>
<el-button size="small" type="danger" @click="deleteRole(scope.row)"
:disabled="scope.row.isSystem">
{{ t('admin.common.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
<!-- 添加/编辑角色对话框 -->
<el-dialog
v-model="roleDialogVisible"
:title="roleDialogTitle"
width="500px"
center
>
<el-form :model="roleForm" label-width="120px" :rules="roleRules" ref="roleFormRef">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="roleForm.roleName" placeholder="请输入角色名称" />
</el-form-item>
<el-form-item label="角色代码" prop="roleCode">
<el-input v-model="roleForm.roleCode" placeholder="请输入角色代码" />
</el-form-item>
<el-form-item label="角色描述" prop="description">
<el-input type="textarea" v-model="roleForm.description" placeholder="请输入角色描述" rows="3" />
</el-form-item>
<el-form-item label="是否系统角色">
<el-switch v-model="roleForm.isSystem" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="roleForm.isActive" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="roleDialogVisible = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="saveRole">{{ t('admin.common.save') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AdminRoleManagement } from './index'
import { useRouter } from 'vue-router'
const { t } = useI18n()
const router = useRouter()
const roleManagement = new AdminRoleManagement()
//
const roleList = ref([])
//
const roleDialogVisible = ref(false)
const roleDialogTitle = ref('')
const roleFormRef = ref(null)
//
const roleForm = reactive({
id: '',
roleName: '',
roleCode: '',
description: '',
isSystem: false,
isActive: true
})
//
const roleRules = {
roleName: [
{ required: true, message: t('admin.roleManagement.roleNameRequired'), trigger: 'blur' }
],
roleCode: [
{ required: true, message: t('admin.roleManagement.roleCodeRequired'), trigger: 'blur' }
]
}
//
const getRoleList = async () => {
try {
const response = await roleManagement.getRoleList()
if (response.success) {
roleList.value = response.data
} else {
ElMessage.error(response.message || t('admin.roleManagement.getListFailed'))
}
} catch (error) {
ElMessage.error(t('admin.roleManagement.getListFailed'))
}
}
//
const showAddRoleDialog = () => {
roleDialogTitle.value = t('admin.roleManagement.addRole')
resetRoleForm()
roleDialogVisible.value = true
}
//
const editRole = (row) => {
roleDialogTitle.value = t('admin.roleManagement.editRole')
Object.assign(roleForm, row)
roleDialogVisible.value = true
}
//
const resetRoleForm = () => {
Object.assign(roleForm, {
id: '',
roleName: '',
roleCode: '',
description: '',
isSystem: false,
isActive: true
})
if (roleFormRef.value) {
roleFormRef.value.resetFields()
}
}
//
const saveRole = async () => {
if (!roleFormRef.value) return
await roleFormRef.value.validate(async (valid) => {
if (valid) {
try {
let response
if (roleForm.id) {
//
response = await roleManagement.updateRole(roleForm)
} else {
//
response = await roleManagement.createRole(roleForm)
}
if (response.success) {
ElMessage.success(response.message || t('admin.common.saveSuccess'))
roleDialogVisible.value = false
getRoleList()
} else {
ElMessage.error(response.message || t('admin.common.saveFailed'))
}
} catch (error) {
ElMessage.error(t('admin.common.saveFailed'))
}
}
})
}
//
const deleteRole = (row) => {
ElMessageBox.confirm(
t('admin.roleManagement.deleteConfirm', { roleName: row.roleName }),
t('admin.common.confirm'),
{
confirmButtonText: t('admin.common.confirm'),
cancelButtonText: t('admin.common.cancel'),
type: 'warning'
}
).then(async () => {
try {
const response = await roleManagement.deleteRole({ roleIds: [row.id] })
if (response.success) {
ElMessage.success(response.message || t('admin.common.deleteSuccess'))
getRoleList()
} else {
ElMessage.error(response.message || t('admin.common.deleteFailed'))
}
} catch (error) {
ElMessage.error(t('admin.common.deleteFailed'))
}
}).catch(() => {
//
})
}
//
const showRoleDetail = (row) => {
router.push(`/admin/role-management/${row.id}`)
}
onMounted(() => {
getRoleList()
})
</script>
<style scoped>
.admin-role-management {
width: 100%;
height: 100%;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.role-management-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.role-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
/* 深色主题适配 */
[data-theme="dark"] .page-title {
color: #fff;
}
</style>

View File

@ -0,0 +1,231 @@
import { adminApi,requestUtils} from '@deotaland/utils';
export class AdminRoleManagement {
constructor() {
}
// 创建角色
async createRole(data) {
let parmas = {
"roleCode": data.roleCode,
"roleName": data.roleName,
"description": data.description,
"isSystem": data.isSystem,
"isActive": data.isActive,
}
return await requestUtils.common(adminApi.default.addRole, parmas);
}
// 更新角色
async updateRole(data) {
let parmas = {
"id": data.id,
"roleCode": data.roleCode,
"roleName": data.roleName,
"description": data.description,
"isSystem": data.isSystem,
"isActive": data.isActive,
}
return await requestUtils.common(adminApi.default.updateRole, parmas);
}
// 删除角色
async deleteRole(data) {
// 角色ID数组拼接成逗号分隔字符串作为路径参数
let roleIds = data.roleIds.join(',');
let requestUrl = {
method: adminApi.default.deleteRole.method,
url: adminApi.default.deleteRole.url + '/' + roleIds,
}
return await requestUtils.common(requestUrl);
}
// 获取角色列表
async getRoleList() {
return await requestUtils.common(adminApi.default.getRoleList);
/**
返回示例
{
"code": 0,
"success": true,
"data": [
{
"id": 9007199254740991,
"roleCode": "string",
"roleName": "string",
"description": "string",
"isSystem": true,
"isActive": true,
"createdAt": "2025-12-18T05:10:16.550Z",
"updatedAt": "2025-12-18T05:10:16.550Z"
}
],
"message": "操作成功"
}
*/
}
//为用户分配角色
async assignRoleToUser(data) {
let requestUrl = {
method: adminApi.default.assignRoleToUser.method,
url: adminApi.default.assignRoleToUser.url.replace('{userId}', data.userId),
}
return await requestUtils.common(requestUrl, data.roleIds);
}
//查询角色详情
async getRoleDetail(data) {
let requestUrl = {
method: adminApi.default.getRoleDetail.method,
url: adminApi.default.getRoleDetail.url.replace('{roleId}', data.roleId),
}
return await requestUtils.common(requestUrl);
/**
返回示例
{
"code": 0,
"success": true,
"data": {
"id": 9007199254740991,
"roleCode": "string",
"roleName": "string",
"description": "string",
"isSystem": true,
"isActive": true,
"createdAt": "2025-12-18T05:14:59.476Z",
"updatedAt": "2025-12-18T05:14:59.476Z"
},
"message": "操作成功"
}
*/
}
//根据用户ID查询角色列表
async getRolesByUserId(data) {
let requestUrl = {
method: adminApi.default.getRolesByUserId.method,
url: adminApi.default.getRolesByUserId.url.replace('{userId}', data.userId),
}
return await requestUtils.common(requestUrl);
/**
返回示例
{
"code": 0,
"success": true,
"data": [
{
"id": 9007199254740991,
"roleCode": "string",
"roleName": "string",
"description": "string",
"isSystem": true,
"isActive": true,
"createdAt": "2025-12-18T05:16:25.955Z",
"updatedAt": "2025-12-18T05:16:25.955Z"
}
],
"message": "操作成功"
}
*/
}
//修改权限
async updatePermission(data) {
return await requestUtils.common(adminApi.default.updatePermission, data);
}
//新增权限
async addPermission(data) {
let parmas = {
permCode: data.permCode,
permName: data.permName,
description: data.description,
module: data.module,
action: data.action,
resource: data.resource,
isActive: data.isActive,
}
return await requestUtils.common(adminApi.default.addPermission, parmas);
}
//为角色分配权限
async assignPermissionToRole(data) {
let requestUrl = {
method: adminApi.default.assignPermissionToRole.method,
url: adminApi.default.assignPermissionToRole.url.replace('{roleId}', data.roleId),
}
return await requestUtils.common(requestUrl, data.permissionIds);
}
//查询权限详情
async getPermissionDetail(data) {
let requestUrl = {
method: adminApi.default.getPermissionDetail.method,
url: adminApi.default.getPermissionDetail.url.replace('{permissionId}', data.permissionId),
}
return await requestUtils.common(requestUrl);
/**
返回示例
{
"code": 0,
"success": true,
"data": {
"id": 9007199254740991,
"permCode": "string",
"permName": "string",
"description": "string",
"module": "string",
"action": "string",
"resource": "string",
"isActive": true,
"createdAt": "2025-12-18T05:19:03.577Z",
"updatedAt": "2025-12-18T05:19:03.577Z"
},
"message": "操作成功"
}
*/
}
//删除权限
async deletePermission(data) {
let requestUrl = {
method: adminApi.default.deletePermission.method,
url: adminApi.default.deletePermission.url.replace('{permissionId}', data.permissionId),
}
return await requestUtils.common(requestUrl);
}
//查询权限列表
async getPermissionList() {
return await requestUtils.common(adminApi.default.getPermissionList);
/**
返回示例
{
"code": 0,
"success": true,
"data": [
{
"id": 9007199254740991,
"permCode": "string",
"permName": "string",
"description": "string",
"module": "string",
"action": "string",
"resource": "string",
"isActive": true,
"createdAt": "2025-12-18T05:20:03.035Z",
"updatedAt": "2025-12-18T05:20:03.035Z"
}
],
"message": "操作成功"
}
*/
}
//根据用户ID查询权限代码集合
async getPermissionCodesByUserId(data) {
let requestUrl = {
method: adminApi.default.getPermissionCodesByUserId.method,
url: adminApi.default.getPermissionCodesByUserId.url.replace('{userId}', data.userId),
}
return await requestUtils.common(requestUrl);
/**
返回示例
{
"code": 0,
"success": true,
"data": [
"string"
],
"message": "操作成功"
}
*/
}
}

View File

@ -0,0 +1,116 @@
<template>
<div class="admin-route-management">
<h2 class="page-title">{{ t('admin.routeManagement.title') }}</h2>
<div class="route-management-content">
<el-card shadow="hover" class="route-card">
<template #header>
<div class="card-header">
<span>{{ t('admin.routeManagement.routeList') }}</span>
</div>
</template>
<div class="card-body">
<el-table :data="routeList" stripe style="width: 100%">
<el-table-column prop="path" :label="t('admin.routeManagement.path')" width="200" />
<el-table-column prop="name" :label="t('admin.routeManagement.name')" width="180" />
<el-table-column prop="title" :label="t('admin.routeManagement.title')" />
<el-table-column prop="requiresAuth" :label="t('admin.routeManagement.requiresAuth')" width="120">
<template #default="scope">
<el-tag type="success" v-if="scope.row.requiresAuth">{{ t('admin.common.yes') }}</el-tag>
<el-tag type="info" v-else>{{ t('admin.common.no') }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('admin.routeManagement.buttonPermissions')" width="200">
<template #default="scope">
<el-button size="small" type="primary" @click="configureButtons(scope.row)">
{{ t('admin.routeManagement.configure') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
const { t } = useI18n()
//
const routeList = ref([
{
id: 1,
path: '/admin',
name: 'AdminDashboard',
title: '仪表板',
requiresAuth: true,
buttons: ['view', 'export']
},
{
id: 2,
path: '/admin/users',
name: 'AdminUsers',
title: '用户管理',
requiresAuth: true,
buttons: ['view', 'add', 'edit', 'delete', 'export']
},
{
id: 3,
path: '/admin/orders',
name: 'AdminOrders',
title: '订单管理',
requiresAuth: true,
buttons: ['view', 'edit', 'delete', 'export']
}
])
//
const configureButtons = (row) => {
ElMessage.info(t('admin.common.functionUnderDevelopment'))
}
onMounted(() => {
// API
})
</script>
<style scoped>
.admin-route-management {
width: 100%;
height: 100%;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.route-management-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.route-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
/* 深色主题适配 */
[data-theme="dark"] .page-title {
color: #fff;
}
</style>

View File

@ -0,0 +1,203 @@
<template>
<div class="admin-user-list">
<h2 class="page-title">{{ t('admin.userList.title') }}</h2>
<div class="user-list-content">
<el-card shadow="hover" class="user-card">
<template #header>
<div class="card-header">
<span>{{ t('admin.userList.userList') }}</span>
<el-button type="primary" size="small">
{{ t('admin.userList.addUser') }}
</el-button>
</div>
</template>
<div class="card-body">
<div class="search-bar">
<el-input :placeholder="t('admin.userList.searchPlaceholder')" v-model="searchQuery" clearable style="width: 300px;">
<template #append>
<el-button type="primary" @click="searchUsers">
{{ t('admin.common.search') }}
</el-button>
</template>
</el-input>
</div>
<el-table :data="userList" stripe style="width: 100%" v-loading="loading">
<el-table-column prop="username" :label="t('admin.userList.username')" width="180" />
<el-table-column prop="nickname" :label="t('admin.userList.nickname')" width="180" />
<el-table-column prop="email" :label="t('admin.userList.email')" />
<el-table-column prop="phone" :label="t('admin.userList.phone')" width="180" />
<el-table-column prop="role" :label="t('admin.userList.role')" width="120">
<template #default="scope">
<el-tag :type="scope.row.role === 'admin' ? 'danger' : 'success'">
{{ scope.row.role === 'admin' ? t('admin.userList.admin') : t('admin.userList.user') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" :label="t('admin.userList.status')" width="120">
<template #default="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : 'warning'">
{{ scope.row.status === 'active' ? t('admin.userList.active') : t('admin.userList.inactive') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="t('admin.userList.createTime')" width="180" />
<el-table-column :label="t('admin.common.action')" width="200" fixed="right">
<template #default="scope">
<el-button size="small" type="primary" @click="editUser(scope.row)">
{{ t('admin.common.edit') }}
</el-button>
<el-button size="small" type="danger" @click="deleteUser(scope.row)">
{{ t('admin.common.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
const { t } = useI18n()
//
const searchQuery = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(100)
const loading = ref(false)
//
const userList = ref([
{
id: 1,
username: 'admin',
nickname: '管理员',
email: 'admin@example.com',
phone: '13800138000',
role: 'admin',
status: 'active',
createTime: '2023-01-01 10:00:00'
},
{
id: 2,
username: 'user1',
nickname: '普通用户',
email: 'user1@example.com',
phone: '13800138001',
role: 'user',
status: 'active',
createTime: '2023-01-02 10:00:00'
},
{
id: 3,
username: 'user2',
nickname: '测试用户',
email: 'user2@example.com',
phone: '13800138002',
role: 'user',
status: 'inactive',
createTime: '2023-01-03 10:00:00'
}
])
//
const searchUsers = () => {
ElMessage.info(t('admin.common.functionUnderDevelopment'))
}
//
const editUser = (row) => {
ElMessage.info(t('admin.common.functionUnderDevelopment'))
}
//
const deleteUser = (row) => {
ElMessage.info(t('admin.common.functionUnderDevelopment'))
}
//
const handleSizeChange = (size) => {
pageSize.value = size
// API
}
const handleCurrentChange = (page) => {
currentPage.value = page
// API
}
onMounted(() => {
// API
})
</script>
<style scoped>
.admin-user-list {
width: 100%;
height: 100%;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.user-list-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.user-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-body {
padding: 20px 0;
}
.search-bar {
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 深色主题适配 */
[data-theme="dark"] .page-title {
color: #fff;
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="user-invites">
<!-- 添加返回按钮 -->
<div class="page-header">
<div class="page-header" style="margin-bottom: 20px;">
<el-button
type="primary"
:icon="ArrowLeft"
@ -11,45 +11,183 @@
</el-button>
</div>
<!-- 列表区域 -->
<div class="invite-table">
<el-table
:data="inviteList"
stripe
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="nickname" label="用户名" min-width="120" align="center" />
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
<el-table-column prop="phone" label="手机号" min-width="120" align="center" />
<el-table-column prop="createdAt" label="注册日期" min-width="160" align="center">
<template #default="{ row }">
{{ formatDateTime(row.createdAt) }}
<!-- 标签页 -->
<el-tabs v-model="activeTab" type="card" @tab-change="handleTabChange">
<!-- 邀请码列表 -->
<el-tab-pane label="邀请码列表" name="inviteCode">
<!-- 邀请码列表内容 -->
<div class="invite-code-section">
<!-- 筛选和生成按钮区域 -->
<div class="filter-section">
<el-form :inline="true" :model="filterForm" class="filter-form">
<el-form-item label="用户ID" v-if="false">
<el-input v-model="filterForm.userId" placeholder="请输入用户ID" style="width: 150px;"></el-input>
</el-form-item>
<el-form-item label="邀请码">
<el-input v-model="filterForm.inviteCode" placeholder="请输入邀请码" style="width: 200px;"></el-input>
</el-form-item>
<el-form-item label="使用状态">
<el-select v-model="filterForm.isUsed" placeholder="请选择使用状态" style="width: 150px;">
<el-option label="全部" value=""></el-option>
<el-option label="已使用" :value="1"></el-option>
<el-option label="未使用" :value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<el-button type="success" @click="showGenerateDialog = true">生成邀请码</el-button>
</div>
<!-- 邀请码列表 -->
<div class="invite-table">
<el-table
:data="inviteCodeList"
stripe
style="width: 100%"
v-loading="loadingCode"
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="userNickname" label="用户名" min-width="120" align="center" />
<el-table-column prop="userEmail" label="邮箱" min-width="180" align="center" />
<el-table-column prop="inviteCode" label="邀请码" min-width="200" align="center" />
<el-table-column prop="isUsed" label="使用状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isUsed ? 'success' : 'warning'">
{{ row.isUsed ? '已使用' : '未使用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="invitedUserNickname" label="邀请用户" min-width="120" align="center" />
<el-table-column prop="usedAt" label="使用时间" min-width="180" align="center">
<template #default="{ row }">
{{ formatDateTime(row.usedAt) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" min-width="180" align="center">
<template #default="{ row }">
{{ formatDateTime(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button
type="danger"
size="small"
@click="handleDeleteCode(row)"
:disabled="Boolean(row.isUsed)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPageCode"
v-model:page-size="pageSizeCode"
:page-sizes="[10, 20, 50, 100]"
:total="totalCodes"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChangeCode"
@current-change="handleCurrentChangeCode"
/>
</div>
</div>
<!-- 生成邀请码对话框 -->
<el-dialog
v-model="showGenerateDialog"
title="生成邀请码"
width="400px"
>
<el-form :model="generateForm" label-position="top">
<el-form-item label="生成数量" required>
<el-input-number
v-model="generateForm.count"
:min="1"
:max="100"
:step="1"
placeholder="请输入生成数量"
></el-input-number>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showGenerateDialog = false">取消</el-button>
<el-button type="primary" @click="handleGenerateCode">确认</el-button>
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">
{{ getStatusLabel(row.status) }}
</el-tag>
</el-dialog>
<!-- 删除确认对话框 -->
<el-dialog
v-model="showDeleteDialog"
title="确认删除"
width="400px"
>
<div>确定要删除邀请码 {{ deleteCodeInfo.inviteCode }} </div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDeleteDialog = false">取消</el-button>
<el-button type="danger" @click="confirmDeleteCode">确认删除</el-button>
</span>
</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>
</el-dialog>
</el-tab-pane>
<!-- 邀请用户列表 -->
<el-tab-pane label="邀请用户列表" name="invitedUsers">
<!-- 列表区域 -->
<div class="invite-table">
<el-table
:data="inviteList"
stripe
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="nickname" label="用户名" min-width="120" align="center" />
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
<el-table-column prop="userRole" label="用户角色" min-width="120" align="center">
<template #default="{ row }">
{{ getUserRoleLabel(row.userRole) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="注册日期" min-width="160" align="center">
<template #default="{ row }">
{{ formatDateTime(row.createdAt) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">
{{ getStatusLabel(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>
</el-tab-pane>
</el-tabs>
</div>
</template>
@ -66,7 +204,10 @@ const router = useRouter()
const { t } = useI18n()
const adminOrders = new AdminOrders()
//
// -
const activeTab = ref('inviteCode')
// -
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(20)
@ -74,6 +215,31 @@ const totalInvites = ref(0)
const inviteList = ref([])
const userId = ref(route.params.id)
// -
const loadingCode = ref(false)
const currentPageCode = ref(1)
const pageSizeCode = ref(20)
const totalCodes = ref(0)
const inviteCodeList = ref([])
//
const filterForm = ref({
userId: '',
inviteCode: '',
isUsed: ''
})
//
const generateForm = ref({
count: 1
})
//
const showGenerateDialog = ref(false)
const showDeleteDialog = ref(false)
const deleteCodeInfo = ref({})
//
const getStatusTagType = (status) => {
const typeMap = {
@ -94,6 +260,16 @@ const getStatusLabel = (status) => {
return labelMap[status] || status
}
//
const getUserRoleLabel = (userRole) => {
const roleMap = {
0: '候补中',
1: '免费会员',
2: '达人会员'
}
return roleMap[userRole] || '未知角色'
}
//
const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return '-'
@ -129,7 +305,70 @@ const getInviteList = async () => {
}
}
//
//
const getInviteCodeList = async () => {
loadingCode.value = true
try {
const result = await adminOrders.getInviteCodeList({
id: filterForm.value.userId || userId.value,
inviteCode: filterForm.value.inviteCode,
isUsed: filterForm.value.isUsed,
pageSize: pageSizeCode.value,
pageNum: currentPageCode.value
})
const data = result.data;
inviteCodeList.value = data.rows || []
totalCodes.value = data.total || 0
} catch (error) {
console.error('获取邀请码列表失败:', error)
ElMessage.error('获取邀请码列表失败')
} finally {
loadingCode.value = false
}
}
//
const handleGenerateCode = async () => {
try {
const result = await adminOrders.generateInviteCode({
id: userId.value,
count: generateForm.value.count
})
ElMessage.success('邀请码生成成功')
showGenerateDialog.value = false
//
generateForm.value.count = 1
//
getInviteCodeList()
} catch (error) {
console.error('生成邀请码失败:', error)
ElMessage.error('生成邀请码失败')
}
}
// -
const handleDeleteCode = (row) => {
deleteCodeInfo.value = row
showDeleteDialog.value = true
}
// -
const confirmDeleteCode = async () => {
try {
const result = await adminOrders.deleteInviteCode({
id: deleteCodeInfo.value.id
})
ElMessage.success('邀请码删除成功')
showDeleteDialog.value = false
//
getInviteCodeList()
} catch (error) {
console.error('删除邀请码失败:', error)
ElMessage.error('删除邀请码失败')
}
}
// -
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
@ -141,6 +380,43 @@ const handleCurrentChange = (page) => {
getInviteList()
}
// -
const handleSizeChangeCode = (size) => {
pageSizeCode.value = size
currentPageCode.value = 1
getInviteCodeList()
}
const handleCurrentChangeCode = (page) => {
currentPageCode.value = page
getInviteCodeList()
}
//
const handleSearch = () => {
currentPageCode.value = 1
getInviteCodeList()
}
const handleReset = () => {
filterForm.value = {
userId: '',
inviteCode: '',
isUsed: ''
}
currentPageCode.value = 1
getInviteCodeList()
}
//
const handleTabChange = (tabName) => {
if (tabName === 'inviteCode') {
getInviteCodeList()
} else if (tabName === 'invitedUsers') {
getInviteList()
}
}
//
const handleBack = () => {
router.push('/admin/users')
@ -148,7 +424,8 @@ const handleBack = () => {
//
onMounted(() => {
getInviteList()
//
getInviteCodeList()
})
</script>
@ -166,6 +443,27 @@ onMounted(() => {
margin-bottom: 24px;
}
.invite-code-section {
margin-top: 20px;
}
.filter-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
}
.filter-form {
display: flex;
gap: 16px;
}
.pagination {
display: flex;
justify-content: flex-end;
@ -202,4 +500,9 @@ onMounted(() => {
text-overflow: ellipsis;
overflow: hidden;
}
/* 标签页样式 */
:deep(.el-tabs__content) {
padding-top: 20px;
}
</style>

View File

@ -85,7 +85,11 @@
<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="phone" :label="t('admin.users.phone')" min-width="120" />
<el-table-column prop="userRole" :label="t('admin.users.userRole')" min-width="120">
<template #default="{ row }">
{{ getUserRoleLabel(row.userRole) }}
</template>
</el-table-column>
<el-table-column prop="status" :label="t('admin.users.status')" min-width="100">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">
@ -98,18 +102,13 @@
{{ formatDateTime(row.lastActive) }}
</template>
</el-table-column>
<el-table-column prop="inviteCode" :label="t('admin.users.inviteCode')" min-width="120" />
<el-table-column prop="invitedBy" :label="t('admin.users.invitedBy')" min-width="120">
<template #default="{ row }">
{{ row.inviterNickname || '-' }}
</template>
</el-table-column>
<!-- <el-table-column prop="inviteCode" :label="t('admin.users.inviteCode')" min-width="120" /> -->
<el-table-column prop="createdAt" :label="t('admin.users.registerDate')" min-width="160">
<template #default="{ row }">
{{ formatDateTime(row.createdAt) }}
</template>
</el-table-column>
<el-table-column :label="t('admin.users.actions')" min-width="300" fixed="right">
<el-table-column :label="t('admin.users.actions')" min-width="400" fixed="right">
<template #default="{ row }">
<div class="actions-container">
<el-button size="small" @click="handleView(row)">
@ -141,12 +140,18 @@
>
邀请列表
</el-button>
<el-button
size="small"
type="warning"
@click="handleChangeRole(row)"
>
角色变更
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
@ -185,8 +190,8 @@
<el-descriptions-item :label="t('admin.users.email')">
{{ selectedUser?.email || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.users.phone')">
{{ selectedUser?.phone || '-' }}
<el-descriptions-item label="用户角色">
{{ selectedUser ? getUserRoleLabel(selectedUser.userRole) : '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.users.status')">
{{ selectedUser ? t(`admin.users.statusOptions.${selectedUser.status}`) : '-' }}
@ -245,6 +250,35 @@
/>
</div>
</el-dialog>
<!-- 角色变更对话框 -->
<el-dialog
title="角色变更"
v-model="roleChangeDialogVisible"
width="400px"
top="20vh"
center
>
<div class="role-change-container">
<p class="dialog-info">当前用户{{ selectedUserForRoleChange?.nickname }}</p>
<p class="dialog-info">当前角色{{ selectedUserForRoleChange ? getUserRoleLabel(selectedUserForRoleChange.userRole) : '-' }}</p>
<el-form-item label="选择新角色" style="margin-top: 20px;">
<el-select v-model="newRole" placeholder="请选择新角色">
<el-option label="候补中" value="0" />
<el-option label="免费会员" value="1" />
<el-option label="达人会员" value="2" />
</el-select>
</el-form-item>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="roleChangeDialogVisible = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="confirmRoleChange" :disabled="!newRole">
{{ t('admin.common.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
@ -288,10 +322,13 @@ const filters = reactive({
const dialogVisible = ref(false)
const detailDialogVisible = ref(false)
const avatarPreviewVisible = ref(false)
const roleChangeDialogVisible = ref(false)
const isEditing = ref(false)
const selectedUser = ref(null)
const selectedUserForRoleChange = ref(null)
const currentAvatar = ref('')
const formRef = ref()
const newRole = ref('')
//
const form = reactive({
@ -428,6 +465,16 @@ const getStatusTagType = (status) => {
return typeMap[status] || 'info'
}
//
const getUserRoleLabel = (userRole) => {
const roleMap = {
0: '候补中',
1: '免费会员',
2: '达人会员'
}
return roleMap[userRole] || '未知角色'
}
const formatDate = (dateString) => {
if (!dateString) return '-'
const date = new Date(dateString)
@ -555,6 +602,35 @@ const handleUnban = async (row) => {
}
}
//
const handleChangeRole = (row) => {
selectedUserForRoleChange.value = row
newRole.value = row.userRole.toString()
roleChangeDialogVisible.value = true
}
//
const confirmRoleChange = async () => {
if (!selectedUserForRoleChange.value || !newRole.value) return
try {
// API
await adminOrders.changeRole({
id: selectedUserForRoleChange.value.id,
userRole: newRole.value
})
ElMessage.success('角色变更成功')
//
refresh()
//
roleChangeDialogVisible.value = false
} catch (error) {
console.error('角色变更失败:', error)
ElMessage.error('角色变更失败')
}
}
//
const handleInviteList = (row) => {
router.push({

View File

@ -66,5 +66,49 @@ export class AdminOrders {
}
return requestUtils.common(requestUrl, params);
}
//变更用户角色(候补会员/免费会员/达人候补升级时自动赠送300积分
async changeRole(data) {
let params = {
id: data.id || '',
userRole: data.userRole || ''
}
const requestUrl = {
method: adminApi.default.changeRole.method,
url: adminApi.default.changeRole.url.replace('USERID', params.id)
}
return requestUtils.common(requestUrl, params);
}
//分页查询邀请码列表支持按用户ID、邀请码、使用状态筛选
async getInviteCodeList(data) {
let params = {
userId: data.id,
inviteCode: data.inviteCode,
isUsed: data.isUsed,
pageSize: data.pageSize || 10,
pageNum: data.pageNum || 1,
orderByColumn: data.orderByColumn || '',
isAsc: data.isAsc || 'asc'
}
return requestUtils.common(adminApi.default.getInviteCodeList, params);
}
//为用户生成邀请码
async generateInviteCode(data) {
let params = {
userId: data.id,
count: data.count || 1
}
return requestUtils.common(adminApi.default.generateInviteCode, params);
}
//根据邀请码ID删除邀请码
async deleteInviteCode(data) {
let params = {
id: data.id || ''
}
const requestUrl = {
method: adminApi.default.deleteInviteCode.method,
url: adminApi.default.deleteInviteCode.url.replace('CODEID', params.id),
isLoading: adminApi.default.deleteInviteCode.isLoading
}
return requestUtils.common(requestUrl, params);
}
}

View File

@ -4,7 +4,6 @@ import { useRouter, useRoute } from 'vue-router'
import MainLayout from '@/components/layout/MainLayout.vue'
import AppHeader from '@/components/layout/AppHeader.vue'
import AppSidebar from '@/components/layout/AppSidebar.vue'
import LoadingCom from './components/LoadingCom/index.vue';
const route = useRoute()
//
const isLoginPage = computed(() => route.path === '/login')
@ -60,16 +59,16 @@ onMounted(() => {
'homepage-mode': isHomePage
}">
<!-- <div v-if="qmLoading" class="sidebar-overlay" :class="{ 'sidebar-overlay-active': qmLoading }"></div> -->
<LoadingCom v-if="qmLoading" />
<DtLoadingCom v-if="qmLoading" />
<!-- 登录页面全屏显示 -->
<main style="position: relative;height: 100%;width: 100%;" v-if="isLoginPage">
<!-- <div v-if="loading" class="sidebar-overlay" :class="{ 'sidebar-overlay-active': loading }"></div> -->
<LoadingCom v-if="loading" />
<DtLoadingCom v-if="loading" />
<router-view />
</main>
<!-- 全屏页面如创建项目 -->
<main v-else-if="isFullScreenPage" class="fullscreen-content">
<LoadingCom v-if="loading" />
<DtLoadingCom v-if="loading" />
<!-- <div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': loading }"></div> -->
<router-view />
</main>

View File

@ -270,7 +270,7 @@ const handleGenerateImage = async () => {
头发纹理细节需针对3D制造进行优化层次平滑且分明兼顾视觉吸引力与可打印性维持整体俏皮且高品质的盲盒角色风格
`:`采用疯狂动物城的设计风格`}
调整背景为极简风格换成中性纯白色,让图片中的人物呈现3D立体效果
保证生成的图片一定要有眼睛一定要有嘴巴
保证生成的图片一定要有眼睛一定要有嘴巴,眼睛效果要可爱童真Q版大眼睛
角色肤色和衣服材质都为纯色一种颜色如下
保证角色全身都为木头材质颜色并且要带一些木头纹理颜色为#e2cfb3
衣服如果不适合做木制一定要简化衣服不能用复杂的衣服设计保留衣服特征即可衣服一定要纯色木质材质

View File

@ -53,6 +53,10 @@ const props = defineProps({
disabled: {
type: Boolean,
default: false
},
code: {
type: String,
default: ''
}
})
@ -96,7 +100,8 @@ const handleGoogleLogin = async () => {
const loginWithidToken = async (idToken) => {
try {clientApi
const res = await requestUtils.common(clientApi.default.OAUTH_GOOGLE,{
googleIdToken:idToken
googleIdToken:idToken,
inviteCode:props.code,
})
// token
let data = res.data;

View File

@ -72,7 +72,11 @@
</span>
<div v-if="loading" class="loading-spinner"></div>
</button>
<div style="text-align: center;font-size: 14px;cursor: pointer;">
<button type="submit"
class="waitlist-button"
:disabled="loading || !isFormValid">{{ t('login.join_waitlist') }}</button>
</div>
<!-- 功能预留提示 -->
<div class="feature-notice" v-if="false">
<el-icon class="info-icon"><InfoFilled /></el-icon>
@ -82,13 +86,15 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { WarningFilled, View, Hide, InfoFilled } from '@element-plus/icons-vue'
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength } from '@vuelidate/validators'
import InviteCodeInput from './InviteCodeInput.vue'
const { t } = useI18n()
const route = useRoute()
const emit = defineEmits(['login', 'error', 'update:inviteCode'])
const props = defineProps({
loading: {
@ -103,6 +109,15 @@ const form = ref({
inviteCode: ''
})
// URL
onMounted(() => {
// URLinviteCode
const inviteCodeFromUrl = route.query.inviteCode
if (inviteCodeFromUrl) {
form.value.inviteCode = inviteCodeFromUrl
}
})
//
const isEmailFocused = ref(false)
const isPasswordFocused = ref(false)
@ -117,15 +132,15 @@ const inviteCodeError = ref('')
const rules = {
email: { required, email },
password: { required, minLength: minLength(6) },
inviteCode: { required }
inviteCode: {}
}
const v$ = useVuelidate(rules, form)
//
const isFormValid = computed(() => {
return form.value.email && form.value.password && form.value.inviteCode &&
!emailError.value && !passwordError.value && !inviteCodeError.value
return form.value.email && form.value.password &&
!emailError.value && !passwordError.value
})
//
@ -150,11 +165,7 @@ const validatePassword = () => {
}
const validateInviteCode = () => {
if (!form.value.inviteCode) {
inviteCodeError.value = t('login.invite_code_empty_error')
} else {
inviteCodeError.value = ''
}
inviteCodeError.value = ''
emit('update:inviteCode', form.value.inviteCode)
}
@ -170,7 +181,6 @@ const handleLogin = async () => {
if (v$.value.$invalid) {
validateEmail()
validatePassword()
validateInviteCode()
return
}
emit('login', form.value)
@ -326,6 +336,48 @@ const handleLogin = async () => {
box-shadow: none;
}
/* 加入候补队列按钮 */
.waitlist-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 36px;
padding: 0 16px;
background: transparent;
border: 1px solid #7C3AED;
border-radius: 8px;
color: #7C3AED;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.waitlist-button:hover:not(:disabled) {
background: rgba(124, 58, 237, 0.08);
border-color: #8B5CF6;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(124, 58, 237, 0.15);
}
.waitlist-button:active:not(:disabled) {
transform: translateY(0);
background: rgba(124, 58, 237, 0.12);
box-shadow: none;
}
.waitlist-button:disabled {
background: transparent;
border-color: #D1D5DB;
color: #9CA3AF;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 按钮文字 */
.button-text {
font-weight: 600;

View File

@ -17,6 +17,7 @@
<CreationIcon v-else-if="item.icon === 'CreationIcon'" />
<GalleryIcon v-else-if="item.icon === 'GalleryIcon'" />
<OrdersIcon v-else-if="item.icon === 'OrdersIcon'" />
<UserIcon v-else-if="item.icon === 'UserIcon'" />
</div>
<transition name="fade">
<span v-if="!collapsed" class="nav-text">{{ item.label }}</span>
@ -36,11 +37,13 @@
<el-avatar :size="32" :src="currentUser.avatarUrl">
<UserIcon />
</el-avatar>
<div class="online-status"></div>
<div class="role-badge" :class="userRole">{{ getRoleDisplayName(currentUser.user_role) }}</div>
</div>
<div class="user-info">
<p class="user-role">{{ currentUser.nickname || 'user' }}</p>
<div class="user-points">
<span class="points-icon">🪄</span>
<span class="points-text">{{ remainingPoints }}</span>
</div>
<!-- 用户信息模块已取消 -->
</div>
<!-- 折叠状态下的用户头像 -->
@ -49,15 +52,15 @@
<el-avatar :size="32" :src="currentUser.avatarUrl">
<UserIcon />
</el-avatar>
<div class="online-status"></div>
<!-- <div class="role-badge" :class="userRole">{{ getRoleDisplayName(currentUser.user_role) }}</div> -->
</div>
</div>
</div>
</aside>
</template>
<script>
import { ref, computed, watch } from 'vue'
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
@ -79,121 +82,122 @@ import {
Key as ApiIcon
} from '@element-plus/icons-vue'
export default {
name: 'AppSidebar',
components: {
BrainIcon,
DashboardIcon,
CreationIcon,
GalleryIcon,
OrdersIcon,
UserIcon,
DocumentIcon,
VideoIcon,
ChatIcon,
AnalyticsIcon,
ProjectIcon,
NotificationIcon,
ApiIcon
},
props: {
collapsed: {
type: Boolean,
default: false
}
},
emits: ['navigate'],
setup(props, { emit }) {
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
//
defineOptions({
name: 'AppSidebar'
})
//
const isMobile = ref(window.innerWidth < 768)
//
const currentUser = computed(() => authStore.user)
const sidebarClasses = computed(() => ({
'sidebar-mobile': isMobile.value
}))
// (6)
const coreMenuItems = computed(() => [
{
id: 'dashboard',
path: '/czhome',
label: t('sidebar.dashboard'),
icon: 'DashboardIcon',
badge: null
},
{
id: 'creation-workspace',
path: '/creation-workspace',
label: t('sidebar.creationWorkspace'),
icon: 'CreationIcon',
badge: null
},
{
id: 'agent-management',
path: '/agent-management',
label: t('sidebar.agentManagement.title'),
icon: 'BrainIcon',
badge: null
},
{
id: 'order-management',
path: '/order-management',
label: t('sidebar.orderManagement'),
icon: 'OrdersIcon',
badge: null
},
])
//
const isActiveRoute = (path) => {
if (path === '/') {
return route.path === '/'
}
return route.path.startsWith(path)
}
//
const handleNavClick = (item) => {
emit('navigate', item)
}
//
const getRoleDisplayName = (role) => {
const roleMap = {
'creator': t('roles.creator'),
'admin': t('roles.admin'),
'viewer': t('roles.viewer')
}
return roleMap[role] || role
}
//
const handleResize = () => {
isMobile.value = window.innerWidth < 768
}
//
window.addEventListener('resize', handleResize)
return {
t,
isMobile,
currentUser,
sidebarClasses,
coreMenuItems,
isActiveRoute,
handleNavClick,
getRoleDisplayName
}
// props
const props = defineProps({
collapsed: {
type: Boolean,
default: false
}
})
//
const emit = defineEmits(['navigate'])
// API
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
//
const isMobile = ref(window.innerWidth < 768)
const remainingPoints = ref(1280)
//
const currentUser = computed(() => authStore.user)
const userRole = computed(() => ({
'1': 'free',
'2': 'creator',
}[authStore.user?.user_role || '1'])) // 'free'
const sidebarClasses = computed(() => ({
'sidebar-mobile': isMobile.value
}))
// (6)
const coreMenuItems = computed(() => [
{
id: 'dashboard',
path: '/czhome',
label: t('sidebar.dashboard'),
icon: 'DashboardIcon',
badge: null
},
{
id: 'creation-workspace',
path: '/creation-workspace',
label: t('sidebar.creationWorkspace'),
icon: 'CreationIcon',
badge: null
},
{
id: 'agent-management',
path: '/agent-management',
label: t('sidebar.agentManagement.title'),
icon: 'BrainIcon',
badge: null
},
{
id: 'order-management',
path: '/order-management',
label: t('sidebar.orderManagement'),
icon: 'OrdersIcon',
badge: null
},
{
id: 'user-center',
path: '/user-center',
label: t('sidebar.userCenter'),
icon: 'UserIcon',
badge: null
},
])
//
const isActiveRoute = (path) => {
if (path === '/') {
return route.path === '/'
}
return route.path.startsWith(path)
}
//
const handleNavClick = (item) => {
emit('navigate', item)
}
//
const getRoleDisplayName = (role) => {
const roleMap = {
'1': t('roles.free'),
'2': t('roles.creator'),
}
return roleMap[role] || role
}
//
const handleResize = () => {
isMobile.value = window.innerWidth < 768
}
// -
onMounted(() => {
window.addEventListener('resize', handleResize)
})
// -
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<script>
// Vue DevTools
// Vue 3.3+defineOptions API<script setup>name
</script>
<style scoped>
@ -447,16 +451,73 @@ export default {
box-shadow: 0 0 12px rgba(107, 70, 193, 0.2);
}
.online-status {
.role-badge {
position: absolute;
bottom: 4px;
right: 2px;
width: 12px;
height: 12px;
background: #10B981;
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 0 4px rgba(16, 185, 129, 0.4);
bottom: -8px;
left: 50%;
transform: translateX(-50%);
padding: 2px 8px;
font-size: 10px;
font-weight: 600;
color: white;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
white-space: nowrap;
z-index: 10;
}
.role-badge.free {
background: linear-gradient(135deg, #9CA3AF 0%, #6B7280 100%);
box-shadow: 0 0 6px rgba(156, 163, 175, 0.5);
}
.role-badge.creator {
background: linear-gradient(135deg, #8B5CF6 0%, #6B46C1 100%);
box-shadow: 0 0 6px rgba(139, 92, 246, 0.5);
}
/* 剩余积分样式 */
.user-points {
display: flex;
align-items: center;
justify-content: center;
margin-top: 8px;
padding: 4px 12px;
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.3);
border-radius: 12px;
font-size: 12px;
font-weight: 600;
color: #8B5CF6;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(139, 92, 246, 0.1);
}
.user-points:hover {
background: rgba(139, 92, 246, 0.15);
border-color: rgba(139, 92, 246, 0.4);
box-shadow: 0 4px 8px rgba(139, 92, 246, 0.15);
transform: translateY(-1px);
}
.points-icon {
margin-right: 4px;
font-size: 14px;
animation: pulse 2s infinite;
}
.points-text {
color: #6B46C1;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.user-info {

View File

@ -23,13 +23,11 @@
<!-- 主内容区域 -->
<div class="main-content" :class="{ 'sidebar-collapsed': !sidebarVisible && !isMobile }">
<!-- <div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': loading }"></div> -->
<LoadingCom v-if="loading" />
<DtLoadingCom v-if="loading" />
<!-- 面包屑导航 -->
<!-- <BreadcrumbNavigation class="breadcrumb-container" /> -->
<!-- 页面内容 -->
<main class="page-content">
<router-view v-slot="{ Component, route }">
<keep-alive v-if="route.meta.keepAlive">
<component :is="Component" :key="route.name" />
@ -46,9 +44,6 @@
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import AppHeader from './AppHeader.vue'
import AppSidebar from './AppSidebar.vue'
import LoadingCom from '../../components/LoadingCom/index.vue'
import BreadcrumbNavigation from './BreadcrumbNavigation.vue'
//
defineOptions({
name: 'MainLayout'

View File

@ -20,7 +20,8 @@ export default {
login: '登录',
register: '注册',
forgotPassword: '忘记密码',
modelPurchase: '模型购买'
modelPurchase: '模型购买',
pointsRecharge: '积分充值'
},
sidebar: {
dashboard: '仪表盘',
@ -44,6 +45,8 @@ export default {
creationWorkspace: '项目',
projectGallery: '画廊',
deviceSettings: '设置',
userCenter: '用户中心',
commissionManagement: '佣金管理',
agentManagement: {
title: '智能体',
description: '管理和配置您的AI智能体',
@ -128,6 +131,28 @@ export default {
generatingIndicator: '正在生成模型...',
progressText: '{percentage}%'
},
commissionManagement: {
title: '佣金管理',
pageTitle: '佣金管理',
commissionRate: '佣金比例',
saveRate: '保存',
defaultRate: '15%',
rateSaved: '佣金比例保存成功',
list: {
creatorName: '达人名称',
userId: '用户ID',
actualPayment: '实际支付金额',
productAmount: '商品金额',
commission: '佣金',
status: '状态',
action: '操作',
approve: '审核通过',
reject: '拒绝',
pending: '待审核',
approved: '已通过',
rejected: '已拒绝'
}
},
orderProcess: {
title: '定制到家流程',
subtitle: '了解您的订单从支付到发货的全过程',
@ -195,9 +220,8 @@ export default {
step: '步骤'
},
roles: {
creator: '创作者',
admin: '管理员',
viewer: '访客'
creator: '达人会员',
free: '免费会员',
},
home: {
welcome: '欢迎使用 Vue3 + Element Plus 模板',
@ -577,6 +601,8 @@ export default {
invite_code_label: '邀请码',
invite_code_placeholder: '请输入邀请码',
invite_code_empty_error: '请输入邀请码',
join_waitlist: '加入候补队列',
join_waitlist_success: '已成功加入候补队列,我们将尽快与您联系',
},
payment: {
methods: '支付方式',
@ -1030,6 +1056,112 @@ export default {
referenceImageRequired: '请上传参考图像或选择草图以继续生成'
}
},
userCenter: {
title: '用户中心',
description: '管理您的账户信息和设置',
points: {
title: '积分信息',
currentPoints: '当前积分',
expiryDate: '积分到期时间',
pointsList: '积分明细',
consumptionRules: {
title: '积分消耗规则',
behavior: '行为',
pointsConsumption: '积分消耗',
rules: {
generateImage: '生成1张图片',
generateModel: '生成1个3D模型'
},
additionalRules: {
title: '额外规则',
rule1: '先进先出FIFO',
rule2: '优先消耗最先到期的积分',
rule3: '到期自动失效'
}
}
},
invitation: {
title: '邀请信息',
inviteCode: '邀请码',
inviteCodes: '邀请码',
inviteCount: '邀请人数',
invitePointsDetails: '邀请积分明细',
invitedUser: '被邀请用户',
pointsEarned: '获得积分',
date: '日期',
copyCode: '复制邀请码',
expired: '已过期',
expiryDate: '到期时间',
copySuccess: '邀请码复制成功',
copyFailed: '复制失败,请手动复制',
rules: {
title: '邀请规则',
freeMember: {
title: '免费会员邀请规则',
reward: '每成功邀请 1 名用户注册:',
pointsReward: '奖励 300 积分'
},
creatorMember: {
title: '达人会员邀请规则',
whenInvite: '当达人邀请码成功邀请 1 名新用户注册:',
permission: '拥有免费会员全部权限',
commissionAbility: '额外具备带货佣金能力',
immediateReward: '立即奖励 300 积分',
binding: '建立绑定关系(达人 ←→ 用户)',
subsequentOrder: '被邀请用户后续下单:',
commissionRate: '达人可获得 15% 佣金'
},
becomeCreator: {
title: '成为达人会员',
qrCode: '达人会员二维码'
},
},
used:'已使用'
},
creator: {
title: '达人会员中心',
commissionTitle: '达人佣金',
invitedUsersList: '邀约用户列表',
userName: '用户名',
consumptionAmount: '消费金额',
commission: '佣金提成',
commissionStats: '佣金统计',
totalConsumption: '总消费金额',
totalCommission: '总佣金',
availableCommission: '可用佣金',
withdrawal: '提现(预留)',
date: '日期'
}
},
pointsRecharge: {
title: '选择您的套餐',
subtitle: '灵感不等候,创意即刻启程。',
plans: {
basic: {
name: '基础套餐'
},
premium: {
name: '高级套餐',
badge: '热门'
}
},
period: '年',
points: '积分',
validity: '有效期',
features: {
unlimitedAccess: '畅享所有功能',
prioritySupport: '优先技术支持',
securePayment: '支付安全保障',
extraBenefits: '额外优惠与福利'
},
purchase: '选择此套餐',
rules: {
title: '积分消耗规则',
rule1: '先进先出FIFO',
rule2: '优先消耗即将过期的积分',
rule3: '到期自动失效'
}
},
iPandCardLeft: {
textPrompt: '文本提示',
placeholder: {
@ -1123,6 +1255,12 @@ export default {
title: '暂无项目',
description: '您还没有创建任何项目,点击下方按钮开始创建吧',
action: '创建新项目'
},
notFound: {
title: '页面未找到',
description: '抱歉,您访问的页面不存在或已被移除。',
goHome: '返回首页',
goBack: '返回上一页'
}
},
en: {
@ -1140,7 +1278,37 @@ export default {
login: 'Login',
register: 'Register',
forgotPassword: 'Forgot Password',
modelPurchase: 'Model Purchase'
modelPurchase: 'Model Purchase',
pointsRecharge: 'Points Recharge'
},
pointsRecharge: {
title: 'Choose Your Plan',
subtitle: 'Inspiration doesn\'t wait, creativity starts now.',
plans: {
basic: {
name: 'Basic Plan',
},
premium: {
name: 'Premium Plan',
badge: 'HOT'
}
},
period: 'year',
points: 'Points',
validity: 'Validity',
features: {
unlimitedAccess: 'Unlimited access to all features',
prioritySupport: 'Priority technical support',
securePayment: 'Secure payment guarantee',
extraBenefits: 'Additional benefits and discounts'
},
purchase: 'Choose This Plan',
rules: {
title: 'Points Consumption Rules',
rule1: 'First In First Out (FIFO)',
rule2: 'Prioritize consumption of points that expire first',
rule3: 'Automatically expire when due'
}
},
sidebar: {
dashboard: 'Dashboard',
@ -1164,6 +1332,8 @@ export default {
creationWorkspace: 'Projects',
projectGallery: 'Gallery',
deviceSettings: 'Settings',
userCenter: 'User Center',
commissionManagement: 'Commission Management',
agentManagement: {
title: 'Agents',
description: 'Manage and configure your AI agents',
@ -1248,6 +1418,28 @@ export default {
generatingIndicator: 'Generating model...',
progressText: '{percentage}%'
},
commissionManagement: {
title: 'Commission Management',
pageTitle: 'Commission Management',
commissionRate: 'Commission Rate',
saveRate: 'Save',
defaultRate: '15%',
rateSaved: 'Commission rate saved successfully',
list: {
creatorName: 'Creator Name',
userId: 'User ID',
actualPayment: 'Actual Payment',
productAmount: 'Product Amount',
commission: 'Commission',
status: 'Status',
action: 'Action',
approve: 'Approve',
reject: 'Reject',
pending: 'Pending',
approved: 'Approved',
rejected: 'Rejected'
}
},
orderProcess: {
title: 'Customize to Home Process',
subtitle: 'Understand the complete process of your order from payment to delivery',
@ -1314,10 +1506,86 @@ export default {
skipGuide: 'Skip Guide',
step: 'Step'
},
userCenter: {
title: 'User Center',
description: 'Manage your account information and settings',
points: {
title: 'Points Information',
currentPoints: 'Current Points',
expiryDate: 'Points Expiry Date',
pointsList: 'Points Details',
consumptionRules: {
title: 'Points Consumption Rules',
behavior: 'Behavior',
pointsConsumption: 'Points Consumption',
rules: {
generateImage: 'Generate 1 image',
generateModel: 'Generate 1 3D model'
},
additionalRules: {
title: 'Additional Rules',
rule1: 'First In First Out (FIFO)',
rule2: 'Prioritize consumption of points that expire first',
rule3: 'Automatically expire when due'
}
}
},
invitation: {
title: 'Invitation Information',
inviteCode: 'Invite Code',
inviteCodes: 'Invite Codes',
inviteCount: 'Invite Count',
invitePointsDetails: 'Invite Points Details',
invitedUser: 'Invited User',
pointsEarned: 'Points Earned',
date: 'Date',
copyCode: 'Copy Code',
expired: 'Expired',
expiryDate: 'Expiry Date',
copySuccess: 'Invite code copied successfully',
copyFailed: 'Copy failed, please copy manually',
rules: {
title: 'Invitation Rules',
freeMember: {
title: 'Free Member Invitation Rules',
reward: 'For each successful invitation of 1 user registration:',
pointsReward: 'Reward 300 points'
},
creatorMember: {
title: 'Creator Member Invitation Rules',
whenInvite: 'When the creator invitation code successfully invites 1 new user to register:',
permission: 'Has all permissions of free members',
commissionAbility: 'Additional product commission capability',
immediateReward: 'Immediately reward 300 points',
binding: 'Establish binding relationship (creator ←→ user)',
subsequentOrder: 'Subsequent orders from invited users:',
commissionRate: 'Creator can get 15% commission'
},
becomeCreator: {
title: 'Become a Creator Member',
qrCode: 'Creator Member QR Code'
}
},
used:'Used'
},
creator: {
title: 'Creator Member Center',
commissionTitle: 'Creator Commission',
invitedUsersList: 'Invited Users List',
userName: 'User Name',
consumptionAmount: 'Consumption Amount',
commission: 'Commission',
commissionStats: 'Commission Statistics',
totalConsumption: 'Total Consumption',
totalCommission: 'Total Commission',
availableCommission: 'Available Commission',
withdrawal: 'Withdrawal (Reserved)',
date: 'Date'
}
},
roles: {
creator: 'Creator',
admin: 'Administrator',
viewer: 'Viewer'
free: 'Free',
},
home: {
welcome: 'Welcome to Vue3 + Element Plus Template',
@ -1696,6 +1964,8 @@ export default {
invite_code_label: 'Invite Code',
invite_code_placeholder: 'Please enter invite code',
invite_code_empty_error: 'Please enter invite code',
join_waitlist: 'Join Waitlist',
join_waitlist_success: 'Successfully joined the waitlist, we will contact you soon',
},
payment: {
methods: 'Payment Methods',
@ -2238,6 +2508,12 @@ export default {
title: 'No Projects',
description: 'You have not created any projects yet, click the button below to start creating',
action: 'Create New Project'
},
notFound: {
title: 'Page Not Found',
description: 'Sorry, the page you are looking for does not exist or has been removed.',
goHome: 'Go Home',
goBack: 'Go Back'
}
}
},

View File

@ -20,10 +20,17 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import 'element-plus/dist/index.css'
import 'nprogress/nprogress.css'
import {ElMessage,ElLoading } from 'element-plus'
import dtUI from '@deotaland/ui'
import '@deotaland/ui/style.css'
import { environmentUtils } from '@deotaland/utils';
const app = createApp(App)
window.setElMessage = (options={})=>{
ElMessage[options.type || 'info'](options.message || '请求失败')
}
// environmentUtils.detectEnvironment().then((env)=>{
// console.log('当前环境:', env)
// })
app.use(dtUI);
// window.setElLoading = ()=>{
// const loading = ElLoading.service({
// lock: true,
@ -37,15 +44,12 @@ window.setElMessage = (options={})=>{
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
// i18n
const i18n = createI18n(i18nConfig)
app.use(i18n)
// Router & UI
app.use(router)
app.use(ElementPlus)
// Lazyload
// app.use(VueLazyload, {
// loading: '/logo.png',

View File

@ -1,5 +1,6 @@
import { createRouter, createWebHistory,createWebHashHistory} from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { nextTick } from 'vue'
import NProgress from 'nprogress'
const ModernHome = () => import('../views/ModernHome/ModernHome.vue')
const List = () => import('../views/List.vue')
@ -16,6 +17,9 @@ const AddAgent = () => import('../views/AddAgent.vue')
const DeviceList = () => import('../views/DeviceList.vue')
const UiTest = () => import('../views/UiTest.vue')
const home = () => import('../views/home/index.vue')
const PointsRecharge = () => import('../views/PointsRecharge.vue')
const UserCenter = () => import('../views/user/index.vue')
const NotFound = () => import('../views/NotFound.vue')
NProgress.configure({
showSpinner: false,
})// 开启轻量模式(顶部细线)
@ -27,6 +31,38 @@ const routes = [
component: home,
meta: { fullScreen: false }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: home, // 显示404页面组件
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
},{
path: '/login',
name: 'login',
component: Login,
meta: { requiresGuest: true }
},
{
path: '/czhome',
name: 'czhome',
component: ModernHome,
meta: { requiresAuth: false, keepAlive: false }
},
{
path: '/register',
name: 'register',
component: Register,
meta: { requiresGuest: true, fullScreen: true }
},
{
path: '/forgot-password',
name: 'forgot-password',
component: ForgotPassword,
meta: { requiresGuest: true, fullScreen: true }
},
]
//免费会员/达人会员动态路由
export const freeRoutes = [
{
path: '/czhome',
name: 'czhome',
@ -69,6 +105,12 @@ const routes = [
component: AgentManagement,
meta: { requiresAuth: true, keepAlive: false }
},
{
path: '/user-center',
name: 'user-center',
component: UserCenter,
meta: { requiresAuth: true, keepAlive: false }
},
{
path: '/add-agent',
name: 'add-agent',
@ -99,42 +141,30 @@ const routes = [
component: () => import('../views/ModelPurchase.vue'),
meta: { requiresAuth: true, fullScreen: true }
},
{
path: '/points-recharge',
name: 'points-recharge',
component: PointsRecharge,
meta: { requiresAuth: true,fullScreen: true }
},
{
path: '/list',
name: 'list',
component: List,
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'login',
component: Login,
meta: { requiresGuest: true }
},
{
path: '/register',
name: 'register',
component: Register,
meta: { requiresGuest: true, fullScreen: true }
},
{
path: '/forgot-password',
name: 'forgot-password',
component: ForgotPassword,
meta: { requiresGuest: true, fullScreen: true }
},
{
path: '/:pathMatch(.*)*',
redirect: '/'
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound, // 显示404页面组件
meta: { requiresAuth: false, keepAlive: false, fullScreen: true }
},
]
const router = createRouter({
// history: createWebHistory(),
history:createWebHashHistory(),
routes,
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
NProgress.start()
@ -150,10 +180,50 @@ router.beforeEach(async (to, from, next) => {
return
}
}
// 检查是否需要添加动态路由
const authStore = useAuthStore();
const user_role = authStore.user?.user_role || '0'
if(user_role != '0' && router.getRoutes().length <= routes.length) {
// 添加动态路由
addDynamicRoutes();
if(isDynamicRoute(to.path)) {
next('/czhome')
setTimeout(() => {
router.push(to.path)
}, 20);
return
}
}
next()
})
// 添加动态路由的函数
function addDynamicRoutes() {
console.log('添加动态路由前路由数量:', router.getRoutes().length);
freeRoutes.forEach(route => {
router.addRoute(route)
})
}
// 检查是否是动态路由
function isDynamicRoute(path) {
return freeRoutes.some(route => {
// 简单匹配,不考虑动态参数
const routePath = route.path.replace(/:[^/]+/g, '[^/]+')
const regex = new RegExp(`^${routePath}$`)
return regex.test(path)
})
}
window.Redirectlogin = () => {
localStorage.removeItem('token')
router.getRoutes().forEach(route => {
if (route.name && !routes.some(r => r.name === route.name)) {
router.removeRoute(route.name)
}
})
router.push('/login')
}
router.afterEach(() => {
NProgress.done()
})
export default router

View File

@ -27,13 +27,12 @@ export const useAuthStore = defineStore('auth', () => {
token.value = data.accessToken
user.value = data
localStorage.setItem('token', token.value);
callback&&callback();
callback&&callback(data);
}
// 登出方法
const logout = async (callback) => {
try {
const res = await requestUtils.common(clientApi.default.LOGOUT)
return res
} catch (error) {
console.error('登出失败:', error)
@ -45,6 +44,7 @@ export const useAuthStore = defineStore('auth', () => {
callback&&callback();
const router = useRouter();
localStorage.removeItem('token')
window?.Redirectlogin()
}
}

View File

@ -23,7 +23,8 @@
<div class="google-login-section">
<GoogleOAuthButton
@success="handleLoginSuccess"
:disabled="!isInviteCodeValid"
:disabled="false"
:code="inviteCode"
/>
</div>
<!-- 分割线 -->
@ -100,13 +101,19 @@ const updateInviteCode = (value) => {
}
const handleLogin = async (data) => {
plugin.login(data)
plugin.login(data)
// if (data.inviteCode) {
// plugin.login(data)
// } else {
// plugin.joinWaitlist(data)
// }
}
//
const handleLoginSuccess = (userData) => {
console.log('登录成功:', userData)
plugin.handleLoginSuccess(userData)
// console.log(':', userData)
//
router.push('/czhome')
// router.push('/czhome')
}
//

View File

@ -10,12 +10,24 @@ export default class Login {
}
async login(data) {
this.loading = true;
this.authStore.login(data,()=>{
this.authStore.login(data,(userData)=>{
this.loading = false;
this.router.push({ name: 'czhome' })
this.handleLoginSuccess(userData);
// this.router.push({ name: 'czhome' })
// this.refreshGoogleRefreshToken()
});
}
//登录成功处理根据不同角色标识
handleLoginSuccess(userData){//0候补1免费2达人
// userData.user_role=1
this.router.push({ name: 'czhome' });
// if(userData.user_role != '0'){
// this.router.push({ name: 'czhome' });
// }else{
// ElMessage.success('You have been added to the waitlist');
// window.localStorage.removeItem('token')
// }
}
//发送邮箱验证码
sendEmailCode(item,callback){
requestUtils.common(clientApi.default.SEND_EMAIL_CODE,{
@ -67,4 +79,10 @@ export default class Login {
callback&&callback();
})
}
//加入候补队列
joinWaitlist(data,callback){
// 这里可以根据实际情况实现加入候补队列的API调用
ElMessage.success('已成功加入候补队列,我们将尽快与您联系');
callback&&callback();
}
}

View File

@ -0,0 +1,219 @@
<template>
<div class="not-found-container">
<div class="not-found-content">
<div class="error-code">404</div>
<div class="error-message">{{ t('notFound.title') }}</div>
<div class="error-description">{{ t('notFound.description') }}</div>
<div class="action-buttons">
<button class="primary-button" @click="goHome">
{{ t('notFound.goHome') }}
</button>
<button class="secondary-button" @click="goBack">
{{ t('notFound.goBack') }}
</button>
</div>
</div>
<div class="decoration">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
<div class="circle circle-3"></div>
</div>
</div>
</template>
<script>
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
export default {
name: 'NotFound',
setup() {
const { t } = useI18n()
const router = useRouter()
const goHome = () => {
router.push('/')
}
const goBack = () => {
router.go(-1)
}
return {
t,
goHome,
goBack
}
}
}
</script>
<style scoped>
.not-found-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #f3f4f6 0%, #e9d5ff 100%);
position: relative;
overflow: hidden;
padding: 20px;
}
.not-found-content {
text-align: center;
z-index: 10;
max-width: 600px;
}
.error-code {
font-size: 10rem;
font-weight: bold;
color: #6B46C1;
line-height: 1;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.error-message {
font-size: 2.5rem;
font-weight: 600;
color: #1F2937;
margin-bottom: 16px;
}
.error-description {
font-size: 1.2rem;
color: #6B7280;
margin-bottom: 40px;
line-height: 1.6;
}
.action-buttons {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
.primary-button, .secondary-button {
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.primary-button {
background-color: #6B46C1;
color: white;
}
.primary-button:hover {
background-color: #5B3FA1;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(107, 70, 193, 0.3);
}
.secondary-button {
background-color: transparent;
color: #6B46C1;
border: 1px solid #6B46C1;
}
.secondary-button:hover {
background-color: rgba(107, 70, 193, 0.1);
transform: translateY(-2px);
}
.decoration {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
pointer-events: none;
}
.circle {
position: absolute;
border-radius: 50%;
opacity: 0.1;
}
.circle-1 {
width: 300px;
height: 300px;
background-color: #6B46C1;
top: -100px;
right: -100px;
}
.circle-2 {
width: 200px;
height: 200px;
background-color: #A78BFA;
bottom: -50px;
left: -50px;
}
.circle-3 {
width: 150px;
height: 150px;
background-color: #8B5CF6;
top: 50%;
left: 10%;
}
@media (max-width: 768px) {
.error-code {
font-size: 6rem;
}
.error-message {
font-size: 2rem;
}
.error-description {
font-size: 1rem;
}
.action-buttons {
flex-direction: column;
align-items: center;
}
.primary-button, .secondary-button {
width: 200px;
}
}
@media (max-width: 480px) {
.error-code {
font-size: 4rem;
}
.error-message {
font-size: 1.5rem;
}
.circle-1 {
width: 200px;
height: 200px;
}
.circle-2 {
width: 150px;
height: 150px;
}
.circle-3 {
width: 100px;
height: 100px;
}
}
</style>

View File

@ -0,0 +1,479 @@
<template>
<div class="points-recharge-page">
<!-- 顶部操作栏 -->
<div class="top-bar">
<!-- 返回按钮 -->
<button class="back-button" @click="goBack">
{{ t('common.back') }}
</button>
<!-- 语言切换组件 -->
<div class="language-switcher">
<LanguageToggle />
</div>
</div>
<!-- 页面标题 -->
<div class="page-header">
<h1 class="page-title">{{ t('pointsRecharge.title') }}</h1>
<p class="page-subtitle">{{ t('pointsRecharge.subtitle') }}</p>
</div>
<!-- 套餐选择 -->
<div class="plans-container">
<!-- 套餐卡片 1: 300积分 -->
<div class="plan-card" :class="{ active: selectedPlan === 'basic' }" @click="selectPlan('basic')">
<div class="plan-header">
<h3 class="plan-name">{{ t('pointsRecharge.plans.basic.name') }}</h3>
</div>
<div class="plan-price">
<span class="price-amount">${{ plans.basic.price }}</span>
<span class="price-period">/ {{ t('pointsRecharge.period') }}</span>
</div>
<div class="plan-points">
{{ plans.basic.points }} {{ t('pointsRecharge.points') }}
</div>
<div class="plan-validity">
{{ t('pointsRecharge.validity') }}: {{ plans.basic.validity }} {{ t('pointsRecharge.period') }}
</div>
<div class="plan-features">
<ul>
<li>{{ t('pointsRecharge.features.unlimitedAccess') }}</li>
<li>{{ t('pointsRecharge.features.prioritySupport') }}</li>
<li>{{ t('pointsRecharge.features.securePayment') }}</li>
</ul>
</div>
<button class="purchase-button" @click.stop="purchasePlan('basic')">
{{ t('pointsRecharge.purchase') }}
</button>
</div>
<!-- 套餐卡片 2: 1000积分 -->
<div class="plan-card premium" :class="{ active: selectedPlan === 'premium' }" @click="selectPlan('premium')">
<div class="plan-header">
<h3 class="plan-name">{{ t('pointsRecharge.plans.premium.name') }}</h3>
<div class="plan-badge">{{ t('pointsRecharge.plans.premium.badge') }}</div>
</div>
<div class="plan-price">
<span class="price-amount">${{ plans.premium.price }}</span>
<span class="price-period">/ {{ t('pointsRecharge.period') }}</span>
</div>
<div class="plan-points">
{{ plans.premium.points }} {{ t('pointsRecharge.points') }}
</div>
<div class="plan-validity">
{{ t('pointsRecharge.validity') }}: {{ plans.premium.validity }} {{ t('pointsRecharge.period') }}
</div>
<div class="plan-features">
<ul>
<li>{{ t('pointsRecharge.features.unlimitedAccess') }}</li>
<li>{{ t('pointsRecharge.features.prioritySupport') }}</li>
<li>{{ t('pointsRecharge.features.securePayment') }}</li>
<li>{{ t('pointsRecharge.features.extraBenefits') }}</li>
</ul>
</div>
<button class="purchase-button premium-button" @click.stop="purchasePlan('premium')">
{{ t('pointsRecharge.purchase') }}
</button>
</div>
</div>
<!-- 积分消耗规则 -->
<div class="points-rules">
<h2 class="rules-title">{{ t('pointsRecharge.rules.title') }}</h2>
<ul class="rules-list">
<li class="rule-item">{{ t('pointsRecharge.rules.rule1') }}</li>
<li class="rule-item">{{ t('pointsRecharge.rules.rule2') }}</li>
<li class="rule-item">{{ t('pointsRecharge.rules.rule3') }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import LanguageToggle from '@/components/ui/LanguageToggle.vue'
const { t } = useI18n()
const router = useRouter()
//
const selectedPlan = ref('basic')
//
const plans = ref({
basic: {
name: 'basic',
points: 300,
price: 30,
validity: '1'
},
premium: {
name: 'premium',
points: 1000,
price: 80,
validity: '1'
}
})
//
const goBack = () => {
router.back()
}
//
const selectPlan = (planName) => {
selectedPlan.value = planName
}
//
const purchasePlan = (planName) => {
console.log('购买套餐:', planName)
//
}
</script>
<style scoped>
.points-recharge-page {
width: 100%;
margin: 0;
padding: 40px 20px;
color: #f9fafb;
background-color: #111827;
min-height: 100vh;
}
/* 顶部操作栏 */
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
/* margin-bottom: 40px; */
width: 100%;
}
/* 返回按钮 */
.back-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: transparent;
color: #F9FAFB;
border: 2px solid #374151;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.back-button:hover {
background: #374151;
border-color: #8B5CF6;
transform: translateX(-4px);
}
/* 语言切换器 */
.language-switcher {
display: flex;
align-items: center;
}
/* 页面标题 */
.page-header {
text-align: center;
margin-bottom: 60px;
}
.page-title {
font-size: 3rem;
font-weight: 700;
margin: 0 0 12px 0;
background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.page-subtitle {
font-size: 1.25rem;
color: #9CA3AF;
margin: 0;
}
/* 套餐容器 */
.plans-container {
display: flex;
gap: 32px;
justify-content: center;
flex-wrap: wrap;
}
/* 套餐卡片 */
.plan-card {
background: #1F2937;
border: 2px solid #374151;
border-radius: 16px;
padding: 32px;
width: 350px;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* .plan-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #8B5CF6 0%, #EC4899 100%);
opacity: 0;
transition: opacity 0.3s ease;
} */
.plan-card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
border-color: #8B5CF6;
}
.plan-card.active {
border-color: #8B5CF6;
box-shadow: 0 20px 40px rgba(139, 92, 246, 0.2);
}
.plan-card.active::before {
opacity: 1;
}
/* 高级套餐样式 */
.plan-card.premium {
background: linear-gradient(135deg, #1F2937 0%, #374151 100%);
border-color: #8B5CF6;
}
/* 套餐头部 */
.plan-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.plan-name {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
color: #F9FAFB;
}
.plan-badge {
background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%);
color: white;
font-size: 0.875rem;
font-weight: 600;
padding: 4px 12px;
border-radius: 20px;
}
/* 价格样式 */
.plan-price {
margin-bottom: 16px;
}
.price-amount {
font-size: 3rem;
font-weight: 700;
color: #F9FAFB;
}
.price-period {
font-size: 1.125rem;
color: #9CA3AF;
margin-left: 4px;
}
/* 积分数量 */
.plan-points {
font-size: 1.5rem;
font-weight: 600;
color: #8B5CF6;
margin-bottom: 12px;
}
/* 有效期 */
.plan-validity {
font-size: 1rem;
color: #9CA3AF;
margin-bottom: 24px;
}
/* 套餐特性 */
.plan-features {
margin-bottom: auto;
margin-top: 24px;
}
.plan-features ul {
list-style: none;
padding: 0;
margin: 0;
}
.plan-features li {
display: flex;
align-items: center;
margin-bottom: 12px;
font-size: 0.9375rem;
color: #D1D5DB;
}
.plan-features li::before {
content: '✓';
color: #8B5CF6;
font-weight: bold;
margin-right: 10px;
font-size: 1.125rem;
}
/* 购买按钮 */
.purchase-button {
width: 100%;
padding: 16px;
margin-top: 32px;
background: linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%);
color: white;
font-size: 1.125rem;
font-weight: 600;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
align-self: stretch;
}
.purchase-button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(139, 92, 246, 0.4);
}
.purchase-button:active {
transform: translateY(0);
}
/* 高级套餐按钮 */
.purchase-button.premium-button {
background: linear-gradient(135deg, #EC4899 0%, #DB2777 100%);
}
.purchase-button.premium-button:hover {
box-shadow: 0 10px 25px rgba(236, 72, 153, 0.4);
}
/* 积分消耗规则样式 */
.points-rules {
margin-top: 60px;
padding: 32px;
background: rgba(31, 41, 55, 0.6);
border: 2px solid rgba(139, 92, 246, 0.2);
border-radius: 16px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
backdrop-filter: blur(10px);
}
.rules-title {
font-size: 1.5rem;
font-weight: 600;
color: #F9FAFB;
margin: 0 0 24px 0;
text-align: center;
}
.rules-list {
list-style: none;
padding: 0;
margin: 0;
}
.rule-item {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
font-size: 1.125rem;
color: #D1D5DB;
line-height: 1.6;
}
.rule-item:last-child {
margin-bottom: 0;
}
.rule-item::before {
content: '•';
color: #8B5CF6;
font-weight: bold;
margin-right: 12px;
font-size: 1.5rem;
line-height: 1.2;
flex-shrink: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.points-recharge-page {
padding: 20px 16px;
}
.page-title {
font-size: 2rem;
}
.page-subtitle {
font-size: 1rem;
}
.plans-container {
flex-direction: column;
align-items: center;
}
.plan-card {
width: 100%;
max-width: 350px;
}
/* 响应式积分规则样式 */
.points-rules {
padding: 24px;
margin-top: 40px;
}
.rules-title {
font-size: 1.25rem;
}
.rule-item {
font-size: 1rem;
}
}
</style>

View File

@ -171,8 +171,8 @@ const projectInfo = ref({});
//
const getGenerateCount = async ()=>{
const {data} = await modernHome.getModelLimits();
Limits.value.generateCount = data[0].model_count;
Limits.value.modelCount = data[1].model_count;
// Limits.value.generateCount = data[0].model_count;
// Limits.value.modelCount = data[1].model_count;
}
//
const cards = ref([

View File

@ -395,10 +395,11 @@
<div class="flex flex-col gap-4">
<h4 class="font-semibold text-gray-500 text-sm uppercase tracking-wider">Socials</h4>
<div class="flex gap-4">
<a href="#" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg></a>
<a href="#" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"></path><path d="M9.75 15.02v-4.04l5.5 2.02-5.5 2.02Z"></path></svg></a>
<a href="#" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"></line></svg></a>
<a href="#" class="text-gray-400 hover:text-white font-bold text-sm flex items-center h-[20px]">TikTok</a>
<a href="https://x.com/deotaland" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg></a>
<a href="https://www.youtube.com/@Deotaland" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"></path><path d="M9.75 15.02v-4.04l5.5 2.02-5.5 2.02Z"></path></svg></a>
<a href="https://www.instagram.com/deotaland/" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"></line></svg></a>
<a href="https://www.tiktok.com/@deotalandofficial" class="text-gray-400 hover:text-white font-bold text-sm flex items-center h-[20px]">TikTok</a>
<a href="https://discord.gg/feuFGPpY" class="text-gray-400 hover:text-white font-bold text-sm flex items-center h-[20px]">Discord</a>
</div>
</div>

View File

@ -21,7 +21,7 @@
<!-- Desktop Nav -->
<nav class="hidden md:flex items-center gap-8">
<a
<!-- <a
href="#"
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
>
@ -32,19 +32,37 @@
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
>
{{ t('nav.land') }}
</a> -->
<!-- <router-link
to="/points-recharge"
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
>
pricing
</router-link> -->
<a
href="#"
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
>
{{ t('nav.creator') }}
</a>
<a
href="#"
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
>
{{ t('nav.pricing') }}
{{ t('nav.done') }}
</a>
<a
href="#"
class="text-sm font-medium text-gray-300 hover:text-white transition-colors"
>
{{ t('nav.about') }}
</a>
</nav>
<!-- Right Action & Mobile Toggle -->
<div class="flex items-center gap-4">
<button
@click="$router.push('/czhome')"
@click="$router.push('czhome')"
class="hidden md:inline-flex items-center justify-center px-5 py-2 text-sm font-semibold text-black bg-white rounded-full hover:bg-gray-200 transition-colors cursor-pointer"
>
{{ t('hero.start') }}
@ -61,32 +79,50 @@
</div>
<!-- Mobile Menu -->
<div v-if="isMobileMenuOpen" class="absolute top-full left-0 right-0 border-t border-gray-800 p-6 flex flex-col gap-4 md:hidden">
<a
href="#"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ t('nav.creator') }}
</a>
<a
href="#"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ t('nav.land') }}
</a>
<a
href="#"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ t('nav.pricing') }}
</a>
<button
@click="$router.push('/czhome')"
class="w-full text-center py-3 text-black bg-white rounded-full font-bold cursor-pointer"
>
{{ t('hero.start') }}
</button>
</div>
<div v-if="isMobileMenuOpen" class="absolute top-full left-0 right-0 border-t border-gray-800 p-6 flex flex-col gap-4 md:hidden">
<!-- <a
href="#"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ t('nav.creator') }}
</a>
<a
href="#"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ t('nav.land') }}
</a>
<router-link
to="/points-recharge"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ t('nav.pricing') }}
</router-link> -->
<a
href="#"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ t('nav.creator') }}
</a>
<a
href="#"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ t('nav.done') }}
</a>
<a
href="#"
class="text-lg font-medium text-gray-300 hover:text-white"
>
{{ t('nav.about') }}
</a>
<button
@click="$router.push('czhome')"
class="w-full text-center py-3 text-black bg-white rounded-full font-bold cursor-pointer"
>
{{ t('hero.start') }}
</button>
</div>
</header>
<!-- Main Content -->
@ -186,7 +222,7 @@
{{ t('hero.explore') }}
</a>
<button
@click="$router.push('/czhome')"
@click="$router.push('czhome')"
class="px-9 py-4 rounded-full bg-white text-black font-semibold hover:bg-gray-200 transition-all text-lg shadow-[0_0_20px_rgba(255,255,255,0.3)] cursor-pointer"
>
{{ t('hero.start') }}
@ -232,7 +268,7 @@
</div>
<div>
<span class="block text-sm font-bold text-gray-200">{{ t('canvas.prompt') }}</span>
<span class="text-xs text-gray-500">"A cute pumpkin dog"</span>
<span class="text-xs text-gray-500">"A youthful and lovely girl"</span>
</div>
</div>
@ -243,7 +279,7 @@
</div>
<div class="flex-1">
<span class="block text-sm font-bold text-gray-200 mb-1">{{ t('canvas.reference') }}</span>
<div class="w-full h-24 rounded-lg overflow-hidden border border-gray-700 relative group">
<div class="w-full h-34 rounded-lg overflow-hidden border border-gray-700 relative group">
<img :src="refImage" alt="Dog Reference" class="w-full h-full object-cover opacity-80 group-hover:scale-110 transition-transform duration-500" />
<div class="absolute bottom-1 right-1 bg-black/60 px-1 rounded text-[10px] text-white">{{ t('canvas.referenceText') }}</div>
</div>
@ -460,10 +496,11 @@
<div class="flex flex-col gap-4">
<h4 class="font-semibold text-gray-500 text-sm uppercase tracking-wider">{{ t('footer.socials') }}</h4>
<div class="flex gap-4">
<a href="#" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg></a>
<a href="#" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"></path><path d="M9.75 15.02v-4.04l5.5 2.02-5.5 2.02Z"></path></svg></a>
<a href="#" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"></line></svg></a>
<a href="#" class="text-gray-400 hover:text-white font-bold text-sm flex items-center h-[20px]">TikTok</a>
<a href="https://x.com/deotaland" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg></a>
<a href="https://www.youtube.com/@Deotaland" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"></path><path d="M9.75 15.02v-4.04l5.5 2.02-5.5 2.02Z"></path></svg></a>
<a href="https://www.instagram.com/deotaland/" class="text-gray-400 hover:text-white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"></line></svg></a>
<a href="https://www.tiktok.com/@deotalandofficial" class="text-gray-400 hover:text-white font-bold text-sm flex items-center h-[20px]">TikTok</a>
<a href="https://discord.gg/feuFGPpY" class="text-gray-400 hover:text-white font-bold text-sm flex items-center h-[20px]">Discord</a>
</div>
</div>
@ -513,8 +550,8 @@ import MotionCom from './motion.vue'
// import spline from './spline.vue';
import { ref, onMounted, onUnmounted, computed } from 'vue';
import Bg from './bg.vue'
import dog from '@/assets/home/dog.webp'
import qdog from '@/assets/home/qdog.webp'
// import dog from '@/assets/home/dog.webp'
// import qdog from '@/assets/home/qdog.webp'
const center = 'https://draft-user.s3.us-east-2.amazonaws.com/images/c175585a-20c2-48b3-8939-32bbdb25814b.webp'
const center1 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/ecf39871-52c5-45ad-9f9e-6eafd838ce54.webp'
const center2 = 'https://draft-user.s3.us-east-2.amazonaws.com/images/f7c4454e-1781-448e-9c70-b087b64f380e.webp'
@ -541,9 +578,12 @@ const isMobile = ref(window.innerWidth < 768);
const i18n = {
en: {
nav: {
// creator: 'Creator',
// land: 'Land',
// pricing: 'Pricing',
creator: 'Creator',
land: 'Land',
pricing: 'Pricing'
done: 'D one',
about: 'About us'
},
hero: {
title: 'Create with Deotaland',
@ -597,9 +637,12 @@ const i18n = {
},
zh: {
nav: {
creator: '创作者',
land: '社区',
pricing: '价格'
// creator: '',
// land: '',
// pricing: '',
creator: 'Creator',
done: 'D one',
about: 'About us'
},
hero: {
title: '使用 Deotaland 创作',
@ -727,9 +770,9 @@ const navLinks = [
{ name: 'Pricing', href: '#' },
];
// Creation Canvas Images
const refImage = dog;
const model3dImage = qdog;
const realRobotImage = 'https://draft-user.s3.us-east-2.amazonaws.com/images/2e93945a-d20e-4a29-8c7f-d6e26260941b.webp';
const refImage = 'https://draft-user.s3.us-east-2.amazonaws.com/images/0185a1f7-563a-4af9-9569-65d81f710c52.webp';
const model3dImage = 'https://draft-user.s3.us-east-2.amazonaws.com/images/e5a1408b-b695-431f-b03e-0b9b06f1b82f.webp';
const realRobotImage = 'https://draft-user.s3.us-east-2.amazonaws.com/images/ce2f6979-4b3c-499f-b179-80abbf4d7431.webp';
// Robot Cards
const cards = [

View File

@ -0,0 +1,56 @@
import { clientApi, requestUtils } from '@deotaland/utils';
export class UserController {
constructor() {
}
//返回当前用户的邀请码列表及使用状态
async getCodes() {
return await requestUtils.common(clientApi.default.INVITE_CODES);
/*
返回示例
{
"code": 0,
"success": true,
"data": [
{
"inviteCode": "string",
"codeType": "string",
"isUsed": 1073741824,
"invitedUserNickname": "string",
"usedAt": "2025-12-18T06:33:05.386Z",
"createdAt": "2025-12-18T06:33:05.386Z"
}
],
"message": "操作成功"
}
*/
}
// 返回邀请的用户列表,包含奖励明细
async getInviteRecords() {
return await requestUtils.common(clientApi.default.INVITE_RECORDS);
/*
返回示例
{
"code": 0,
"success": true,
"data": [
{
"invitedUserId": 9007199254740991,
"invitedUserNickname": "string",
"invitedUserAvatar": "string",
"invitedAt": "2025-12-18T06:33:05.383Z",
"rewards": [
{
"rewardType": 1073741824,
"rewardTypeName": "string",
"rewardScore": 0,
"grantedAt": "2025-12-18T06:33:05.383Z"
}
]
}
],
"message": "操作成功"
}
*/
}
}

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"import": "./dist/index.es.js",
"require": "./dist/index.umd.js"
},
"./dist/*": "./dist/*",
"./style.css": "./dist/ui.css"
},
"scripts": {

View File

@ -1,153 +0,0 @@
<template>
<el-button
:type="buttonType"
:size="buttonSize"
:disabled="disabled"
:loading="loading"
:plain="plain"
:round="round"
:circle="circle"
@click="handleClick"
:class="buttonClasses"
>
<slot>{{ text }}</slot>
</el-button>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
text: {
type: String,
default: 'Button'
},
type: {
type: String,
default: 'primary',
validator: (value) => ['primary', 'success', 'warning', 'danger', 'info', 'text'].includes(value)
},
size: {
type: String,
default: 'default',
validator: (value) => ['large', 'default', 'small'].includes(value)
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
plain: {
type: Boolean,
default: false
},
round: {
type: Boolean,
default: false
},
circle: {
type: Boolean,
default: false
},
responsive: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['click'])
const buttonType = computed(() => props.type)
const buttonSize = computed(() => props.size)
const buttonClasses = computed(() => {
const classes = ['deotaland-button']
if (props.responsive) {
classes.push('responsive-button')
}
return classes
})
const handleClick = (event) => {
if (!props.disabled && !props.loading) {
emit('click', event)
}
}
</script>
<style scoped>
.deotaland-button {
transition: all 0.3s ease;
font-weight: 500;
}
.responsive-button {
/* 移动端优化 */
min-height: 44px;
padding: 12px 20px;
font-size: 14px;
}
/* 平板端适配 */
@media (min-width: 768px) and (max-width: 1024px) {
.responsive-button {
min-height: 40px;
padding: 10px 18px;
font-size: 15px;
}
}
/* 桌面端适配 */
@media (min-width: 1024px) {
.responsive-button {
min-height: 36px;
padding: 8px 16px;
font-size: 16px;
cursor: pointer;
}
.responsive-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
}
}
.responsive-button:active:not(:disabled) {
transform: translateY(0);
}
/* 加载状态样式 */
.responsive-button .is-loading {
display: inline-flex;
align-items: center;
}
/* 圆角按钮特殊样式 */
.responsive-button.is-round {
border-radius: 20px;
}
@media (min-width: 1024px) {
.responsive-button.is-round {
border-radius: 22px;
}
}
/* 圆形按钮特殊样式 */
.responsive-button.is-circle {
border-radius: 50%;
width: 44px;
height: 44px;
padding: 0;
}
@media (min-width: 1024px) {
.responsive-button.is-circle {
width: 36px;
height: 36px;
}
}
</style>

View File

@ -1,236 +0,0 @@
<template>
<el-card
:class="cardClasses"
:shadow="shadow"
:body-style="bodyStyle"
@click="handleClick"
>
<template #header v-if="hasHeader">
<div class="card-header">
<div class="header-left">
<slot name="header">
<span class="header-title">{{ title }}</span>
</slot>
</div>
<div class="header-right">
<slot name="extra"></slot>
</div>
</div>
</template>
<div class="card-content">
<slot></slot>
</div>
<template #footer v-if="hasFooter">
<slot name="footer"></slot>
</template>
</el-card>
</template>
<script setup>
import { computed, useSlots } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
shadow: {
type: String,
default: 'hover',
validator: (value) => ['always', 'hover', 'never'].includes(value)
},
clickable: {
type: Boolean,
default: false
},
responsive: {
type: Boolean,
default: true
},
size: {
type: String,
default: 'default',
validator: (value) => ['large', 'default', 'small'].includes(value)
},
variant: {
type: String,
default: 'default',
validator: (value) => ['default', 'borderless', 'elevation'].includes(value)
}
})
const emit = defineEmits(['click'])
const slots = useSlots()
const hasHeader = computed(() => {
return props.title || !!slots.header || !!slots.extra
})
const hasFooter = computed(() => {
return !!slots.footer
})
const cardClasses = computed(() => {
const classes = ['deotaland-card']
if (props.responsive) {
classes.push('responsive-card')
}
if (props.clickable) {
classes.push('clickable-card')
}
if (props.variant !== 'default') {
classes.push(`card-${props.variant}`)
}
classes.push(`card-${props.size}`)
return classes
})
const bodyStyle = computed(() => {
const style = {}
if (props.responsive) {
style.padding = '16px'
}
return style
})
const handleClick = (event) => {
if (props.clickable) {
emit('click', event)
}
}
</script>
<style scoped>
.deotaland-card {
transition: all 0.3s ease;
border-radius: 8px;
overflow: hidden;
}
.responsive-card {
/* 移动端优化 */
margin-bottom: 16px;
min-height: 120px;
}
/* 平板端适配 */
@media (min-width: 768px) and (max-width: 1024px) {
.responsive-card {
margin-bottom: 20px;
min-height: 140px;
}
}
/* 桌面端适配 */
@media (min-width: 1024px) {
.responsive-card {
margin-bottom: 24px;
min-height: 160px;
}
.clickable-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
}
.clickable-card {
cursor: pointer;
}
.clickable-card:active {
transform: translateY(0);
}
/* 卡片头部样式 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.header-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
/* 不同尺寸适配 */
.card-large {
border-radius: 12px;
}
.card-large .card-content {
padding: 24px;
}
.card-small {
border-radius: 6px;
}
.card-small .card-content {
padding: 12px;
}
/* 无边框卡片 */
.card-borderless {
border: none;
background: transparent;
box-shadow: none;
}
.card-borderless .el-card__body {
padding: 0;
}
/* 阴影提升卡片 */
.card-elevation {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-elevation:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
@media (min-width: 1024px) {
.card-elevation {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.card-elevation:hover {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
}
}
/* 内容区域样式 */
.card-content {
position: relative;
}
/* 响应式图片适配 */
:deep(.card-content img) {
max-width: 100%;
height: auto;
border-radius: 4px;
}
/* 移动端触摸优化 */
@media (max-width: 768px) {
.clickable-card {
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
}
</style>

View File

@ -1,351 +0,0 @@
<template>
<div v-if="visible" :class="loadingClasses" :style="loadingStyle">
<div class="loading-container">
<div class="loading-spinner">
<div class="spinner-ring" :style="spinnerStyle"></div>
</div>
<div v-if="text" class="loading-text">
{{ text }}
</div>
<div v-if="description" class="loading-description">
{{ description }}
</div>
</div>
<div v-if="mask" class="loading-mask"></div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: true
},
text: {
type: String,
default: '加载中...'
},
description: {
type: String,
default: ''
},
size: {
type: String,
default: 'default',
validator: (value) => ['large', 'default', 'small'].includes(value)
},
color: {
type: String,
default: '#409eff'
},
background: {
type: String,
default: 'rgba(255, 255, 255, 0.9)'
},
mask: {
type: Boolean,
default: true
},
fullscreen: {
type: Boolean,
default: false
},
responsive: {
type: Boolean,
default: true
}
})
const loadingClasses = computed(() => {
const classes = ['deotaland-loading']
if (props.responsive) {
classes.push('responsive-loading')
}
if (props.fullscreen) {
classes.push('fullscreen-loading')
}
classes.push(`loading-${props.size}`)
return classes
})
const loadingStyle = computed(() => {
const style = {}
if (props.fullscreen) {
style.position = 'fixed'
style.top = '0'
style.left = '0'
style.width = '100vw'
style.height = '100vh'
style.zIndex = '9999'
}
return style
})
const spinnerStyle = computed(() => {
const style = {}
switch (props.size) {
case 'large':
style.width = '60px'
style.height = '60px'
style.borderWidth = '6px'
break
case 'small':
style.width = '24px'
style.height = '24px'
style.borderWidth = '2px'
break
default:
style.width = '40px'
style.height = '40px'
style.borderWidth = '4px'
}
style.borderColor = `${props.color}20`
style.borderTopColor = props.color
return style
})
</script>
<style scoped>
.deotaland-loading {
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.responsive-loading {
/* 移动端优化 */
}
.fullscreen-loading {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(4px);
z-index: 9999;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
position: relative;
z-index: 1;
}
.loading-spinner {
position: relative;
display: inline-block;
}
.spinner-ring {
border-radius: 50%;
border-style: solid;
animation: spin 1s linear infinite;
}
/* 加载文字样式 */
.loading-text {
margin-top: 16px;
font-size: 16px;
color: #606266;
font-weight: 500;
}
.loading-description {
margin-top: 8px;
font-size: 14px;
color: #909399;
max-width: 240px;
word-wrap: break-word;
}
/* 遮罩层 */
.loading-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: v-bind(background);
backdrop-filter: blur(2px);
border-radius: inherit;
}
/* 动画 */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 不同尺寸适配 */
.loading-large .loading-container {
padding: 32px;
}
.loading-large .loading-text {
font-size: 18px;
margin-top: 20px;
}
.loading-large .loading-description {
font-size: 16px;
margin-top: 12px;
max-width: 280px;
}
.loading-small .loading-container {
padding: 12px;
}
.loading-small .loading-text {
font-size: 14px;
margin-top: 12px;
}
.loading-small .loading-description {
font-size: 12px;
margin-top: 8px;
max-width: 200px;
}
/* 移动端适配 */
@media (max-width: 768px) {
.responsive-loading {
padding: 20px;
}
.responsive-loading .loading-text {
font-size: 16px;
margin-top: 16px;
}
.responsive-loading .loading-description {
font-size: 14px;
margin-top: 8px;
max-width: 280px;
}
.fullscreen-loading {
padding: 40px 20px;
}
.fullscreen-loading .loading-text {
font-size: 18px;
margin-top: 24px;
}
.fullscreen-loading .loading-description {
font-size: 16px;
margin-top: 12px;
max-width: 320px;
}
}
/* 平板端适配 */
@media (min-width: 768px) and (max-width: 1024px) {
.responsive-loading {
padding: 24px;
}
.fullscreen-loading {
padding: 60px 40px;
}
}
/* 桌面端适配 */
@media (min-width: 1024px) {
.responsive-loading {
padding: 32px;
}
.fullscreen-loading {
padding: 80px 60px;
}
.responsive-loading .loading-container {
transition: transform 0.2s ease;
}
.responsive-loading:hover .loading-container {
transform: scale(1.02);
}
}
/* 主题适配 */
:global(.dark) .fullscreen-loading {
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
}
:global(.dark) .loading-text {
color: #e4e7ed;
}
:global(.dark) .loading-description {
color: #a3a6ad;
}
/* 高对比度支持 */
@media (prefers-contrast: high) {
.loading-mask {
background: rgba(255, 255, 255, 0.95) !important;
border: 2px solid #000;
}
.loading-text,
.loading-description {
color: #000 !important;
font-weight: 600;
}
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
.spinner-ring {
animation: none;
border-top-color: transparent;
background: conic-gradient(from 0deg, v-bind(color) 0deg, v-bind(color) 90deg, transparent 90deg, transparent 180deg, v-bind(color) 180deg, v-bind(color) 270deg, transparent 270deg);
}
}
/* 触摸设备优化 */
@media (max-width: 768px) {
.fullscreen-loading {
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
}
/* 文字选择优化 */
.loading-text,
.loading-description {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style>

View File

@ -1,8 +1,13 @@
<template>
<div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': true }">
</div>
</template>
<script>
export default {
name: 'LoadingCom',
}
</script>
<style>
/* 侧边栏过渡动画蒙层 */
.sidebar-overlay {
@ -59,6 +64,15 @@
100% { transform: rotate(360deg); }
}
/* 加载文本样式 */
.loading-text {
margin-top: 80px;
color: #714DC7;
font-size: 16px;
font-weight: 500;
text-align: center;
}
/* 暗色主题下的蒙层效果 */
html.dark .sidebar-overlay {
background: rgba(31, 41, 55, 0.85);
@ -75,4 +89,8 @@ html.dark .sidebar-overlay::after {
border-bottom: 2px solid rgba(167, 139, 250, 0.4);
}
html.dark .loading-text {
color: #A78BFA;
}
</style>

View File

@ -1,357 +0,0 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="title"
:width="dialogWidth"
:top="dialogTop"
:class="modalClasses"
:destroy-on-close="destroyOnClose"
:close-on-click-modal="closeOnClickModal"
:close-on-press-escape="closeOnPressEscape"
:show-close="showClose"
@close="handleClose"
@open="handleOpen"
>
<template #header>
<div class="modal-header">
<slot name="header">
<span class="modal-title">{{ title }}</span>
</slot>
</div>
</template>
<div class="modal-content">
<slot></slot>
</div>
<template #footer v-if="showFooter">
<div class="modal-footer">
<slot name="footer">
<el-button
v-if="showCancel"
:size="buttonSize"
@click="handleCancel"
>
{{ cancelText }}
</el-button>
<el-button
v-if="showConfirm"
:type="confirmType"
:size="buttonSize"
:loading="confirmLoading"
@click="handleConfirm"
>
{{ confirmText }}
</el-button>
</slot>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
required: true
},
title: {
type: String,
default: ''
},
width: {
type: [String, Number],
default: '50%'
},
top: {
type: String,
default: '15vh'
},
responsive: {
type: Boolean,
default: true
},
showFooter: {
type: Boolean,
default: true
},
showCancel: {
type: Boolean,
default: true
},
showConfirm: {
type: Boolean,
default: true
},
showClose: {
type: Boolean,
default: true
},
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
},
confirmType: {
type: String,
default: 'primary',
validator: (value) => ['primary', 'success', 'warning', 'danger', 'info'].includes(value)
},
confirmLoading: {
type: Boolean,
default: false
},
destroyOnClose: {
type: Boolean,
default: false
},
closeOnClickModal: {
type: Boolean,
default: false
},
closeOnPressEscape: {
type: Boolean,
default: true
},
size: {
type: String,
default: 'default',
validator: (value) => ['large', 'default', 'small'].includes(value)
}
})
const emit = defineEmits(['update:modelValue', 'close', 'cancel', 'confirm', 'open'])
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const dialogWidth = computed(() => {
if (!props.responsive) {
return props.width
}
return props.width
})
const dialogTop = computed(() => {
return props.top
})
const modalClasses = computed(() => {
const classes = ['deotaland-modal']
if (props.responsive) {
classes.push('responsive-modal')
}
classes.push(`modal-${props.size}`)
return classes
})
const buttonSize = computed(() => {
if (!props.responsive) {
return props.size
}
return props.size
})
const handleClose = () => {
emit('close')
}
const handleCancel = () => {
emit('cancel')
dialogVisible.value = false
}
const handleConfirm = () => {
emit('confirm')
}
const handleOpen = () => {
emit('open')
}
</script>
<style scoped>
.deotaland-modal {
transition: all 0.3s ease;
}
.responsive-modal {
/* 移动端全屏模态框 */
}
@media (max-width: 768px) {
.responsive-modal {
--el-dialog-width: 90% !important;
margin: 0 auto;
top: 5vh !important;
bottom: 5vh !important;
max-height: 90vh;
overflow: hidden;
}
.responsive-modal :deep(.el-dialog) {
margin: 0 auto;
height: auto;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.responsive-modal :deep(.el-dialog__body) {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.responsive-modal :deep(.el-dialog__footer) {
padding: 16px;
border-top: 1px solid #ebeef5;
flex-shrink: 0;
}
}
/* 平板端适配 */
@media (min-width: 768px) and (max-width: 1024px) {
.responsive-modal {
--el-dialog-width: 70% !important;
top: 10vh !important;
}
.responsive-modal :deep(.el-dialog__body) {
padding: 20px;
}
}
/* 桌面端适配 */
@media (min-width: 1024px) {
.responsive-modal {
--el-dialog-width: 50% !important;
top: 15vh !important;
}
.responsive-modal :deep(.el-dialog__body) {
padding: 24px;
}
.responsive-modal :deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.responsive-modal :deep(.el-dialog__header) {
padding: 20px 24px;
background: #fafafa;
border-bottom: 1px solid #ebeef5;
}
}
/* 模态框头部样式 */
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
/* 模态框内容样式 */
.modal-content {
color: #606266;
line-height: 1.6;
}
/* 模态框底部样式 */
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.modal-footer {
flex-direction: column-reverse;
}
.modal-footer .el-button {
width: 100%;
margin-bottom: 8px;
}
.modal-footer .el-button:last-child {
margin-bottom: 0;
}
}
/* 不同尺寸适配 */
.modal-large :deep(.el-dialog) {
border-radius: 12px;
}
.modal-large :deep(.el-dialog__body) {
padding: 32px;
}
.modal-small :deep(.el-dialog) {
border-radius: 6px;
}
.modal-small :deep(.el-dialog__body) {
padding: 16px;
}
/* 动画优化 */
@media (prefers-reduced-motion: no-preference) {
.deotaland-modal :deep(.el-dialog) {
animation: modal-enter 0.3s ease-out;
}
}
@keyframes modal-enter {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* 触摸设备优化 */
@media (max-width: 768px) {
.responsive-modal :deep(.el-button) {
min-height: 44px;
font-size: 16px;
-webkit-tap-highlight-color: transparent;
}
}
/* 高对比度支持 */
@media (prefers-contrast: high) {
.deotaland-modal :deep(.el-dialog) {
border: 2px solid #000;
}
.deotaland-modal :deep(.el-dialog__header) {
background: #fff;
border-bottom: 2px solid #000;
}
}
</style>

View File

@ -1,40 +1,34 @@
// UI组件库入口文件
import Button from './components/Button.vue'
import Card from './components/Card.vue'
import Modal from './components/Modal.vue'
import Loading from './components/Loading.vue'
import LoadingCom from './components/LoadingCom/index.vue'
import './style.css'
// 创建带有Dt前缀的组件
const DtButton = Object.assign({}, Button, { name: 'DtButton' })
const DtCard = Object.assign({}, Card, { name: 'DtCard' })
const DtModal = Object.assign({}, Modal, { name: 'DtModal' })
const DtLoading = Object.assign({}, Loading, { name: 'DtLoading' })
const DtLoadingCom = {
...LoadingCom,
name: 'DtLoadingCom',
install(app) {
app.component('DtLoadingCom', DtLoadingCom)
}
}
// 组件列表
const components = [
DtButton,
DtCard,
DtModal,
DtLoading
DtLoadingCom
]
// 导出组件
export {
DtButton,
DtCard,
DtModal,
DtLoading,
// 同时导出原名称以保持兼容性
Button,
Card,
Modal,
Loading
DtLoadingCom
}
// 批量注册组件的函数
export function registerComponents(app) {
components.forEach(component => {
app.component(component.name, component)
if (component.install) {
app.use(component)
} else {
app.component(component.name, component)
}
})
}

19
packages/ui/src/style.css Normal file
View File

@ -0,0 +1,19 @@
/* Deotaland UI 组件库全局样式 */
/* CSS 变量定义 */
:root {
--dt-primary-color: #6B46C1;
--dt-secondary-color: #A78BFA;
--dt-text-color: #1F2937;
--dt-bg-color: #F3F4F6;
--dt-border-radius: 8px;
--dt-transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 暗色主题变量 */
html.dark {
--dt-primary-color: #A78BFA;
--dt-secondary-color: #6B46C1;
--dt-text-color: #F3F4F6;
--dt-bg-color: #1F2937;
}

View File

@ -1,6 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
@ -26,6 +27,10 @@ export default defineConfig({
globals: {
vue: 'Vue',
'element-plus': 'ElementPlus'
},
assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') return 'ui.css'
return assetInfo.name
}
}
}

View File

@ -4,6 +4,7 @@ import gemini from './gemini.js';
import meshy from './meshy.js';
import logistics from './logistics.js';
import user from './user.js';
import permission from './permission.js';
export default {
...login,
...order,
@ -11,4 +12,5 @@ export default {
...meshy,
...logistics,
...user,
...permission,
};

View File

@ -0,0 +1,17 @@
const permission = {
updateRole: {url: '/api-base/admin/role', method: 'PUT'},//修改角色
addRole: {url: '/api-base/admin/role', method: 'POST'},//新增角色
assignRoleToUser: {url: '/api-base/admin/role/assign/{userId}', method: 'POST'},//为用户分配角色
getRoleDetail: {url: '/api-base/admin/role/{roleId}', method: 'GET'},//查询角色详情
getRolesByUserId: {url: '/api-base/admin/role/user/{userId}', method: 'GET'},//根据用户ID查询角色列表
getRoleList: {url: '/api-base/admin/role/list', method: 'GET'},//查询角色列表
deleteRole: {url: '/api-base/admin/role/{roleIds}', method: 'DELETE'},//删除角色
updatePermission: {url: '/api-base/admin/permission', method: 'PUT'},//修改权限
addPermission: {url: '/api-base/admin/permission', method: 'POST'},//新增权限
assignPermissionToRole: {url: '/api-base/admin/permission/assign/{roleId}', method: 'POST'},//为角色分配权限
getPermissionDetail: {url: '/api-base/admin/permission/{permissionId}', method: 'GET'},//查询权限详情
deletePermission: {url: '/api-base/admin/permission/{permissionId}', method: 'DELETE'},//删除权限
getPermissionList: {url: '/api-base/admin/permission/list', method: 'GET'},//查询权限列表
getPermissionCodesByUserId: {url: '/api-base/admin/permission/codes/user/{userId}', method: 'GET'},//根据用户ID查询权限代码集合
}
export default permission;

View File

@ -1,8 +1,12 @@
const order = {
getUsersList:{url:'/api-base/admin/user/list',method:'GET'},//分页查询C端用户列表支持按昵称、邮箱、状态筛选
getUsersList:{url:'/api-base/admin/user/list',method:'GET',isLoading:true},//分页查询C端用户列表支持按昵称、邮箱、状态筛选
getUsersinvites:{url:'/api-base/admin/user/USERID/invites',method:'GET'},//分页查询指定用户邀请的人列表
updateUserStatus:{url:'/api-base/admin/user/USERID/status',method:'PUT'},//修改用户状态active/disabled
updateUserName:{url:'/api-base/admin/user/USERID',method:'PUT'},//编辑用户基本信息(昵称)
getUserDetail:{url:'/api-base/admin/user/USERID',method:'GET'},//根据用户ID查询用户详情
updateUserStatus:{url:'/api-base/admin/user/USERID/status',method:'PUT',isLoading:true},//修改用户状态active/disabled
updateUserName:{url:'/api-base/admin/user/USERID',method:'PUT',isLoading:true},//编辑用户基本信息(昵称)
getUserDetail:{url:'/api-base/admin/user/USERID',method:'GET',isLoading:true},//根据用户ID查询用户详情
changeRole:{url:'/api-base/admin/user/USERID/role',method:'PUT',isLoading:true},//变更用户角色(候补会员/免费会员/达人候补升级时自动赠送300积分
getInviteCodeList:{url:'/api-base/admin/invite-code/list',method:'GET'},//分页查询邀请码列表
generateInviteCode:{url:'/api-base/admin/invite-code/generate',method:'POST',isLoading:true},//为用户生成邀请码(支持批量生成)
deleteInviteCode:{url:'/api-base/admin/invite-code/CODEID',method:'DELETE',isLoading:true},//根据邀请码ID删除邀请码
}
export default order;

View File

@ -1,5 +1,7 @@
const login = {
MODEL_LIMITS:{url:'/api-core/front/user/model-limits',method:'GET'},// 模型限制
USER_STATISTICS:{url:'/api-core/front/user/statistics',method:'GET'},// 用户统计
INVITE_CODES:{url:'/api-base/user/invite/codes',method:'GET'},// 返回当前用户的邀请码列表及使用状态
INVITE_RECORDS:{url:'/api-base/user/invite/records',method:'GET'},// 返回邀请的用户列表,包含奖励明细
}
export default login;

View File

@ -11,6 +11,7 @@ import * as dateUtils from './utils/date.js'
import * as fileUtils from './utils/file.js'
import * as validateUtils from './utils/validate.js'
import * as formatUtils from './utils/format.js'
import * as environmentUtils from './utils/environment.js'
import { request as requestUtils } from './utils/request.js'
import * as adminApi from './api/FrontendDesigner/index.js';
import * as clientApi from './api/frontend/index.js';
@ -31,6 +32,7 @@ const deotalandUtils = {
file: fileUtils,
validate: validateUtils,
format: formatUtils,
environment: environmentUtils,
request: requestUtils,
FileServer,
adminApi,
@ -64,6 +66,7 @@ export {
FileServer,
validateUtils,
formatUtils,
environmentUtils,
requestUtils,
adminApi,
clientApi,

View File

@ -297,15 +297,15 @@ export class FileServer {
let file = await this.fileToBlob(url);//将文件或者base64文件转为blob对象
// 检查文件大小如果超过10MB则进行压缩
const maxSizeInBytes = 10 * 1024 * 1024; // 10MB
if (file.size > maxSizeInBytes) {
// if (file.size > maxSizeInBytes) {
if (true) {
try {
console.log(`文件大小为 ${(file.size / 1024 / 1024).toFixed(2)}MB超过10MB限制开始压缩...`);
console.log(`文件大小为 ${(file.size / 1024 / 1024).toFixed(2)}MB,开始压缩...`);
// 将Blob转换为File对象以便压缩
const fileName = this.extractFileName(url);
const fileObject = new File([file], fileName, { type: file.type });
const compressedFile = await this.compressFile(fileObject, 0.7); // 使用0.7质量压缩
const compressedFile = await this.compressFile(fileObject, 0.2); // 使用0.2质量压缩
if (compressedFile && compressedFile.length < file.size) {
// 将压缩后的base64转换回Blob
const response = await fetch(compressedFile);
@ -317,7 +317,6 @@ export class FileServer {
console.warn('文件压缩失败,使用原文件上传:', error.message);
}
}
// const formData = new FormData();
// 从URL中提取文件名如果没有则使用默认文件名
const fileName = this.extractFileName(url);

View File

@ -169,7 +169,6 @@ export class GiminiServer extends FileServer {
if (images.length > 5) {
reject(`参考图片数量不能超过5张`);
}
// 处理多个参考图片
const imageParts = await Promise.all(images.map(async image =>{
return await this.dataUrlToGenerativePart(image,'url');
@ -186,7 +185,7 @@ export class GiminiServer extends FileServer {
// "aspectRatio": "9:16"
// },
"aspect_ratio": "16:9",
"model": "gemini-3-pro-image-preview",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image",
"model": "doubao",//models/gemini-3-pro-image-preview/"gemini-2.5-flash-image"/"doubao",
"location": "global",
"vertexai": true,
...config,
@ -195,6 +194,9 @@ export class GiminiServer extends FileServer {
{ text: promptStr }
]
}
if(params.model=='doubao'){
params.aspect_ratio = '768x1344';
}
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":"任务已提交,正在处理"}}

View File

@ -0,0 +1,262 @@
/**
* 环境检测工具函数
* 用于判断当前运行环境是国内还是国外
*/
// 缓存地理位置信息,避免重复请求
const locationCache = {
data: null,
timestamp: 0,
ttl: 24 * 60 * 60 * 1000 // 24小时缓存
};
/**
* 获取浏览器语言
*/
export function getBrowserLanguage() {
return navigator.language || navigator.userLanguage || 'en';
}
/**
* 判断是否为中文语言环境
*/
export function isChineseLanguage() {
const language = getBrowserLanguage();
return language.startsWith('zh') || language.includes('cn');
}
/**
* 获取当前时区
*/
export function getTimezone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
/**
* 判断是否为中国时区
*/
export function isChinaTimezone() {
const timezone = getTimezone();
const chinaTimezones = [
'Asia/Shanghai',
'Asia/Chongqing',
'Asia/Beijing',
'Asia/Harbin',
'Asia/Urumqi'
];
return chinaTimezones.includes(timezone);
}
/**
* 通过IP获取地理位置信息
* @param {Object} options - 配置选项
* @param {number} options.timeout - 请求超时时间毫秒
* @param {string} options.api - 使用的API服务
* @returns {Promise<Object>} 地理位置信息
*/
export async function getLocationByIP(options = {}) {
const { timeout = 3000, api = 'ipapi' } = options;
const apis = {
ipapi: {
url: 'https://ipapi.co/json/',
parser: (data) => ({
country: data.country_name,
countryCode: data.country_code,
city: data.city,
region: data.region,
ip: data.ip,
isChina: data.country_code === 'CN'
})
},
ipinfo: {
url: 'https://ipinfo.io/json',
parser: (data) => ({
country: data.country,
countryCode: data.country,
city: data.city,
region: data.region,
ip: data.ip,
isChina: data.country === 'CN'
})
},
freegeoip: {
url: 'https://freegeoip.app/json/',
parser: (data) => ({
country: data.country_name,
countryCode: data.country_code,
city: data.city,
region: data.region_name,
ip: data.ip,
isChina: data.country_code === 'CN'
})
}
};
const selectedApi = apis[api] || apis.ipapi;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(selectedApi.url, {
signal: controller.signal,
headers: {
'Accept': 'application/json'
}
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return selectedApi.parser(data);
} catch (error) {
console.warn('IP地理位置检测失败:', error.message);
return null;
}
}
/**
* 综合判断环境是否为国内
* @param {Object} options - 配置选项
* @returns {Promise<Object>} 环境检测结果
*/
export async function detectEnvironment(options = {}) {
const { useCache = true, ipDetection = true } = options;
// 检查缓存
if (useCache && locationCache.data && (Date.now() - locationCache.timestamp) < locationCache.ttl) {
return locationCache.data;
}
// 1. 获取浏览器语言(最快,无网络请求)
const browserLanguage = getBrowserLanguage();
const isChineseLanguageEnv = isChineseLanguage();
// 2. 获取时区(快速,无网络请求)
const timezone = getTimezone();
const isChinaTimezoneEnv = isChinaTimezone();
// 3. IP地理位置判断最准确但需要网络请求
let ipLocationInfo = null;
let isChinaIP = false;
if (ipDetection) {
ipLocationInfo = await getLocationByIP(options);
isChinaIP = ipLocationInfo ? ipLocationInfo.isChina : false;
}
// 4. 综合决策
const isDomestic = isChinaIP || (isChineseLanguageEnv && isChinaTimezoneEnv);
// 5. 确定置信度
let confidence = 'low';
if (isChinaIP) {
confidence = 'high';
} else if (isChineseLanguageEnv && isChinaTimezoneEnv) {
confidence = 'medium';
}
const result = {
isDomestic,
confidence,
methods: {
ip: isChinaIP,
language: isChineseLanguageEnv,
timezone: isChinaTimezoneEnv
},
browserLanguage,
timezone,
ipLocationInfo
};
// 更新缓存
if (useCache) {
locationCache.data = result;
locationCache.timestamp = Date.now();
}
return result;
}
/**
* 快速判断是否为国内环境仅使用本地信息无网络请求
* @returns {Object} 快速检测结果
*/
export function quickDetectEnvironment() {
const browserLanguage = getBrowserLanguage();
const isChineseLanguageEnv = isChineseLanguage();
const timezone = getTimezone();
const isChinaTimezoneEnv = isChinaTimezone();
const isDomestic = isChineseLanguageEnv && isChinaTimezoneEnv;
return {
isDomestic,
confidence: 'medium',
methods: {
language: isChineseLanguageEnv,
timezone: isChinaTimezoneEnv
},
browserLanguage,
timezone
};
}
/**
* 清除地理位置缓存
*/
export function clearLocationCache() {
locationCache.data = null;
locationCache.timestamp = 0;
}
/**
* 设置缓存过期时间
* @param {number} ttl - 缓存时间毫秒
*/
export function setCacheTTL(ttl) {
locationCache.ttl = ttl;
}
/**
* 获取当前环境信息同步版本仅使用本地信息
*/
export function getEnvironmentInfo() {
return {
browserLanguage: getBrowserLanguage(),
timezone: getTimezone(),
isChineseLanguage: isChineseLanguage(),
isChinaTimezone: isChinaTimezone(),
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
languages: navigator.languages || []
};
}
/**
* 检查是否为移动设备
*/
export function isMobileDevice() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
/**
* 检查是否为桌面设备
*/
export function isDesktopDevice() {
return !isMobileDevice();
}
/**
* 检查是否为平板设备
*/
export function isTabletDevice() {
const userAgent = navigator.userAgent.toLowerCase();
return /ipad|android(?!.*mobile)/i.test(userAgent);
}

View File

@ -58,6 +58,9 @@ service.interceptors.response.use(
if(res.code&&res.code==200){
return res;
}
if(res.code==1004){//重定向登录
window?.Redirectlogin()
}
if(!res.success){
window?.setElMessage({
message: res.message,
@ -92,7 +95,6 @@ service.interceptors.response.use(
// 请求已发送但没有收到响应
message = '网络连接失败,请检查网络';
}
console.error(message);
return Promise.reject(error);
}
@ -186,6 +188,7 @@ export const request = {
requestConfig.data = data;
}
if(config.isLoading&&window.setElLoading){
closeMethods = window.setElLoading(config.isqp)
}
return service(requestConfig);