最新功能
This commit is contained in:
parent
d0ad3fcad9
commit
0b6ec14230
|
|
@ -0,0 +1,24 @@
|
|||
## 实现计划
|
||||
|
||||
### 1. 更新国际化翻译
|
||||
- 在 `src/locales/index.js` 中添加积分消耗规则相关翻译
|
||||
- 包含中英文翻译
|
||||
- 添加以下翻译键:
|
||||
- 消耗规则标题
|
||||
- 消耗规则表格标题(行为、积分消耗)
|
||||
- 具体消耗规则(生成1张图片:1积分,生成1个3D模型:30积分)
|
||||
|
||||
### 2. 修改用户中心组件
|
||||
- 在 `src/views/user/index.vue` 的积分列表后添加新的消耗规则区域
|
||||
- 创建包含指定数据的消耗规则表格
|
||||
- 使用现有样式模式保持设计一致性
|
||||
- 支持响应式设计
|
||||
|
||||
### 3. 更新样式
|
||||
- 为新的消耗规则区域添加CSS样式
|
||||
- 确保与现有积分区域样式一致
|
||||
- 添加与现有设计匹配的悬停效果和过渡
|
||||
- 支持明暗主题
|
||||
|
||||
## 预期结果
|
||||
用户中心积分区域将新增"积分消耗规则"部分,以表格形式展示指定的消耗规则,设计风格与现有界面保持一致。
|
||||
|
|
@ -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 中的实际用户角色
|
||||
- 实现保持与现有代码库的向后兼容性
|
||||
- 响应式设计在所有屏幕尺寸下都能保持
|
||||
- 样式遵循项目现有的设计系统
|
||||
|
||||
该方案将为免费会员和达人会员创建清晰的视觉区分,通过提供即时的角色识别来改善用户体验。
|
||||
|
|
@ -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` 处理异步操作
|
||||
|
|
@ -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
|
||||
|
|
@ -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`从计算属性改为响应式数据
|
||||
- 添加数据获取逻辑
|
||||
- 调整数据结构映射
|
||||
|
|
@ -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` - 创建佣金管理页面组件
|
||||
|
|
@ -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`中添加权限管理相关的翻译
|
||||
- 包括菜单名称和页面标题
|
||||
|
||||
## 预期效果
|
||||
- 侧边栏新增权限管理菜单,点击展开二级目录
|
||||
- 点击二级菜单项跳转到对应页面
|
||||
- 页面包含基本的布局结构
|
||||
- 支持中英文切换
|
||||
- 支持主题切换
|
||||
|
||||
## 后续优化方向
|
||||
- 实现角色管理功能(增删改查)
|
||||
- 实现路由权限配置功能
|
||||
- 实现用户列表展示和管理功能
|
||||
- 添加表单验证和错误处理
|
||||
- 优化页面样式和交互体验
|
||||
|
|
@ -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. 测试
|
||||
- 测试页面显示效果
|
||||
- 测试中英文切换功能
|
||||
- 测试路由跳转
|
||||
|
|
@ -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
|
||||
- 模拟数据(或后端接口)
|
||||
|
|
@ -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}"
|
||||
|
|
@ -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链接选择合适的显示方式(图标或文本)
|
||||
- 保留所有现有代码,仅添加注释和新内容
|
||||
|
|
@ -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
|
||||
|
||||
* 点的颜色适配当前主题
|
||||
|
||||
* 缩放和拖动画布时,点网格保持正确的位置关系
|
||||
|
||||
* 不影响现有卡片元素的交互
|
||||
|
||||
|
|
|
|||
|
|
@ -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` 文件
|
||||
|
||||
# 预期效果
|
||||
|
||||
- 页面顶部显示两个标签页,可切换查看不同列表
|
||||
- 邀请码列表支持筛选和生成邀请码功能
|
||||
- 现有邀请用户列表功能保持不变
|
||||
- 整体布局美观,用户体验良好
|
||||
|
|
@ -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. 预期效果
|
||||
- 同时展示免费会员和达人会员的邀请规则
|
||||
- 清晰区分两个规则区块
|
||||
- 仅免费会员能看到成为达人会员的二维码模块
|
||||
- 整体设计与现有界面风格统一
|
||||
- 支持深色模式切换
|
||||
|
||||
这个实现将清晰地向所有用户展示完整的邀请规则,同时根据用户角色提供个性化的升级入口。
|
||||
|
|
@ -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 组件库的相关组件
|
||||
- 保持代码的可维护性和扩展性
|
||||
- 确保与现有项目结构和代码规范保持一致
|
||||
|
|
@ -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. 进行整体测试和优化
|
||||
|
||||
## 六、注意事项
|
||||
|
||||
- 保持代码风格与现有项目一致
|
||||
- 确保所有功能都有适当的错误处理
|
||||
- 遵循现有项目的国际化规范
|
||||
- 不对用户列表模块进行任何修改
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# 邀请码样式重构计划
|
||||
|
||||
## 1. 数据结构修改
|
||||
- 将单个邀请码改为数组形式,包含3个邀请码
|
||||
- 更新用户数据模型,添加邀请码有效期等额外信息
|
||||
|
||||
## 2. 模板布局调整
|
||||
- 将当前的单个邀请码展示改为横向排列的卡片布局
|
||||
- 使用 flex 或 grid 布局实现3个邀请码的横向排列
|
||||
- 确保在不同屏幕尺寸下有良好的响应式表现
|
||||
|
||||
## 3. 样式设计
|
||||
- **卡片样式**:使用渐变背景,参考图中的蓝绿色渐变效果
|
||||
- **邀请码文本**:居中显示,大号字体,清晰易读
|
||||
- **复制按钮**:底部添加"复制邀请码"按钮,带有悬停效果
|
||||
- **品牌标识**:每个卡片底部添加品牌logo或名称
|
||||
|
||||
## 4. 功能实现
|
||||
- 添加邀请码复制功能,点击按钮自动复制到剪贴板
|
||||
- 显示复制成功的反馈信息
|
||||
- 添加悬停效果,提升交互体验
|
||||
|
||||
## 5. 响应式设计
|
||||
- 在桌面端显示3个横向排列的邀请码卡片
|
||||
- 在平板端根据屏幕宽度调整卡片大小
|
||||
- 在移动端改为垂直排列
|
||||
|
||||
## 6. 暗色主题支持
|
||||
- 确保在暗色主题下邀请码卡片有良好的视觉效果
|
||||
- 调整渐变颜色和文字颜色,适配暗色背景
|
||||
|
||||
## 7. 代码实现
|
||||
- 修改模板结构,使用 v-for 循环渲染3个邀请码
|
||||
- 添加复制功能的事件处理
|
||||
- 设计符合参考图风格的 CSS 样式
|
||||
- 确保与现有页面设计风格保持一致
|
||||
|
||||
## 8. 测试验证
|
||||
- 检查在不同屏幕尺寸下的显示效果
|
||||
- 测试复制功能是否正常工作
|
||||
- 验证暗色主题下的表现
|
||||
- 确保与页面其他元素的样式协调
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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(响应式)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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: '积分列表'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 || '请求失败')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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": "操作成功"
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ const handleGenerateImage = async () => {
|
|||
头发纹理细节需针对3D制造进行优化——层次平滑且分明,兼顾视觉吸引力与可打印性,维持整体俏皮且高品质的盲盒角色风格。
|
||||
`:`采用疯狂动物城的设计风格`}
|
||||
调整背景为极简风格,换成中性纯白色,让图片中的人物呈现3D立体效果。
|
||||
保证生成的图片一定要有眼睛,一定要有嘴巴。
|
||||
保证生成的图片一定要有眼睛,一定要有嘴巴,眼睛效果要可爱童真,Q版大眼睛。
|
||||
角色肤色和衣服材质都为纯色一种颜色如下:
|
||||
保证角色全身都为木头材质颜色,并且要带一些木头纹理,颜色为#e2cfb3。
|
||||
衣服如果不适合做木制一定要简化衣服,不能用复杂的衣服设计,保留衣服特征即可,衣服一定要纯色木质材质。
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
// 从URL查询参数中获取inviteCode
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
||||
// 跳转到忘记密码页面
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -14,6 +14,7 @@
|
|||
"import": "./dist/index.es.js",
|
||||
"require": "./dist/index.umd.js"
|
||||
},
|
||||
"./dist/*": "./dist/*",
|
||||
"./style.css": "./dist/ui.css"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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":"任务已提交,正在处理"}}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue