22
CI/CD / build (push) Successful in 5m2s Details

This commit is contained in:
13121765685 2026-01-13 17:30:01 +08:00
parent ffe8fe79be
commit 7caff23099
49 changed files with 4794 additions and 1231 deletions

View File

@ -0,0 +1,188 @@
# 企业级管理系统设计风格指南
## 1. 风格概述
当前页面采用的是**Element Plus企业级管理系统设计风格**这是一种基于Element Plus组件库的现代化、专业化的管理系统设计范式。它以清晰的层次结构、统一的视觉语言和流畅的交互体验为核心广泛应用于各类企业后台管理系统。
## 2. 设计特点
### 2.1 布局设计
- **卡片式布局**:使用`el-card`组件包裹主要内容区域,形成清晰的视觉边界
- **头部-内容-底部**:典型的三段式布局,结构清晰
- **响应式设计**:适配不同屏幕尺寸,在移动端自动调整布局
- **留白合理**:各元素间保持适当间距,提升可读性
### 2.2 色彩系统
- **主色调**使用Element Plus默认的蓝色系(#409EFF)作为主色调,代表专业、信任
- **辅助色**:绿色(#67C23A)表示成功,红色(#F56C6C)表示危险,黄色(#E6A23C)表示警告,灰色(#909399)表示禁用/次要信息
- **中性色**:白色背景配合浅灰色边框,形成干净清爽的视觉效果
- **深色主题支持**通过CSS变量实现深色主题适配
### 2.3 排版规范
- **字体层次**:清晰的标题层级(h1-h3)正文使用14px字体
- **统一字体**:使用系统默认无衬线字体,保证跨平台一致性
- **对齐方式**:左对齐为主,保持视觉流的一致性
- **行高合理**正文行高约1.5-1.6,提升可读性
### 2.4 组件风格
- **表格设计**:使用`el-table`组件,支持排序、筛选、分页
- **表单设计**:使用`el-form`组件,支持表单验证、布局调整
- **按钮设计**:统一的按钮样式,区分主要操作和次要操作
- **弹窗设计**:使用`el-dialog`组件,模态弹窗居中显示
- **状态标签**:使用`el-tag`组件,直观展示不同状态
## 3. 技术实现
### 3.1 组件库
- **Element Plus**基于Vue 3的企业级UI组件库
- **组件化开发**:封装通用组件,提高代码复用性
- **按需引入**:减小打包体积,提高性能
### 3.2 样式方案
- **CSS变量**使用CSS变量管理主题色支持动态切换
- **Scoped CSS**:组件样式隔离,避免样式冲突
- **深度选择器**:使用`:deep()`实现样式穿透,覆盖组件默认样式
- **响应式设计**:结合媒体查询适配不同屏幕尺寸
### 3.3 交互设计
- **即时反馈**:操作后立即给出视觉反馈(如`ElMessage`提示)
- **加载状态**:使用`v-loading`属性显示加载状态
- **表单验证**:实时表单验证,提供清晰的错误提示
- **键盘支持**:支持键盘导航和快捷键
## 4. 设计原则
### 4.1 一致性
- **视觉一致性**:相同类型的元素使用相同的样式
- **交互一致性**:相似功能的交互方式保持一致
- **布局一致性**:页面布局遵循统一的网格系统
### 4.2 清晰性
- **信息层级清晰**:重要信息突出显示
- **操作流程清晰**:减少用户思考成本
- **反馈清晰**:操作结果即时反馈
### 4.3 易用性
- **操作简单**:减少操作步骤,简化复杂流程
- **易于学习**:符合用户习惯的交互方式
- **容错性高**:提供明确的错误提示和恢复机制
### 4.4 可访问性
- **键盘导航支持**:支持纯键盘操作
- **适当的对比度**:保证文本和背景的对比度
- **语义化HTML**使用正确的HTML标签提高可访问性
## 5. 适用场景
- **企业后台管理系统**:如订单管理、用户管理、数据分析等
- **SaaS平台控制台**:多租户管理、权限管理等
- **内部业务系统**:流程审批、数据监控等
- **数据分析平台**:报表展示、数据可视化等
## 6. 代码示例
### 6.1 卡片布局示例
```vue
<el-card shadow="hover" class="content-card">
<template #header>
<div class="card-header">
<span>标题</span>
<el-button type="primary">操作按钮</el-button>
</div>
</template>
<div class="card-body">
<!-- 内容区域 -->
</div>
</el-card>
```
### 6.2 表格示例
```vue
<el-table
:data="tableData"
v-loading="loading"
:header-cell-style="{background: '#fafafa'}"
stripe
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'warning'">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button size="small" type="primary">编辑</el-button>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
```
### 6.3 表单示例
```vue
<el-form :model="formData" label-width="120px" :rules="rules" ref="formRef">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="resetForm">重置</el-button>
<el-button type="primary" @click="submitForm">提交</el-button>
</el-form-item>
</el-form>
```
## 7. 最佳实践
### 7.1 组件使用
- **优先使用Element Plus组件**:保持风格一致性
- **合理选择组件**:根据场景选择合适的组件类型
- **组件配置统一**:相同组件的配置参数保持一致
### 7.2 样式管理
- **避免全局样式污染**使用Scoped CSS
- **使用CSS变量**:便于主题切换和维护
- **统一间距规范**:定义统一的间距变量
### 7.3 性能优化
- **按需引入组件**:减小打包体积
- **合理使用v-if和v-show**:根据场景选择合适的条件渲染方式
- **虚拟滚动**:处理大数据量列表
### 7.4 国际化支持
- **使用i18n**:支持多语言切换
- **统一语言包结构**:便于维护和扩展
- **考虑RTL支持**:为国际化做好准备
## 8. 总结
Element Plus企业级管理系统设计风格是一种成熟、专业的设计范式具有清晰的视觉层次、统一的交互体验和良好的可扩展性。它适用于各类企业后台管理系统能够帮助开发者快速构建高质量、易用的管理界面。
通过遵循本指南中的设计原则和最佳实践,可以确保系统具有一致的视觉风格、流畅的交互体验和良好的可维护性,为用户提供专业、高效的管理系统使用体验。

View File

@ -1,19 +0,0 @@
## 问题分析
用户希望添加解封功能,但实际上该功能已经实现!不过,模板条件中存在一个拼写错误,导致解封按钮无法正确显示。
## 问题所在
- 在 `AdminUsers.vue` 第129行条件判断为 `v-else-if="row.status === 'disable'"`
- 但整个代码库中实际使用的状态值是 `'disabled'`(末尾有'd'
- 这种不匹配导致解封按钮永远不会在应该显示的时候出现
## 解决方案
1. 修复模板条件中的拼写错误,将条件改为检查 `'disabled'` 而不是 `'disable'`
## 需要修改的内容
**文件**: `d:\work\Aiproject\DeotalandAi\apps\FrontendDesigner\src\views\admin\AdminUsers\AdminUsers.vue`
- **第129行**: 将 `row.status === 'disable'` 改为 `row.status === 'disabled'`
## 验证信息
- `index.js` 中的 `updateUserStatus` 方法已经支持 `active``disabled` 两种状态
- `handleUnban` 函数已经调用 `updateUserStatus` 并传入 `status: 'active'`
- 修复拼写错误后,解封按钮将对禁用用户正确显示

View File

@ -1,49 +0,0 @@
## 实施计划
### 1. 分析当前代码结构
- **PhoneLogin.vue**:手机号登录页面主组件,包含登录卡片和子组件调用
- **PhoneLoginForm.vue**:登录表单组件,包含登录方式切换和表单字段
- **login.js**包含登录相关的API调用方法如`sendPhoneCode`和`phoneLoginCode`
### 2. 修改PhoneLoginForm.vue组件
#### 模板部分修改
- **更新登录模式切换**:将现有的三种模式(密码登录、注册、重置密码)改为仅保留登录相关的模式
- **添加密码/验证码登录切换**:在登录模式下,添加一个新的切换开关,允许用户在密码登录和验证码登录之间切换
- **调整表单字段显示**:根据选择的登录方式显示相应的表单字段
#### 脚本部分修改
- **添加登录方式状态**:添加`loginMethod` ref用于跟踪用户选择的是密码登录还是验证码登录
- **设置默认登录方式**:将默认登录方式设置为验证码登录
- **更新表单验证逻辑**:根据选择的登录方式调整验证规则
- **更新发送验证码逻辑**:发送登录验证码时传递`purpose: 'login'`
- **更新表单提交逻辑**:验证码登录时调用`phoneLoginCode`函数
### 3. 修改PhoneLogin.vue组件
- **更新handleLogin方法**:确保根据登录方式调用正确的登录函数
### 4. 实施细节
#### PhoneLoginForm.vue修改点
- **第155行**:将默认`loginMode`改为'code'
- **第38-70行**:添加密码/验证码登录切换按钮
- **第256-285行**:更新`handleSendCode`方法确保发送验证码时传递正确的purpose
- **第288-316行**:更新`handleSubmit`方法,处理验证码登录逻辑
#### PhoneLogin.vue修改点
- **第79-81行**:更新`handleLogin`方法,根据登录方式调用正确的登录函数
### 5. 预期行为
- 用户默认看到验证码登录界面
- 用户可以在密码登录和验证码登录之间切换
- 发送登录验证码时,传递`purpose: 'login'`
- 验证码登录调用`phoneLoginCode`函数
- 密码登录保持原有功能不变
### 6. 需要修改的文件
- `d:\work\Aiproject\DeotalandAi\apps\frontend\src\components\auth\PhoneLoginForm.vue`
- `d:\work\Aiproject\DeotalandAi\apps\frontend\src\views\Login\PhoneLogin.vue`

View File

@ -0,0 +1,56 @@
# 实现店铺管理页面优化
## 需求分析
1. 移除新增和编辑弹窗中的经纬度填写项
2. 将省市区的输入框改为下拉选择组件,实现三级联动
3. 确保编辑弹窗中省市区数据能正确回显
## 技术方案
### 1. 移除经纬度填写项
- 修改`shop.vue`文件中的表单部分,删除经纬度相关的表单项
- 移除表单验证规则中的经纬度验证
- 确保在提交表单时不会传递经纬度数据
### 2. 实现省市区下拉选择
- 由于项目已使用Element Plus我们可以实现一个三级联动的省市区选择组件
- 使用Element Plus的级联选择器(`el-cascader`)实现省市区选择
- 准备省市区数据,支持三级联动
### 3. 实现编辑弹窗回显
- 在编辑店铺时,将后端返回的省市区数据转换为级联选择器所需的格式
- 确保级联选择器能正确回显已选择的省市区数据
- 实现数据转换逻辑,将级联选择器的输出转换为后端所需的格式
## 实现步骤
1. **修改表单结构**
- 移除`shop.vue`中表单的经纬度输入项
- 将省市区的`el-input`替换为`el-cascader`组件
- 调整表单布局,确保组件样式一致
2. **准备省市区数据**
- 实现省市区数据的加载逻辑
- 确保数据格式符合Element Plus级联选择器的要求
3. **实现联动和回显逻辑**
- 实现级联选择器的基本功能
- 实现编辑时省市区数据的回显逻辑
- 实现数据转换,将级联选择器输出转换为后端所需格式
4. **更新表单处理**
- 更新表单验证规则
- 更新表单提交逻辑,确保省市区数据正确传递
- 更新编辑店铺时的数据填充逻辑
5. **测试功能完整性**
- 测试新增店铺功能,确保省市区选择正常
- 测试编辑店铺功能,确保省市区数据正确回显
- 测试表单提交,确保数据格式正确
## 预期效果
- 新增和编辑弹窗中不再显示经纬度输入项
- 省市区选择使用Element Plus级联选择器支持三级联动
- 编辑弹窗中省市区数据能正确回显
- 表单提交时正确传递省市区数据
- 页面样式保持一致,交互流畅

View File

@ -0,0 +1,90 @@
# 员工管理页面实现计划
## 页面结构设计
1. **页面头部**
- 标题:员工管理
- 搜索栏:支持按员工编号、姓名、电话搜索
- 筛选条件:员工类型、部门、职位、工作状态、员工状态
2. **操作区域**
- 添加员工按钮
- 批量删除按钮
3. **员工列表**
- 使用Element Plus的`el-table`组件展示员工信息
- 支持分页
- 支持单行操作:查看详情、编辑、删除、更新状态
- 支持批量选择
4. **弹窗组件**
- 添加/编辑员工表单:使用`el-dialog`和`el-form`组件
- 包含店铺下拉列表调用getShopList接口
- 包含管理员用户下拉列表调用getAdminUsersList接口
- 员工详情查看:使用`el-dialog`组件
- 确认删除弹窗:使用`el-dialog`组件
- 状态更新弹窗:使用`el-dialog`组件
## 功能实现
1. **数据交互**
- 调用`getShopList`获取店铺列表,用于下拉选择
- 调用`getAdminUsersList`获取管理员用户列表,用于下拉选择
- 调用`getEmployeeList`获取员工列表
- 调用`createShopEmployee`创建员工
- 调用`updateEmployee`更新员工
- 调用`deleteEmployee`删除员工
- 调用`batchDeleteEmployee`批量删除员工
- 调用`getEmployeeById`查询员工详情
- 调用`updateWorkStatus`更新工作状态
- 调用`updateEmployeeStatus`更新员工状态
- 调用`employeeResign`处理员工离职
2. **表单实现**
- 店铺选择从getShopList返回的数据中选择
- 管理员选择从getAdminUsersList返回的数据中选择
- 其他表单字段验证
3. **国际化支持**
- 在页面中使用`t()`函数进行文本翻译
- 添加员工管理相关的中英文对照
4. **响应式设计**
- 使用CSS Grid和Flexbox进行布局
- 使用媒体查询适配不同屏幕尺寸
- 确保在移动端、平板和桌面端都有良好的显示效果
5. **UI交互**
- 使用Element Plus组件库
- 实现各种表单验证
- 实现加载状态和提示信息
- 实现主题切换支持
## 代码实现
1. **更新国际化配置**
- 在`zh-CN.js`和`en-US.js`中添加员工管理相关的翻译
2. **实现employee.vue页面**
- 使用Composition API编写逻辑
- 实现页面布局和交互
- 调用API进行数据交互
- 实现店铺和管理员用户下拉列表
3. **样式设计**
- 使用Scoped CSS
- 使用CSS变量定义主题色
- 实现响应式布局
- 遵循Element Plus企业级管理系统设计风格
## 技术栈
- Vue 3 (Composition API)
- Element Plus
- Vue Router
- Pinia
- Vue I18n
- CSS Grid + Flexbox
## 实现步骤
1. 首先更新国际化配置文件
2. 然后实现employee.vue页面的基本结构
3. 接着实现数据交互逻辑,包括店铺和管理员用户下拉列表
4. 实现员工列表和各种操作功能
5. 最后实现样式和响应式设计

View File

@ -0,0 +1,107 @@
# 佣金提现页面设计与实现
## 页面功能设计
基于提供的API方法设计一个完整的佣金提现管理页面包含以下功能
1. **结算申请列表**
- 展示所有结算申请,支持分页
- 显示申请ID、用户名、申请金额、佣金金额、状态、申请时间等信息
- 支持按用户名搜索、按状态筛选、按时间范围筛选
2. **结算申请详情**
- 查看单个结算申请的详细信息
- 查看关联的订单列表
- 支持审核操作(通过/拒绝)
3. **审核功能**
- 审核通过:填写佣金金额和备注
- 审核拒绝:填写拒绝理由
4. **订单详情**
- 查看结算申请关联的订单列表
- 显示订单号、金额、状态等信息
## 技术实现方案
1. **组件结构**
- 使用 Vue 3 Composition API 开发
- 采用响应式设计,适配移动端、桌面端、平板端
- 使用 Scoped CSS + CSS 变量实现主题适配
2. **国际化**
- 使用现有的 i18n 框架,基于 `src/locales/lang` 配置
- 支持中英文切换
- 在语言文件中添加佣金提现相关的翻译字段
3. **API 调用**
- 实例化 `AdminCommissionManagement`
- 调用提供的四个 API 方法:
- `getSettlementApplications`:获取结算申请列表
- `approveSettlementApplication`:审核通过
- `rejectSettlementApplication`:审核拒绝
- `getSettlementOrders`:获取关联订单
4. **UI 设计**
- 简洁明了的表格布局,支持响应式调整
- 使用卡片式设计展示详情
- 模态框实现审核操作
- 加载状态和错误处理
## 页面布局设计
1. **桌面端布局**
- 顶部:搜索和筛选区域
- 中间:结算申请列表(表格形式)
- 右侧/底部:操作按钮
- 模态框:审核操作和详情查看
2. **移动端布局**
- 顶部:搜索和筛选区域(折叠式)
- 中间:结算申请列表(卡片形式)
- 底部:操作按钮
- 全屏模态框:审核操作和详情查看
3. **主题适配**
- 使用 CSS 变量定义主题色
- 支持浅色/深色主题切换
- 遵循项目现有的主题设计规范
## 实现步骤
1. **更新语言文件**
- 在 `zh-CN.js``en-US.js` 中添加佣金提现相关的翻译字段
2. **开发主页面组件**
- 实现结算申请列表
- 实现搜索和筛选功能
- 实现分页功能
3. **开发详情组件**
- 实现结算申请详情展示
- 实现关联订单列表展示
4. **开发审核组件**
- 实现审核通过模态框
- 实现审核拒绝模态框
5. **响应式设计**
- 添加媒体查询,适配不同屏幕尺寸
- 调整布局和组件样式
6. **主题适配**
- 使用 CSS 变量实现主题切换
- 确保所有组件支持主题变化
7. **测试与优化**
- 测试所有功能是否正常工作
- 优化性能和用户体验
- 确保中英文切换正常
## 预期效果
- 一个功能完整的佣金提现管理页面
- 支持响应式设计,适配各种设备
- 支持中英文切换
- 支持主题色切换
- 良好的用户体验和交互效果

View File

@ -1,386 +0,0 @@
# 实现竖屏移动端卡片展示组件
## 组件设计目标
* 适配竖屏移动端的卡片展示组件
* 包含图片展示、加载蒙层、预览功能
* 底部功能按钮(修改、编辑、场景图、下载、删除)
* 向父组件抛出事件
## 实现步骤
### 1. 实现 `shu.vue` 组件
* 设计组件结构:主容器、图片区域、功能按钮区域
* 实现响应式布局,适配竖屏移动端
* 使用 Scoped CSS + CSS 变量实现样式隔离与主题定制
### 2. 图片展示与加载效果
* 使用 `el-image` 组件实现图片展示
* 添加加载状态管理,显示加载蒙层
* 实现图片加载完成后的过渡效果
### 3. 图片预览功能
* 集成图片预览功能,适配 H5 移动端
* 点击图片触发预览,支持手势操作
### 4. 底部功能按钮
* 实现修改、编辑、场景图、下载、删除按钮
* 每个按钮向父组件抛出对应的事件
* 按钮样式设计符合 UI/UX 要求
### 5. 与父组件交互
* 在 `CreateProjectShu.vue` 中引入并使用 `shu.vue` 组件
* 实现事件处理逻辑,接收并处理子组件抛出的事件
## 代码实现
### `shu.vue` 组件
```vue
<template>
<div class="shu-card-container">
<!-- 图片展示区域 -->
<div class="image-wrapper" @click="handlePreview">
<el-image
:src="props.imageUrl"
:fit="'cover'"
:preview-src-list="[props.imageUrl]"
@load="handleImageLoad"
@error="handleImageError"
>
<!-- 加载蒙层 -->
<template #loading>
<div class="loading-mask">
<div class="loading-spinner"></div>
<div class="loading-text">图片加载中...</div>
</div>
</template>
</el-image>
</div>
<!-- 功能按钮区域 -->
<div class="action-buttons">
<button class="action-btn" @click="handleModify" title="修改">
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
<span class="btn-text">修改</span>
</button>
<button class="action-btn" @click="handleEdit" title="编辑">
<el-icon class="btn-icon"><EditPen /></el-icon>
<span class="btn-text">编辑</span>
</button>
<button class="action-btn" @click="handleScene" title="场景图">
<el-icon class="btn-icon"><Grid /></el-icon>
<span class="btn-text">场景图</span>
</button>
<button class="action-btn" @click="handleDownload" title="下载">
<el-icon class="btn-icon"><Download /></el-icon>
<span class="btn-text">下载</span>
</button>
<button class="action-btn delete-btn" @click="handleDelete" title="删除">
<el-icon class="btn-icon"><Delete /></el-icon>
<span class="btn-text">删除</span>
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ElImage, ElIcon } from 'element-plus';
import { ChatDotRound, EditPen, Grid, Download, Delete } from '@element-plus/icons-vue';
// 定义组件属性
const props = defineProps({
// 图片URL
imageUrl: {
type: String,
default: ''
},
// 卡片数据
cardData: {
type: Object,
default: () => ({})
}
});
// 定义事件
const emit = defineEmits(['preview', 'modify', 'edit', 'scene', 'download', 'delete']);
// 图片加载状态
const isLoading = ref(true);
// 处理图片加载完成
const handleImageLoad = () => {
isLoading.value = false;
};
// 处理图片加载错误
const handleImageError = () => {
isLoading.value = false;
};
// 处理图片预览
const handlePreview = () => {
emit('preview', props.imageUrl);
};
// 处理修改按钮点击
const handleModify = (e) => {
e.stopPropagation();
emit('modify', props.cardData);
};
// 处理编辑按钮点击
const handleEdit = (e) => {
e.stopPropagation();
emit('edit', props.cardData);
};
// 处理场景图按钮点击
const handleScene = (e) => {
e.stopPropagation();
emit('scene', props.cardData);
};
// 处理下载按钮点击
const handleDownload = (e) => {
e.stopPropagation();
emit('download', props.imageUrl);
};
// 处理删除按钮点击
const handleDelete = (e) => {
e.stopPropagation();
emit('delete', props.cardData);
};
</script>
<style scoped>
/* 组件样式 */
.shu-card-container {
display: flex;
flex-direction: column;
width: 100%;
max-width: 400px;
margin: 0 auto;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
/* 图片区域样式 */
.image-wrapper {
position: relative;
width: 100%;
padding-bottom: 150%; /* 2:3 竖屏比例 */
overflow: hidden;
cursor: pointer;
}
.image-wrapper :deep(.el-image) {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: transform 0.3s ease;
}
.image-wrapper :deep(.el-image__inner) {
object-fit: cover;
transition: transform 0.3s ease;
}
.image-wrapper:hover :deep(.el-image__inner) {
transform: scale(1.02);
}
/* 加载蒙层样式 */
.loading-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 249, 250, 0.95) 100%);
transition: opacity 0.3s ease;
z-index: 10;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(107, 70, 193, 0.2);
border-top: 4px solid #6B46C1;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
.loading-text {
font-size: 14px;
color: #6B46C1;
font-weight: 500;
}
/* 功能按钮区域样式 */
.action-buttons {
display: flex;
flex-wrap: wrap;
padding: 12px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.action-btn {
flex: 1;
min-width: calc(20% - 8px);
margin: 4px;
padding: 12px 8px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.3);
}
.action-btn:active {
transform: translateY(0);
}
.action-btn.delete-btn {
background: linear-gradient(135deg, #EF4444 0%, #F87171 100%);
}
.action-btn.delete-btn:hover {
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.btn-icon {
font-size: 20px;
}
.btn-text {
font-size: 12px;
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 768px) {
.shu-card-container {
margin: 8px;
border-radius: 8px;
}
.action-buttons {
padding: 8px;
}
.action-btn {
padding: 10px 6px;
min-width: calc(20% - 6px);
margin: 3px;
}
.btn-icon {
font-size: 18px;
}
.btn-text {
font-size: 11px;
}
}
@media (max-width: 480px) {
.shu-card-container {
margin: 4px;
}
.action-btn {
padding: 8px 4px;
}
.btn-icon {
font-size: 16px;
}
.btn-text {
font-size: 10px;
}
}
/* 动画效果 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
```
### 2. 集成到父组件 `CreateProjectShu.vue`
* 在父组件中引入 `shu.vue` 组件
* 实现事件处理逻辑,接收子组件抛出的事件
* 在模板中使用新组件展示卡片
## 设计规范
* 主色调:深紫色(#6B46C1、浅紫色#A78BFA
* 辅助色:深灰色(#1F2937、浅灰色#F3F4F6
* 按钮样式圆角设计8px半径、微妙阴影、悬停效果
* 字体排版Inter字体系列、16px基础大小、响应式缩放
* 布局风格基于卡片的设计、统一间距8px网格系统
* 动画效果平滑过渡200ms缓入缓出、加载蒙层
## 测试与验证
* 确保组件在竖屏移动端正常显示
* 测试图片加载、预览功能
* 验证按钮事件正确抛出
* 检查响应式布局适配情况

View File

@ -1,70 +0,0 @@
## 实施计划
### 1. 分析当前代码结构
* **PhoneLogin.vue**:手机号登录页面主组件,包含登录卡片和子组件调用
* **PhoneLoginForm.vue**:登录表单组件,包含登录方式切换和表单字段
* **login.js**包含登录相关的API调用方法如`sendPhoneCode`和`phoneLoginCode`
### 2. 修改PhoneLoginForm.vue组件
#### 模板部分修改
* **更新登录模式切换**:将现有的三种模式(密码登录、注册、重置密码)改为仅保留登录相关的模式
* **添加密码/验证码登录切换**:在登录模式下,添加一个新的切换开关,允许用户在密码登录和验证码登录之间切换
* **调整表单字段显示**:根据选择的登录方式显示相应的表单字段
#### 脚本部分修改
* **添加登录方式状态**:添加`loginMethod` ref用于跟踪用户选择的是密码登录还是验证码登录
* **设置默认登录方式**:将默认登录方式设置为验证码登录
* **更新表单验证逻辑**:根据选择的登录方式调整验证规则
* **更新发送验证码逻辑**:发送登录验证码时传递`purpose: 'login'`
* **更新表单提交逻辑**:验证码登录时调用`phoneLoginCode`函数
### 3. 修改PhoneLogin.vue组件
* **更新handleLogin方法**:确保根据登录方式调用正确的登录函数
### 4. 实施细节
#### PhoneLoginForm.vue修改点
* **第155行**:将默认`loginMode`改为'code'
* **第38-70行**:添加密码/验证码登录切换按钮
* **第256-285行**:更新`handleSendCode`方法确保发送验证码时传递正确的purpose
* **第288-316行**:更新`handleSubmit`方法,处理验证码登录逻辑
#### PhoneLogin.vue修改点
* **第79-81行**:更新`handleLogin`方法,根据登录方式调用正确的登录函数
### 5. 预期行为
* 用户默认看到验证码登录界面
* 用户可以在密码登录和验证码登录之间切换
* 发送登录验证码时,传递`purpose: 'login'`
* 验证码登录调用`phoneLoginCode`函数
* 密码登录保持原有功能不变
### 6. 需要修改的文件
* `d:\work\Aiproject\DeotalandAi\apps\frontend\src\components\auth\PhoneLoginForm.vue`
* `d:\work\Aiproject\DeotalandAi\apps\frontend\src\views\Login\PhoneLogin.vue`

View File

@ -1,62 +0,0 @@
1. **添加选项卡**:在 `el-tabs` 组件中添加一个新的 `el-tab-pane`,标签名为 "佣金结算"name 为 "settlement"
2. **添加响应式数据**
* 添加 `settlementRecords` 数组用于存储结算记录
* 添加 `settlementTotal` 用于存储总记录数
* 在 `loading` 对象中添加 `settlement` 字段
* 在 `pagination` 对象中添加 `settlement` 分页配置
* 添加弹窗相关的响应式数据:`dialogVisible`、`applyRemark`、`applyLoading`
3. **实现获取结算列表方法**
* 创建 `fetchSettlementList` 方法,调用 `userController.getSettlementList` 接口
* 处理接口返回的数据,转换为需要的格式
* 处理加载状态和错误情况
4. **实现申请结算弹窗**
* 添加 `el-dialog` 组件用于申请结算弹窗
* 添加表单用于输入备注
* 实现 `handleApplySettlement` 方法,打开弹窗
* 实现 `handleApplySubmit` 方法,提交表单并调用 `applySettlement` 接口
* 实现 `handleApplyCancel` 方法,关闭弹窗
5. **添加结算列表的表格展示**
* 在新的选项卡内容中添加表格,展示结算记录
* 添加分页组件
* 添加空状态和加载状态的处理
* 添加申请结算按钮
6. **添加样式**
* 确保新添加的内容符合现有样式风格
* 参考 ui-ux-pro-max.md 的极简风格要求
* 添加暗色主题支持
7. **更新页面挂载和选项卡切换逻辑**
* 在 `onMounted` 中添加获取结算列表的调用
* 在 `handleTabChange` 中添加切换到结算选项卡时的处理
8. **国际化支持**
* 确保所有新增的文本都支持中英文切换

View File

@ -0,0 +1,94 @@
# 实现店铺选择功能
## 1. 需求分析
- 在 PurchaseModal 组件中添加店铺选择下拉列表
- 店铺数据通过 `getShopList()` 方法获取
- 店铺选择是非必选的
- 如果选择了店铺,提交订单时需要添加 `shop_id` 字段
## 2. 实现步骤
### 2.1 添加状态和方法
- 在 `script setup` 中添加 `shopList` 状态存储店铺列表
- 添加 `selectedShop` 状态存储选中的店铺
- 添加 `loadingShops` 状态控制加载状态
- 导入 `PurchaseModal` 类并创建实例
- 添加 `getShopList` 方法获取店铺数据
### 2.2 修改模板
- 在配置区域添加店铺选择下拉列表
- 显示加载状态和空状态
- 使用 Element Plus 的 `el-select` 组件实现下拉选择
### 2.3 修改提交逻辑
- 在 `goShopify` 方法中,当 `selectedShop` 存在时,添加 `shop_id` 字段到 params
## 3. 代码实现
### 3.1 导入和状态定义
```javascript
import { PurchaseModal as PurchaseModalClass } from './index.js'
const purchaseModal = new PurchaseModalClass()
const shopList = ref([])
const selectedShop = ref(null)
const loadingShops = ref(false)
```
### 3.2 获取店铺列表方法
```javascript
const fetchShopList = async () => {
loadingShops.value = true
try {
const res = await purchaseModal.getShopList()
if (res.code === 0) {
shopList.value = res.data || []
}
} catch (error) {
console.error('获取店铺列表失败:', error)
shopList.value = []
} finally {
loadingShops.value = false
}
}
```
### 3.3 添加模板代码
```vue
<div class="config-item shop-select">
<div class="label">{{ $t('checkout.shop') }}</div>
<el-select
v-model="selectedShop"
:placeholder="$t('checkout.chooseShop')"
filterable
:loading="loadingShops"
class="shop-select-input"
>
<el-option
v-for="shop in shopList"
:key="shop.id"
:label="shop.shopName"
:value="shop"
/>
</el-select>
</div>
```
### 3.4 修改提交逻辑
```javascript
let params = {
quantity: qty.value,
project_id: props.modelData.projectId,
project_details: project_details,
order_info: order_info,
}
if (selectedShop.value) {
params.shop_id = selectedShop.value.id
}
```
## 4. 国际化支持
- 需要在国际化文件中添加 `checkout.shop``checkout.chooseShop`
## 5. 样式调整
- 为店铺选择添加适当的样式,确保在各种设备上显示正常

View File

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

View File

@ -1,354 +0,0 @@
# 实现竖屏移动端卡片展示组件
## 组件设计目标
- 适配竖屏移动端的卡片展示组件
- 包含图片展示、加载蒙层、预览功能
- 底部功能按钮(修改、编辑、场景图、下载、删除)
- 向父组件抛出事件
## 实现步骤
### 1. 实现 `shu.vue` 组件
- 设计组件结构:主容器、图片区域、功能按钮区域
- 实现响应式布局,适配竖屏移动端
- 使用 Scoped CSS + CSS 变量实现样式隔离与主题定制
### 2. 图片展示与加载效果
- 使用 `el-image` 组件实现图片展示
- 添加加载状态管理,显示加载蒙层
- 实现图片加载完成后的过渡效果
### 3. 图片预览功能
- 集成图片预览功能,适配 H5 移动端
- 点击图片触发预览,支持手势操作
### 4. 底部功能按钮
- 实现修改、编辑、场景图、下载、删除按钮
- 每个按钮向父组件抛出对应的事件
- 按钮样式设计符合 UI/UX 要求
### 5. 与父组件交互
- 在 `CreateProjectShu.vue` 中引入并使用 `shu.vue` 组件
- 实现事件处理逻辑,接收并处理子组件抛出的事件
## 代码实现
### `shu.vue` 组件
```vue
<template>
<div class="shu-card-container">
<!-- 图片展示区域 -->
<div class="image-wrapper" @click="handlePreview">
<el-image
:src="props.imageUrl"
:fit="'cover'"
:preview-src-list="[props.imageUrl]"
@load="handleImageLoad"
@error="handleImageError"
>
<!-- 加载蒙层 -->
<template #loading>
<div class="loading-mask">
<div class="loading-spinner"></div>
<div class="loading-text">图片加载中...</div>
</div>
</template>
</el-image>
</div>
<!-- 功能按钮区域 -->
<div class="action-buttons">
<button class="action-btn" @click="handleModify" title="修改">
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
<span class="btn-text">修改</span>
</button>
<button class="action-btn" @click="handleEdit" title="编辑">
<el-icon class="btn-icon"><EditPen /></el-icon>
<span class="btn-text">编辑</span>
</button>
<button class="action-btn" @click="handleScene" title="场景图">
<el-icon class="btn-icon"><Grid /></el-icon>
<span class="btn-text">场景图</span>
</button>
<button class="action-btn" @click="handleDownload" title="下载">
<el-icon class="btn-icon"><Download /></el-icon>
<span class="btn-text">下载</span>
</button>
<button class="action-btn delete-btn" @click="handleDelete" title="删除">
<el-icon class="btn-icon"><Delete /></el-icon>
<span class="btn-text">删除</span>
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ElImage, ElIcon } from 'element-plus';
import { ChatDotRound, EditPen, Grid, Download, Delete } from '@element-plus/icons-vue';
// 定义组件属性
const props = defineProps({
// 图片URL
imageUrl: {
type: String,
default: ''
},
// 卡片数据
cardData: {
type: Object,
default: () => ({})
}
});
// 定义事件
const emit = defineEmits(['preview', 'modify', 'edit', 'scene', 'download', 'delete']);
// 图片加载状态
const isLoading = ref(true);
// 处理图片加载完成
const handleImageLoad = () => {
isLoading.value = false;
};
// 处理图片加载错误
const handleImageError = () => {
isLoading.value = false;
};
// 处理图片预览
const handlePreview = () => {
emit('preview', props.imageUrl);
};
// 处理修改按钮点击
const handleModify = (e) => {
e.stopPropagation();
emit('modify', props.cardData);
};
// 处理编辑按钮点击
const handleEdit = (e) => {
e.stopPropagation();
emit('edit', props.cardData);
};
// 处理场景图按钮点击
const handleScene = (e) => {
e.stopPropagation();
emit('scene', props.cardData);
};
// 处理下载按钮点击
const handleDownload = (e) => {
e.stopPropagation();
emit('download', props.imageUrl);
};
// 处理删除按钮点击
const handleDelete = (e) => {
e.stopPropagation();
emit('delete', props.cardData);
};
</script>
<style scoped>
/* 组件样式 */
.shu-card-container {
display: flex;
flex-direction: column;
width: 100%;
max-width: 400px;
margin: 0 auto;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
/* 图片区域样式 */
.image-wrapper {
position: relative;
width: 100%;
padding-bottom: 150%; /* 2:3 竖屏比例 */
overflow: hidden;
cursor: pointer;
}
.image-wrapper :deep(.el-image) {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: transform 0.3s ease;
}
.image-wrapper :deep(.el-image__inner) {
object-fit: cover;
transition: transform 0.3s ease;
}
.image-wrapper:hover :deep(.el-image__inner) {
transform: scale(1.02);
}
/* 加载蒙层样式 */
.loading-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 249, 250, 0.95) 100%);
transition: opacity 0.3s ease;
z-index: 10;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(107, 70, 193, 0.2);
border-top: 4px solid #6B46C1;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
.loading-text {
font-size: 14px;
color: #6B46C1;
font-weight: 500;
}
/* 功能按钮区域样式 */
.action-buttons {
display: flex;
flex-wrap: wrap;
padding: 12px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.action-btn {
flex: 1;
min-width: calc(20% - 8px);
margin: 4px;
padding: 12px 8px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #6B46C1 0%, #A78BFA 100%);
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(107, 70, 193, 0.3);
}
.action-btn:active {
transform: translateY(0);
}
.action-btn.delete-btn {
background: linear-gradient(135deg, #EF4444 0%, #F87171 100%);
}
.action-btn.delete-btn:hover {
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.btn-icon {
font-size: 20px;
}
.btn-text {
font-size: 12px;
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 768px) {
.shu-card-container {
margin: 8px;
border-radius: 8px;
}
.action-buttons {
padding: 8px;
}
.action-btn {
padding: 10px 6px;
min-width: calc(20% - 6px);
margin: 3px;
}
.btn-icon {
font-size: 18px;
}
.btn-text {
font-size: 11px;
}
}
@media (max-width: 480px) {
.shu-card-container {
margin: 4px;
}
.action-btn {
padding: 8px 4px;
}
.btn-icon {
font-size: 16px;
}
.btn-text {
font-size: 10px;
}
}
/* 动画效果 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
```
### 2. 集成到父组件 `CreateProjectShu.vue`
- 在父组件中引入 `shu.vue` 组件
- 实现事件处理逻辑,接收子组件抛出的事件
- 在模板中使用新组件展示卡片
## 设计规范
- 主色调:深紫色(#6B46C1、浅紫色#A78BFA
- 辅助色:深灰色(#1F2937、浅灰色#F3F4F6
- 按钮样式圆角设计8px半径、微妙阴影、悬停效果
- 字体排版Inter字体系列、16px基础大小、响应式缩放
- 布局风格基于卡片的设计、统一间距8px网格系统
- 动画效果平滑过渡200ms缓入缓出、加载蒙层
## 测试与验证
- 确保组件在竖屏移动端正常显示
- 测试图片加载、预览功能
- 验证按钮事件正确抛出
- 检查响应式布局适配情况

View File

@ -1,46 +0,0 @@
## 实现步骤
### 1. 新建邀请明细页面组件
- 创建 `d:\work\Aiproject\DeotalandAi\apps\frontend\src\views\user\invite-records.vue` 文件
- 实现页面布局,包括标题、表格和分页组件
- 设计双选项卡切换:"积分明细"和"佣金记录"(仅达人会员可见)
- 采用极简风格设计,遵循 ui-ux-pro-max.md 中的设计规则
### 2. 修改路由配置
- 在 `freeRoutes` 数组中添加新路由 `/user-center/invite-records`
- 指向新建的邀请明细页面组件
### 3. 修改用户中心页面
- 在"邀请码"部分的右侧添加"邀请明细"按钮
- 移除"邀请积分明细"模块(即 `.invite-related-section`
- 添加按钮点击事件,使用 `router.push()` 跳转到新页面
### 4. 实现 API 调用和数据处理
- 在邀请明细页面中调用 `UserController``getInviteRecords()` 方法获取积分明细
- 对于达人会员,额外调用 `getInviteCommissionRecords()` 方法获取佣金记录
- 处理返回的数据,将其转换为表格所需的格式
- 实现数据加载状态和空状态处理
### 5. 设计风格调整
- 采用极简风格设计,使用简洁的颜色和字体
- 确保响应式设计,适配移动端、桌面端、平板端
- 使用合适的间距和布局,保持页面整洁
- 实现平滑的过渡效果和交互反馈
## 技术要点
- 使用 Vue3 Composition API 开发
- 根据用户角色(达人会员/免费会员)动态显示不同内容
- 实现中英文国际化支持
- 确保页面性能和加载速度
- 遵循 ui-ux-pro-max.md 中的设计规则,特别是极简风格要求
## 预期效果
- 用户中心页面的邀请码部分右侧会出现"邀请明细"按钮
- 点击按钮会跳转到新的邀请明细页面
- 免费会员只能看到"积分明细"选项卡,展示被邀请用户、获得积分和日期
- 达人会员可以看到"积分明细"和"佣金记录"两个选项卡
- "佣金记录"选项卡展示被邀请用户邮箱、实际消费金额、佣金比例、佣金金额和订单时间
- 页面采用极简风格设计,视觉效果简洁美观
- 支持响应式布局,适配不同设备尺寸

View File

@ -1,70 +0,0 @@
## 实施计划
### 1. 分析当前代码结构
* **PhoneLogin.vue**:手机号登录页面主组件,包含登录卡片和子组件调用
* **PhoneLoginForm.vue**:登录表单组件,包含登录方式切换和表单字段
* **login.js**包含登录相关的API调用方法如`sendPhoneCode`和`phoneLoginCode`
### 2. 修改PhoneLoginForm.vue组件
#### 模板部分修改
* **更新登录模式切换**:将现有的三种模式(密码登录、注册、重置密码)改为仅保留登录相关的模式
* **添加密码/验证码登录切换**:在登录模式下,添加一个新的切换开关,允许用户在密码登录和验证码登录之间切换
* **调整表单字段显示**:根据选择的登录方式显示相应的表单字段
#### 脚本部分修改
* **添加登录方式状态**:添加`loginMethod` ref用于跟踪用户选择的是密码登录还是验证码登录
* **设置默认登录方式**:将默认登录方式设置为验证码登录
* **更新表单验证逻辑**:根据选择的登录方式调整验证规则
* **更新发送验证码逻辑**:发送登录验证码时传递`purpose: 'login'`
* **更新表单提交逻辑**:验证码登录时调用`phoneLoginCode`函数
### 3. 修改PhoneLogin.vue组件
* **更新handleLogin方法**:确保根据登录方式调用正确的登录函数
### 4. 实施细节
#### PhoneLoginForm.vue修改点
* **第155行**:将默认`loginMode`改为'code'
* **第38-70行**:添加密码/验证码登录切换按钮
* **第256-285行**:更新`handleSendCode`方法确保发送验证码时传递正确的purpose
* **第288-316行**:更新`handleSubmit`方法,处理验证码登录逻辑
#### PhoneLogin.vue修改点
* **第79-81行**:更新`handleLogin`方法,根据登录方式调用正确的登录函数
### 5. 预期行为
* 用户默认看到验证码登录界面
* 用户可以在密码登录和验证码登录之间切换
* 发送登录验证码时,传递`purpose: 'login'`
* 验证码登录调用`phoneLoginCode`函数
* 密码登录保持原有功能不变
### 6. 需要修改的文件
* `d:\work\Aiproject\DeotalandAi\apps\frontend\src\components\auth\PhoneLoginForm.vue`
* `d:\work\Aiproject\DeotalandAi\apps\frontend\src\views\Login\PhoneLogin.vue`

View File

@ -0,0 +1,34 @@
1. 在shop.vue的表单模板中添加两个新的表单项
* 店铺证书号licenseNumber使用el-input组件
* 店铺证书图片licenseImage使用el-upload组件支持图片上传、预览和删除
2. 实现图片上传功能:
* 引入fileServer如果尚未引入
* 添加图片上传方法参考AddModal中的实现
* 处理图片上传成功后的回调
3. 确保编辑弹窗能够正确回显:
* 表单数据已经包含这两个字段编辑时会从API响应中获取
* 确保el-upload组件能够正确显示已上传的图片
4. 响应式设计:
* 确保新添加的表单项在不同设备尺寸下都能正常显示
* 遵循项目现有的响应式设计规范
5. 中英文切换支持:
* 添加对应的中英文翻译字段
修改的文件:
* d:\work\Aiproject\DeotalandAi\apps\FrontendDesigner\src\views\admin

View File

@ -0,0 +1,164 @@
# 佣金提现页面设计与实现
## 页面功能设计
基于提供的API方法设计一个完整的佣金提现管理页面包含以下功能
1. **结算申请列表**
* 展示所有结算申请,支持分页
* 显示申请ID、用户名、申请金额、佣金金额、状态、申请时间等信息
* 支持按用户名搜索、按状态筛选、按时间范围筛选
2. **结算申请详情**
* 查看单个结算申请的详细信息
* 查看关联的订单列表
* 支持审核操作(通过/拒绝)
3. **审核功能**
* 审核通过:填写佣金金额和备注
* 审核拒绝:填写拒绝理由
4. **订单详情**
* 查看结算申请关联的订单列表
* 显示订单号、金额、状态等信息
## 技术实现方案
1. **组件结构**
* 使用 Vue 3 Composition API 开发
* 采用响应式设计,适配移动端、桌面端、平板端
* 使用 Scoped CSS + CSS 变量实现主题适配
2. **国际化**
* 使用现有的 i18n 框架,基于 `src/locales/lang` 配置
* 支持中英文切换
* 在语言文件中添加佣金提现相关的翻译字段
3. **API 调用**
* 实例化 `AdminCommissionManagement`
* 调用提供的四个 API 方法:
* `getSettlementApplications`:获取结算申请列表
* `approveSettlementApplication`:审核通过
* `rejectSettlementApplication`:审核拒绝
* `getSettlementOrders`:获取关联订单
4. **UI 设计**
* 简洁明了的表格布局,支持响应式调整
* 使用卡片式设计展示详情
* 模态框实现审核操作
* 加载状态和错误处理
## 页面布局设计
1. **桌面端布局**
* 顶部:搜索和筛选区域
* 中间:结算申请列表(表格形式)
* 右侧/底部:操作按钮
* 模态框:审核操作和详情查看
2. **移动端布局**
* 顶部:搜索和筛选区域(折叠式)
* 中间:结算申请列表(卡片形式)
* 底部:操作按钮
* 全屏模态框:审核操作和详情查看
3. **主题适配**
* 使用 CSS 变量定义主题色
* 支持浅色/深色主题切换
* 遵循项目现有的主题设计规范
## 实现步骤
1. **更新语言文件**
* 在 `zh-CN.js``en-US.js` 中添加佣金提现相关的翻译字段
2. **开发主页面组件**
* 实现结算申请列表
* 实现搜索和筛选功能
* 实现分页功能
3. **开发详情组件**
* 实现结算申请详情展示
* 实现关联订单列表展示
4. **开发审核组件**
* 实现审核通过模态框
* 实现审核拒绝模态框
5. **响应式设计**
* 添加媒体查询,适配不同屏幕尺寸
* 调整布局和组件样式
6. **主题适配**
* 使用 CSS 变量实现主题切换
* 确保所有组件支持主题变化
7. **测试与优化**
* 测试所有功能是否正常工作
* 优化性能和用户体验
* 确保中英文切换正常
## 预期效果
* 一个功能完整的佣金提现管理页面
* 支持响应式设计,适配各种设备
* 支持中英文切换
* 支持主题色切换
* 良好的用户体验和交互效果

View File

@ -0,0 +1,54 @@
# 设计Shop.vue页面
## 功能分析
根据index.js中的接口shop.vue页面需要实现以下功能
1. 店铺列表展示(表格)
2. 搜索和筛选功能
3. 分页功能
4. 店铺状态和营业状态切换
5. 店铺创建、编辑、删除功能
6. 批量删除功能
## 布局设计
按照设计风格指南,采用卡片式布局,分为三个区域:
1. **头部区域**:包含标题和操作按钮(新增店铺)
2. **搜索区域**:包含各种筛选条件(店铺名称、店铺编码、店铺类型、省份、城市、区域、营业状态、状态)
3. **表格区域**:展示店铺列表,包含各种操作按钮
4. **分页区域**:实现分页功能
## 组件使用
- 使用Element Plus组件库
- 表格el-table
- 表单el-form
- 按钮el-button
- 弹窗el-dialog
- 状态标签el-tag
- 分页el-pagination
## 数据处理
- 使用Vue3 Composition API
- 调用index.js中的接口获取数据
- 实现响应式数据管理
## 交互设计
- 点击新增按钮打开创建弹窗
- 点击编辑按钮打开编辑弹窗
- 点击删除按钮弹出确认对话框
- 支持批量选择和批量删除
- 状态切换使用开关组件
- 操作后显示成功/失败提示
## 代码实现
1. 导入必要的组件和工具
2. 定义响应式数据
3. 实现接口调用
4. 设计页面模板
5. 实现交互逻辑
6. 添加样式
## 预期效果
- 页面布局清晰,符合设计风格
- 功能完整,交互流畅
- 支持响应式设计
- 支持中英文切换
- 支持主题色切换

View File

@ -1,5 +1,4 @@
如果前端项目,需要考虑以下几点:
1.你是一名精通 Vue3纯 JavaScript不使用 TypeScript的前端开发专家专注于构建可适配移动端、桌面端、平板端的响应式页面并且开发的页面都会基于当前项目搭建的中英文框架支持中英文切换(页面中英文切换内容都是本地的,所以需要在项目中配置好对应的英文内容),和主题色切换请基于以下要求完成开发任务:
2.技术栈规范
核心框架:使用 Vue3Options API 或 Composition API 均可,优先推荐 Composition API 以提升代码复用性),禁止使用 TypeScript所有逻辑用原生 JavaScript 实现。
@ -29,10 +28,10 @@
11.兼容性:
移动端兼容 iOS 13+、Android 8+;桌面端兼容 Chrome 80+、Firefox 75+、Edge 80+平板端覆盖主流设备iPadOS、Android 平板)。
避免使用 ES6 + 以上高级语法(或通过 Babel 转译),确保低版本浏览器兼容性。
12.设计风格
- 极简风格
13.交付标准
12.交付标准
代码结构清晰,遵循 Vue3 最佳实践(如组件拆分粒度合理、逻辑与 UI 分离)。
提供完整的多端测试报告(说明在不同设备 / 尺寸下的测试结果及适配方案)。
附带 README说明项目启动、打包命令以及响应式布局的核心实现逻辑。
13.风格
- 极简风格
- 项目我已经启动了,你只负责修改

View File

@ -16,6 +16,7 @@
"@element-plus/icons-vue": "^2.3.2",
"@google/genai": "^1.27.0",
"@types/three": "^0.180.0",
"element-china-area-data": "^6.1.0",
"element-plus": "^2.11.7",
"konva": "^10.0.12",
"pinia": "^2.2.6",

View File

@ -38,7 +38,8 @@ import {
Coin,
Ticket,
ChatDotRound,
ShoppingCartFull
ShoppingCartFull,
Shop
} from '@element-plus/icons-vue'
const props = defineProps({
@ -66,7 +67,8 @@ const iconMap = {
Coin,
Ticket,
ChatDotRound,
ShoppingCartFull
ShoppingCartFull,
Shop
}
const getIconComponent = (iconName) => {

View File

@ -18,7 +18,61 @@ export default {
warning: 'Warning',
info: 'Info',
close: 'Close',
back: 'Back'
back: 'Back',
submit: 'Submit',
all: 'All',
},
// Shop Management
shop: {
pleaseEnterAddress: 'Please enter address',
pleaseEnterContactPerson: 'Please enter contact person',
pleaseEnterContactPhone: 'Please enter contact phone',
shopManagement: 'Shop Management',
addShop: 'Add Shop',
shopName: 'Shop Name',
pleaseEnterShopName: 'Please enter shop name',
shopCode: 'Shop Code',
pleaseEnterShopCode: 'Please enter shop code',
shopType: 'Shop Type',
pleaseSelectShopType: 'Please select shop type',
physicalShop: 'Physical Shop',
onlineShop: 'Online Shop',
businessStatus: 'Business Status',
pleaseSelectBusinessStatus: 'Please select business status',
closed: 'Closed',
open: 'Open',
status: 'Status',
pleaseSelectStatus: 'Please select status',
disabled: 'Disabled',
enabled: 'Enabled',
address: 'Address',
contactPerson: 'Contact Person',
contactPhone: 'Contact Phone',
createdAt: 'Created At',
operation: 'Operation',
disable: 'Disable',
enable: 'Enable',
closeShop: 'Close Business',
openShop: 'Open Business',
batchDelete: 'Batch Delete',
latitude: 'Latitude',
pleaseEnterLatitude: 'Please enter latitude',
longitude: 'Longitude',
pleaseEnterLongitude: 'Please enter longitude',
province: 'Province',
pleaseEnterProvince: 'Please enter province',
city: 'City',
pleaseEnterCity: 'Please enter city',
district: 'District',
pleaseEnterDistrict: 'Please enter district',
contactEmail: 'Contact Email',
pleaseEnterContactEmail: 'Please enter contact email',
businessHours: 'Business Hours',
pleaseEnterBusinessHours: 'Please enter business hours',
shopDesc: 'Shop Description',
pleaseEnterShopDesc: 'Please enter shop description',
pleaseSelectProvinceCityDistrict: 'Please select province/city/district'
},
// 3D Model Viewer
@ -61,6 +115,95 @@ export default {
pointsList: 'Points List'
}
},
// Employee Management
employee: {
idCardImageTip: 'Please upload ID card image',
title: 'Employee Management',
employeeManagement: 'Employee Management',
addEmployee: 'Add Employee',
editEmployee: 'Edit Employee',
employeeList: 'Employee List',
employeeNo: 'Employee No.',
employeeName: 'Employee Name',
employeeType: 'Employee Type',
position: 'Position',
department: 'Department',
level: 'Level',
phone: 'Phone',
email: 'Email',
address: 'Address',
gender: 'Gender',
birthday: 'Birthday',
avatar: 'Avatar',
hireDate: 'Hire Date',
probationEndDate: 'Probation End Date',
contractStartDate: 'Contract Start Date',
contractEndDate: 'Contract End Date',
salary: 'Salary',
salaryType: 'Salary Type',
workHours: 'Work Hours',
workLocation: 'Work Location',
workStatus: 'Work Status',
status: 'Status',
shopId: 'Shop',
adminId: 'Admin',
employeeDetail: 'Employee Detail',
pleaseEnterEmployeeNo: 'Please enter employee number',
pleaseEnterEmployeeName: 'Please enter employee name',
pleaseSelectEmployeeType: 'Please select employee type',
pleaseEnterPosition: 'Please enter position',
pleaseEnterDepartment: 'Please enter department',
pleaseEnterLevel: 'Please enter level',
pleaseEnterPhone: 'Please enter phone',
pleaseEnterEmail: 'Please enter email',
pleaseEnterAddress: 'Please enter address',
pleaseSelectGender: 'Please select gender',
pleaseSelectShop: 'Please select shop',
pleaseSelectAdmin: 'Please select admin',
male: 'Male',
female: 'Female',
other: 'Other',
regular: 'Regular',
partTime: 'Part Time',
temporary: 'Temporary',
workStatusOptions: {
resigned: 'Resigned',
onJob: 'On Job',
onLeave: 'On Leave',
suspended: 'Suspended'
},
statusOptions: {
normal: 'Normal',
disabled: 'Disabled'
},
basicInfo: 'Basic Info',
workInfo: 'Work Info',
contactInfo: 'Contact Info',
contractInfo: 'Contract Info',
financialInfo: 'Financial Info',
operation: 'Operation',
viewDetail: 'View Detail',
updateStatus: 'Update Status',
resign: 'Resign',
confirmResign: 'Are you sure to process this employee\'s resignation?',
deleteConfirm: 'Are you sure to delete this employee?',
batchDeleteConfirm: 'Are you sure to batch delete selected employees?',
createSuccess: 'Employee created successfully',
updateSuccess: 'Employee updated successfully',
deleteSuccess: 'Employee deleted successfully',
statusUpdateSuccess: 'Status updated successfully',
resignSuccess: 'Employee resignation processed successfully',
searchPlaceholder: 'Search by employee number, name or phone',
idCard: 'ID Card',
pleaseEnterIdCard: 'Please enter ID card number',
idCardImageFront: 'ID Card Front',
idCardImageBack: 'ID Card Back',
uploadIdCardImageFront: 'Upload ID Card Front',
uploadIdCardImageBack: 'Upload ID Card Back',
imageUploadTip: 'Supports JPG, PNG formats, max size 2MB',
imageUploadSuccess: 'Image uploaded successfully',
imageUploadFailed: 'Image upload failed'
},
// App
app: {
@ -237,6 +380,7 @@ export default {
delete: 'Delete',
edit: 'Edit',
add: 'Add',
all: 'All',
noData: 'No Data',
error: 'Error',
success: 'Success',
@ -254,6 +398,8 @@ export default {
operationFailed: 'Operation failed'
},
layout: {
employee: 'Employee Management',
shop: 'Shop Management',
dashboard: 'Dashboard',
content: 'Content Review',
contentReview: 'Content Review',
@ -267,6 +413,8 @@ export default {
userList: 'User List',
pointsManagement: 'Points Management',
commissionManagement: 'Commission Management',
commissionConfiguration: 'Commission Configuration',
commissionWithdrawal: 'Commission Withdrawal',
promptManagement: 'Prompt Management',
productManagement: 'Product Management',
voucherManagement: 'Voucher Management',
@ -756,6 +904,7 @@ export default {
commissionManagement: {
title: 'Commission Management',
configTitle: 'Commission Configuration',
commissionWithdrawal: 'Commission Withdrawal',
commissionRate: 'Commission Rate',
minWithdrawAmount: 'Minimum Withdraw Amount',
withdrawFeeRate: 'Withdraw Fee Rate',
@ -792,6 +941,65 @@ export default {
pending: 'Pending',
approved: 'Approved',
rejected: 'Rejected'
},
withdrawal: {
title: 'Commission Withdrawal Management',
settlementApplications: 'Settlement Applications',
applicationId: 'Application ID',
username: 'Username',
applyAmount: 'Apply Amount',
commissionAmount: 'Commission Amount',
applyRemark: 'Apply Remark',
reviewRemark: 'Review Remark',
reviewerId: 'Reviewer ID',
reviewTime: 'Review Time',
paidAt: 'Paid At',
paymentMethod: 'Payment Method',
transactionId: 'Transaction ID',
createdAt: 'Created At',
updatedAt: 'Updated At',
status: {
pending: 'Pending',
approved: 'Approved',
rejected: 'Rejected',
paid: 'Paid'
},
searchPlaceholder: 'Search username',
filters: {
status: 'Status Filter',
timeRange: 'Time Range',
username: 'Username'
},
actions: {
view: 'View Detail',
approve: 'Approve',
reject: 'Reject'
},
detail: {
title: 'Settlement Application Detail',
basicInfo: 'Basic Information',
orderList: 'Related Orders',
orderId: 'Order ID',
orderNo: 'Order Number',
amount: 'Amount',
actualAmount: 'Actual Amount',
quantity: 'Quantity',
orderStatus: 'Order Status',
paymentStatus: 'Payment Status',
refundStatus: 'Refund Status'
},
approval: {
title: 'Review Settlement Application',
approve: 'Approve',
reject: 'Reject',
enterCommissionAmount: 'Please enter commission amount',
enterRemark: 'Please enter review remark',
enterReason: 'Please enter reject reason',
commissionAmount: 'Commission Amount',
reason: 'Reject Reason',
success: 'Review successful',
failed: 'Review failed'
}
}
},
promptManagement: {

View File

@ -22,7 +22,8 @@ export default {
saveSuccess: '保存成功',
saveFailed: '保存失败',
deleteSuccess: '删除成功',
deleteFailed: '删除失败'
deleteFailed: '删除失败',
all: '全部',
},
orderManagement: {
title: '订单',
@ -236,6 +237,8 @@ orderManagement: {
}
},
common: {
actions: '操作',
all: '全部',
detail: '详情',
active: '活跃',
inactive: '非活跃',
@ -268,6 +271,8 @@ orderManagement: {
operationFailed: '操作失败'
},
layout: {
employee: '员工管理',
shop: '店铺管理',
dashboard: '仪表板',
content: '内容审核',
contentReview: '订单审核',
@ -281,6 +286,8 @@ orderManagement: {
userList: '用户列表',
pointsManagement: '充值包管理',
commissionManagement: '佣金管理',
commissionConfiguration: '佣金配置',
commissionWithdrawal: '佣金提现',
promptManagement: '提示词管理',
productManagement: '产品管理',
voucherManagement: '优惠券管理',
@ -749,6 +756,7 @@ orderManagement: {
commissionManagement: {
title: '佣金管理',
configTitle: '佣金配置',
commissionWithdrawal: '佣金提现',
commissionRate: '佣金比例',
minWithdrawAmount: '最低提现金额',
withdrawFeeRate: '提现费率',
@ -785,6 +793,65 @@ orderManagement: {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝'
},
withdrawal: {
title: '佣金提现管理',
settlementApplications: '结算申请',
applicationId: '申请ID',
username: '用户名',
applyAmount: '申请金额',
commissionAmount: '佣金金额',
applyRemark: '申请备注',
reviewRemark: '审核备注',
reviewerId: '审核人ID',
reviewTime: '审核时间',
paidAt: '支付时间',
paymentMethod: '支付方式',
transactionId: '交易ID',
createdAt: '创建时间',
updatedAt: '更新时间',
status: {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
paid: '已支付'
},
searchPlaceholder: '搜索用户名',
filters: {
status: '状态筛选',
timeRange: '时间范围',
username: '用户名'
},
actions: {
view: '查看详情',
approve: '审核通过',
reject: '审核拒绝'
},
detail: {
title: '结算申请详情',
basicInfo: '基本信息',
orderList: '关联订单',
orderId: '订单ID',
orderNo: '订单号',
amount: '金额',
actualAmount: '实际金额',
quantity: '数量',
orderStatus: '订单状态',
paymentStatus: '支付状态',
refundStatus: '退款状态'
},
approval: {
title: '审核结算申请',
approve: '审核通过',
reject: '审核拒绝',
enterCommissionAmount: '请输入佣金金额',
enterRemark: '请输入审核备注',
enterReason: '请输入拒绝理由',
commissionAmount: '佣金金额',
reason: '拒绝理由',
success: '审核成功',
failed: '审核失败'
}
}
},
promptManagement: {
@ -1045,7 +1112,66 @@ orderManagement: {
warning: '警告',
info: '信息',
close: '关闭',
back: '返回'
back: '返回',
submit: '提交'
},
// 店铺管理
shop: {
pleaseEnterAddress: '请输入地址',
pleaseEnterContactPerson: '请输入联系人',
pleaseEnterContactPhone: '请输入联系电话',
shopManagement: '店铺管理',
addShop: '新增店铺',
shopName: '店铺名称',
pleaseEnterShopName: '请输入店铺名称',
shopCode: '店铺编码',
pleaseEnterShopCode: '请输入店铺编码',
shopType: '店铺类型',
pleaseSelectShopType: '请选择店铺类型',
physicalShop: '实体店',
onlineShop: '网店',
businessStatus: '营业状态',
pleaseSelectBusinessStatus: '请选择营业状态',
closed: '关闭',
open: '营业',
status: '状态',
pleaseSelectStatus: '请选择状态',
disabled: '禁用',
enabled: '启用',
address: '地址',
contactPerson: '联系人',
contactPhone: '联系电话',
createdAt: '创建时间',
operation: '操作',
disable: '禁用',
enable: '启用',
closeShop: '关闭营业',
openShop: '开启营业',
batchDelete: '批量删除',
latitude: '纬度',
pleaseEnterLatitude: '请输入纬度',
longitude: '经度',
pleaseEnterLongitude: '请输入经度',
province: '省份',
pleaseEnterProvince: '请输入省份',
city: '城市',
pleaseEnterCity: '请输入城市',
district: '区域',
pleaseEnterDistrict: '请输入区域',
contactEmail: '联系邮箱',
pleaseEnterContactEmail: '请输入联系邮箱',
businessHours: '营业时间',
pleaseEnterBusinessHours: '请输入营业时间',
shopDesc: '店铺描述',
pleaseEnterShopDesc: '请输入店铺描述',
pleaseSelectProvinceCityDistrict: '请选择省市区',
licenseNumber: '店铺证书号',
pleaseEnterLicenseNumber: '请输入店铺证书号',
licenseImage: '店铺证书图片',
licenseImageTip: '请上传店铺证书图片',
licenseImageUploadSuccess: '证书图片上传成功',
licenseImageUploadFailed: '证书图片上传失败'
},
// 3D模型预览器
@ -1086,5 +1212,94 @@ orderManagement: {
points: {
pointsList: '积分列表'
}
},
// 员工管理
employee: {
idCardImageTip: '请上传身份证图片',
title: '员工管理',
employeeManagement: '员工管理',
addEmployee: '添加员工',
editEmployee: '编辑员工',
employeeList: '员工列表',
employeeNo: '员工编号',
employeeName: '员工姓名',
employeeType: '员工类型',
position: '职位',
department: '部门',
level: '级别',
phone: '电话',
email: '邮箱',
address: '地址',
gender: '性别',
birthday: '生日',
avatar: '头像',
hireDate: '入职日期',
probationEndDate: '试用期结束日期',
contractStartDate: '合同开始日期',
contractEndDate: '合同结束日期',
salary: '薪资',
salaryType: '薪资类型',
workHours: '工作时间',
workLocation: '工作地点',
workStatus: '工作状态',
status: '状态',
shopId: '店铺',
adminId: '管理员',
employeeDetail: '员工详情',
pleaseEnterEmployeeNo: '请输入员工编号',
pleaseEnterEmployeeName: '请输入员工姓名',
pleaseSelectEmployeeType: '请选择员工类型',
pleaseEnterPosition: '请输入职位',
pleaseEnterDepartment: '请输入部门',
pleaseEnterLevel: '请输入级别',
pleaseEnterPhone: '请输入电话',
pleaseEnterEmail: '请输入邮箱',
pleaseEnterAddress: '请输入地址',
pleaseSelectGender: '请选择性别',
pleaseSelectShop: '请选择店铺',
pleaseSelectAdmin: '请选择管理员',
male: '男',
female: '女',
other: '其他',
regular: '正式员工',
partTime: '兼职',
temporary: '临时工',
workStatusOptions: {
resigned: '离职',
onJob: '在职',
onLeave: '请假',
suspended: '停职'
},
statusOptions: {
normal: '正常',
disabled: '禁用'
},
basicInfo: '基本信息',
workInfo: '工作信息',
contactInfo: '联系信息',
contractInfo: '合同信息',
financialInfo: '财务信息',
operation: '操作',
viewDetail: '查看详情',
updateStatus: '更新状态',
resign: '离职',
confirmResign: '确定要办理该员工离职吗?',
deleteConfirm: '确定要删除该员工吗?',
batchDeleteConfirm: '确定要批量删除选中的员工吗?',
createSuccess: '创建员工成功',
updateSuccess: '更新员工成功',
deleteSuccess: '删除员工成功',
statusUpdateSuccess: '状态更新成功',
resignSuccess: '员工离职办理成功',
searchPlaceholder: '搜索员工编号、姓名或电话',
idCard: '身份证号',
pleaseEnterIdCard: '请输入身份证号',
idCardImageFront: '身份证正面',
idCardImageBack: '身份证背面',
uploadIdCardImageFront: '上传身份证正面',
uploadIdCardImageBack: '上传身份证背面',
imageUploadTip: '支持JPG、PNG格式大小不超过2MB',
imageUploadSuccess: '图片上传成功',
imageUploadFailed: '图片上传失败'
}
}

View File

@ -26,7 +26,9 @@ const AdminProductManagement = () => import('@/views/admin/ProductManagement/Pro
const AdminVoucherManagement = () => import('@/views/admin/VoucherManagement/VoucherManagement.vue')
const AdminOperationLog = () => import('@/views/admin/AdminOperationLog/AdminOperationLog.vue')
const AdminAgent = () => import('@/views/admin/Adminagent/index.vue')
const AdminCommissionWithdrawal = () => import('@/views/admin/AdminCommissionManagement/AdminCommissionWithdrawal.vue')
const AdminShop = () => import('@/views/admin/AdminShop/shop.vue')
const AdminEmployee = () => import('@/views/admin/AdminShop/employee.vue')
//权限路由映射表
export const permissionRoutes = [
{
@ -97,6 +99,75 @@ export const permissionRoutes = [
}
]
},
{
path: 'commission',
name: 'Commission',
meta: {
title: 'admin.layout.commissionManagement',
icon: 'Coin',
menuOrder: 3,
requiresAuth: true
},
children:[
{
path: 'commission-management',
name: 'CommissionManagement',
component: AdminCommissionManagement,
meta: {
title: 'admin.layout.commissionConfiguration',
icon: 'Coin',
menuOrder: 1,
requiresAuth: true
}
}
,
{
path: 'commission-withdrawal',
name: 'CommissionWithdrawal',
component: AdminCommissionWithdrawal,
meta: {
title: 'admin.layout.commissionWithdrawal',
icon: 'Coin',
menuOrder: 2,
requiresAuth: true
}
}
]
},
{
path: 'adminshop',
name: 'AdminShop',
meta: {
title: 'admin.layout.shop',
icon: 'Shop',
menuOrder: 3,
requiresAuth: true
},
children:[
{
path: 'shop',
name: 'Shop',
component: AdminShop,
meta: {
title: 'admin.layout.shop',
icon: 'Shop',
menuOrder: 1,
requiresAuth: true
}
},
{
path: 'employee',
name: 'Employee',
component: AdminEmployee,
meta: {
title: 'admin.layout.employee',
icon: 'UserFilled',
menuOrder: 2,
requiresAuth: true
}
}
]
},
{
path: 'users',
name: 'AdminUsers',
@ -141,17 +212,7 @@ export const permissionRoutes = [
requiresAuth: true
}
},
{
path: 'commission-management',
name: 'AdminCommissionManagement',
component: AdminCommissionManagement,
meta: {
title: 'admin.layout.commissionManagement',
icon: 'Coin',
menuOrder: 5,
requiresAuth: true
}
},
{
path: 'prompt-management',
name: 'AdminPromptManagement',

View File

@ -0,0 +1,640 @@
<template>
<div class="admin-commission-withdrawal">
<h2 class="page-title">{{ t('admin.commissionManagement.withdrawal.title') }}</h2>
<div class="commission-withdrawal-content">
<el-card shadow="hover" class="commission-card">
<template #header>
<div class="card-header">
<span>{{ t('admin.commissionManagement.withdrawal.settlementApplications') }}</span>
</div>
</template>
<div class="card-body">
<!-- 筛选表单 -->
<div class="filter-form">
<el-form :model="filterForm" inline>
<el-form-item :label="t('admin.commissionManagement.withdrawal.username')">
<el-input
v-model="filterForm.username"
:placeholder="t('admin.commissionManagement.withdrawal.searchPlaceholder')"
style="width: 200px"
@keyup.enter="getSettlementApplications"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item :label="t('admin.commissionManagement.withdrawal.filters.status')">
<el-select v-model="filterForm.status" :placeholder="t('admin.common.all')" style="width: 120px">
<el-option :label="t('admin.common.all')" value="" />
<el-option :label="t('admin.commissionManagement.withdrawal.status.pending')" :value="0" />
<el-option :label="t('admin.commissionManagement.withdrawal.status.approved')" :value="1" />
<el-option :label="t('admin.commissionManagement.withdrawal.status.rejected')" :value="2" />
<el-option :label="t('admin.commissionManagement.withdrawal.status.paid')" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getSettlementApplications">
<el-icon><Search /></el-icon>
{{ t('admin.common.search') }}
</el-button>
<el-button @click="resetFilters">{{ t('admin.common.reset') }}</el-button>
</el-form-item>
</el-form>
</div>
<!-- 表格 -->
<el-table
:data="applications"
style="width: 100%"
v-loading="loading"
:header-cell-style="{background: '#fafafa'}"
stripe
fit
>
<el-table-column prop="id" :label="t('admin.commissionManagement.withdrawal.applicationId')" width="120" />
<el-table-column prop="username" :label="t('admin.commissionManagement.withdrawal.username')" min-width="180" />
<el-table-column prop="apply_amount" :label="t('admin.commissionManagement.withdrawal.applyAmount')" min-width="120" />
<el-table-column prop="commission_amount" :label="t('admin.commissionManagement.withdrawal.commissionAmount')" min-width="120" />
<el-table-column :label="t('admin.commissionManagement.withdrawal.status.pending')" min-width="120">
<template #default="scope">
<el-tag
:type="scope.row.status === 0 ? 'warning' : scope.row.status === 1 ? 'success' : scope.row.status === 2 ? 'danger' : 'info'"
size="small"
>
{{ scope.row.status === 0 ? t('admin.commissionManagement.withdrawal.status.pending') :
scope.row.status === 1 ? t('admin.commissionManagement.withdrawal.status.approved') :
scope.row.status === 2 ? t('admin.commissionManagement.withdrawal.status.rejected') :
t('admin.commissionManagement.withdrawal.status.paid') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" :label="t('admin.commissionManagement.withdrawal.createdAt')" min-width="180">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column :label="t('admin.common.actions')" width="300" fixed="right">
<template #default="scope">
<el-button size="small" type="primary" @click="viewDetail(scope.row)">
<el-icon><View /></el-icon>
{{ t('admin.commissionManagement.withdrawal.actions.view') }}
</el-button>
<el-button
v-if="scope.row.status === 0"
size="small"
type="success"
@click="approveApplication(scope.row)"
>
<el-icon><Check /></el-icon>
{{ t('admin.commissionManagement.withdrawal.actions.approve') }}
</el-button>
<el-button
v-if="scope.row.status === 0"
size="small"
type="danger"
@click="rejectApplication(scope.row)"
>
<el-icon><Close /></el-icon>
{{ t('admin.commissionManagement.withdrawal.actions.reject') }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</el-card>
</div>
<!-- 详情弹窗 -->
<el-dialog
:title="t('admin.commissionManagement.withdrawal.detail.title')"
v-model="detailDialogVisible"
width="800px"
append-to-body
>
<div class="detail-content">
<!-- 基本信息 -->
<div class="detail-section">
<h3>{{ t('admin.commissionManagement.withdrawal.detail.basicInfo') }}</h3>
<el-descriptions :column="2" border>
<el-descriptions-item :label="t('admin.commissionManagement.withdrawal.applicationId')">{{ currentApplication.id }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.commissionManagement.withdrawal.username')">{{ currentApplication.username }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.commissionManagement.withdrawal.applyAmount')">{{ currentApplication.apply_amount }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.commissionManagement.withdrawal.commissionAmount')">{{ currentApplication.commission_amount }}</el-descriptions-item>
<el-descriptions-item :label="t('admin.commissionManagement.withdrawal.status.pending')">
<el-tag
:type="currentApplication.status === 0 ? 'warning' : currentApplication.status === 1 ? 'success' : currentApplication.status === 2 ? 'danger' : 'info'"
size="small"
>
{{ currentApplication.status === 0 ? t('admin.commissionManagement.withdrawal.status.pending') :
currentApplication.status === 1 ? t('admin.commissionManagement.withdrawal.status.approved') :
currentApplication.status === 2 ? t('admin.commissionManagement.withdrawal.status.rejected') :
t('admin.commissionManagement.withdrawal.status.paid') }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="t('admin.commissionManagement.withdrawal.createdAt')">{{ formatDate(currentApplication.created_at) }}</el-descriptions-item>
<el-descriptions-item v-if="currentApplication.apply_remark" :label="t('admin.commissionManagement.withdrawal.applyRemark')">{{ currentApplication.apply_remark }}</el-descriptions-item>
<el-descriptions-item v-if="currentApplication.review_remark" :label="t('admin.commissionManagement.withdrawal.reviewRemark')">{{ currentApplication.review_remark }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 关联订单 -->
<div class="detail-section">
<h3>{{ t('admin.commissionManagement.withdrawal.detail.orderList') }}</h3>
<el-table
:data="relatedOrders"
style="width: 100%"
:header-cell-style="{background: '#fafafa'}"
stripe
fit
v-if="relatedOrders.length > 0"
>
<el-table-column prop="order_no" :label="t('admin.commissionManagement.withdrawal.detail.orderNo')" min-width="200" />
<el-table-column prop="amount" :label="t('admin.commissionManagement.withdrawal.detail.amount')" min-width="120" />
<el-table-column prop="quantity" :label="t('admin.commissionManagement.withdrawal.detail.quantity')" min-width="100" />
<el-table-column prop="created_at" :label="t('admin.commissionManagement.withdrawal.createdAt')" min-width="180">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
</template>
</el-table-column>
</el-table>
<div v-else class="no-data">{{ t('admin.common.noData') }}</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailDialogVisible = false">{{ t('admin.common.close') }}</el-button>
</span>
</template>
</el-dialog>
<!-- 审核通过弹窗 -->
<el-dialog
:title="t('admin.commissionManagement.withdrawal.approval.approve')"
v-model="approveDialogVisible"
width="600px"
append-to-body
>
<el-form :model="approvalForm" label-width="120px" :rules="approvalRules" ref="approvalFormRef">
<el-form-item :label="t('admin.commissionManagement.withdrawal.approval.commissionAmount')" prop="commission_amount">
<el-input-number
v-model="approvalForm.commission_amount"
:min="0.01"
:precision="2"
style="width: 100%"
:placeholder="t('admin.commissionManagement.withdrawal.approval.enterCommissionAmount')"
/>
</el-form-item>
<el-form-item :label="t('admin.commissionManagement.withdrawal.reviewRemark')" prop="remark">
<el-input
v-model="approvalForm.remark"
:placeholder="t('admin.commissionManagement.withdrawal.approval.enterRemark')"
type="textarea"
:rows="3"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="approveDialogVisible = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="submitApprove">{{ t('admin.common.confirm') }}</el-button>
</span>
</template>
</el-dialog>
<!-- 审核拒绝弹窗 -->
<el-dialog
:title="t('admin.commissionManagement.withdrawal.approval.reject')"
v-model="rejectDialogVisible"
width="600px"
append-to-body
>
<el-form :model="rejectForm" label-width="120px" :rules="rejectRules" ref="rejectFormRef">
<el-form-item :label="t('admin.commissionManagement.withdrawal.approval.reason')" prop="reason">
<el-input
v-model="rejectForm.reason"
:placeholder="t('admin.commissionManagement.withdrawal.approval.enterReason')"
type="textarea"
:rows="3"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="rejectDialogVisible = false">{{ t('admin.common.cancel') }}</el-button>
<el-button type="primary" @click="submitReject">{{ t('admin.common.confirm') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElLoading } from 'element-plus'
import { Search, View, Check, Close } from '@element-plus/icons-vue'
import { AdminCommissionManagement } from './index.js'
const { t } = useI18n()
// AdminCommissionManagement
const adminCommissionManagement = new AdminCommissionManagement()
//
const applications = ref([])
//
const pagination = reactive({
pageNum: 1,
pageSize: 10,
total: 0
})
//
const filterForm = reactive({
username: '',
status: '',
created_at_start: '',
created_at_end: ''
})
//
const loading = ref(false)
//
const detailDialogVisible = ref(false)
const currentApplication = ref({})
const relatedOrders = ref([])
//
const approveDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const approvalFormRef = ref(null)
const rejectFormRef = ref(null)
const approvalForm = reactive({
commission_amount: 0,
remark: ''
})
const rejectForm = reactive({
reason: ''
})
const currentApplicationId = ref(null)
//
const approvalRules = {
commission_amount: [
{ required: true, message: t('admin.commissionManagement.withdrawal.approval.enterCommissionAmount'), trigger: 'blur' },
{ type: 'number', min: 0.01, message: t('admin.commissionManagement.withdrawal.approval.enterCommissionAmount'), trigger: 'blur' }
]
}
const rejectRules = {
reason: [
{ required: true, message: t('admin.commissionManagement.withdrawal.approval.enterReason'), trigger: 'blur' }
]
}
//
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleString()
}
//
const getSettlementApplications = async () => {
loading.value = true
try {
const data = {
page: pagination.pageNum,
page_size: pagination.pageSize,
username: filterForm.username,
status: filterForm.status ? [parseInt(filterForm.status)] : [],
created_at_start: filterForm.created_at_start,
created_at_end: filterForm.created_at_end
}
const response = await adminCommissionManagement.getSettlementApplications(data)
applications.value = response.data.items || []
pagination.total = response.data.total || 0
} catch (error) {
ElMessage.error(t('admin.common.requestFailed'))
console.error('Failed to get settlement applications:', error)
} finally {
loading.value = false
}
}
//
const resetFilters = () => {
filterForm.username = ''
filterForm.status = ''
filterForm.created_at_start = ''
filterForm.created_at_end = ''
pagination.pageNum = 1
getSettlementApplications()
}
//
const handleSizeChange = (size) => {
pagination.pageSize = size
pagination.pageNum = 1
getSettlementApplications()
}
//
const handleCurrentChange = (current) => {
pagination.pageNum = current
getSettlementApplications()
}
//
const viewDetail = async (application) => {
currentApplication.value = application
currentApplicationId.value = application.id
//
try {
loading.value = true
const response = await adminCommissionManagement.getSettlementOrders({
commission_settlement_id: application.id,
page: 1,
page_size: 100
})
relatedOrders.value = response.data.items || []
detailDialogVisible.value = true
} catch (error) {
ElMessage.error(t('admin.common.requestFailed'))
console.error('Failed to get related orders:', error)
relatedOrders.value = []
} finally {
loading.value = false
}
}
//
const approveApplication = (application) => {
currentApplicationId.value = application.id
approvalForm.commission_amount = application.apply_amount
approvalForm.remark = ''
approveDialogVisible.value = true
}
//
const rejectApplication = (application) => {
currentApplicationId.value = application.id
rejectForm.reason = ''
rejectDialogVisible.value = true
}
//
const submitApprove = async () => {
if (approvalFormRef.value) {
approvalFormRef.value.validate(async (valid) => {
if (valid) {
try {
loading.value = true
const result = await adminCommissionManagement.approveSettlementApplication({
id: currentApplicationId.value,
commission_amount: approvalForm.commission_amount,
remark: approvalForm.remark
})
if (result.success) {
ElMessage.success(t('admin.commissionManagement.withdrawal.approval.success'))
approveDialogVisible.value = false
getSettlementApplications()
} else {
ElMessage.error(result.message || t('admin.commissionManagement.withdrawal.approval.failed'))
}
} catch (error) {
ElMessage.error(t('admin.commissionManagement.withdrawal.approval.failed'))
console.error('Failed to approve application:', error)
} finally {
loading.value = false
}
}
})
}
}
//
const submitReject = async () => {
if (rejectFormRef.value) {
rejectFormRef.value.validate(async (valid) => {
if (valid) {
try {
loading.value = true
const result = await adminCommissionManagement.rejectSettlementApplication({
id: currentApplicationId.value,
reason: rejectForm.reason
})
if (result.success) {
ElMessage.success(t('admin.commissionManagement.withdrawal.approval.success'))
rejectDialogVisible.value = false
getSettlementApplications()
} else {
ElMessage.error(result.message || t('admin.commissionManagement.withdrawal.approval.failed'))
}
} catch (error) {
ElMessage.error(t('admin.commissionManagement.withdrawal.approval.failed'))
console.error('Failed to reject application:', error)
} finally {
loading.value = false
}
}
})
}
}
//
onMounted(() => {
getSettlementApplications()
})
</script>
<style scoped>
.admin-commission-withdrawal {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.commission-withdrawal-content {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
flex: 1;
}
.commission-card {
margin-bottom: 20px;
width: 100%;
flex: 1;
}
/* 使用深度选择器确保卡片占满宽度 */
:deep(.el-card) {
width: 100% !important;
display: flex;
flex-direction: column;
}
:deep(.el-card__body) {
padding: 20px;
width: 100% !important;
box-sizing: border-box;
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0;
}
.card-body {
padding: 20px;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 1;
}
/* 确保表格容器占满宽度 */
.filter-form {
background: #fafafa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
border: 1px solid #e0e0e0;
width: 100%;
box-sizing: border-box;
}
/* 深度选择器优化表格样式 */
:deep(.el-table) {
width: 100% !important;
flex: 1;
}
:deep(.el-table__inner-wrapper) {
width: 100% !important;
min-width: 100% !important;
}
:deep(.el-table__header-wrapper) {
width: 100% !important;
min-width: 100% !important;
}
:deep(.el-table__body-wrapper) {
width: 100% !important;
min-width: 100% !important;
}
:deep(.el-table__container) {
width: 100% !important;
min-width: 100% !important;
}
:deep(.el-table__header) {
width: 100% !important;
}
:deep(.el-table__body) {
width: 100% !important;
}
:deep(.el-table__row) {
width: 100% !important;
}
/* 确保表格列正确计算宽度 */
:deep(.el-table__column) {
min-width: auto !important;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
width: 100%;
box-sizing: border-box;
}
/* 详情样式 */
.detail-content {
max-height: 60vh;
overflow-y: auto;
}
.detail-section {
margin-bottom: 20px;
}
.detail-section h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
color: #333;
}
.no-data {
text-align: center;
padding: 40px;
color: #909399;
font-size: 14px;
}
/* 深色主题适配 */
:deep(.el-card__header) {
background: #fafafa;
border-bottom: 1px solid #e0e0e0;
}
:deep(.el-dialog__header) {
background: #fafafa;
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
}
:deep(.el-dialog__title) {
font-weight: 600;
}
/* 响应式设计 */
@media (max-width: 768px) {
.filter-form {
padding: 10px;
}
.el-form--inline .el-form-item {
margin-right: 0;
margin-bottom: 10px;
width: 100%;
}
.el-form--inline .el-form-item__content {
width: 100%;
}
}
</style>

View File

@ -4,15 +4,28 @@ export class AdminCommissionManagement {
}
// 更新达人佣金配置参数
async updateCommissionConfig(data) {
let parmas = {
commissionRate: data.commissionRate,
minWithdrawAmount: data.minWithdrawAmount,
withdrawFeeRate: data.withdrawFeeRate,
settlementCycle: data.settlementCycle,
status: data.status,
remark: data.remark
let params = {};
if (data.commissionRate !== undefined) {
params.commissionRate = data.commissionRate;
}
return requestUtils.common(adminApi.default.commissionConfigUpdate,parmas)
if (data.minWithdrawAmount !== undefined) {
params.minWithdrawAmount = data.minWithdrawAmount;
}
if (data.withdrawFeeRate !== undefined) {
params.withdrawFeeRate = data.withdrawFeeRate;
}
if (data.settlementCycle !== undefined) {
params.settlementCycle = data.settlementCycle;
}
if (data.status !== undefined) {
params.status = data.status;
}
if (data.remark) {
params.remark = data.remark;
}
return requestUtils.common(adminApi.default.commissionConfigUpdate,params)
}
// 获取佣金配置信息
async viewCommissionConfig() {
@ -39,4 +52,149 @@ export class AdminCommissionManagement {
*/
}
//获取结算申请列表
async getSettlementApplications(data) {
let params = {
page: data.page || 1,
page_size: data.page_size || 10
};
// 只传递有实际值的筛选条件
if (data.username) {
params.username = data.username;
}
if (data.status && data.status.length > 0) {
params.status = data.status;
}
if (data.created_at_start) {
params.created_at_start = data.created_at_start;//格式2026-01-13T02:32:01.462Z
}
if (data.created_at_end) {
params.created_at_end = data.created_at_end;//格式2026-01-13T02:32:01.462Z
}
if (data.sort_by) {
params.sort_by = data.sort_by;
}
if (data.sort_order) {
params.sort_order = data.sort_order;
}
return requestUtils.common(adminApi.default.getSettlementList,params)
/**
* 返回示例
{
"code": 0,
"message": "",
"success": true,
"data": {
"items": [
{
"id": 4,
"user_id": 9,
"username": "超级会员",
"apply_amount": 19.8,
"commission_amount": 0,
"status": 0,
"apply_remark": "string",
"review_remark": null,
"reviewer_id": null,
"review_time": null,
"paid_at": null,
"payment_method": null,
"transaction_id": null,
"created_at": "2026-01-12T10:15:43.455732+00:00",
"updated_at": "2026-01-12T10:15:43.455732+00:00"
}
],
"page": 1,
"page_size": 10,
"total": 1
}
}
*/
}
//审核通过结算申请
async approveSettlementApplication(data) {
let params = {
"id": data.id,
"commission_amount": data.commission_amount,//佣金金额
"remark": data.remark
}
return requestUtils.common(adminApi.default.approveSettlement,params)
}
//审核拒绝结算申请
async rejectSettlementApplication(data) {
let params = {
"id": data.id,
"reason": data.reason//拒绝理由
}
return requestUtils.common(adminApi.default.rejectSettlement,params)
}
//根据结算ID获取订单列表
async getSettlementOrders(data) {
let params = {
"commission_settlement_id": data.commission_settlement_id ,//结算ID
"page": data.page || 1,
"page_size": data.page_size || 10
}
return requestUtils.common(adminApi.default.getSettlementOrders,params)
/*
返回示例
{
"code": 0,
"message": "",
"success": true,
"data": {
"items": [
{
"id": 80,
"order_no": "ORD202601120924054F70E7BE",
"user_id": 13,
"amount": 99,
"actual_amount": 99,
"quantity": 1,
"order_status": 5,
"payment_status": 1,
"refund_status": 0,
"is_commission_settled": 1,
"commission_settlement_id": 4,
"order_info": {
"ipName": "角马",
"contact": {
"emailOrPhone": "13121765685"
},
"quantity": 1,
"shipping": {
"city": "test",
"phone": "test",
"state": "AL",
"country": "US",
"address1": "test",
"address2": "test",
"lastName": "王",
"firstName": "闯",
"postalCode": "test",
"stateLabel": "Alabama",
"countryLabel": "美国"
},
"modelData": {
"imageUrl": "https://draft-user.s3.us-east-2.amazonaws.com/gemini/d422a403-3f9b-43d7-80dd-355c80e6f801.jpg"
},
"product_name": "E1"
},
"project_details": {
"imageUrl": "https://draft-user.s3.us-east-2.amazonaws.com/gemini/d422a403-3f9b-43d7-80dd-355c80e6f801.jpg"
},
"remark": null,
"created_at": "2026-01-12T09:24:03.847070+00:00",
"updated_at": "2026-01-12T10:14:49.085863+00:00"
}
],
"page": 1,
"page_size": 10,
"total": 1
}
}
*/
}
}

View File

@ -635,7 +635,7 @@ const refresh = () => {
//
const handleConfirmOrder = (row) => {
//
router.push('/admin/content-review')
router.push('/admin/orders/content-review')
}
//

View File

@ -657,7 +657,7 @@ const handleStatusUpdate = async () => {
//
const handleConfirmOrder = (row) => {
//
router.push(`/admin/content-review?order_no=${row.order_no}`)
router.push(`/admin/orders/content-review?order_no=${row.order_no}`)
}
//

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,519 @@
import {requestUtils,adminApi} from '@deotaland/utils';
export class AdminShop {
constructor() {
}
//获取店铺列表
async getShopList(data){
let params = {
shopName:data.shopName,
shopCode:data.shopCode,
shopType:data.shopType ,
province:data.province ,
city:data.city ,
district:data.district ,
businessStatus:data.businessStatus,
status:data.status ,
pageSize:data.pageSize || 10,
pageNum:data.pageNum || 1,
orderByColumn:data.orderByColumn ,
isAsc:data.isAsc ,
}
return await requestUtils.common(adminApi.default.getShopList,params);
/**
返回示例
{
"code": 0,
"success": true,
"data": {
"total": 9007199254740991,
"rows": [
{
"id": 9007199254740991,
"shopName": "string",
"shopCode": "string",
"shopType": 1073741824,
"shopTypeName": "string",
"latitude": 0,
"longitude": 0,
"address": "string",
"province": "string",
"city": "string",
"district": "string",
"fullAddress": "string",
"contactPerson": "string",
"contactPhone": "string",
"contactEmail": "string",
"businessHours": "string",
"businessStatus": 1073741824,
"businessStatusName": "string",
"shopLogo": "string",
"shopImages": [
"string"
],
"shopDesc": "string",
"licenseNumber": "string",
"licenseImage": "string",
"area": 0,
"employeeCount": 1073741824,
"extraInfo": {
"additionalProp1": {},
"additionalProp2": {},
"additionalProp3": {}
},
"remark": "string",
"status": 1073741824,
"statusName": "string",
"createdAt": "2026-01-13T03:54:34.648Z",
"updatedAt": "2026-01-13T03:54:34.648Z"
}
],
"code": 1073741824,
"msg": "string"
},
"message": "操作成功"
}
*/
}
//获取店铺详情
async getShopDetail(data){
let params = {
id :data.shopId ,
}
const requestData = {
method:adminApi.default.getShopDetail.method,
url:adminApi.default.getShopDetail.url.replace('{id}',params.id ),
isLoading:adminApi.default.getShopDetail.isLoading,
}
return await requestUtils.common(requestData,params);
/*
返回示例
{
"code": 0,
"success": true,
"data": {
"id": 9007199254740991,
"shopName": "string",
"shopCode": "string",
"shopType": 1073741824,
"latitude": 0,
"longitude": 0,
"address": "string",
"province": "string",
"city": "string",
"district": "string",
"contactPerson": "string",
"contactPhone": "string",
"contactEmail": "string",
"businessHours": "string",
"businessStatus": 1073741824,
"shopLogo": "string",
"shopImages": [
"string"
],
"shopDesc": "string",
"licenseNumber": "string",
"licenseImage": "string",
"area": 0,
"employeeCount": 1073741824,
"extraInfo": {
"additionalProp1": {},
"additionalProp2": {},
"additionalProp3": {}
},
"remark": "string",
"status": 1073741824,
"isDelete": 1073741824,
"createdAt": "2026-01-13T05:03:33.438Z",
"updatedAt": "2026-01-13T05:03:33.438Z"
},
"message": "操作成功"
}
*/
}
//更新店铺状态
async updateShopStatus(data){
let params = {
shopId:data.shopId ,
status:data.status ,//状态0禁用,1启用
}
const requestData = {
method:adminApi.default.updateShopStatus.method,
url:adminApi.default.updateShopStatus.url.replace('{id}',params.shopId).replace('status',`status?status=${params.status}`),
isLoading:adminApi.default.updateShopStatus.isLoading,
}
return await requestUtils.common(requestData,params);
}
//更新营业状态
async updateBusinessStatus(data){
let params = {
shopId:data.shopId ,
businessStatus:data.businessStatus ,//营业状态0关闭,1营业
}
const requestData = {
method:adminApi.default.updateBusinessStatus.method,
url:adminApi.default.updateBusinessStatus.url.replace('{id}',params.shopId).replace('business-status',`business-status?businessStatus=${params.businessStatus}`),
isLoading:adminApi.default.updateBusinessStatus.isLoading,
}
return await requestUtils.common(requestData,params);
}
//更新店铺
async updateShop(data){
let params = {
shopId:data.shopId ,
shopName: data.shopName ,
shopCode: data.shopCode ,
shopType: data.shopType ,
latitude: data.latitude || 0,
longitude: data.longitude || 0,
address: data.address ,
province: data.province ,
city: data.city ,
district: data.district ,
contactPerson: data.contactPerson ,
contactPhone: data.contactPhone ,
contactEmail: data.contactEmail ,
businessHours: data.businessHours ,
businessStatus: data.businessStatus ,
shopLogo: data.shopLogo ,
shopImages: data.shopImages || [],
shopDesc: data.shopDesc ,
licenseNumber: data.licenseNumber ,
licenseImage: data.licenseImage ,
area: data.area || 0,
extraInfo: data.extraInfo || {},
remark: data.remark ,
status: data.status
}
const requestData = {
method:adminApi.default.updateShop.method,
url:adminApi.default.updateShop.url.replace('{id}',params.shopId),
isLoading:adminApi.default.updateShop.isLoading,
}
return await requestUtils.common(requestData,params);
}
//删除店铺
async deleteShop(data){
let params = {
shopId:data.shopId ,
}
const requestData = {
method:adminApi.default.deleteShop.method,
url:adminApi.default.deleteShop.url.replace('{id}',params.shopId),
isLoading:adminApi.default.deleteShop.isLoading,
}
return await requestUtils.common(requestData,params);
}
//批量删除店铺
async batchDeleteShop(data){
let ids = data.ids || []
return await requestUtils.common(adminApi.default.batchDeleteShop,ids);
}
//创建店铺
async createShop(data){
let params = {
shopName: data.shopName ,
shopCode: data.shopCode ,
shopType: data.shopType ,
latitude: data.latitude || 0,
longitude: data.longitude || 0,
address: data.address ,
province: data.province ,
city: data.city ,
district: data.district ,
contactPerson: data.contactPerson ,
contactPhone: data.contactPhone ,
contactEmail: data.contactEmail ,
businessHours: data.businessHours ,
businessStatus: data.businessStatus ,
shopLogo: data.shopLogo ,
shopImages: data.shopImages || [],
shopDesc: data.shopDesc ,
licenseNumber: data.licenseNumber ,//店铺证书号
licenseImage: data.licenseImage ,//店铺证书图片
area: data.area || 0,
extraInfo: data.extraInfo || {},
remark: data.remark ,
status: data.status
};
return await requestUtils.common(adminApi.default.createShop,params);
}
/* 员工相关接口 */
//创建员工
async createShopEmployee(data){
let params = {
shopId: data.shopId,
employeeNo: data.employeeNo,
employeeName: data.employeeName,
adminId: data.adminId,
employeeType: data.employeeType,
position: data.position,
department: data.department,
level: data.level,
phone: data.phone,
email: data.email,
address: data.address,
idCard: data.idCard,
idCardImageFront: data.idCardImageFront,
idCardImageBack: data.idCardImageBack,
gender: data.gender,
birthday: data.birthday,
avatar: data.avatar,
hireDate: data.hireDate,
probationEndDate: data.probationEndDate,
contractStartDate: data.contractStartDate,
contractEndDate: data.contractEndDate,
salary: data.salary,
salaryType: data.salaryType,
workHours: data.workHours,
workLocation: data.workLocation,
extraInfo: data.extraInfo || {},
remark: data.remark,
status: data.status
};
return await requestUtils.common(adminApi.default.createShopEmployee,params);
}
//更新工作状态
async updateWorkStatus(data){
let params = {
id: data.id,
workStatus: data.workStatus,
}
const requestData = {
method:adminApi.default.updateWorkStatus.method,
url:adminApi.default.updateWorkStatus.url.replace('{id}',params.id).replace('work-status',`work-status?workStatus=${params.workStatus}`),
isLoading:adminApi.default.updateWorkStatus.isLoading,
}
return await requestUtils.common(requestData,params);
}
//更新员工状态
async updateEmployeeStatus(data){
let params = {
id: data.id,
status: data.status,//状态: 0禁用 1启用
}
const requestData = {
method:adminApi.default.updateEmployeeStatus.method,
url:adminApi.default.updateEmployeeStatus.url.replace('{id}',params.id).replace('status',`status?status=${params.status}`),
isLoading:adminApi.default.updateEmployeeStatus.isLoading,
}
return await requestUtils.common(requestData,params);
}
//员工离职
async employeeResign(data){
let params = {
id: data.id,
}
const requestData = {
method:adminApi.default.employeeResign.method,
url:adminApi.default.employeeResign.url.replace('{id}',params.id),
isLoading:adminApi.default.employeeResign.isLoading,
}
return await requestUtils.common(requestData,params);
}
//更新员工
async updateEmployee(data){
let params = {
shopId: data.shopId,
employeeNo: data.employeeNo,
employeeName: data.employeeName,
adminId: data.adminId,
employeeType: data.employeeType,
position: data.position,
department: data.department,
level: data.level,
phone: data.phone,
email: data.email,
address: data.address,
idCard: data.idCard,
idCardImageFront: data.idCardImageFront,
idCardImageBack: data.idCardImageBack,
gender: data.gender,
birthday: data.birthday,
avatar: data.avatar,
hireDate: data.hireDate,
probationEndDate: data.probationEndDate,
contractStartDate: data.contractStartDate,
contractEndDate: data.contractEndDate,
salary: data.salary,
salaryType: data.salaryType,
workHours: data.workHours,
workLocation: data.workLocation,
extraInfo: data.extraInfo || {},
remark: data.remark,
status: data.status,
id: data.id,
};
const requestData = {
method:adminApi.default.updateEmployee.method,
url:adminApi.default.updateEmployee.url.replace('{id}',params.id),
isLoading:adminApi.default.updateEmployee.isLoading,
}
return await requestUtils.common(requestData,params);
}
//删除员工
async deleteEmployee(data){
let params = {
id: data.id,
}
const requestData = {
method:adminApi.default.deleteEmployee.method,
url:adminApi.default.deleteEmployee.url.replace('{id}',params.id),
isLoading:adminApi.default.deleteEmployee.isLoading,
}
return await requestUtils.common(requestData,params);
}
//批量删除员工
async batchDeleteEmployee(data){
let ids = data.ids;
const requestData = {
method:adminApi.default.batchDeleteEmployee.method,
url:adminApi.default.batchDeleteEmployee.url,
isLoading:adminApi.default.batchDeleteEmployee.isLoading,
}
return await requestUtils.common(requestData,ids);
}
//查询员工详情
async getEmployeeById(data){
let params = {
id: data.id,
}
const requestData = {
method:adminApi.default.getEmployeeById.method,
url:adminApi.default.getEmployeeById.url.replace('{id}',params.id),
isLoading:adminApi.default.getEmployeeById.isLoading,
}
return await requestUtils.common(requestData,params);
/*
返回示例
{
"code": 0,
"success": true,
"data": {
"id": 9007199254740991,
"shopId": 9007199254740991,
"adminId": 9007199254740991,
"employeeNo": "string",
"employeeName": "string",
"employeeType": 1073741824,
"position": "string",
"department": "string",
"level": "string",
"phone": "string",
"email": "string",
"address": "string",
"idCard": "string",
"idCardImageFront": "string",
"idCardImageBack": "string",
"gender": 1073741824,
"birthday": "2026-01-13",
"avatar": "string",
"hireDate": "2026-01-13",
"probationEndDate": "2026-01-13",
"contractStartDate": "2026-01-13",
"contractEndDate": "2026-01-13",
"salary": 0,
"salaryType": 1073741824,
"workHours": "string",
"workLocation": "string",
"role": "string",
"permissions": [
"string"
],
"workStatus": 1073741824,
"status": 1073741824,
"resignDate": "2026-01-13",
"resignReason": "string",
"extraInfo": {
"additionalProp1": {},
"additionalProp2": {},
"additionalProp3": {}
},
"remark": "string",
"isDelete": 1073741824,
"createdAt": "2026-01-13T07:29:33.333Z",
"updatedAt": "2026-01-13T07:29:33.333Z"
},
"message": "操作成功"
}
*/
}
//分页查询员工列表
async getEmployeeList(data){
let params = {
shopId: data.shopId,
employeeNo: data.employeeNo,
employeeName: data.employeeName,
employeeType: data.employeeType,//员工类型: 0正式员工 1兼职 2临时工
department: data.department,
position: data.position,
phone: data.phone,//手机号
workStatus: data.workStatus,//工作状态: 0离职 1在职 2请假 3停职
status: data.status,//状态: 0正常 1禁用
pageSize: data.pageSize,
pageNum: data.pageNum,
}
const requestData = {
method:adminApi.default.getEmployeeList.method,
url:adminApi.default.getEmployeeList.url,
isLoading:adminApi.default.getEmployeeList.isLoading,
}
return await requestUtils.common(requestData,params);
/*
返回示例
{
"total": 9007199254740991,
"rows": [
{
"id": 9007199254740991,
"shopId": 9007199254740991,
"shopName": "string",
"adminId": 9007199254740991,
"employeeNo": "string",
"employeeName": "string",
"employeeType": 1073741824,
"employeeTypeName": "string",
"position": "string",
"department": "string",
"level": "string",
"phone": "string",
"email": "string",
"address": "string",
"gender": 1073741824,
"genderName": "string",
"birthday": "2026-01-13",
"avatar": "string",
"hireDate": "2026-01-13",
"probationEndDate": "2026-01-13",
"contractStartDate": "2026-01-13",
"contractEndDate": "2026-01-13",
"salary": 0,
"salaryType": 1073741824,
"salaryTypeName": "string",
"workHours": "string",
"workLocation": "string",
"role": "string",
"permissions": [
"string"
],
"workStatus": 1073741824,
"workStatusName": "string",
"status": 1073741824,
"statusName": "string",
"resignDate": "2026-01-13",
"resignReason": "string",
"extraInfo": {
"additionalProp1": {},
"additionalProp2": {},
"additionalProp3": {}
},
"remark": "string",
"createdAt": "2026-01-13T07:29:46.082Z",
"updatedAt": "2026-01-13T07:29:46.082Z"
}
],
"code": 1073741824,
"msg": "string"
}
*/
}
}

View File

@ -0,0 +1,713 @@
<template>
<div class="shop-management">
<!-- 卡片容器 -->
<el-card shadow="hover" class="content-card">
<!-- 头部区域 -->
<template #header>
<div class="card-header">
<span>{{ $t('shop.shopManagement') }}</span>
<el-button type="primary" @click="handleAddShop">{{ $t('shop.addShop') }}</el-button>
</div>
</template>
<!-- 搜索区域 -->
<div class="search-section">
<el-form :model="searchForm" label-width="100px" inline>
<el-form-item :label="$t('shop.shopName')">
<el-input v-model="searchForm.shopName" :placeholder="$t('shop.pleaseEnterShopName')" style="width: 200px;"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.shopCode')">
<el-input v-model="searchForm.shopCode" :placeholder="$t('shop.pleaseEnterShopCode')" style="width: 200px;"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.shopType')">
<el-select v-model="searchForm.shopType" :placeholder="$t('shop.pleaseSelectShopType')" style="width: 200px;">
<el-option :label="$t('shop.physicalShop')" :value="1"></el-option>
<el-option :label="$t('shop.onlineShop')" :value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('shop.businessStatus')">
<el-select v-model="searchForm.businessStatus" :placeholder="$t('shop.pleaseSelectBusinessStatus')" style="width: 200px;">
<el-option :label="$t('shop.closed')" :value="0"></el-option>
<el-option :label="$t('shop.open')" :value="1"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('shop.status')">
<el-select v-model="searchForm.status" :placeholder="$t('shop.pleaseSelectStatus')" style="width: 200px;">
<el-option :label="$t('shop.disabled')" :value="0"></el-option>
<el-option :label="$t('shop.enabled')" :value="1"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">{{ $t('common.search') }}</el-button>
<el-button @click="handleReset">{{ $t('common.reset') }}</el-button>
</el-form-item>
</el-form>
</div>
<!-- 表格区域 -->
<div class="table-section">
<div class="table-header">
<el-button type="danger" @click="handleBatchDelete" :disabled="selectedIds.length === 0">
{{ $t('shop.batchDelete') }}
</el-button>
</div>
<div class="table-container">
<el-table
:data="tableData"
v-loading="loading"
:header-cell-style="{background: '#fafafa'}"
stripe
@selection-change="handleSelectionChange"
:min-width="1200"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="shopName" :label="$t('shop.shopName')" min-width="150"></el-table-column>
<el-table-column prop="shopCode" :label="$t('shop.shopCode')" width="150"></el-table-column>
<el-table-column prop="shopTypeName" :label="$t('shop.shopType')" width="120"></el-table-column>
<el-table-column prop="fullAddress" :label="$t('shop.address')" min-width="200"></el-table-column>
<el-table-column prop="contactPerson" :label="$t('shop.contactPerson')" width="120"></el-table-column>
<el-table-column prop="contactPhone" :label="$t('shop.contactPhone')" width="150"></el-table-column>
<el-table-column prop="businessStatusName" :label="$t('shop.businessStatus')" width="120">
<template #default="scope">
<el-tag :type="scope.row.businessStatus === 1 ? 'success' : 'warning'">
{{ scope.row.businessStatus === 1 ? $t('shop.open') : $t('shop.closed') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="statusName" :label="$t('shop.status')" width="120">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? $t('shop.enabled') : $t('shop.disabled') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" :label="$t('shop.createdAt')" width="180"></el-table-column>
<el-table-column :label="$t('shop.operation')" width="280" fixed="right">
<template #default="scope">
<el-button size="small" type="primary" @click="handleEditShop(scope.row)">{{ $t('common.edit') }}</el-button>
<el-button size="small" type="warning" @click="handleUpdateStatus(scope.row)">
{{ scope.row.status === 1 ? $t('shop.disable') : $t('shop.enable') }}
</el-button>
<el-button size="small" type="info" @click="handleUpdateBusinessStatus(scope.row)">
{{ scope.row.businessStatus === 1 ? $t('shop.closeShop') : $t('shop.openShop') }}
</el-button>
<el-button size="small" type="danger" @click="handleDeleteShop(scope.row)">{{ $t('common.delete') }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页区域 -->
<div class="pagination-section">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</div>
</el-card>
<!-- 店铺表单弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="700px"
:close-on-click-modal="false"
>
<el-form :model="formData" label-width="120px" :rules="rules" ref="formRef">
<el-form-item :label="$t('shop.shopName')" prop="shopName">
<el-input v-model="formData.shopName" :placeholder="$t('shop.pleaseEnterShopName')"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.shopCode')" prop="shopCode">
<el-input v-model="formData.shopCode" :placeholder="$t('shop.pleaseEnterShopCode')"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.shopType')" prop="shopType">
<el-select v-model="formData.shopType" :placeholder="$t('shop.pleaseSelectShopType')">
<el-option :label="$t('shop.physicalShop')" :value="1"></el-option>
<el-option :label="$t('shop.onlineShop')" :value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('shop.province')" prop="province">
<el-cascader
v-model="cascaderValue"
:options="addressOptions"
:placeholder="$t('shop.pleaseSelectProvinceCityDistrict')"
@change="handleCascaderChange"
:props="{ expandTrigger: 'hover', checkStrictly: false }"
></el-cascader>
</el-form-item>
<el-form-item :label="$t('shop.address')" prop="address">
<el-input v-model="formData.address" :placeholder="$t('shop.pleaseEnterAddress')"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.contactPerson')" prop="contactPerson">
<el-input v-model="formData.contactPerson" :placeholder="$t('shop.pleaseEnterContactPerson')"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.contactPhone')" prop="contactPhone">
<el-input v-model="formData.contactPhone" :placeholder="$t('shop.pleaseEnterContactPhone')"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.contactEmail')" prop="contactEmail">
<el-input v-model="formData.contactEmail" :placeholder="$t('shop.pleaseEnterContactEmail')" type="email"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.businessHours')" prop="businessHours">
<el-input v-model="formData.businessHours" :placeholder="$t('shop.pleaseEnterBusinessHours')"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.shopDesc')" prop="shopDesc">
<el-input v-model="formData.shopDesc" :placeholder="$t('shop.pleaseEnterShopDesc')" type="textarea" ></el-input>
</el-form-item>
<el-form-item :label="$t('shop.licenseNumber')" prop="licenseNumber">
<el-input v-model="formData.licenseNumber" :placeholder="$t('shop.pleaseEnterLicenseNumber')"></el-input>
</el-form-item>
<el-form-item :label="$t('shop.licenseImage')" prop="licenseImage">
<el-upload
class="license-uploader"
action=""
:auto-upload="false"
@change="handleLicenseImageChange"
:file-list="licenseImageList"
list-type="picture-card"
:limit="1"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">{{ $t('shop.licenseImageTip') }}</div>
</template>
</el-upload>
</el-form-item>
<el-form-item :label="$t('shop.status')" prop="status">
<el-select v-model="formData.status" :placeholder="$t('shop.pleaseSelectStatus')">
<el-option :label="$t('shop.disabled')" :value="0"></el-option>
<el-option :label="$t('shop.enabled')" :value="1"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="handleSubmit">{{ $t('common.submit') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import { AdminShop } from './index';
import {regionData,codeToText} from 'element-china-area-data';
import { FileServer } from '@deotaland/utils';
const fileServer = new FileServer();
// AdminShop
const adminShop = new AdminShop();
//
const loading = ref(false);
const tableData = ref([]);
const selectedIds = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref('');
const formRef = ref(null);
//
const addressOptions = regionData;
//
const cascaderValue = ref([]);
//
const licenseImageList = ref([]);
//
const searchForm = reactive({
shopName: '',
shopCode: '',
shopType: '',
province: '',
city: '',
district: '',
businessStatus: '',
status: ''
});
//
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
});
//
const formData = reactive({
shopId: '',
shopName: '',
shopCode: '',
shopType: '',
address: '',
province: '',
city: '',
district: '',
contactPerson: '',
contactPhone: '',
contactEmail: '',
businessHours: '',
businessStatus: 1,
shopLogo: '',
shopImages: [],
shopDesc: '',
licenseNumber: '',
licenseImage: '',
area: 0,
extraInfo: {},
remark: '',
status: 1
});
//
const rules = {
shopName: [
{ required: true, message: '请输入店铺名称', trigger: 'blur' },
{ min: 2, max: 50, message: '店铺名称长度在 2 到 50 个字符', trigger: 'blur' }
],
shopCode: [
{ required: true, message: '请输入店铺编码', trigger: 'blur' },
{ min: 2, max: 20, message: '店铺编码长度在 2 到 20 个字符', trigger: 'blur' }
],
shopType: [
{ required: true, message: '请选择店铺类型', trigger: 'change' }
],
contactPerson: [
{ required: true, message: '请输入联系人', trigger: 'blur' }
],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
};
//
const getShopList = async () => {
loading.value = true;
try {
const params = {
...searchForm,
pageSize: pagination.pageSize,
pageNum: pagination.currentPage
};
const response = await adminShop.getShopList(params);
if (response.success) {
tableData.value = response.data.rows;
pagination.total = response.data.total;
} else {
ElMessage.error(response.message || '获取店铺列表失败');
}
} catch (error) {
ElMessage.error('获取店铺列表失败');
console.error('获取店铺列表失败:', error);
} finally {
loading.value = false;
}
};
//
const handleSearch = () => {
pagination.currentPage = 1;
getShopList();
};
//
const handleReset = () => {
Object.keys(searchForm).forEach(key => {
searchForm[key] = '';
});
pagination.currentPage = 1;
getShopList();
};
//
const handleSizeChange = (size) => {
pagination.pageSize = size;
getShopList();
};
//
const handleCurrentChange = (current) => {
pagination.currentPage = current;
getShopList();
};
//
const handleSelectionChange = (selection) => {
selectedIds.value = selection.map(item => item.id);
};
//
const getRegionCode = (value, data = regionData) => {
for (const item of data) {
if (item.label === value) {
return item.value;
}
if (item.children) {
const found = getRegionCode(value, item.children);
if (found) return found;
}
}
return '';
};
//
const handleCascaderChange = (value) => {
if (value && value.length === 3) {
formData.province = codeToText[value[0]];
formData.city = codeToText[value[1]];
formData.district = codeToText[value[2]];
formData.extraInfo.province =value[0];
formData.extraInfo.city =value[1] ;
formData.extraInfo.district =value[2] ;
} else {
formData.province = '';
formData.city = '';
formData.district = '';
}
};
//
const handleLicenseImageChange = async (file) => {
try {
const reader = new FileReader();
reader.onload = async (e) => {
const url = await fileServer.uploadFile(e.target.result);
formData.licenseImage = url;
licenseImageList.value = [{ name: 'licenseImage', url: url }];
ElMessage.success($t('shop.licenseImageUploadSuccess'));
};
reader.readAsDataURL(file.raw);
} catch (error) {
console.error('上传证书图片失败:', error);
ElMessage.error($t('shop.licenseImageUploadFailed'));
}
};
//
const handleAddShop = () => {
dialogTitle.value = '新增店铺';
//
Object.keys(formData).forEach(key => {
formData[key] = '';
});
formData.businessStatus = 1;
formData.area = 0;
formData.extraInfo = {};
formData.shopImages = [];
formData.status = 1;
cascaderValue.value = [];
licenseImageList.value = [];
dialogVisible.value = true;
};
//
const handleEditShop = async (row) => {
dialogTitle.value = '编辑店铺';
try {
const response = await adminShop.getShopDetail({ shopId: row.id });
if (response.success) {
const data = response.data;
//
formData.shopId = data.id;
formData.shopName = data.shopName;
formData.shopCode = data.shopCode;
formData.shopType = data.shopType;
formData.address = data.address;
formData.province = getRegionCode(data.province);
formData.city = getRegionCode(data.city);
formData.district = getRegionCode(data.district);
formData.contactPerson = data.contactPerson;
formData.contactPhone = data.contactPhone;
formData.contactEmail = data.contactEmail;
formData.businessHours = data.businessHours;
formData.businessStatus = data.businessStatus;
formData.shopLogo = data.shopLogo;
formData.shopImages = data.shopImages || [];
formData.shopDesc = data.shopDesc;
formData.licenseNumber = data.licenseNumber;
formData.licenseImage = data.licenseImage;
formData.area = data.area;
formData.extraInfo = data.extraInfo || {};
formData.remark = data.remark;
formData.status = data.status;
//
licenseImageList.value = data.licenseImage ? [{ name: 'licenseImage', url: data.licenseImage }] : [];
//
if (data.extraInfo.province && data.extraInfo.city && data.extraInfo.district) {
cascaderValue.value = [
data.extraInfo.province,
data.extraInfo.city,
data.extraInfo.district
];
} else {
cascaderValue.value = [];
}
dialogVisible.value = true;
} else {
ElMessage.error(response.message || '获取店铺详情失败');
}
} catch (error) {
ElMessage.error('获取店铺详情失败');
console.error('获取店铺详情失败:', error);
}
};
//
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
try {
let response;
if (formData.shopId) {
//
response = await adminShop.updateShop(formData);
} else {
//
response = await adminShop.createShop(formData);
}
if (response.success) {
ElMessage.success(formData.shopId ? '更新店铺成功' : '创建店铺成功');
dialogVisible.value = false;
getShopList();
} else {
ElMessage.error(response.message || (formData.shopId ? '更新店铺失败' : '创建店铺失败'));
}
} catch (error) {
ElMessage.error(formData.shopId ? '更新店铺失败' : '创建店铺失败');
console.error(formData.shopId ? '更新店铺失败:' : '创建店铺失败:', error);
}
}
});
};
//
const handleUpdateStatus = async (row) => {
const newStatus = row.status === 1 ? 0 : 1;
const confirmMessage = newStatus === 1 ? '确定要启用该店铺吗?' : '确定要禁用该店铺吗?';
try {
await ElMessageBox.confirm(confirmMessage, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
const response = await adminShop.updateShopStatus({
shopId: row.id,
status: newStatus
});
if (response.success) {
ElMessage.success(newStatus === 1 ? '启用店铺成功' : '禁用店铺成功');
getShopList();
} else {
ElMessage.error(response.message || (newStatus === 1 ? '启用店铺失败' : '禁用店铺失败'));
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(newStatus === 1 ? '启用店铺失败' : '禁用店铺失败');
console.error(newStatus === 1 ? '启用店铺失败:' : '禁用店铺失败:', error);
}
}
};
//
const handleUpdateBusinessStatus = async (row) => {
const newBusinessStatus = row.businessStatus === 1 ? 0 : 1;
const confirmMessage = newBusinessStatus === 1 ? '确定要开启营业吗?' : '确定要关闭营业吗?';
try {
await ElMessageBox.confirm(confirmMessage, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
const response = await adminShop.updateBusinessStatus({
shopId: row.id,
businessStatus: newBusinessStatus
});
if (response.success) {
ElMessage.success(newBusinessStatus === 1 ? '开启营业成功' : '关闭营业成功');
getShopList();
} else {
ElMessage.error(response.message || (newBusinessStatus === 1 ? '开启营业失败' : '关闭营业失败'));
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(newBusinessStatus === 1 ? '开启营业失败' : '关闭营业失败');
console.error(newBusinessStatus === 1 ? '开启营业失败:' : '关闭营业失败:', error);
}
}
};
//
const handleDeleteShop = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该店铺吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
});
const response = await adminShop.deleteShop({ shopId: row.id });
if (response.success) {
ElMessage.success('删除店铺成功');
getShopList();
} else {
ElMessage.error(response.message || '删除店铺失败');
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除店铺失败');
console.error('删除店铺失败:', error);
}
}
};
//
const handleBatchDelete = async () => {
if (selectedIds.value.length === 0) {
ElMessage.warning('请选择要删除的店铺');
return;
}
try {
await ElMessageBox.confirm('确定要删除选中的店铺吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
});
const response = await adminShop.batchDeleteShop({ ids: selectedIds.value });
if (response.success) {
ElMessage.success('批量删除店铺成功');
getShopList();
selectedIds.value = [];
} else {
ElMessage.error(response.message || '批量删除店铺失败');
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('批量删除店铺失败');
console.error('批量删除店铺失败:', error);
}
}
};
//
onMounted(() => {
getShopList();
});
</script>
<style scoped>
.shop-management {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.content-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
font-weight: bold;
}
.search-section {
margin-bottom: 20px;
padding: 15px;
background-color: #fafafa;
border-radius: 4px;
}
.table-section {
margin-top: 20px;
}
.table-header {
margin-bottom: 15px;
display: flex;
justify-content: flex-start;
align-items: center;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
.pagination-section {
margin-top: 20px;
display: flex;
justify-content: flex-end;
align-items: center;
}
.dialog-footer {
text-align: right;
}
/* 响应式设计 */
@media (max-width: 768px) {
.shop-management {
padding: 10px;
}
.card-header {
flex-direction: column;
align-items: flex-start;
}
.card-header .el-button {
margin-top: 10px;
}
.search-section {
padding: 10px;
}
.el-form--inline .el-form-item {
display: block;
margin-right: 0;
margin-bottom: 10px;
width: 100%;
}
.el-form--inline .el-form-item__label {
display: block;
margin-bottom: 5px;
}
.el-form--inline .el-input,
.el-form--inline .el-select {
width: 100% !important;
}
.pagination-section {
justify-content: center;
}
}
</style>

View File

@ -0,0 +1,47 @@
import { requestUtils,clientApi } from "@deotaland/utils";
export class PurchaseModal {
constructor() {
}
//获取店铺列表
async getShopList() {
return await requestUtils.common(clientApi.default.getNearbyShops);
/**
返回示例
{
"code": 0,
"success": true,
"data": [
{
"id": 9007199254740991,
"shopName": "string",
"shopCode": "string",
"shopType": 1073741824,
"shopTypeName": "string",
"latitude": 0,
"longitude": 0,
"address": "string",
"province": "string",
"city": "string",
"district": "string",
"fullAddress": "string",
"contactPerson": "string",
"contactPhone": "string",
"businessHours": "string",
"businessStatus": 1073741824,
"businessStatusName": "string",
"shopLogo": "string",
"shopImages": [
"string"
],
"shopDesc": "string",
"distance": 0.1,
"distanceUnit": "string"
}
],
"message": "操作成功"
}
*/
}
}

View File

@ -50,6 +50,25 @@
class="ip-name-input"
/>
</div>
<div class="config-item shop-select">
<div class="label">{{ $t('checkout.shop') }}</div>
<el-select
v-model="selectedShop"
:placeholder="$t('checkout.chooseShop')"
filterable
:loading="loadingShops"
class="shop-select-input"
clearable
value-key="id"
>
<el-option
v-for="shop in shopList"
:key="shop.id"
:label="shop.shopName"
:value="shop"
/>
</el-select>
</div>
</div>
</div>
@ -218,7 +237,9 @@ import { Country, State } from 'country-state-city'
import { useI18n } from 'vue-i18n'
import { PayServer } from '@deotaland/utils'
import { requestUtils,clientApi } from '@deotaland/utils'
import { PurchaseModal as PurchaseModalClass } from './index.js'
const payserver = new PayServer();
const purchaseModal = new PurchaseModalClass();
const props = defineProps({
modelData: { type: Object, default: () => ({}) },
show: { type: Boolean, default: false },
@ -248,6 +269,12 @@ const amountCents = computed(() => {
return total
})
//
const shopList = ref([])
const selectedShop = ref(null)
const loadingShops = ref(false)
//
const isPayButtonDisabled = computed(() => {
return !(
@ -326,6 +353,24 @@ const getPrice = async () => {
}
}
}
//
const fetchShopList = async () => {
loadingShops.value = true
try {
const res = await purchaseModal.getShopList()
if (res.code === 0) {
shopList.value = res.data || []
}
} catch (error) {
console.error('获取店铺列表失败:', error)
shopList.value = []
} finally {
loadingShops.value = false
}
}
const incQty = () => { qty.value = Math.min(qty.value+1, 99) }
const decQty = () => { qty.value = Math.max(qty.value-1, 1) }
const goShopify = () => {//
@ -367,6 +412,9 @@ const goShopify = () => {//用户点击购买
}else{
params.coupon_ids = []
}
if(selectedShop.value){
params.shop_id = selectedShop.value.id
}
//
showPayingOverlay.value = true
// 5
@ -418,6 +466,7 @@ onMounted(() => {
updateStates()
getPrice();
getVoucherList();
fetchShopList();
} catch (e) {}
})
watch(() => shipping.value.country, () => { updateStates() })
@ -684,7 +733,7 @@ const updateStates = () => {
/* Configuration Grid */
.config-grid {
display: grid;
grid-template-columns: 200px 1fr;
grid-template-columns: 200px 1fr 1fr;
gap: 24px;
align-items: start;
}
@ -703,22 +752,51 @@ const updateStates = () => {
align-items: flex-start;
}
.config-item.shop-select {
align-items: flex-start;
}
.ip-name-input {
width: 100%;
max-width: 300px;
}
.ip-name-input :deep(.el-input__wrapper) {
.shop-select-input {
width: 100%;
max-width: 300px;
}
.ip-name-input :deep(.el-input__wrapper),
.shop-select-input :deep(.el-select__wrapper) {
background: rgba(17,24,39,0.8);
border-color: rgba(255,255,255,0.2);
color: #ffffff;
border-radius: 8px;
}
.ip-name-input :deep(.el-input__inner) {
.ip-name-input :deep(.el-input__inner),
.shop-select-input :deep(.el-select__placeholder) {
color: #ffffff;
}
.shop-select-input :deep(.el-select__popper) {
background: rgba(17,24,39,0.95);
border-color: rgba(255,255,255,0.2);
color: #ffffff;
}
.shop-select-input :deep(.el-select__popper .el-select-dropdown__item) {
color: #ffffff;
}
.shop-select-input :deep(.el-select__popper .el-select-dropdown__item:hover) {
background: rgba(139,92,246,0.2);
}
.shop-select-input :deep(.el-select__popper .el-select-dropdown__item.selected) {
background: rgba(139,92,246,0.3);
}
/* Voucher Section */
.voucher-section {
display: flex;
@ -1213,7 +1291,8 @@ const updateStates = () => {
gap: 20px;
}
.ip-name-input {
.ip-name-input,
.shop-select-input {
max-width: 100%;
}

View File

@ -778,6 +778,8 @@ export default {
quantity: '数量',
ipName: 'IP名称',
ipNamePlaceholder: '请输入IP名称',
shop: '店铺',
chooseShop: '选择店铺',
buy: '购买',
processTitle: '我们的流程如下',
orderConfirmation: '订单确认在下单后的1个工作日内我们会确认信息后开始处理。',
@ -1245,6 +1247,7 @@ export default {
copySuccess: '邀请码复制成功',
copyFailed: '复制失败,请手动复制',
inviteLinkMessage: '🎉 限时福利!送你专属邀请码:{code},注册立得积分+解锁高级功能,快来一起体验吧!{url}',
noInviteRecords: '暂无邀请记录',
rules: {
title: '邀请规则',
freeMember: {
@ -1307,6 +1310,13 @@ export default {
pending: '待处理',
approved: '已通过',
rejected: '已拒绝',
pendingSettlement: '待结算',
unsettled: '未结算',
settled: '已结算',
pendingCommission: '待结算佣金',
unsettledCommission: '未结算佣金',
settledCommission: '已结算佣金',
currentCommissionRate: '当前佣金比例',
},
voucher: {
title: '优惠券',
@ -1893,6 +1903,7 @@ export default {
copySuccess: 'Invite code copied successfully',
copyFailed: 'Copy failed, please copy manually',
inviteLinkMessage: '🎉 Limited time offer! Here is your exclusive invite code: {code}, register now to get points + unlock premium features, come and experience it together! {url}',
noInviteRecords: 'No invite records available',
rules: {
title: 'Invitation Rules',
freeMember: {
@ -1955,6 +1966,13 @@ export default {
pending: 'Pending',
approved: 'Approved',
rejected: 'Rejected',
pendingSettlement: 'Pending Settlement',
unsettled: 'Unsettled',
settled: 'Settled',
pendingCommission: 'Pending Commission',
unsettledCommission: 'Unsettled Commission',
settledCommission: 'Settled Commission',
currentCommissionRate: 'Current Commission Rate',
},
voucher: {
title: 'Vouchers',
@ -2439,6 +2457,8 @@ export default {
quantity: 'Quantity',
ipName: 'IP Name',
ipNamePlaceholder: 'Please enter IP name',
shop: 'Shop',
chooseShop: 'Select Shop',
buy: 'Buy',
processTitle: 'Our Process',
orderConfirmation: 'Order Confirmation: Within 1 business day after placing the order, we will confirm the information and start processing.',

View File

@ -328,10 +328,10 @@
class="knowledge-base-item"
>
<el-checkbox-group v-model="agentForm.mcp_endpoints" style="width: 100%;">
<el-checkbox label="2" style="margin-right: 20px;">{{ t('agentTemplate.weather') }}</el-checkbox>
<el-checkbox label="8" style="margin-right: 20px;">{{ t('agentTemplate.jokes') }}</el-checkbox>
<el-checkbox label="9" style="margin-right: 20px;">{{ t('agentTemplate.music') }}</el-checkbox>
<el-checkbox label="101">{{ t('agentTemplate.news') }}</el-checkbox>
<el-checkbox value="2" style="margin-right: 20px;">{{ t('agentTemplate.weather') }}</el-checkbox>
<el-checkbox value="8" style="margin-right: 20px;">{{ t('agentTemplate.jokes') }}</el-checkbox>
<el-checkbox value="9" style="margin-right: 20px;">{{ t('agentTemplate.music') }}</el-checkbox>
<el-checkbox value="101">{{ t('agentTemplate.news') }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</div>
@ -564,7 +564,6 @@ const availableLanguages = computed(() => {
const availableVoices = computed(() => {
const currentLang = agentForm.language.split('-')[0] // 'zh-CN' -> 'zh'
const list = allVoices.value.filter(voice => voice.language === currentLang&&voice.name != '湾湾小何')
console.log(list);
return list
})
@ -655,7 +654,6 @@ const handleLanguageChange = (languageCode) => {
const handleVoiceChange = (voiceId) => {
//
console.log('Voice changed to:', voiceId)
}
const playVoiceSample = (voice) => {
if (playingVoice.value === voice.id && currentAudio.value) {
@ -749,7 +747,6 @@ const saveAgent = async () => {
let result
// idupdateAgent
// console.log(agentData,'agentDataagentData');
// return
if (agentForm.id) {
agentData.id = agentForm.id
@ -884,7 +881,6 @@ const getAgentDetail = async (agentId) => {
agentForm.id = agentId
}
} catch (error) {
console.error('获取智能体详情失败:', error)
ElMessage.error('获取智能体详情失败')
}
}
@ -914,8 +910,6 @@ const getTtsList = async ()=>{
}
voices = voices.filter(voice => voice.name != '湾湾小何')
allVoices.value = voices
console.log(allVoices.value,'allVoices.value');
//
if(!agentForm.voice && voices.length > 0){
agentForm.voice = voices[0].id

View File

@ -178,25 +178,24 @@ export class UserController {
endTime:data.endTime,////格式要求2026-02-13T00:00:00+08:00
pageSize:data.pageSize,
pageNum:data.pageNum,
orderByColumn:data.orderByColumn,
isAsc:data.isAsc,//排序的方向desc或者asc
}
return await requestUtils.common(clientApi.default.INVITE_COMMISSION_RECORDS,params);
/*
返回示例
{
"total": 9007199254740991,
"total": 1,
"rows": [
{
"maskedEmail": "string",
"actualAmount": 0,
"commissionRate": 0,
"commissionAmount": 0,
"orderTime": "2026-01-12T08:45:25.630Z"
"maskedEmail": "1***6@qq.com",
"actualAmount": 99.00,
"commissionRate": 20.00,
"commissionAmount": 19.80,
"orderTime": "2026-01-12T09:24:31.979148Z"
}
],
"code": 1073741824,
"msg": "string"
"code": 200,
"msg": "查询成功"
}
*/
@ -217,4 +216,8 @@ export class UserController {
}
return await requestUtils.common(clientApi.default.COMMISSION_SETTLEMENT_LIST,params);
}
// 获取佣金结算统计信息:待结算金额、未结算金额、已结算金额
async getSettlementStatistics() {
return await requestUtils.common(clientApi.default.COMMISSION_SETTLEMENT_STATISTICS);
}
}

View File

@ -58,7 +58,7 @@
<span class="points-value">{{total_score}}</span>
<el-button
type="primary"
size="medium"
size="default"
class="recharge-btn"
@click="router.push('/points-recharge')"
>
@ -186,7 +186,7 @@
<h3>{{ $t('userCenter.invitation.inviteCodes') }}</h3>
<el-button
type="primary"
size="medium"
size="default"
class="invite-details-btn"
@click="router.push('/user-center/invite-records')"
>

View File

@ -2,7 +2,11 @@
<div class="invite-records-container">
<!-- 页面标题 -->
<div class="page-header">
<h1>{{ $t('userCenter.invitation.inviteRecords') }}</h1>
<el-button @click="handleBack" class="back-button">
<el-icon><ArrowLeft /></el-icon>
{{ $t('common.back') }}
</el-button>
<!-- <h1>{{ $t('userCenter.invitation.inviteRecords') }}</h1> -->
</div>
<!-- 选项卡切换 - 仅达人会员显示佣金记录选项 -->
@ -138,8 +142,38 @@
<el-tab-pane :label="$t('userCenter.creator.commissionSettlement')" name="settlement">
<!-- 佣金结算内容 -->
<div class="tab-content">
<!-- 申请结算按钮 -->
<!-- 申请结算按钮和统计信息 -->
<div class="settlement-header">
<div class="settlement-statistics">
<div class="stat-item">
<div class="stat-label">{{ $t('userCenter.creator.pendingSettlement') }}</div>
<div class="stat-value">{{ settlementStatistics.pendingSettlement }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('userCenter.creator.unsettled') }}</div>
<div class="stat-value">{{ settlementStatistics.unsettled }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('userCenter.creator.settled') }}</div>
<div class="stat-value">{{ settlementStatistics.settled }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('userCenter.creator.pendingCommission') }}</div>
<div class="stat-value">{{ settlementStatistics.pendingCommission }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('userCenter.creator.unsettledCommission') }}</div>
<div class="stat-value">{{ settlementStatistics.unsettledCommission }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('userCenter.creator.settledCommission') }}</div>
<div class="stat-value">{{ settlementStatistics.settledCommission }}</div>
</div>
<div class="stat-item">
<div class="stat-label">{{ $t('userCenter.creator.currentCommissionRate') }}</div>
<div class="stat-value">{{ settlementStatistics.commissionRate }}%</div>
</div>
</div>
<el-button type="primary" @click="handleApplySettlement">{{ $t('userCenter.creator.applySettlement') }}</el-button>
</div>
@ -278,12 +312,18 @@ import { useAuthStore } from '@/stores/auth'
import { UserController } from './index.js'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
//
const { t } = useI18n()
const authStore = useAuthStore()
const userController = new UserController()
//
const handleBack = () => {
window.history.back()
}
//
const userData = ref({
role: authStore.user?.userRole == '2' ? 'creator' : 'free'
@ -297,6 +337,17 @@ const pointsRecords = ref([])
const commissionRecords = ref([])
const settlementRecords = ref([])
//
const settlementStatistics = ref({
pendingSettlement: 0, //
unsettled: 0, //
settled: 0, //
pendingCommission: 0, //
unsettledCommission: 0, //
settledCommission: 0, //
commissionRate: 0 //
})
//
const commissionTotal = ref(0)
const settlementTotal = ref(0)
@ -305,7 +356,8 @@ const settlementTotal = ref(0)
const loading = ref({
points: false,
commission: false,
settlement: false
settlement: false,
statistics: false
})
//
@ -393,12 +445,12 @@ const fetchCommissionRecords = async () => {
isAsc: 'desc'
}
//
// 2026-02-13T00:00:00+08:00
if (filters.value.startTime) {
params.startTime = filters.value.startTime
params.startTime = new Date(filters.value.startTime).toISOString().slice(0, 19) + '+08:00'
}
if (filters.value.endTime) {
params.endTime = filters.value.endTime
params.endTime = new Date(filters.value.endTime).toISOString().slice(0, 19) + '+08:00'
}
if (filters.value.email) {
params.email = filters.value.email
@ -406,7 +458,7 @@ const fetchCommissionRecords = async () => {
const response = await userController.getInviteCommissionRecords(params)
if (response.code === 0 || response.code === 1073741824) {
if (response.code === 200 || response.code === 0) {
// API
commissionRecords.value = response.rows.map(item => ({
maskedEmail: item.maskedEmail,
@ -452,6 +504,7 @@ const handleTabChange = (tabName) => {
fetchCommissionRecords()
} else if (tabName === 'settlement') {
fetchSettlementList()
fetchSettlementStatistics()
}
}
@ -499,6 +552,38 @@ const fetchSettlementList = async () => {
}
}
//
const fetchSettlementStatistics = async () => {
if (userData.value.role !== 'creator') return
try {
loading.value.statistics = true
const response = await userController.getSettlementStatistics()
if (response.code === 0 || response.success) {
// 便
console.log('Settlement Statistics Response:', response)
//
if (response.data) {
settlementStatistics.value = {
pendingSettlement: response.data.pending_amount || 0,
unsettled: response.data.unsettled_amount || 0,
settled: response.data.settled_amount || 0,
pendingCommission: response.data.pending_commission || 0,
unsettledCommission: response.data.unsettled_commission || 0,
settledCommission: response.data.settled_commission || 0,
commissionRate: response.data.commission_rate || 0
}
}
}
} catch (error) {
console.error('获取佣金结算统计信息失败:', error)
} finally {
loading.value.statistics = false
}
}
//
const handleApplySettlement = () => {
dialogVisible.value = true
@ -562,11 +647,8 @@ const handleReset = () => {
//
onMounted(() => {
fetchPointsRecords()
if (userData.value.role === 'creator') {
fetchCommissionRecords()
fetchSettlementList()
}
//
handleTabChange(activeTab.value)
})
</script>
@ -583,6 +665,39 @@ onMounted(() => {
/* 页面标题 */
.page-header {
margin-bottom: 32px;
display: flex;
align-items: center;
gap: 16px;
}
.back-button {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-primary, #1f2937);
font-size: 14px;
font-weight: 500;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.3s ease;
background: rgba(139, 92, 246, 0.1);
border: 1px solid transparent;
color: #8B5CF6;
}
.back-button:hover {
background: rgba(139, 92, 246, 0.1);
color: #8B5CF6;
border-color: rgba(139, 92, 246, 0.2);
}
.back-button .el-icon {
font-size: 16px;
transition: transform 0.3s ease;
}
.back-button:hover .el-icon {
transform: translateX(-2px);
}
.page-header h1 {
@ -593,6 +708,7 @@ onMounted(() => {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.page-header h1::before {
@ -604,6 +720,17 @@ onMounted(() => {
border-radius: 2px;
}
/* 暗色主题下的返回按钮 */
html.dark .back-button {
color: #F3F4F6;
}
html.dark .back-button:hover {
background: rgba(139, 92, 246, 0.2);
color: #A78BFA;
border-color: rgba(139, 92, 246, 0.3);
}
/* 选项卡样式 */
.invite-tabs {
margin-bottom: 24px;
@ -728,8 +855,53 @@ onMounted(() => {
/* 结算记录头部样式 */
.settlement-header {
display: flex;
justify-content: flex-end;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px;
background: rgba(107, 70, 193, 0.03);
border-radius: 12px;
border: 1px solid rgba(107, 70, 193, 0.1);
}
/* 结算统计信息样式 */
.settlement-statistics {
display: flex;
justify-content: space-around;
gap: 40px;
flex: 1;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-label {
font-size: 14px;
color: var(--text-secondary, #6B7280);
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #8B5CF6;
}
/* 暗色主题下的结算统计样式 */
html.dark .settlement-header {
background: rgba(139, 92, 246, 0.1);
border-color: rgba(139, 92, 246, 0.2);
}
html.dark .stat-label {
color: #9CA3AF;
}
html.dark .stat-value {
color: #A78BFA;
}
/* 响应式设计 */
@ -742,6 +914,15 @@ onMounted(() => {
font-size: 20px;
}
.back-button {
padding: 6px 10px;
font-size: 13px;
}
.back-button .el-icon {
font-size: 14px;
}
.details-table th,
.details-table td {
padding: 12px 16px;
@ -757,9 +938,20 @@ onMounted(() => {
}
.settlement-header {
flex-direction: column;
gap: 16px;
justify-content: center;
margin-bottom: 16px;
}
.settlement-statistics {
gap: 20px;
margin-bottom: 16px;
}
.stat-value {
font-size: 20px;
}
}
/* 暗色主题支持 */

View File

@ -21,7 +21,7 @@ AdminLayout.vue (根布局)
/login - 登录页面
/admin - 管理后台根路径
├── /admin/dashboard - 仪表板
├── /admin/content-review - 内容审核
├── /admin/orders/content-review - 内容审核
├── /admin/orders - 订单管理
└── /admin/users - 用户管理
```

View File

@ -8,7 +8,7 @@
**Then** 显示包含统计卡片、图表区域、快捷操作的仪表板布局
#### Scenario: 内容审核页面展示
**Given** 用户访问 `/admin/content-review`
**Given** 用户访问 `/admin/orders/content-review`
**When** 页面加载
**Then** 显示内容审核列表、筛选功能、审核操作按钮的页面结构

View File

@ -176,7 +176,7 @@ const submitShipping = async () => {
// 跳转到完成页面或返回列表
setTimeout(() => {
router.push('/admin/content-review')
router.push('/admin/orders/content-review')
}, 2000)
} catch (error) {

View File

@ -11,7 +11,7 @@ import configuration from './configuration.js';
import seriespriceConfig from './seriespriceConfig.js';
import voucher from './voucher.js';
import agent from './agent.js';
import shop from './shop.js';
export default {
...login,
...order,
@ -26,4 +26,5 @@ export default {
...seriespriceConfig,
...voucher,
...agent,
...shop,
};

View File

@ -11,8 +11,10 @@ const permission = {
assignPermissionToRole: {url: '/api-base/admin/permission/assign/{roleId}', method: 'POST', isLoading: true},//为角色分配权限
getPermissionDetail: {url: '/api-base/admin/permission/{permissionId}', method: 'GET', isLoading: true},//查询权限详情
deletePermission: {url: '/api-base/admin/permission/{permissionId}', method: 'DELETE', isLoading: true},//删除权限
// getPermissionList: {url: '/api-base/admin/permission/list', method: 'GET', isLoading: true},//查询权限列表
getPermissionList: {url: '/api-base/admin/permission/list', method: 'GET', isLoading: true},//查询权限列表
getPermissionCodesByUserId: {url: '/api-base/admin/permission/codes/user/{userId}', method: 'GET', isLoading: true},//根据用户ID查询权限代码集合
// getPermissionCodesByUserId: {url: '/api-base/admin/permission/codes/user/{userId}', method: 'GET', isLoading: true},//根据用户ID查询权限代码集合
getPermissionCodesByUserId: {url: '/api-base/admin/permission/codes/current', method: 'GET', isLoading: true},//根据用户ID查询权限代码集合
getPermissionCodesByRoleId: {url: '/api-base/admin/permission/role/{roleId}', method: 'GET', isLoading: true},//根据角色ID查询权限代码集合
updateAdminUser: {url: '/api-base/admin/admin-user/update', method: 'POST', isLoading: true},//更新管理员用户
toggleAdminUserStatus: {url: '/api-base/admin/admin-user/toggle-status/{userId}', method: 'POST', isLoading: true},//启用/禁用用户

View File

@ -0,0 +1,21 @@
const shop = {
updateShopStatus:{url:'/api-base/admin/shop/{id}/status',method:'POST'},// 更新店铺状态
updateBusinessStatus:{url:'/api-base/admin/shop/{id}/business-status',method:'POST'},// 更新营业状态
updateShop:{url:'/api-base/admin/shop/update/{id}',method:'POST'},// 更新店铺
deleteShop:{url:'/api-base/admin/shop/delete/{id}',method:'POST'},// 删除店铺
createShop:{url:'/api-base/admin/shop/create',method:'POST'},// 创建店铺
batchDeleteShop:{url:'/api-base/admin/shop/batch-delete',method:'POST'},// 批量删除店铺
getShopDetail:{url:'/api-base/admin/shop/{id}',method:'GET'},// 查询店铺详情
getShopList:{url:'/api-base/admin/shop/list',method:'GET'},// 分页查询店铺列表
// 店铺员工相关接口
createShopEmployee:{url:'/api-base/admin/shop-employee',method:'POST'},// 创建员工
updateWorkStatus:{url:'/api-base/admin/shop-employee/{id}/work-status',method:'POST'},// 更新工作状态
updateEmployeeStatus:{url:'/api-base/admin/shop-employee/{id}/status',method:'POST'},// 更新员工状态
employeeResign:{url:'/api-base/admin/shop-employee/{id}/resign',method:'POST'},// 员工离职
updateEmployee:{url:'/api-base/admin/shop-employee/update/{id}',method:'POST'},// 更新员工
deleteEmployee:{url:'/api-base/admin/shop-employee/delete/{id}',method:'POST'},// 删除员工
batchDeleteEmployee:{url:'/api-base/admin/shop-employee/batch-delete',method:'POST'},// 批量删除员工
getEmployeeById:{url:'/api-base/admin/shop-employee/{id}',method:'GET'},// 根据ID查询员工
getEmployeeList:{url:'/api-base/admin/shop-employee/list',method:'GET'},// 分页查询员工列表
}
export default shop;

View File

@ -9,5 +9,9 @@ const order = {
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删除邀请码
getOperationLogList:{url:'/api-base/admin/operation-log/list',method:'GET'},//分页查询操作日志列表
getSettlementList:{url:'/api-core/admin/order/commission-settlement/list',method:'POST'},//获取结算申请列表
approveSettlement:{url:'/api-core/admin/order/commission-settlement/approve',method:'POST'},//审核通过结算申请
rejectSettlement:{url:'/api-core/admin/order/commission-settlement/reject',method:'POST'},//审核拒绝结算申请
getSettlementOrders:{url:'/api-core/admin/order/commission-settlement/orders',method:'POST'},//根据结算ID获取订单列表
}
export default order;

View File

@ -9,6 +9,7 @@ import logistics from './logistics.js';
import agent from './agent.js';
import rechargeconfig from './rechargeconfig.js';
import voucher from './voucher.js';
import shop from './shop.js';
export default {
...meshy,
...login,
@ -21,4 +22,5 @@ export default {
...agent,
...rechargeconfig,
...voucher,
...shop,
};

View File

@ -0,0 +1,4 @@
const login = {
getNearbyShops:{url:'/api-base/shops/nearby',method:'GET'},// 查询店铺
}
export default login;

View File

@ -10,5 +10,6 @@ const login = {
INVITE_COMMISSION_RECORDS:{url:'/api-base/user/invite/commission-records',method:'GET',isLoading:true},// 返回邀请的用户佣金记录
COMMISSION_SETTLEMENT_APPLY:{url:'/api-core/front/user/commission-settlement/apply',method:'POST',isLoading:true},// 申请结算佣金 - 只能结算已完成30天的订单
COMMISSION_SETTLEMENT_LIST:{url:'/api-core/front/user/commission-settlement/list',method:'GET',isLoading:true},// 获取结算申请列表
COMMISSION_SETTLEMENT_STATISTICS:{url:'/api-core/front/user/commission-settlement/statistics',method:'GET',isLoading:true},// 获取佣金结算统计信息:待结算金额、未结算金额、已结算金额
}
export default login;

View File

@ -155,6 +155,7 @@ export class PayServer {
payReducerUrl = `${window.location.origin}/#/order-management`
pamras = {
product_id: orderInfo.product_id,
shop_id: orderInfo.shop_id || '',
"methods": [
"card"
],

View File

@ -56,6 +56,9 @@ importers:
'@types/three':
specifier: ^0.180.0
version: 0.180.0
element-china-area-data:
specifier: ^6.1.0
version: 6.1.0
element-plus:
specifier: ^2.11.7
version: 2.11.7(vue@3.5.24)
@ -1735,6 +1738,10 @@ packages:
supports-color: 7.2.0
dev: true
/china-division@2.7.0:
resolution: {integrity: sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==}
dev: false
/chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@ -1962,6 +1969,12 @@ packages:
resolution: {integrity: sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==}
dev: true
/element-china-area-data@6.1.0:
resolution: {integrity: sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==}
dependencies:
china-division: 2.7.0
dev: false
/element-plus@2.11.7(vue@3.5.24):
resolution: {integrity: sha512-Bh47wuzsqaNBNDkbtlOlZER1cGcOB8GsXp/+C9b95MOrk0wvoHUV4NKKK7xMkfYNFYdYysQ752oMhnExgAL6+g==}
peerDependencies: