其他管理
This commit is contained in:
parent
5ede4b87a7
commit
5242fae158
572
.kiro/specs/content-auxiliary-frontend/design.md
Normal file
572
.kiro/specs/content-auxiliary-frontend/design.md
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
# Design Document
|
||||
|
||||
## Overview
|
||||
|
||||
本设计文档描述"内容与辅助"模块从老项目(PHP ThinkPHP + Layui)迁移到新项目(ASP.NET Core + Vue 3 + Element Plus)的技术设计方案。该模块包含三个子模块:单页管理(Danye)、悬浮球配置(FloatBall)、福利屋入口(WelfareHouse)。
|
||||
|
||||
## Architecture
|
||||
|
||||
### 系统架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 前端 (Vue 3 + Element Plus) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ views/business/ │
|
||||
│ ├── danye/ # 单页管理页面 │
|
||||
│ │ ├── list.vue # 单页列表 │
|
||||
│ │ └── components/ # 组件 │
|
||||
│ ├── floatball/ # 悬浮球配置页面 │
|
||||
│ │ ├── list.vue # 悬浮球列表 │
|
||||
│ │ └── components/ # 组件 │
|
||||
│ └── welfarehouse/ # 福利屋入口页面 │
|
||||
│ ├── list.vue # 福利屋列表 │
|
||||
│ └── components/ # 组件 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ api/business/ │
|
||||
│ ├── danye.ts # 单页API │
|
||||
│ ├── floatball.ts # 悬浮球API │
|
||||
│ └── welfarehouse.ts # 福利屋API │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 后端 (ASP.NET Core) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ HoneyBox.Admin.Business/ │
|
||||
│ ├── Controllers/ │
|
||||
│ │ ├── DanyeController.cs │
|
||||
│ │ ├── FloatBallController.cs │
|
||||
│ │ └── WelfareHouseController.cs │
|
||||
│ ├── Services/ │
|
||||
│ │ ├── DanyeService.cs │
|
||||
│ │ ├── FloatBallService.cs │
|
||||
│ │ └── WelfareHouseService.cs │
|
||||
│ └── Models/ │
|
||||
│ ├── Danye/ │
|
||||
│ ├── FloatBall/ │
|
||||
│ └── WelfareHouse/ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 数据库 (SQL Server) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ├── danye # 单页内容表 (已存在) │
|
||||
│ ├── float_ball_config # 悬浮球配置表 (已存在) │
|
||||
│ └── welfare_house # 福利屋配置表 (已存在) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 数据库实体(已存在)
|
||||
|
||||
#### Danye 实体
|
||||
```csharp
|
||||
public class Danye
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public int UpdateTime { get; set; }
|
||||
public byte Status { get; set; }
|
||||
public byte IsImageOptimizer { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### FloatBallConfig 实体
|
||||
```csharp
|
||||
public class FloatBallConfig
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public byte Status { get; set; }
|
||||
public byte Type { get; set; } // 1展示图片 2跳转页面
|
||||
public string Image { get; set; }
|
||||
public string LinkUrl { get; set; }
|
||||
public string PositionX { get; set; }
|
||||
public string PositionY { get; set; }
|
||||
public string Width { get; set; }
|
||||
public string Height { get; set; }
|
||||
public byte Effect { get; set; } // 0无 1缩放动画
|
||||
public string? Title { get; set; }
|
||||
public string? ImageDetails { get; set; }
|
||||
public string? ImageBj { get; set; }
|
||||
public string? ImageDetailsX { get; set; }
|
||||
public string? ImageDetailsY { get; set; }
|
||||
public string? ImageDetailsW { get; set; }
|
||||
public string? ImageDetailsH { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### WelfareHouse 实体
|
||||
```csharp
|
||||
public class WelfareHouse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Image { get; set; }
|
||||
public string Url { get; set; }
|
||||
public int Sort { get; set; }
|
||||
public byte Status { get; set; }
|
||||
public int? CreateTime { get; set; }
|
||||
public int? UpdateTime { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 后端组件
|
||||
|
||||
#### 1. DanyeController
|
||||
- `GET /api/business/danye` - 获取单页列表
|
||||
- `GET /api/business/danye/{id}` - 获取单页详情
|
||||
- `PUT /api/business/danye/{id}` - 更新单页内容
|
||||
- `PUT /api/business/danye/{id}/image-optimizer` - 切换图片优化状态
|
||||
|
||||
#### 2. FloatBallController
|
||||
- `GET /api/business/floatball` - 获取悬浮球列表(分页)
|
||||
- `GET /api/business/floatball/{id}` - 获取悬浮球详情
|
||||
- `POST /api/business/floatball` - 新增悬浮球
|
||||
- `PUT /api/business/floatball/{id}` - 更新悬浮球
|
||||
- `DELETE /api/business/floatball/{id}` - 删除悬浮球
|
||||
- `PUT /api/business/floatball/{id}/status` - 切换状态
|
||||
|
||||
#### 3. WelfareHouseController
|
||||
- `GET /api/business/welfarehouse` - 获取福利屋列表(分页)
|
||||
- `GET /api/business/welfarehouse/{id}` - 获取福利屋详情
|
||||
- `POST /api/business/welfarehouse` - 新增福利屋入口
|
||||
- `PUT /api/business/welfarehouse/{id}` - 更新福利屋入口
|
||||
- `DELETE /api/business/welfarehouse/{id}` - 删除福利屋入口
|
||||
- `PUT /api/business/welfarehouse/{id}/status` - 切换状态
|
||||
|
||||
### 前端组件
|
||||
|
||||
#### 1. 单页管理模块
|
||||
- `DanyeList.vue` - 单页列表页面
|
||||
- `DanyeTable.vue` - 单页表格组件
|
||||
- `DanyeFormDialog.vue` - 单页编辑弹窗(含富文本编辑器)
|
||||
|
||||
#### 2. 悬浮球配置模块
|
||||
- `FloatBallList.vue` - 悬浮球列表页面
|
||||
- `FloatBallTable.vue` - 悬浮球表格组件
|
||||
- `FloatBallFormDialog.vue` - 悬浮球表单弹窗
|
||||
|
||||
#### 3. 福利屋入口模块
|
||||
- `WelfareHouseList.vue` - 福利屋列表页面
|
||||
- `WelfareHouseTable.vue` - 福利屋表格组件
|
||||
- `WelfareHouseFormDialog.vue` - 福利屋表单弹窗
|
||||
|
||||
## Data Models
|
||||
|
||||
### 后端 Models
|
||||
|
||||
#### Danye Models
|
||||
```csharp
|
||||
// 列表响应
|
||||
public class DanyeResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
public bool IsImageOptimizer { get; set; }
|
||||
public DateTime UpdateTime { get; set; }
|
||||
}
|
||||
|
||||
// 详情响应
|
||||
public class DanyeDetailResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public bool IsImageOptimizer { get; set; }
|
||||
}
|
||||
|
||||
// 更新请求
|
||||
public class DanyeUpdateRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### FloatBall Models
|
||||
```csharp
|
||||
// 列表请求
|
||||
public class FloatBallListRequest
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 10;
|
||||
}
|
||||
|
||||
// 响应
|
||||
public class FloatBallResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public int Type { get; set; }
|
||||
public string Image { get; set; }
|
||||
public string? ImageBj { get; set; }
|
||||
public string? ImageDetails { get; set; }
|
||||
public string LinkUrl { get; set; }
|
||||
public string PositionX { get; set; }
|
||||
public string PositionY { get; set; }
|
||||
public string Width { get; set; }
|
||||
public string Height { get; set; }
|
||||
public string? ImageDetailsX { get; set; }
|
||||
public string? ImageDetailsY { get; set; }
|
||||
public string? ImageDetailsW { get; set; }
|
||||
public string? ImageDetailsH { get; set; }
|
||||
public int Effect { get; set; }
|
||||
public int Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
// 创建/更新请求
|
||||
public class FloatBallCreateRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public int Type { get; set; }
|
||||
public string Image { get; set; }
|
||||
public string? ImageBj { get; set; }
|
||||
public string? ImageDetails { get; set; }
|
||||
public string? LinkUrl { get; set; }
|
||||
public string PositionX { get; set; }
|
||||
public string PositionY { get; set; }
|
||||
public string Width { get; set; }
|
||||
public string Height { get; set; }
|
||||
public string? ImageDetailsX { get; set; }
|
||||
public string? ImageDetailsY { get; set; }
|
||||
public string? ImageDetailsW { get; set; }
|
||||
public string? ImageDetailsH { get; set; }
|
||||
public int Effect { get; set; }
|
||||
public int Status { get; set; } = 1;
|
||||
}
|
||||
```
|
||||
|
||||
#### WelfareHouse Models
|
||||
```csharp
|
||||
// 列表请求
|
||||
public class WelfareHouseListRequest
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 10;
|
||||
}
|
||||
|
||||
// 响应
|
||||
public class WelfareHouseResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Image { get; set; }
|
||||
public string Url { get; set; }
|
||||
public int Sort { get; set; }
|
||||
public int Status { get; set; }
|
||||
public DateTime? CreateTime { get; set; }
|
||||
}
|
||||
|
||||
// 创建/更新请求
|
||||
public class WelfareHouseCreateRequest
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Image { get; set; }
|
||||
public string Url { get; set; }
|
||||
public int Sort { get; set; }
|
||||
public int Status { get; set; } = 1;
|
||||
}
|
||||
```
|
||||
|
||||
### 前端 TypeScript 接口
|
||||
|
||||
```typescript
|
||||
// Danye
|
||||
interface DanyeResponse {
|
||||
id: number
|
||||
title: string
|
||||
isImageOptimizer: boolean
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
interface DanyeDetailResponse {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
isImageOptimizer: boolean
|
||||
}
|
||||
|
||||
interface DanyeUpdateRequest {
|
||||
title?: string
|
||||
content: string
|
||||
}
|
||||
|
||||
// FloatBall
|
||||
interface FloatBallListRequest {
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
interface FloatBallResponse {
|
||||
id: number
|
||||
title?: string
|
||||
type: number
|
||||
image: string
|
||||
imageBj?: string
|
||||
imageDetails?: string
|
||||
linkUrl: string
|
||||
positionX: string
|
||||
positionY: string
|
||||
width: string
|
||||
height: string
|
||||
imageDetailsX?: string
|
||||
imageDetailsY?: string
|
||||
imageDetailsW?: string
|
||||
imageDetailsH?: string
|
||||
effect: number
|
||||
status: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface FloatBallCreateRequest {
|
||||
title?: string
|
||||
type: number
|
||||
image: string
|
||||
imageBj?: string
|
||||
imageDetails?: string
|
||||
linkUrl?: string
|
||||
positionX: string
|
||||
positionY: string
|
||||
width: string
|
||||
height: string
|
||||
imageDetailsX?: string
|
||||
imageDetailsY?: string
|
||||
imageDetailsW?: string
|
||||
imageDetailsH?: string
|
||||
effect: number
|
||||
status?: number
|
||||
}
|
||||
|
||||
// WelfareHouse
|
||||
interface WelfareHouseListRequest {
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
interface WelfareHouseResponse {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
url: string
|
||||
sort: number
|
||||
status: number
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
interface WelfareHouseCreateRequest {
|
||||
name: string
|
||||
image: string
|
||||
url: string
|
||||
sort: number
|
||||
status?: number
|
||||
}
|
||||
```
|
||||
|
||||
## API Design
|
||||
|
||||
### 单页管理 API
|
||||
|
||||
| 方法 | 路径 | 描述 | 请求体 | 响应 |
|
||||
|------|------|------|--------|------|
|
||||
| GET | /api/business/danye | 获取单页列表 | - | `ApiResponse<List<DanyeResponse>>` |
|
||||
| GET | /api/business/danye/{id} | 获取单页详情 | - | `ApiResponse<DanyeDetailResponse>` |
|
||||
| PUT | /api/business/danye/{id} | 更新单页内容 | `DanyeUpdateRequest` | `ApiResponse<bool>` |
|
||||
| PUT | /api/business/danye/{id}/image-optimizer | 切换图片优化 | `{ isImageOptimizer: bool }` | `ApiResponse<bool>` |
|
||||
|
||||
### 悬浮球配置 API
|
||||
|
||||
| 方法 | 路径 | 描述 | 请求体 | 响应 |
|
||||
|------|------|------|--------|------|
|
||||
| GET | /api/business/floatball | 获取悬浮球列表 | Query: page, pageSize | `PagedResponse<FloatBallResponse>` |
|
||||
| GET | /api/business/floatball/{id} | 获取悬浮球详情 | - | `ApiResponse<FloatBallResponse>` |
|
||||
| POST | /api/business/floatball | 新增悬浮球 | `FloatBallCreateRequest` | `ApiResponse<int>` |
|
||||
| PUT | /api/business/floatball/{id} | 更新悬浮球 | `FloatBallCreateRequest` | `ApiResponse<bool>` |
|
||||
| DELETE | /api/business/floatball/{id} | 删除悬浮球 | - | `ApiResponse<bool>` |
|
||||
| PUT | /api/business/floatball/{id}/status | 切换状态 | `{ status: int }` | `ApiResponse<bool>` |
|
||||
|
||||
### 福利屋入口 API
|
||||
|
||||
| 方法 | 路径 | 描述 | 请求体 | 响应 |
|
||||
|------|------|------|--------|------|
|
||||
| GET | /api/business/welfarehouse | 获取福利屋列表 | Query: page, pageSize | `PagedResponse<WelfareHouseResponse>` |
|
||||
| GET | /api/business/welfarehouse/{id} | 获取福利屋详情 | - | `ApiResponse<WelfareHouseResponse>` |
|
||||
| POST | /api/business/welfarehouse | 新增福利屋入口 | `WelfareHouseCreateRequest` | `ApiResponse<int>` |
|
||||
| PUT | /api/business/welfarehouse/{id} | 更新福利屋入口 | `WelfareHouseCreateRequest` | `ApiResponse<bool>` |
|
||||
| DELETE | /api/business/welfarehouse/{id} | 删除福利屋入口 | - | `ApiResponse<bool>` |
|
||||
| PUT | /api/business/welfarehouse/{id}/status | 切换状态 | `{ status: int }` | `ApiResponse<bool>` |
|
||||
|
||||
## UI Design
|
||||
|
||||
### 单页管理页面
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 单页管理 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ID │ 标题 │ 图片优化 │ 更新时间 │ 操作 │ │
|
||||
│ ├────┼────────────────────────┼─────────┼───────────┼───────┤ │
|
||||
│ │ 1 │ 关于我们 │ [开关] │ 2026-01-18│ [编辑]│ │
|
||||
│ │ 2 │ 用户协议 │ [开关] │ 2026-01-18│ [编辑]│ │
|
||||
│ │ 3 │ 隐私政策 │ [开关] │ 2026-01-18│ [编辑]│ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 单页编辑弹窗
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 编辑单页 [X] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 标题: [关于我们________________] (ID 2-20 不可编辑) │
|
||||
│ │
|
||||
│ 内容: │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ [B] [I] [U] [图片] [链接] ... ││
|
||||
│ ├─────────────────────────────────────────────────────────────┤│
|
||||
│ │ ││
|
||||
│ │ 富文本编辑区域 ││
|
||||
│ │ ││
|
||||
│ └─────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ [取消] [保存] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 悬浮球列表页面
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 悬浮球配置 [+ 新增] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ID │ 标题 │ 图片 │ 背景图 │ 详情图 │ 类型 │ 链接 │ 位置 │ ... │ │
|
||||
│ ├────┼─────┼─────┼───────┼───────┼─────┼─────┼─────┼─────┤ │
|
||||
│ │ 1 │ 活动 │ [img]│ [img] │ [img] │ 跳转 │ /act│ 10,20│ ... │ │
|
||||
│ │ 2 │ 客服 │ [img]│ - │ - │ 展示 │ - │ 10,80│ ... │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 共 2 条 < 1 > │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 悬浮球表单弹窗
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 新增悬浮球 [X] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 标题: [____________________] │
|
||||
│ │
|
||||
│ 类型: (●) 展示图片 ( ) 跳转页面 │
|
||||
│ │
|
||||
│ 悬浮球图片: [上传] [预览] │
|
||||
│ 背景图片: [上传] [预览] │
|
||||
│ 详情图片: [上传] [预览] │
|
||||
│ │
|
||||
│ 跳转链接: [____________________] (类型为跳转时显示) │
|
||||
│ │
|
||||
│ 位置设置: │
|
||||
│ X: [____] Y: [____] │
|
||||
│ │
|
||||
│ 尺寸设置: │
|
||||
│ 宽: [____] 高: [____] │
|
||||
│ │
|
||||
│ 详情图位置: │
|
||||
│ X: [____] Y: [____] 宽: [____] 高: [____] │
|
||||
│ │
|
||||
│ 特效: [无特效 ▼] │
|
||||
│ 状态: [开关] │
|
||||
│ │
|
||||
│ [取消] [保存] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 福利屋入口列表页面
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 福利屋入口 [+ 新增] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ID │ 名称 │ 图片 │ 链接 │ 排序 │ 状态 │ 创建时间 │ 操作│
|
||||
│ ├────┼─────────┼──────┼──────────┼─────┼─────┼─────────┼─────┤ │
|
||||
│ │ 1 │ 签到有礼 │ [img] │ /sign │ 1 │ [开关]│ 01-18 │ [编辑][删除]│
|
||||
│ │ 2 │ 新人福利 │ [img] │ /newbie │ 2 │ [开关]│ 01-18 │ [编辑][删除]│
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 共 2 条 < 1 > │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 福利屋入口表单弹窗
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 新增福利屋入口 [X] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 名称: [____________________] * │
|
||||
│ │
|
||||
│ 图片: [上传] [预览] * │
|
||||
│ │
|
||||
│ 跳转链接: [____________________] * │
|
||||
│ │
|
||||
│ 排序: [____] * │
|
||||
│ │
|
||||
│ 状态: [开关] │
|
||||
│ │
|
||||
│ [取消] [保存] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **权限控制**: 所有 API 需要验证管理员登录状态和权限
|
||||
2. **输入验证**: 后端对所有输入进行验证,防止 XSS 和 SQL 注入
|
||||
3. **富文本安全**: 富文本内容需要进行 HTML 净化处理
|
||||
4. **图片上传**: 验证图片类型和大小,防止恶意文件上传
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **分页加载**: 悬浮球和福利屋列表使用分页加载
|
||||
2. **图片懒加载**: 表格中的图片使用懒加载
|
||||
3. **富文本编辑器**: 按需加载富文本编辑器组件
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **单元测试**: 后端 Service 层单元测试
|
||||
2. **API 测试**: 使用 Swagger 测试 API 接口
|
||||
3. **前端组件测试**: Vue 组件单元测试
|
||||
4. **属性测试**: 验证搜索、分页、表单验证等通用属性
|
||||
|
||||
## Dependencies
|
||||
|
||||
### 后端依赖
|
||||
- Entity Framework Core (已有)
|
||||
- Mapster (已有)
|
||||
|
||||
### 前端依赖
|
||||
- Element Plus (已有)
|
||||
- WangEditor 或 TinyMCE (富文本编辑器,需确认项目中使用的编辑器)
|
||||
- Vue Router (已有)
|
||||
|
||||
## References
|
||||
|
||||
- Requirements: `.kiro/specs/content-auxiliary-frontend/requirements.md`
|
||||
- PHP 参考代码: `server/php/app/admin/controller/Danye.php`
|
||||
- PHP 参考代码: `server/php/app/admin/controller/FloatBall.php`
|
||||
- PHP 参考代码: `server/php/app/admin/controller/WelfareHouse.php`
|
||||
- 实体定义: `server/HoneyBox/src/HoneyBox.Model/Entities/Danye.cs`
|
||||
- 实体定义: `server/HoneyBox/src/HoneyBox.Model/Entities/FloatBallConfig.cs`
|
||||
- 实体定义: `server/HoneyBox/src/HoneyBox.Model/Entities/WelfareHouse.cs`
|
||||
146
.kiro/specs/content-auxiliary-frontend/requirements.md
Normal file
146
.kiro/specs/content-auxiliary-frontend/requirements.md
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
本文档定义了"内容与辅助"模块从老项目(PHP ThinkPHP + Layui)迁移到新项目(ASP.NET Core + Vue 3 + Element Plus)的需求。该模块包含单页管理、悬浮球配置和福利屋入口三个子模块。
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Admin_System**: 后台管理系统
|
||||
- **Danye_Module**: 单页管理模块,用于管理静态页面内容(如关于我们、用户协议等)
|
||||
- **FloatBall_Module**: 悬浮球配置模块,用于配置首页悬浮球
|
||||
- **WelfareHouse_Module**: 福利屋入口模块,用于配置福利屋入口
|
||||
- **Rich_Text_Editor**: 富文本编辑器,用于编辑HTML内容
|
||||
- **Image_Optimizer**: 图片优化开关,开启后只显示图片,支持长按识别二维码
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: 单页管理列表
|
||||
|
||||
**User Story:** As an administrator, I want to view and manage static pages, so that I can maintain content like "About Us" and "User Agreement".
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator accesses the Danye list page, THE Admin_System SHALL display a list of all static pages with ID, title, and image optimizer status
|
||||
2. WHEN an administrator clicks the edit button, THE Admin_System SHALL open an edit dialog with the page content
|
||||
3. WHEN an administrator toggles the image optimizer switch, THE Admin_System SHALL update the page's image optimizer status immediately
|
||||
4. THE Admin_System SHALL display the image optimizer status as a toggle switch for each page
|
||||
|
||||
### Requirement 2: 单页编辑
|
||||
|
||||
**User Story:** As an administrator, I want to edit static page content, so that I can update the information displayed to users.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator opens the edit dialog, THE Admin_System SHALL display the page title and content in a rich text editor
|
||||
2. WHEN an administrator modifies the content and clicks save, THE Admin_System SHALL update the page content
|
||||
3. IF the page ID is between 2 and 20, THEN THE Admin_System SHALL prevent editing the title field
|
||||
4. THE Rich_Text_Editor SHALL support image upload and basic formatting
|
||||
|
||||
### Requirement 3: 悬浮球列表
|
||||
|
||||
**User Story:** As an administrator, I want to view and manage float ball configurations, so that I can control the floating buttons on the homepage.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator accesses the FloatBall list page, THE Admin_System SHALL display a list of all float ball configurations
|
||||
2. THE Admin_System SHALL display title, image, background image, detail image, type, link URL, position (X/Y), size (width/height), detail image position and size, effect, status, and create time
|
||||
3. THE Admin_System SHALL display images as thumbnails with preview capability
|
||||
4. THE Admin_System SHALL support pagination for the float ball list
|
||||
|
||||
### Requirement 4: 悬浮球新增
|
||||
|
||||
**User Story:** As an administrator, I want to create new float ball configurations, so that I can add floating buttons to the homepage.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator clicks the add button, THE Admin_System SHALL open a form dialog for creating a new float ball
|
||||
2. THE Admin_System SHALL require type, image, position X, position Y, width, height, and effect fields
|
||||
3. THE Admin_System SHALL provide type selection (show image/jump to page)
|
||||
4. THE Admin_System SHALL provide image upload for image, background image, and detail image
|
||||
5. THE Admin_System SHALL provide link URL input when type is "jump to page"
|
||||
6. THE Admin_System SHALL provide position and size inputs
|
||||
7. THE Admin_System SHALL provide effect selection (none/scale animation)
|
||||
8. THE Admin_System SHALL provide status toggle
|
||||
|
||||
### Requirement 5: 悬浮球编辑
|
||||
|
||||
**User Story:** As an administrator, I want to edit existing float ball configurations, so that I can update floating button settings.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator clicks the edit button, THE Admin_System SHALL open a form dialog with the configuration data pre-filled
|
||||
2. THE Admin_System SHALL allow editing all fields
|
||||
3. WHEN an administrator saves changes, THE Admin_System SHALL update the configuration
|
||||
|
||||
### Requirement 6: 悬浮球状态管理
|
||||
|
||||
**User Story:** As an administrator, I want to manage float ball status, so that I can enable or disable floating buttons.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator toggles the status switch, THE Admin_System SHALL update the float ball status immediately
|
||||
2. WHEN an administrator clicks delete, THE Admin_System SHALL show a confirmation dialog and delete the configuration upon confirmation
|
||||
|
||||
### Requirement 7: 福利屋入口列表
|
||||
|
||||
**User Story:** As an administrator, I want to view and manage welfare house entries, so that I can control the welfare house navigation.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator accesses the WelfareHouse list page, THE Admin_System SHALL display a list of all welfare house entries
|
||||
2. THE Admin_System SHALL display ID, name, image, URL, sort order, status, and create time
|
||||
3. THE Admin_System SHALL display images as thumbnails with preview capability
|
||||
4. THE Admin_System SHALL support pagination for the welfare house list
|
||||
|
||||
### Requirement 8: 福利屋入口新增
|
||||
|
||||
**User Story:** As an administrator, I want to create new welfare house entries, so that I can add navigation items.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator clicks the add button, THE Admin_System SHALL open a form dialog for creating a new entry
|
||||
2. THE Admin_System SHALL require name, image, URL, and sort order fields
|
||||
3. THE Admin_System SHALL provide image upload for the entry image
|
||||
4. THE Admin_System SHALL provide status toggle
|
||||
|
||||
### Requirement 9: 福利屋入口编辑
|
||||
|
||||
**User Story:** As an administrator, I want to edit existing welfare house entries, so that I can update navigation items.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator clicks the edit button, THE Admin_System SHALL open a form dialog with the entry data pre-filled
|
||||
2. THE Admin_System SHALL allow editing all fields
|
||||
3. WHEN an administrator saves changes, THE Admin_System SHALL update the entry
|
||||
|
||||
### Requirement 10: 福利屋入口状态管理
|
||||
|
||||
**User Story:** As an administrator, I want to manage welfare house entry status, so that I can enable or disable navigation items.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an administrator toggles the status switch, THE Admin_System SHALL update the entry status immediately
|
||||
2. WHEN an administrator clicks delete, THE Admin_System SHALL show a confirmation dialog and delete the entry upon confirmation
|
||||
|
||||
### Requirement 11: 后端API
|
||||
|
||||
**User Story:** As a developer, I want backend APIs for all content and auxiliary modules, so that the frontend can interact with the data.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Admin_System SHALL provide Danye API endpoints: GET list, GET by ID, PUT update, PUT toggle image optimizer
|
||||
2. THE Admin_System SHALL provide FloatBall API endpoints: GET list, GET by ID, POST create, PUT update, DELETE, PUT status
|
||||
3. THE Admin_System SHALL provide WelfareHouse API endpoints: GET list, GET by ID, POST create, PUT update, DELETE, PUT status
|
||||
4. THE Admin_System SHALL return consistent response format for all APIs
|
||||
5. THE Admin_System SHALL validate required fields before saving
|
||||
|
||||
### Requirement 12: 路由和菜单配置
|
||||
|
||||
**User Story:** As an administrator, I want to access content and auxiliary management pages from the menu, so that I can navigate to these features.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Admin_System SHALL add "内容管理" menu group with sub-menus for Danye, FloatBall, and WelfareHouse
|
||||
2. THE Admin_System SHALL configure routes for all content and auxiliary pages
|
||||
3. THE Admin_System SHALL configure appropriate permissions for each page
|
||||
413
.kiro/specs/content-auxiliary-frontend/tasks.md
Normal file
413
.kiro/specs/content-auxiliary-frontend/tasks.md
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
# Implementation Plan: 内容与辅助模块前端迁移
|
||||
|
||||
## Overview
|
||||
|
||||
本实现计划将"内容与辅助"模块从老项目(PHP ThinkPHP + Layui)迁移到新项目(ASP.NET Core + Vue 3 + Element Plus)。该模块包含三个子模块:单页管理(Danye)、悬浮球配置(FloatBall)、福利屋入口(WelfareHouse)。
|
||||
|
||||
实现分为后端 API 开发和前端页面开发两个主要部分。
|
||||
|
||||
## 任务概览
|
||||
|
||||
| 阶段 | 任务数 | 预计工时 |
|
||||
|------|--------|----------|
|
||||
| 1. 后端单页管理API开发 | 4 | 2h |
|
||||
| 2. 后端悬浮球API开发 | 4 | 2.5h |
|
||||
| 3. 后端福利屋API开发 | 4 | 2h |
|
||||
| 4. Checkpoint - 后端API验证 | 1 | 0.5h |
|
||||
| 5. 前端API层开发 | 3 | 1.5h |
|
||||
| 6. 前端单页管理页面开发 | 3 | 2.5h |
|
||||
| 7. 前端悬浮球页面开发 | 4 | 3h |
|
||||
| 8. 前端福利屋页面开发 | 4 | 2.5h |
|
||||
| 9. Checkpoint - 前端页面验证 | 1 | 0.5h |
|
||||
| 10. 路由和菜单配置 | 2 | 1h |
|
||||
| 11. Checkpoint - 完整功能验证 | 1 | 0.5h |
|
||||
| 12. 属性测试 | 4 | 1.5h |
|
||||
| 13. Final Checkpoint | 1 | 0.5h |
|
||||
| **总计** | **36** | **20.5h** |
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. 后端单页管理API开发
|
||||
- [x] 1.1 创建单页管理Models
|
||||
- 在 `HoneyBox.Admin.Business/Models/Danye/` 创建目录
|
||||
- 创建 `DanyeModels.cs` 定义请求和响应模型
|
||||
- 定义 `DanyeResponse` 列表响应模型(Id, Title, IsImageOptimizer, UpdateTime)
|
||||
- 定义 `DanyeDetailResponse` 详情响应模型(Id, Title, Content, IsImageOptimizer)
|
||||
- 定义 `DanyeUpdateRequest` 更新请求模型(Title?, Content)
|
||||
- 定义 `ImageOptimizerRequest` 图片优化切换请求模型
|
||||
- _Requirements: 11.1_
|
||||
|
||||
- [x] 1.2 创建单页管理Service接口和实现
|
||||
- 在 `HoneyBox.Admin.Business/Services/Interfaces/` 创建 `IDanyeService.cs`
|
||||
- 定义 `GetDanyeListAsync` 列表查询方法
|
||||
- 定义 `GetDanyeByIdAsync` 详情查询方法
|
||||
- 定义 `UpdateDanyeAsync` 更新方法
|
||||
- 定义 `ToggleImageOptimizerAsync` 切换图片优化方法
|
||||
- 在 `HoneyBox.Admin.Business/Services/` 创建 `DanyeService.cs`
|
||||
- 实现所有方法,ID 2-20 的单页标题不可编辑
|
||||
- _Requirements: 1.1, 2.1, 2.2, 2.3, 11.1_
|
||||
|
||||
- [x] 1.3 创建单页管理Controller
|
||||
- 在 `HoneyBox.Admin.Business/Controllers/` 创建 `DanyeController.cs`
|
||||
- 实现 `GET /api/business/danye` 列表查询接口
|
||||
- 实现 `GET /api/business/danye/{id}` 详情查询接口
|
||||
- 实现 `PUT /api/business/danye/{id}` 更新接口
|
||||
- 实现 `PUT /api/business/danye/{id}/image-optimizer` 切换图片优化接口
|
||||
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 11.1_
|
||||
|
||||
- [x] 1.4 注册单页管理服务
|
||||
- 在 `ServiceCollectionExtensions.cs` 注册 `IDanyeService` 和 `DanyeService`
|
||||
- _Requirements: 11.1_
|
||||
|
||||
- [x] 2. 后端悬浮球API开发
|
||||
- [x] 2.1 创建悬浮球Models
|
||||
- 在 `HoneyBox.Admin.Business/Models/FloatBall/` 创建目录
|
||||
- 创建 `FloatBallModels.cs` 定义请求和响应模型
|
||||
- 定义 `FloatBallListRequest` 列表查询请求(Page, PageSize)
|
||||
- 定义 `FloatBallResponse` 响应模型(所有字段)
|
||||
- 定义 `FloatBallCreateRequest` 新增请求模型
|
||||
- 定义 `FloatBallUpdateRequest` 更新请求模型
|
||||
- 定义 `FloatBallStatusRequest` 状态切换请求模型
|
||||
- _Requirements: 11.2_
|
||||
|
||||
- [x] 2.2 创建悬浮球Service接口和实现
|
||||
- 在 `HoneyBox.Admin.Business/Services/Interfaces/` 创建 `IFloatBallService.cs`
|
||||
- 定义 `GetFloatBallsAsync` 列表查询方法(分页)
|
||||
- 定义 `GetFloatBallByIdAsync` 详情查询方法
|
||||
- 定义 `CreateFloatBallAsync` 新增方法
|
||||
- 定义 `UpdateFloatBallAsync` 更新方法
|
||||
- 定义 `DeleteFloatBallAsync` 删除方法
|
||||
- 定义 `UpdateStatusAsync` 状态切换方法
|
||||
- 在 `HoneyBox.Admin.Business/Services/` 创建 `FloatBallService.cs`
|
||||
- 实现所有方法,验证必填字段
|
||||
- _Requirements: 3.1, 3.4, 4.1, 4.2, 5.1, 5.3, 6.1, 6.2, 11.2_
|
||||
|
||||
- [x] 2.3 创建悬浮球Controller
|
||||
- 在 `HoneyBox.Admin.Business/Controllers/` 创建 `FloatBallController.cs`
|
||||
- 实现 `GET /api/business/floatball` 列表查询接口(分页)
|
||||
- 实现 `GET /api/business/floatball/{id}` 详情查询接口
|
||||
- 实现 `POST /api/business/floatball` 新增接口
|
||||
- 实现 `PUT /api/business/floatball/{id}` 更新接口
|
||||
- 实现 `DELETE /api/business/floatball/{id}` 删除接口
|
||||
- 实现 `PUT /api/business/floatball/{id}/status` 状态切换接口
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 4.1, 4.2, 5.1, 5.3, 6.1, 6.2, 11.2_
|
||||
|
||||
- [x] 2.4 注册悬浮球服务
|
||||
- 在 `ServiceCollectionExtensions.cs` 注册 `IFloatBallService` 和 `FloatBallService`
|
||||
- _Requirements: 11.2_
|
||||
|
||||
- [x] 3. 后端福利屋API开发
|
||||
- [x] 3.1 创建福利屋Models
|
||||
- 在 `HoneyBox.Admin.Business/Models/WelfareHouse/` 创建目录
|
||||
- 创建 `WelfareHouseModels.cs` 定义请求和响应模型
|
||||
- 定义 `WelfareHouseListRequest` 列表查询请求(Page, PageSize)
|
||||
- 定义 `WelfareHouseResponse` 响应模型(Id, Name, Image, Url, Sort, Status, CreateTime)
|
||||
- 定义 `WelfareHouseCreateRequest` 新增请求模型
|
||||
- 定义 `WelfareHouseUpdateRequest` 更新请求模型
|
||||
- 定义 `WelfareHouseStatusRequest` 状态切换请求模型
|
||||
- _Requirements: 11.3_
|
||||
|
||||
- [x] 3.2 创建福利屋Service接口和实现
|
||||
- 在 `HoneyBox.Admin.Business/Services/Interfaces/` 创建 `IWelfareHouseService.cs`
|
||||
- 定义 `GetWelfareHousesAsync` 列表查询方法(分页)
|
||||
- 定义 `GetWelfareHouseByIdAsync` 详情查询方法
|
||||
- 定义 `CreateWelfareHouseAsync` 新增方法
|
||||
- 定义 `UpdateWelfareHouseAsync` 更新方法
|
||||
- 定义 `DeleteWelfareHouseAsync` 删除方法
|
||||
- 定义 `UpdateStatusAsync` 状态切换方法
|
||||
- 在 `HoneyBox.Admin.Business/Services/` 创建 `WelfareHouseService.cs`
|
||||
- 实现所有方法,验证必填字段
|
||||
- _Requirements: 7.1, 7.4, 8.1, 8.2, 9.1, 9.3, 10.1, 10.2, 11.3_
|
||||
|
||||
- [x] 3.3 创建福利屋Controller
|
||||
- 在 `HoneyBox.Admin.Business/Controllers/` 创建 `WelfareHouseController.cs`
|
||||
- 实现 `GET /api/business/welfarehouse` 列表查询接口(分页)
|
||||
- 实现 `GET /api/business/welfarehouse/{id}` 详情查询接口
|
||||
- 实现 `POST /api/business/welfarehouse` 新增接口
|
||||
- 实现 `PUT /api/business/welfarehouse/{id}` 更新接口
|
||||
- 实现 `DELETE /api/business/welfarehouse/{id}` 删除接口
|
||||
- 实现 `PUT /api/business/welfarehouse/{id}/status` 状态切换接口
|
||||
- _Requirements: 7.1, 7.2, 7.3, 7.4, 8.1, 8.2, 9.1, 9.3, 10.1, 10.2, 11.3_
|
||||
|
||||
- [x] 3.4 注册福利屋服务
|
||||
- 在 `ServiceCollectionExtensions.cs` 注册 `IWelfareHouseService` 和 `WelfareHouseService`
|
||||
- _Requirements: 11.3_
|
||||
|
||||
- [x] 4. Checkpoint - 后端API验证
|
||||
- 确保所有API编译通过
|
||||
- 使用Swagger测试API基本功能
|
||||
- 确保权限验证正常工作
|
||||
|
||||
- [x] 5. 前端API层开发
|
||||
- [x] 5.1 创建单页管理API模块
|
||||
- 创建 `src/api/business/danye.ts`
|
||||
- 定义 TypeScript 接口(DanyeResponse, DanyeDetailResponse, DanyeUpdateRequest)
|
||||
- 实现 `getDanyeList` 列表查询API调用
|
||||
- 实现 `getDanyeById` 详情查询API调用
|
||||
- 实现 `updateDanye` 更新API调用
|
||||
- 实现 `toggleImageOptimizer` 切换图片优化API调用
|
||||
- _Requirements: 11.1_
|
||||
|
||||
- [x] 5.2 创建悬浮球API模块
|
||||
- 创建 `src/api/business/floatball.ts`
|
||||
- 定义 TypeScript 接口(FloatBallListRequest, FloatBallResponse, FloatBallCreateRequest)
|
||||
- 实现 `getFloatBalls` 列表查询API调用
|
||||
- 实现 `getFloatBallById` 详情查询API调用
|
||||
- 实现 `createFloatBall` 新增API调用
|
||||
- 实现 `updateFloatBall` 更新API调用
|
||||
- 实现 `deleteFloatBall` 删除API调用
|
||||
- 实现 `updateFloatBallStatus` 状态切换API调用
|
||||
- _Requirements: 11.2_
|
||||
|
||||
- [x] 5.3 创建福利屋API模块
|
||||
- 创建 `src/api/business/welfarehouse.ts`
|
||||
- 定义 TypeScript 接口(WelfareHouseListRequest, WelfareHouseResponse, WelfareHouseCreateRequest)
|
||||
- 实现 `getWelfareHouses` 列表查询API调用
|
||||
- 实现 `getWelfareHouseById` 详情查询API调用
|
||||
- 实现 `createWelfareHouse` 新增API调用
|
||||
- 实现 `updateWelfareHouse` 更新API调用
|
||||
- 实现 `deleteWelfareHouse` 删除API调用
|
||||
- 实现 `updateWelfareHouseStatus` 状态切换API调用
|
||||
- _Requirements: 11.3_
|
||||
|
||||
- [x] 6. 前端单页管理页面开发
|
||||
- [x] 6.1 创建单页列表主页面
|
||||
- 创建 `src/views/business/danye/list.vue`
|
||||
- 实现页面布局(表格区)
|
||||
- 集成表格组件
|
||||
- 实现编辑操作
|
||||
- 实现图片优化开关切换
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4_
|
||||
|
||||
- [x] 6.2 创建单页表格组件
|
||||
- 创建 `src/views/business/danye/components/DanyeTable.vue`
|
||||
- 实现表格列配置(ID、标题、图片优化开关、更新时间、操作)
|
||||
- 实现图片优化开关组件
|
||||
- 实现编辑按钮
|
||||
- _Requirements: 1.1, 1.4_
|
||||
|
||||
- [x] 6.3 创建单页编辑弹窗
|
||||
- 创建 `src/views/business/danye/components/DanyeFormDialog.vue`
|
||||
- 实现标题输入框(ID 2-20 禁用)
|
||||
- 集成富文本编辑器
|
||||
- 实现表单验证
|
||||
- 实现提交逻辑
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4_
|
||||
|
||||
- [x] 7. 前端悬浮球页面开发
|
||||
- [x] 7.1 创建悬浮球列表主页面
|
||||
- 创建 `src/views/business/floatball/list.vue`
|
||||
- 实现页面布局(新增按钮 + 表格区)
|
||||
- 集成表格组件
|
||||
- 实现分页逻辑
|
||||
- 实现新增、编辑、删除操作
|
||||
- _Requirements: 3.1, 3.4_
|
||||
|
||||
- [x] 7.2 创建悬浮球表格组件
|
||||
- 创建 `src/views/business/floatball/components/FloatBallTable.vue`
|
||||
- 实现表格列配置(ID、标题、图片、背景图、详情图、类型、链接、位置、尺寸、特效、状态、创建时间、操作)
|
||||
- 实现图片预览功能
|
||||
- 实现状态开关组件
|
||||
- 实现操作按钮(编辑、删除)
|
||||
- 实现删除确认弹窗
|
||||
- _Requirements: 3.1, 3.2, 3.3, 6.1, 6.2_
|
||||
|
||||
- [x] 7.3 创建悬浮球表单弹窗
|
||||
- 创建 `src/views/business/floatball/components/FloatBallFormDialog.vue`
|
||||
- 实现标题输入
|
||||
- 实现类型选择(展示图片/跳转页面)
|
||||
- 实现图片上传(悬浮球图片、背景图、详情图)
|
||||
- 实现跳转链接输入(类型为跳转时显示)
|
||||
- 实现位置输入(X、Y)
|
||||
- 实现尺寸输入(宽、高)
|
||||
- 实现详情图位置和尺寸输入
|
||||
- 实现特效选择(无/缩放动画)
|
||||
- 实现状态开关
|
||||
- 实现表单验证
|
||||
- 实现提交逻辑
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 5.1, 5.2, 5.3_
|
||||
|
||||
- [x] 7.4 实现悬浮球类型条件显示
|
||||
- 当类型为"展示图片"时,隐藏跳转链接输入
|
||||
- 当类型为"跳转页面"时,显示跳转链接输入
|
||||
- _Requirements: 4.5_
|
||||
|
||||
- [x] 8. 前端福利屋页面开发
|
||||
- [x] 8.1 创建福利屋列表主页面
|
||||
- 创建 `src/views/business/welfarehouse/list.vue`
|
||||
- 实现页面布局(新增按钮 + 表格区)
|
||||
- 集成表格组件
|
||||
- 实现分页逻辑
|
||||
- 实现新增、编辑、删除操作
|
||||
- _Requirements: 7.1, 7.4_
|
||||
|
||||
- [x] 8.2 创建福利屋表格组件
|
||||
- 创建 `src/views/business/welfarehouse/components/WelfareHouseTable.vue`
|
||||
- 实现表格列配置(ID、名称、图片、链接、排序、状态、创建时间、操作)
|
||||
- 实现图片预览功能
|
||||
- 实现状态开关组件
|
||||
- 实现操作按钮(编辑、删除)
|
||||
- 实现删除确认弹窗
|
||||
- _Requirements: 7.1, 7.2, 7.3, 10.1, 10.2_
|
||||
|
||||
- [x] 8.3 创建福利屋表单弹窗
|
||||
- 创建 `src/views/business/welfarehouse/components/WelfareHouseFormDialog.vue`
|
||||
- 实现名称输入
|
||||
- 实现图片上传
|
||||
- 实现跳转链接输入
|
||||
- 实现排序输入
|
||||
- 实现状态开关
|
||||
- 实现表单验证
|
||||
- 实现提交逻辑
|
||||
- _Requirements: 8.1, 8.2, 8.3, 8.4, 9.1, 9.2, 9.3_
|
||||
|
||||
- [x] 8.4 实现福利屋排序功能
|
||||
- 列表按排序值升序显示
|
||||
- _Requirements: 7.2_
|
||||
|
||||
- [x] 9. Checkpoint - 前端页面验证
|
||||
- 确保单页管理列表页面正常显示
|
||||
- 确保单页编辑功能正常(富文本编辑器)
|
||||
- 确保图片优化开关功能正常
|
||||
- 确保悬浮球列表页面正常显示
|
||||
- 确保悬浮球新增、编辑、删除功能正常
|
||||
- 确保悬浮球类型条件显示正常
|
||||
- 确保福利屋列表页面正常显示
|
||||
- 确保福利屋新增、编辑、删除功能正常
|
||||
- 测试分页功能
|
||||
|
||||
- [x] 10. 路由和菜单配置
|
||||
- [x] 10.1 配置路由
|
||||
- 在 `src/router/modules/business.ts` 添加内容管理路由配置
|
||||
- 配置单页管理路由 `/business/danye/list`
|
||||
- 配置悬浮球配置路由 `/business/floatball/list`
|
||||
- 配置福利屋入口路由 `/business/welfarehouse/list`
|
||||
- 配置权限标识
|
||||
- _Requirements: 12.1, 12.2, 12.3_
|
||||
|
||||
- [x] 10.2 创建菜单SQL脚本并执行
|
||||
- 在 `HoneyBox/scripts/` 创建 `seed_content_auxiliary_menus.sql`
|
||||
- 添加"内容管理"菜单组
|
||||
- 添加单页管理菜单项
|
||||
- 添加悬浮球配置菜单项
|
||||
- 添加福利屋入口菜单项
|
||||
- 添加相关权限配置
|
||||
- 执行SQL脚本
|
||||
- _Requirements: 12.1, 12.2, 12.3_
|
||||
|
||||
- [x] 11. Checkpoint - 完整功能验证
|
||||
- 确保所有页面正常访问
|
||||
- 测试完整的单页管理流程
|
||||
- 测试完整的悬浮球配置流程
|
||||
- 测试完整的福利屋入口流程
|
||||
- 验证权限控制正常工作
|
||||
- 验证菜单显示正常
|
||||
|
||||
- [x] 12. 属性测试
|
||||
- [x] 12.1 编写分页参数传递属性测试
|
||||
- **Property 1: 分页参数正确传递**
|
||||
- 验证悬浮球和福利屋列表分页参数正确传递
|
||||
- **Validates: Requirements 3.4, 7.4**
|
||||
|
||||
- [x] 12.2 编写表单必填字段验证属性测试
|
||||
- **Property 2: 表单必填字段验证**
|
||||
- 验证悬浮球表单必填字段(类型、图片、位置、尺寸、特效)
|
||||
- 验证福利屋表单必填字段(名称、图片、链接、排序)
|
||||
- **Validates: Requirements 4.2, 8.2**
|
||||
|
||||
- [x] 12.3 编写条件显示字段属性测试
|
||||
- **Property 3: 条件显示字段正确切换**
|
||||
- 验证悬浮球类型切换时跳转链接字段的显示/隐藏
|
||||
- **Validates: Requirements 4.5**
|
||||
|
||||
- [x] 12.4 编写API响应格式一致性属性测试
|
||||
- **Property 4: API响应格式一致性**
|
||||
- 验证所有API返回统一的响应格式
|
||||
- **Validates: Requirements 11.4, 11.5**
|
||||
|
||||
- [x] 13. Final Checkpoint - 最终验证
|
||||
- 确保所有测试通过
|
||||
- 确保所有功能正常工作
|
||||
- 如有问题,询问用户
|
||||
|
||||
---
|
||||
|
||||
## 验收检查清单
|
||||
|
||||
### 功能验收
|
||||
- [ ] 单页管理列表正常显示
|
||||
- [ ] 单页编辑功能正常(富文本编辑器)
|
||||
- [ ] 单页标题编辑限制正常(ID 2-20 不可编辑)
|
||||
- [ ] 图片优化开关功能正常
|
||||
- [ ] 悬浮球列表正常显示,支持分页
|
||||
- [ ] 悬浮球新增功能正常
|
||||
- [ ] 悬浮球编辑功能正常
|
||||
- [ ] 悬浮球删除功能正常(有二次确认)
|
||||
- [ ] 悬浮球状态切换功能正常
|
||||
- [ ] 悬浮球类型条件显示正常
|
||||
- [ ] 福利屋列表正常显示,支持分页
|
||||
- [ ] 福利屋新增功能正常
|
||||
- [ ] 福利屋编辑功能正常
|
||||
- [ ] 福利屋删除功能正常(有二次确认)
|
||||
- [ ] 福利屋状态切换功能正常
|
||||
- [ ] 福利屋排序功能正常
|
||||
|
||||
### 非功能验收
|
||||
- [ ] 分页加载性能正常
|
||||
- [ ] 图片预览功能正常
|
||||
- [ ] 图片懒加载生效
|
||||
- [ ] 表单验证实时反馈
|
||||
- [ ] 操作提示正确显示
|
||||
- [ ] 危险操作有二次确认
|
||||
- [x] 属性测试全部通过
|
||||
|
||||
## Notes
|
||||
|
||||
- 每个Checkpoint确保增量验证
|
||||
- 后端API需要先完成才能进行前端开发
|
||||
- 属性测试验证通用正确性属性
|
||||
- 所有任务均为必需任务,确保全面测试覆盖
|
||||
- 单页管理不需要新增/删除功能,只需要编辑
|
||||
- 悬浮球和福利屋需要完整的CRUD功能
|
||||
- 富文本编辑器需要确认项目中使用的编辑器类型
|
||||
- 数据库实体已存在,无需创建迁移
|
||||
|
||||
## 文件路径参考
|
||||
|
||||
### 后端文件
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Models/Danye/DanyeModels.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Models/FloatBall/FloatBallModels.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Models/WelfareHouse/WelfareHouseModels.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IDanyeService.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IFloatBallService.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/Interfaces/IWelfareHouseService.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/DanyeService.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/FloatBallService.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Services/WelfareHouseService.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/DanyeController.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/FloatBallController.cs`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin.Business/Controllers/WelfareHouseController.cs`
|
||||
|
||||
### 前端文件
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/danye.ts`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/floatball.ts`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/welfarehouse.ts`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/list.vue`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeTable.vue`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/danye/components/DanyeFormDialog.vue`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/list.vue`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallTable.vue`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/floatball/components/FloatBallFormDialog.vue`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/list.vue`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseTable.vue`
|
||||
- `server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/welfarehouse/components/WelfareHouseFormDialog.vue`
|
||||
|
||||
### 参考文件
|
||||
- PHP 单页管理: `server/php/app/admin/controller/Danye.php`
|
||||
- PHP 悬浮球: `server/php/app/admin/controller/FloatBall.php`
|
||||
- PHP 福利屋: `server/php/app/admin/controller/WelfareHouse.php`
|
||||
- 实体 Danye: `server/HoneyBox/src/HoneyBox.Model/Entities/Danye.cs`
|
||||
- 实体 FloatBallConfig: `server/HoneyBox/src/HoneyBox.Model/Entities/FloatBallConfig.cs`
|
||||
- 实体 WelfareHouse: `server/HoneyBox/src/HoneyBox.Model/Entities/WelfareHouse.cs`
|
||||
203
server/HoneyBox/scripts/seed_content_auxiliary_menus.sql
Normal file
203
server/HoneyBox/scripts/seed_content_auxiliary_menus.sql
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
-- =============================================
|
||||
-- 内容与辅助模块菜单初始化脚本
|
||||
-- 用于在后台管理系统中添加内容管理相关菜单
|
||||
-- 包含:单页管理、悬浮球配置、福利屋入口
|
||||
--
|
||||
-- 注意:此脚本需要在 Admin 数据库中执行
|
||||
-- 表名使用小写(menus, roles, permissions, role_menus, role_permissions)
|
||||
-- =============================================
|
||||
|
||||
-- 注意:执行此脚本前请确保:
|
||||
-- 1. 已存在超级管理员角色 (Code = 'super_admin')
|
||||
-- 2. 数据库中已有基础菜单数据
|
||||
|
||||
-- 声明变量
|
||||
DECLARE @ContentMenuId BIGINT;
|
||||
DECLARE @SuperAdminRoleId BIGINT;
|
||||
|
||||
-- 获取超级管理员角色ID
|
||||
SELECT @SuperAdminRoleId = Id FROM roles WHERE Code = 'super_admin';
|
||||
|
||||
-- =============================================
|
||||
-- 1. 创建内容管理目录(顶级菜单)
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/content')
|
||||
BEGIN
|
||||
INSERT INTO menus (ParentId, Name, Path, Component, Icon, MenuType, Permission, SortOrder, Status, IsExternal, IsCache, CreatedAt)
|
||||
VALUES (0, N'内容管理', '/business/content', 'Layout', 'Document', 1, NULL, 70, 1, 0, 0, GETDATE());
|
||||
|
||||
SET @ContentMenuId = SCOPE_IDENTITY();
|
||||
PRINT N'创建内容管理目录,ID: ' + CAST(@ContentMenuId AS VARCHAR);
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
SELECT @ContentMenuId = Id FROM menus WHERE Path = '/business/content';
|
||||
UPDATE menus SET ParentId = 0, Component = 'Layout' WHERE Id = @ContentMenuId AND ParentId <> 0;
|
||||
PRINT N'内容管理目录已存在,ID: ' + CAST(@ContentMenuId AS VARCHAR);
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 2. 创建单页管理子菜单
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/danye/list')
|
||||
BEGIN
|
||||
INSERT INTO menus (ParentId, Name, Path, Component, Icon, MenuType, Permission, SortOrder, Status, IsExternal, IsCache, CreatedAt)
|
||||
VALUES (@ContentMenuId, N'单页管理', '/business/danye/list', 'business/danye/list', 'Notebook', 2, 'danye:list', 1, 1, 0, 1, GETDATE());
|
||||
PRINT N'创建单页管理菜单';
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 3. 创建悬浮球配置子菜单
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/floatball/list')
|
||||
BEGIN
|
||||
INSERT INTO menus (ParentId, Name, Path, Component, Icon, MenuType, Permission, SortOrder, Status, IsExternal, IsCache, CreatedAt)
|
||||
VALUES (@ContentMenuId, N'悬浮球配置', '/business/floatball/list', 'business/floatball/list', 'Pointer', 2, 'floatball:list', 2, 1, 0, 1, GETDATE());
|
||||
PRINT N'创建悬浮球配置菜单';
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 4. 创建福利屋入口子菜单
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/welfarehouse/list')
|
||||
BEGIN
|
||||
INSERT INTO menus (ParentId, Name, Path, Component, Icon, MenuType, Permission, SortOrder, Status, IsExternal, IsCache, CreatedAt)
|
||||
VALUES (@ContentMenuId, N'福利屋入口', '/business/welfarehouse/list', 'business/welfarehouse/list', 'Present', 2, 'welfarehouse:list', 3, 1, 0, 1, GETDATE());
|
||||
PRINT N'创建福利屋入口菜单';
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 5. 添加单页管理相关权限
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'danye:list')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'单页列表', 'danye:list', N'内容管理', GETDATE());
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'danye:edit')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'编辑单页', 'danye:edit', N'内容管理', GETDATE());
|
||||
|
||||
-- =============================================
|
||||
-- 6. 添加悬浮球配置相关权限
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'floatball:list')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'悬浮球列表', 'floatball:list', N'内容管理', GETDATE());
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'floatball:add')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'新增悬浮球', 'floatball:add', N'内容管理', GETDATE());
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'floatball:edit')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'编辑悬浮球', 'floatball:edit', N'内容管理', GETDATE());
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'floatball:delete')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'删除悬浮球', 'floatball:delete', N'内容管理', GETDATE());
|
||||
|
||||
-- =============================================
|
||||
-- 7. 添加福利屋入口相关权限
|
||||
-- =============================================
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'welfarehouse:list')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'福利屋列表', 'welfarehouse:list', N'内容管理', GETDATE());
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'welfarehouse:add')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'新增福利屋入口', 'welfarehouse:add', N'内容管理', GETDATE());
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'welfarehouse:edit')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'编辑福利屋入口', 'welfarehouse:edit', N'内容管理', GETDATE());
|
||||
IF NOT EXISTS (SELECT 1 FROM permissions WHERE Code = 'welfarehouse:delete')
|
||||
INSERT INTO permissions (Name, Code, Module, CreatedAt) VALUES (N'删除福利屋入口', 'welfarehouse:delete', N'内容管理', GETDATE());
|
||||
|
||||
-- =============================================
|
||||
-- 8. 为超级管理员角色分配新菜单
|
||||
-- =============================================
|
||||
IF @SuperAdminRoleId IS NOT NULL
|
||||
BEGIN
|
||||
-- 分配内容管理目录
|
||||
IF NOT EXISTS (SELECT 1 FROM role_menus WHERE RoleId = @SuperAdminRoleId AND MenuId = @ContentMenuId)
|
||||
BEGIN
|
||||
INSERT INTO role_menus (RoleId, MenuId) VALUES (@SuperAdminRoleId, @ContentMenuId);
|
||||
PRINT N'为超级管理员分配内容管理目录';
|
||||
END
|
||||
|
||||
-- 分配所有新创建的子菜单
|
||||
INSERT INTO role_menus (RoleId, MenuId)
|
||||
SELECT @SuperAdminRoleId, m.Id
|
||||
FROM menus m
|
||||
WHERE m.ParentId = @ContentMenuId
|
||||
AND NOT EXISTS (SELECT 1 FROM role_menus rm WHERE rm.RoleId = @SuperAdminRoleId AND rm.MenuId = m.Id);
|
||||
|
||||
PRINT N'为超级管理员分配内容管理子菜单';
|
||||
|
||||
-- 分配新增的权限
|
||||
INSERT INTO role_permissions (RoleId, PermissionId)
|
||||
SELECT @SuperAdminRoleId, p.Id
|
||||
FROM permissions p
|
||||
WHERE p.Code IN (
|
||||
'danye:list', 'danye:edit',
|
||||
'floatball:list', 'floatball:add', 'floatball:edit', 'floatball:delete',
|
||||
'welfarehouse:list', 'welfarehouse:add', 'welfarehouse:edit', 'welfarehouse:delete'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM role_permissions rp
|
||||
WHERE rp.RoleId = @SuperAdminRoleId AND rp.PermissionId = p.Id
|
||||
);
|
||||
|
||||
PRINT N'为超级管理员分配内容管理权限';
|
||||
END
|
||||
|
||||
-- =============================================
|
||||
-- 9. 验证结果
|
||||
-- =============================================
|
||||
PRINT N'';
|
||||
PRINT N'========== 菜单创建结果 ==========';
|
||||
SELECT
|
||||
m.Id,
|
||||
m.ParentId,
|
||||
m.Name,
|
||||
m.Path,
|
||||
m.Component,
|
||||
m.MenuType,
|
||||
m.Permission,
|
||||
m.SortOrder
|
||||
FROM menus m
|
||||
WHERE m.Path LIKE '/business/content%'
|
||||
OR m.Path LIKE '/business/danye%'
|
||||
OR m.Path LIKE '/business/floatball%'
|
||||
OR m.Path LIKE '/business/welfarehouse%'
|
||||
ORDER BY m.ParentId, m.SortOrder;
|
||||
|
||||
PRINT N'';
|
||||
PRINT N'========== 权限创建结果 ==========';
|
||||
SELECT
|
||||
p.Id,
|
||||
p.Name,
|
||||
p.Code,
|
||||
p.Module
|
||||
FROM permissions p
|
||||
WHERE p.Code LIKE 'danye:%'
|
||||
OR p.Code LIKE 'floatball:%'
|
||||
OR p.Code LIKE 'welfarehouse:%'
|
||||
ORDER BY p.Code;
|
||||
|
||||
PRINT N'';
|
||||
PRINT N'========== 角色菜单分配结果 ==========';
|
||||
SELECT
|
||||
r.Name AS RoleName,
|
||||
m.Name AS MenuName,
|
||||
m.Path
|
||||
FROM role_menus rm
|
||||
INNER JOIN roles r ON rm.RoleId = r.Id
|
||||
INNER JOIN menus m ON rm.MenuId = m.Id
|
||||
WHERE m.Path LIKE '/business/content%'
|
||||
OR m.Path LIKE '/business/danye%'
|
||||
OR m.Path LIKE '/business/floatball%'
|
||||
OR m.Path LIKE '/business/welfarehouse%'
|
||||
ORDER BY r.Name, m.Path;
|
||||
|
||||
PRINT N'';
|
||||
PRINT N'========== 角色权限分配结果 ==========';
|
||||
SELECT
|
||||
r.Name AS RoleName,
|
||||
p.Name AS PermissionName,
|
||||
p.Code
|
||||
FROM role_permissions rp
|
||||
INNER JOIN roles r ON rp.RoleId = r.Id
|
||||
INNER JOIN permissions p ON rp.PermissionId = p.Id
|
||||
WHERE p.Code LIKE 'danye:%'
|
||||
OR p.Code LIKE 'floatball:%'
|
||||
OR p.Code LIKE 'welfarehouse:%'
|
||||
ORDER BY r.Name, p.Code;
|
||||
|
||||
PRINT N'';
|
||||
PRINT N'内容与辅助模块菜单初始化完成';
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
using HoneyBox.Admin.Business.Attributes;
|
||||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.Danye;
|
||||
using HoneyBox.Admin.Business.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 单页管理控制器
|
||||
/// </summary>
|
||||
[Route("api/admin/business/[controller]")]
|
||||
public class DanyeController : BusinessControllerBase
|
||||
{
|
||||
private readonly IDanyeService _danyeService;
|
||||
|
||||
public DanyeController(IDanyeService danyeService)
|
||||
{
|
||||
_danyeService = danyeService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单页列表
|
||||
/// </summary>
|
||||
/// <returns>单页列表</returns>
|
||||
[HttpGet]
|
||||
[BusinessPermission("danye:list")]
|
||||
public async Task<IActionResult> GetDanyeList()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _danyeService.GetDanyeListAsync();
|
||||
return Ok(result);
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单页详情
|
||||
/// </summary>
|
||||
/// <param name="id">单页ID</param>
|
||||
/// <returns>单页详情</returns>
|
||||
[HttpGet("{id}")]
|
||||
[BusinessPermission("danye:list")]
|
||||
public async Task<IActionResult> GetDanyeById(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _danyeService.GetDanyeByIdAsync(id);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFoundError("单页不存在");
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新单页内容
|
||||
/// </summary>
|
||||
/// <param name="id">单页ID</param>
|
||||
/// <param name="request">更新请求</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPut("{id}")]
|
||||
[BusinessPermission("danye:edit")]
|
||||
public async Task<IActionResult> UpdateDanye(int id, [FromBody] DanyeUpdateRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _danyeService.UpdateDanyeAsync(id, request);
|
||||
return result ? Ok("更新成功") : Error(BusinessErrorCodes.InternalError, "更新失败");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换图片优化状态
|
||||
/// </summary>
|
||||
/// <param name="id">单页ID</param>
|
||||
/// <param name="request">图片优化请求</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPut("{id}/image-optimizer")]
|
||||
[BusinessPermission("danye:edit")]
|
||||
public async Task<IActionResult> ToggleImageOptimizer(int id, [FromBody] ImageOptimizerRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _danyeService.ToggleImageOptimizerAsync(id, request);
|
||||
return result ? Ok("切换成功") : Error(BusinessErrorCodes.InternalError, "切换失败");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
using HoneyBox.Admin.Business.Attributes;
|
||||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.FloatBall;
|
||||
using HoneyBox.Admin.Business.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球配置控制器
|
||||
/// </summary>
|
||||
[Route("api/admin/business/[controller]")]
|
||||
public class FloatBallController : BusinessControllerBase
|
||||
{
|
||||
private readonly IFloatBallService _floatBallService;
|
||||
|
||||
public FloatBallController(IFloatBallService floatBallService)
|
||||
{
|
||||
_floatBallService = floatBallService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取悬浮球列表(分页)
|
||||
/// </summary>
|
||||
/// <param name="request">分页请求</param>
|
||||
/// <returns>悬浮球列表</returns>
|
||||
[HttpGet]
|
||||
[BusinessPermission("floatball:list")]
|
||||
public async Task<IActionResult> GetFloatBalls([FromQuery] FloatBallListRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _floatBallService.GetFloatBallsAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取悬浮球详情
|
||||
/// </summary>
|
||||
/// <param name="id">悬浮球ID</param>
|
||||
/// <returns>悬浮球详情</returns>
|
||||
[HttpGet("{id}")]
|
||||
[BusinessPermission("floatball:list")]
|
||||
public async Task<IActionResult> GetFloatBallById(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _floatBallService.GetFloatBallByIdAsync(id);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFoundError("悬浮球配置不存在");
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增悬浮球
|
||||
/// </summary>
|
||||
/// <param name="request">创建请求</param>
|
||||
/// <returns>新创建的悬浮球ID</returns>
|
||||
[HttpPost]
|
||||
[BusinessPermission("floatball:add")]
|
||||
public async Task<IActionResult> CreateFloatBall([FromBody] FloatBallCreateRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = await _floatBallService.CreateFloatBallAsync(request);
|
||||
return Ok(new { id }, "创建成功");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新悬浮球
|
||||
/// </summary>
|
||||
/// <param name="id">悬浮球ID</param>
|
||||
/// <param name="request">更新请求</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPut("{id}")]
|
||||
[BusinessPermission("floatball:edit")]
|
||||
public async Task<IActionResult> UpdateFloatBall(int id, [FromBody] FloatBallUpdateRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _floatBallService.UpdateFloatBallAsync(id, request);
|
||||
return result ? Ok("更新成功") : Error(BusinessErrorCodes.InternalError, "更新失败");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除悬浮球
|
||||
/// </summary>
|
||||
/// <param name="id">悬浮球ID</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpDelete("{id}")]
|
||||
[BusinessPermission("floatball:delete")]
|
||||
public async Task<IActionResult> DeleteFloatBall(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _floatBallService.DeleteFloatBallAsync(id);
|
||||
return result ? Ok("删除成功") : Error(BusinessErrorCodes.InternalError, "删除失败");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换悬浮球状态
|
||||
/// </summary>
|
||||
/// <param name="id">悬浮球ID</param>
|
||||
/// <param name="request">状态请求</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPut("{id}/status")]
|
||||
[BusinessPermission("floatball:edit")]
|
||||
public async Task<IActionResult> UpdateStatus(int id, [FromBody] FloatBallStatusRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _floatBallService.UpdateStatusAsync(id, request);
|
||||
return result ? Ok("状态更新成功") : Error(BusinessErrorCodes.InternalError, "状态更新失败");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
using HoneyBox.Admin.Business.Attributes;
|
||||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.WelfareHouse;
|
||||
using HoneyBox.Admin.Business.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 福利屋入口控制器
|
||||
/// </summary>
|
||||
[Route("api/admin/business/[controller]")]
|
||||
public class WelfareHouseController : BusinessControllerBase
|
||||
{
|
||||
private readonly IWelfareHouseService _welfareHouseService;
|
||||
|
||||
public WelfareHouseController(IWelfareHouseService welfareHouseService)
|
||||
{
|
||||
_welfareHouseService = welfareHouseService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取福利屋入口列表(分页)
|
||||
/// </summary>
|
||||
/// <param name="request">分页请求</param>
|
||||
/// <returns>福利屋入口列表</returns>
|
||||
[HttpGet]
|
||||
[BusinessPermission("welfarehouse:list")]
|
||||
public async Task<IActionResult> GetWelfareHouses([FromQuery] WelfareHouseListRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _welfareHouseService.GetWelfareHousesAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取福利屋入口详情
|
||||
/// </summary>
|
||||
/// <param name="id">福利屋入口ID</param>
|
||||
/// <returns>福利屋入口详情</returns>
|
||||
[HttpGet("{id}")]
|
||||
[BusinessPermission("welfarehouse:list")]
|
||||
public async Task<IActionResult> GetWelfareHouseById(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _welfareHouseService.GetWelfareHouseByIdAsync(id);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFoundError("福利屋入口不存在");
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 新增福利屋入口
|
||||
/// </summary>
|
||||
/// <param name="request">创建请求</param>
|
||||
/// <returns>新创建的福利屋入口ID</returns>
|
||||
[HttpPost]
|
||||
[BusinessPermission("welfarehouse:add")]
|
||||
public async Task<IActionResult> CreateWelfareHouse([FromBody] WelfareHouseCreateRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = await _welfareHouseService.CreateWelfareHouseAsync(request);
|
||||
return Ok(new { id }, "创建成功");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新福利屋入口
|
||||
/// </summary>
|
||||
/// <param name="id">福利屋入口ID</param>
|
||||
/// <param name="request">更新请求</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPut("{id}")]
|
||||
[BusinessPermission("welfarehouse:edit")]
|
||||
public async Task<IActionResult> UpdateWelfareHouse(int id, [FromBody] WelfareHouseUpdateRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _welfareHouseService.UpdateWelfareHouseAsync(id, request);
|
||||
return result ? Ok("更新成功") : Error(BusinessErrorCodes.InternalError, "更新失败");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除福利屋入口
|
||||
/// </summary>
|
||||
/// <param name="id">福利屋入口ID</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpDelete("{id}")]
|
||||
[BusinessPermission("welfarehouse:delete")]
|
||||
public async Task<IActionResult> DeleteWelfareHouse(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _welfareHouseService.DeleteWelfareHouseAsync(id);
|
||||
return result ? Ok("删除成功") : Error(BusinessErrorCodes.InternalError, "删除失败");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换福利屋入口状态
|
||||
/// </summary>
|
||||
/// <param name="id">福利屋入口ID</param>
|
||||
/// <param name="request">状态请求</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPut("{id}/status")]
|
||||
[BusinessPermission("welfarehouse:edit")]
|
||||
public async Task<IActionResult> UpdateStatus(int id, [FromBody] WelfareHouseStatusRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _welfareHouseService.UpdateStatusAsync(id, request);
|
||||
return result ? Ok("状态更新成功") : Error(BusinessErrorCodes.InternalError, "状态更新失败");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return Error(ex.Code, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
namespace HoneyBox.Admin.Business.Models.Danye;
|
||||
|
||||
#region Response Models
|
||||
|
||||
/// <summary>
|
||||
/// 单页列表响应模型
|
||||
/// </summary>
|
||||
public class DanyeResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 单页ID
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用图片优化
|
||||
/// </summary>
|
||||
public bool IsImageOptimizer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime UpdateTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单页详情响应模型
|
||||
/// </summary>
|
||||
public class DanyeDetailResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 单页ID
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 内容(富文本HTML)
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用图片优化
|
||||
/// </summary>
|
||||
public bool IsImageOptimizer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题是否可编辑(ID 2-20 不可编辑)
|
||||
/// </summary>
|
||||
public bool IsTitleEditable { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Request Models
|
||||
|
||||
/// <summary>
|
||||
/// 单页更新请求模型
|
||||
/// </summary>
|
||||
public class DanyeUpdateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 标题(可选,ID 2-20 的单页标题不可编辑)
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容(富文本HTML)
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图片优化切换请求模型
|
||||
/// </summary>
|
||||
public class ImageOptimizerRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用图片优化
|
||||
/// </summary>
|
||||
public bool IsImageOptimizer { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
namespace HoneyBox.Admin.Business.Models.FloatBall;
|
||||
|
||||
#region Response Models
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球列表响应模型
|
||||
/// </summary>
|
||||
public class FloatBallResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键ID
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型: 1展示图片 2跳转页面
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球图片URL
|
||||
/// </summary>
|
||||
public string Image { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 背景图片URL
|
||||
/// </summary>
|
||||
public string? ImageBj { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片URL
|
||||
/// </summary>
|
||||
public string? ImageDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳转链接
|
||||
/// </summary>
|
||||
public string LinkUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// X轴位置
|
||||
/// </summary>
|
||||
public string PositionX { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Y轴位置
|
||||
/// </summary>
|
||||
public string PositionY { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 宽度
|
||||
/// </summary>
|
||||
public string Width { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 高度
|
||||
/// </summary>
|
||||
public string Height { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片X偏移
|
||||
/// </summary>
|
||||
public string? ImageDetailsX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片Y偏移
|
||||
/// </summary>
|
||||
public string? ImageDetailsY { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片宽度
|
||||
/// </summary>
|
||||
public string? ImageDetailsW { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片高度
|
||||
/// </summary>
|
||||
public string? ImageDetailsH { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 特效: 0无 1缩放动画
|
||||
/// </summary>
|
||||
public int Effect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态: 0关闭 1开启
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Request Models
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球列表查询请求
|
||||
/// </summary>
|
||||
public class FloatBallListRequest : PagedRequest
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球创建请求模型
|
||||
/// </summary>
|
||||
public class FloatBallCreateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型: 1展示图片 2跳转页面
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球图片URL(必填)
|
||||
/// </summary>
|
||||
public string Image { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 背景图片URL
|
||||
/// </summary>
|
||||
public string? ImageBj { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片URL
|
||||
/// </summary>
|
||||
public string? ImageDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳转链接(类型为跳转页面时使用)
|
||||
/// </summary>
|
||||
public string? LinkUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// X轴位置(必填)
|
||||
/// </summary>
|
||||
public string PositionX { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Y轴位置(必填)
|
||||
/// </summary>
|
||||
public string PositionY { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 宽度(必填)
|
||||
/// </summary>
|
||||
public string Width { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 高度(必填)
|
||||
/// </summary>
|
||||
public string Height { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片X偏移
|
||||
/// </summary>
|
||||
public string? ImageDetailsX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片Y偏移
|
||||
/// </summary>
|
||||
public string? ImageDetailsY { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片宽度
|
||||
/// </summary>
|
||||
public string? ImageDetailsW { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片高度
|
||||
/// </summary>
|
||||
public string? ImageDetailsH { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 特效: 0无 1缩放动画(必填)
|
||||
/// </summary>
|
||||
public int Effect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态: 0关闭 1开启,默认开启
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球更新请求模型
|
||||
/// </summary>
|
||||
public class FloatBallUpdateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型: 1展示图片 2跳转页面
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球图片URL(必填)
|
||||
/// </summary>
|
||||
public string Image { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 背景图片URL
|
||||
/// </summary>
|
||||
public string? ImageBj { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片URL
|
||||
/// </summary>
|
||||
public string? ImageDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳转链接(类型为跳转页面时使用)
|
||||
/// </summary>
|
||||
public string? LinkUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// X轴位置(必填)
|
||||
/// </summary>
|
||||
public string PositionX { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Y轴位置(必填)
|
||||
/// </summary>
|
||||
public string PositionY { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 宽度(必填)
|
||||
/// </summary>
|
||||
public string Width { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 高度(必填)
|
||||
/// </summary>
|
||||
public string Height { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片X偏移
|
||||
/// </summary>
|
||||
public string? ImageDetailsX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片Y偏移
|
||||
/// </summary>
|
||||
public string? ImageDetailsY { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片宽度
|
||||
/// </summary>
|
||||
public string? ImageDetailsW { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详情图片高度
|
||||
/// </summary>
|
||||
public string? ImageDetailsH { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 特效: 0无 1缩放动画(必填)
|
||||
/// </summary>
|
||||
public int Effect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态: 0关闭 1开启
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球状态切换请求模型
|
||||
/// </summary>
|
||||
public class FloatBallStatusRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态: 0关闭 1开启
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
namespace HoneyBox.Admin.Business.Models.WelfareHouse;
|
||||
|
||||
#region Response Models
|
||||
|
||||
/// <summary>
|
||||
/// 福利屋入口列表响应模型
|
||||
/// </summary>
|
||||
public class WelfareHouseResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键ID
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 图片URL
|
||||
/// </summary>
|
||||
public string Image { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 跳转链接
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 排序值
|
||||
/// </summary>
|
||||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态: 0禁用 1启用
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime? CreateTime { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Request Models
|
||||
|
||||
/// <summary>
|
||||
/// 福利屋入口列表查询请求
|
||||
/// </summary>
|
||||
public class WelfareHouseListRequest : PagedRequest
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 福利屋入口创建请求模型
|
||||
/// </summary>
|
||||
public class WelfareHouseCreateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称(必填)
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 图片URL(必填)
|
||||
/// </summary>
|
||||
public string Image { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 跳转链接(必填)
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 排序值(必填)
|
||||
/// </summary>
|
||||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态: 0禁用 1启用,默认启用
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 福利屋入口更新请求模型
|
||||
/// </summary>
|
||||
public class WelfareHouseUpdateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称(必填)
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 图片URL(必填)
|
||||
/// </summary>
|
||||
public string Image { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 跳转链接(必填)
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 排序值(必填)
|
||||
/// </summary>
|
||||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态: 0禁用 1启用
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 福利屋入口状态切换请求模型
|
||||
/// </summary>
|
||||
public class WelfareHouseStatusRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态: 0禁用 1启用
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.Danye;
|
||||
using HoneyBox.Admin.Business.Services.Interfaces;
|
||||
using HoneyBox.Model.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 单页管理服务实现
|
||||
/// </summary>
|
||||
public class DanyeService : IDanyeService
|
||||
{
|
||||
private readonly HoneyBoxDbContext _dbContext;
|
||||
private readonly ILogger<DanyeService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 标题不可编辑的ID范围(2-20)
|
||||
/// </summary>
|
||||
private const int TitleEditableMinId = 2;
|
||||
private const int TitleEditableMaxId = 20;
|
||||
|
||||
public DanyeService(
|
||||
HoneyBoxDbContext dbContext,
|
||||
ILogger<DanyeService> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<DanyeResponse>> GetDanyeListAsync()
|
||||
{
|
||||
var danyes = await _dbContext.Danyes
|
||||
.AsNoTracking()
|
||||
.OrderBy(d => d.Id)
|
||||
.ToListAsync();
|
||||
|
||||
return danyes.Select(d => new DanyeResponse
|
||||
{
|
||||
Id = d.Id,
|
||||
Title = d.Title,
|
||||
IsImageOptimizer = d.IsImageOptimizer == 1,
|
||||
UpdateTime = UnixTimeToDateTime(d.UpdateTime)
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DanyeDetailResponse?> GetDanyeByIdAsync(int id)
|
||||
{
|
||||
var danye = await _dbContext.Danyes
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(d => d.Id == id);
|
||||
|
||||
if (danye == null)
|
||||
return null;
|
||||
|
||||
return new DanyeDetailResponse
|
||||
{
|
||||
Id = danye.Id,
|
||||
Title = danye.Title,
|
||||
Content = danye.Content,
|
||||
IsImageOptimizer = danye.IsImageOptimizer == 1,
|
||||
IsTitleEditable = !IsTitleProtected(danye.Id)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateDanyeAsync(int id, DanyeUpdateRequest request)
|
||||
{
|
||||
var danye = await _dbContext.Danyes.FirstOrDefaultAsync(d => d.Id == id);
|
||||
if (danye == null)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.NotFound, "单页不存在");
|
||||
}
|
||||
|
||||
// 验证内容不能为空
|
||||
if (string.IsNullOrWhiteSpace(request.Content))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "内容不能为空");
|
||||
}
|
||||
|
||||
// 更新标题(仅当ID不在2-20范围内且提供了标题时)
|
||||
if (!string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
if (IsTitleProtected(id))
|
||||
{
|
||||
_logger.LogWarning("尝试修改受保护的单页标题: Id={Id}", id);
|
||||
// 不抛出异常,只是忽略标题更新
|
||||
}
|
||||
else
|
||||
{
|
||||
danye.Title = request.Title;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新内容
|
||||
danye.Content = request.Content;
|
||||
danye.UpdateTime = GetUnixTimestamp();
|
||||
|
||||
var result = await _dbContext.SaveChangesAsync() > 0;
|
||||
|
||||
_logger.LogInformation("更新单页成功: Id={Id}", id);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> ToggleImageOptimizerAsync(int id, ImageOptimizerRequest request)
|
||||
{
|
||||
var danye = await _dbContext.Danyes.FirstOrDefaultAsync(d => d.Id == id);
|
||||
if (danye == null)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.NotFound, "单页不存在");
|
||||
}
|
||||
|
||||
danye.IsImageOptimizer = (byte)(request.IsImageOptimizer ? 1 : 0);
|
||||
danye.UpdateTime = GetUnixTimestamp();
|
||||
|
||||
var result = await _dbContext.SaveChangesAsync() > 0;
|
||||
|
||||
_logger.LogInformation("切换单页图片优化状态: Id={Id}, IsImageOptimizer={IsImageOptimizer}",
|
||||
id, request.IsImageOptimizer);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#region Private Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// 判断标题是否受保护(ID 2-20 的单页标题不可编辑)
|
||||
/// </summary>
|
||||
private static bool IsTitleProtected(int id)
|
||||
{
|
||||
return id >= TitleEditableMinId && id <= TitleEditableMaxId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unix时间戳转DateTime
|
||||
/// </summary>
|
||||
private static DateTime UnixTimeToDateTime(int unixTime)
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(unixTime).LocalDateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前Unix时间戳
|
||||
/// </summary>
|
||||
private static int GetUnixTimestamp()
|
||||
{
|
||||
return (int)DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.FloatBall;
|
||||
using HoneyBox.Admin.Business.Services.Interfaces;
|
||||
using HoneyBox.Model.Data;
|
||||
using HoneyBox.Model.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球配置服务实现
|
||||
/// </summary>
|
||||
public class FloatBallService : IFloatBallService
|
||||
{
|
||||
private readonly HoneyBoxDbContext _dbContext;
|
||||
private readonly ILogger<FloatBallService> _logger;
|
||||
|
||||
public FloatBallService(
|
||||
HoneyBoxDbContext dbContext,
|
||||
ILogger<FloatBallService> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<FloatBallResponse>> GetFloatBallsAsync(FloatBallListRequest request)
|
||||
{
|
||||
var query = _dbContext.FloatBallConfigs.AsNoTracking();
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.Skip(request.Skip)
|
||||
.Take(request.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var list = items.Select(MapToResponse).ToList();
|
||||
|
||||
return PagedResult<FloatBallResponse>.Create(list, total, request.Page, request.PageSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<FloatBallResponse?> GetFloatBallByIdAsync(int id)
|
||||
{
|
||||
var entity = await _dbContext.FloatBallConfigs
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(f => f.Id == id);
|
||||
|
||||
return entity == null ? null : MapToResponse(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> CreateFloatBallAsync(FloatBallCreateRequest request)
|
||||
{
|
||||
// 验证必填字段
|
||||
ValidateCreateRequest(request);
|
||||
|
||||
var entity = new FloatBallConfig
|
||||
{
|
||||
Title = request.Title,
|
||||
Type = (byte)request.Type,
|
||||
Image = request.Image,
|
||||
ImageBj = request.ImageBj,
|
||||
ImageDetails = request.ImageDetails,
|
||||
LinkUrl = request.LinkUrl ?? string.Empty,
|
||||
PositionX = request.PositionX,
|
||||
PositionY = request.PositionY,
|
||||
Width = request.Width,
|
||||
Height = request.Height,
|
||||
ImageDetailsX = request.ImageDetailsX,
|
||||
ImageDetailsY = request.ImageDetailsY,
|
||||
ImageDetailsW = request.ImageDetailsW,
|
||||
ImageDetailsH = request.ImageDetailsH,
|
||||
Effect = (byte)request.Effect,
|
||||
Status = (byte)request.Status,
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
_dbContext.FloatBallConfigs.Add(entity);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("创建悬浮球成功: Id={Id}, Title={Title}", entity.Id, entity.Title);
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateFloatBallAsync(int id, FloatBallUpdateRequest request)
|
||||
{
|
||||
var entity = await _dbContext.FloatBallConfigs.FirstOrDefaultAsync(f => f.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.NotFound, "悬浮球配置不存在");
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
ValidateUpdateRequest(request);
|
||||
|
||||
entity.Title = request.Title;
|
||||
entity.Type = (byte)request.Type;
|
||||
entity.Image = request.Image;
|
||||
entity.ImageBj = request.ImageBj;
|
||||
entity.ImageDetails = request.ImageDetails;
|
||||
entity.LinkUrl = request.LinkUrl ?? string.Empty;
|
||||
entity.PositionX = request.PositionX;
|
||||
entity.PositionY = request.PositionY;
|
||||
entity.Width = request.Width;
|
||||
entity.Height = request.Height;
|
||||
entity.ImageDetailsX = request.ImageDetailsX;
|
||||
entity.ImageDetailsY = request.ImageDetailsY;
|
||||
entity.ImageDetailsW = request.ImageDetailsW;
|
||||
entity.ImageDetailsH = request.ImageDetailsH;
|
||||
entity.Effect = (byte)request.Effect;
|
||||
entity.Status = (byte)request.Status;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
|
||||
var result = await _dbContext.SaveChangesAsync() > 0;
|
||||
|
||||
_logger.LogInformation("更新悬浮球成功: Id={Id}", id);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> DeleteFloatBallAsync(int id)
|
||||
{
|
||||
var entity = await _dbContext.FloatBallConfigs.FirstOrDefaultAsync(f => f.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.NotFound, "悬浮球配置不存在");
|
||||
}
|
||||
|
||||
_dbContext.FloatBallConfigs.Remove(entity);
|
||||
var result = await _dbContext.SaveChangesAsync() > 0;
|
||||
|
||||
_logger.LogInformation("删除悬浮球成功: Id={Id}", id);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateStatusAsync(int id, FloatBallStatusRequest request)
|
||||
{
|
||||
var entity = await _dbContext.FloatBallConfigs.FirstOrDefaultAsync(f => f.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.NotFound, "悬浮球配置不存在");
|
||||
}
|
||||
|
||||
entity.Status = (byte)request.Status;
|
||||
entity.UpdatedAt = DateTime.Now;
|
||||
|
||||
var result = await _dbContext.SaveChangesAsync() > 0;
|
||||
|
||||
_logger.LogInformation("更新悬浮球状态: Id={Id}, Status={Status}", id, request.Status);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#region Private Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// 验证创建请求必填字段
|
||||
/// </summary>
|
||||
private static void ValidateCreateRequest(FloatBallCreateRequest request)
|
||||
{
|
||||
if (request.Type < 1 || request.Type > 2)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "类型必须为1(展示图片)或2(跳转页面)");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Image))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "悬浮球图片不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.PositionX))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "X轴位置不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.PositionY))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "Y轴位置不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Width))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "宽度不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Height))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "高度不能为空");
|
||||
}
|
||||
|
||||
if (request.Effect < 0 || request.Effect > 1)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "特效必须为0(无)或1(缩放动画)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证更新请求必填字段
|
||||
/// </summary>
|
||||
private static void ValidateUpdateRequest(FloatBallUpdateRequest request)
|
||||
{
|
||||
if (request.Type < 1 || request.Type > 2)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "类型必须为1(展示图片)或2(跳转页面)");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Image))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "悬浮球图片不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.PositionX))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "X轴位置不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.PositionY))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "Y轴位置不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Width))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "宽度不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Height))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "高度不能为空");
|
||||
}
|
||||
|
||||
if (request.Effect < 0 || request.Effect > 1)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "特效必须为0(无)或1(缩放动画)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将实体映射为响应模型
|
||||
/// </summary>
|
||||
private static FloatBallResponse MapToResponse(FloatBallConfig entity)
|
||||
{
|
||||
return new FloatBallResponse
|
||||
{
|
||||
Id = entity.Id,
|
||||
Title = entity.Title,
|
||||
Type = entity.Type,
|
||||
Image = entity.Image,
|
||||
ImageBj = entity.ImageBj,
|
||||
ImageDetails = entity.ImageDetails,
|
||||
LinkUrl = entity.LinkUrl,
|
||||
PositionX = entity.PositionX,
|
||||
PositionY = entity.PositionY,
|
||||
Width = entity.Width,
|
||||
Height = entity.Height,
|
||||
ImageDetailsX = entity.ImageDetailsX,
|
||||
ImageDetailsY = entity.ImageDetailsY,
|
||||
ImageDetailsW = entity.ImageDetailsW,
|
||||
ImageDetailsH = entity.ImageDetailsH,
|
||||
Effect = entity.Effect,
|
||||
Status = entity.Status,
|
||||
CreatedAt = entity.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using HoneyBox.Admin.Business.Models.Danye;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 单页管理服务接口
|
||||
/// </summary>
|
||||
public interface IDanyeService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取单页列表
|
||||
/// </summary>
|
||||
/// <returns>单页列表</returns>
|
||||
Task<List<DanyeResponse>> GetDanyeListAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取单页详情
|
||||
/// </summary>
|
||||
/// <param name="id">单页ID</param>
|
||||
/// <returns>单页详情</returns>
|
||||
Task<DanyeDetailResponse?> GetDanyeByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 更新单页内容
|
||||
/// </summary>
|
||||
/// <param name="id">单页ID</param>
|
||||
/// <param name="request">更新请求</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> UpdateDanyeAsync(int id, DanyeUpdateRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 切换图片优化状态
|
||||
/// </summary>
|
||||
/// <param name="id">单页ID</param>
|
||||
/// <param name="request">图片优化请求</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> ToggleImageOptimizerAsync(int id, ImageOptimizerRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.FloatBall;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 悬浮球配置服务接口
|
||||
/// </summary>
|
||||
public interface IFloatBallService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取悬浮球列表(分页)
|
||||
/// </summary>
|
||||
/// <param name="request">分页请求</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PagedResult<FloatBallResponse>> GetFloatBallsAsync(FloatBallListRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 获取悬浮球详情
|
||||
/// </summary>
|
||||
/// <param name="id">悬浮球ID</param>
|
||||
/// <returns>悬浮球详情</returns>
|
||||
Task<FloatBallResponse?> GetFloatBallByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 创建悬浮球
|
||||
/// </summary>
|
||||
/// <param name="request">创建请求</param>
|
||||
/// <returns>新创建的悬浮球ID</returns>
|
||||
Task<int> CreateFloatBallAsync(FloatBallCreateRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 更新悬浮球
|
||||
/// </summary>
|
||||
/// <param name="id">悬浮球ID</param>
|
||||
/// <param name="request">更新请求</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> UpdateFloatBallAsync(int id, FloatBallUpdateRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 删除悬浮球
|
||||
/// </summary>
|
||||
/// <param name="id">悬浮球ID</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> DeleteFloatBallAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 更新悬浮球状态
|
||||
/// </summary>
|
||||
/// <param name="id">悬浮球ID</param>
|
||||
/// <param name="request">状态请求</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> UpdateStatusAsync(int id, FloatBallStatusRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.WelfareHouse;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 福利屋入口服务接口
|
||||
/// </summary>
|
||||
public interface IWelfareHouseService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取福利屋入口列表(分页)
|
||||
/// </summary>
|
||||
/// <param name="request">分页请求</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PagedResult<WelfareHouseResponse>> GetWelfareHousesAsync(WelfareHouseListRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 获取福利屋入口详情
|
||||
/// </summary>
|
||||
/// <param name="id">福利屋入口ID</param>
|
||||
/// <returns>福利屋入口详情</returns>
|
||||
Task<WelfareHouseResponse?> GetWelfareHouseByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 创建福利屋入口
|
||||
/// </summary>
|
||||
/// <param name="request">创建请求</param>
|
||||
/// <returns>新创建的福利屋入口ID</returns>
|
||||
Task<int> CreateWelfareHouseAsync(WelfareHouseCreateRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 更新福利屋入口
|
||||
/// </summary>
|
||||
/// <param name="id">福利屋入口ID</param>
|
||||
/// <param name="request">更新请求</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> UpdateWelfareHouseAsync(int id, WelfareHouseUpdateRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 删除福利屋入口
|
||||
/// </summary>
|
||||
/// <param name="id">福利屋入口ID</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> DeleteWelfareHouseAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 更新福利屋入口状态
|
||||
/// </summary>
|
||||
/// <param name="id">福利屋入口ID</param>
|
||||
/// <param name="request">状态请求</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> UpdateStatusAsync(int id, WelfareHouseStatusRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
using HoneyBox.Admin.Business.Models;
|
||||
using HoneyBox.Admin.Business.Models.WelfareHouse;
|
||||
using HoneyBox.Admin.Business.Services.Interfaces;
|
||||
using HoneyBox.Model.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HoneyBox.Admin.Business.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 福利屋入口服务实现
|
||||
/// </summary>
|
||||
public class WelfareHouseService : IWelfareHouseService
|
||||
{
|
||||
private readonly HoneyBoxDbContext _dbContext;
|
||||
private readonly ILogger<WelfareHouseService> _logger;
|
||||
|
||||
public WelfareHouseService(
|
||||
HoneyBoxDbContext dbContext,
|
||||
ILogger<WelfareHouseService> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<WelfareHouseResponse>> GetWelfareHousesAsync(WelfareHouseListRequest request)
|
||||
{
|
||||
var query = _dbContext.WelfareHouses.AsNoTracking();
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
.OrderBy(w => w.Sort)
|
||||
.ThenByDescending(w => w.CreateTime)
|
||||
.Skip(request.Skip)
|
||||
.Take(request.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var list = items.Select(MapToResponse).ToList();
|
||||
|
||||
return PagedResult<WelfareHouseResponse>.Create(list, total, request.Page, request.PageSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WelfareHouseResponse?> GetWelfareHouseByIdAsync(int id)
|
||||
{
|
||||
var entity = await _dbContext.WelfareHouses
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(w => w.Id == id);
|
||||
|
||||
return entity == null ? null : MapToResponse(entity);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> CreateWelfareHouseAsync(WelfareHouseCreateRequest request)
|
||||
{
|
||||
// 验证必填字段
|
||||
ValidateCreateRequest(request);
|
||||
|
||||
var now = (int)DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
var entity = new Model.Entities.WelfareHouse
|
||||
{
|
||||
Name = request.Name,
|
||||
Image = request.Image,
|
||||
Url = request.Url,
|
||||
Sort = request.Sort,
|
||||
Status = (byte)request.Status,
|
||||
CreateTime = now,
|
||||
UpdateTime = now
|
||||
};
|
||||
|
||||
_dbContext.WelfareHouses.Add(entity);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("创建福利屋入口成功: Id={Id}, Name={Name}", entity.Id, entity.Name);
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateWelfareHouseAsync(int id, WelfareHouseUpdateRequest request)
|
||||
{
|
||||
var entity = await _dbContext.WelfareHouses.FirstOrDefaultAsync(w => w.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.NotFound, "福利屋入口不存在");
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
ValidateUpdateRequest(request);
|
||||
|
||||
entity.Name = request.Name;
|
||||
entity.Image = request.Image;
|
||||
entity.Url = request.Url;
|
||||
entity.Sort = request.Sort;
|
||||
entity.Status = (byte)request.Status;
|
||||
entity.UpdateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
|
||||
var result = await _dbContext.SaveChangesAsync() > 0;
|
||||
|
||||
_logger.LogInformation("更新福利屋入口成功: Id={Id}", id);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> DeleteWelfareHouseAsync(int id)
|
||||
{
|
||||
var entity = await _dbContext.WelfareHouses.FirstOrDefaultAsync(w => w.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.NotFound, "福利屋入口不存在");
|
||||
}
|
||||
|
||||
_dbContext.WelfareHouses.Remove(entity);
|
||||
var result = await _dbContext.SaveChangesAsync() > 0;
|
||||
|
||||
_logger.LogInformation("删除福利屋入口成功: Id={Id}", id);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateStatusAsync(int id, WelfareHouseStatusRequest request)
|
||||
{
|
||||
var entity = await _dbContext.WelfareHouses.FirstOrDefaultAsync(w => w.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.NotFound, "福利屋入口不存在");
|
||||
}
|
||||
|
||||
entity.Status = (byte)request.Status;
|
||||
entity.UpdateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
|
||||
var result = await _dbContext.SaveChangesAsync() > 0;
|
||||
|
||||
_logger.LogInformation("更新福利屋入口状态: Id={Id}, Status={Status}", id, request.Status);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#region Private Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// 验证创建请求必填字段
|
||||
/// </summary>
|
||||
private static void ValidateCreateRequest(WelfareHouseCreateRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "名称不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Image))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "图片不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "跳转链接不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证更新请求必填字段
|
||||
/// </summary>
|
||||
private static void ValidateUpdateRequest(WelfareHouseUpdateRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "名称不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Image))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "图片不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
{
|
||||
throw new BusinessException(BusinessErrorCodes.ValidationFailed, "跳转链接不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将实体映射为响应模型
|
||||
/// </summary>
|
||||
private static WelfareHouseResponse MapToResponse(Model.Entities.WelfareHouse entity)
|
||||
{
|
||||
return new WelfareHouseResponse
|
||||
{
|
||||
Id = entity.Id,
|
||||
Name = entity.Name,
|
||||
Image = entity.Image,
|
||||
Url = entity.Url,
|
||||
Sort = entity.Sort,
|
||||
Status = entity.Status,
|
||||
CreateTime = entity.CreateTime.HasValue
|
||||
? DateTimeOffset.FromUnixTimeSeconds(entity.CreateTime.Value).LocalDateTime
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { request, type ApiResponse } from '@/utils/request'
|
||||
|
||||
// ==================== 单页管理类型定义 ====================
|
||||
|
||||
/** 单页列表响应 */
|
||||
export interface DanyeResponse {
|
||||
/** 单页ID */
|
||||
id: number
|
||||
/** 标题 */
|
||||
title: string
|
||||
/** 是否启用图片优化 */
|
||||
isImageOptimizer: boolean
|
||||
/** 更新时间 */
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
/** 单页详情响应 */
|
||||
export interface DanyeDetailResponse {
|
||||
/** 单页ID */
|
||||
id: number
|
||||
/** 标题 */
|
||||
title: string
|
||||
/** 内容(富文本HTML) */
|
||||
content: string
|
||||
/** 是否启用图片优化 */
|
||||
isImageOptimizer: boolean
|
||||
/** 标题是否可编辑(ID 2-20 不可编辑) */
|
||||
isTitleEditable: boolean
|
||||
}
|
||||
|
||||
/** 单页更新请求 */
|
||||
export interface DanyeUpdateRequest {
|
||||
/** 标题(可选,ID 2-20 的单页标题不可编辑) */
|
||||
title?: string
|
||||
/** 内容(富文本HTML) */
|
||||
content: string
|
||||
}
|
||||
|
||||
/** 图片优化切换请求 */
|
||||
export interface ImageOptimizerRequest {
|
||||
/** 是否启用图片优化 */
|
||||
isImageOptimizer: boolean
|
||||
}
|
||||
|
||||
// ==================== API 基础路径 ====================
|
||||
|
||||
const DANYE_BASE_URL = '/admin/business/danye'
|
||||
|
||||
// ==================== 单页管理 API ====================
|
||||
|
||||
/**
|
||||
* 获取单页列表
|
||||
* @returns 单页列表
|
||||
*/
|
||||
export function getDanyeList(): Promise<ApiResponse<DanyeResponse[]>> {
|
||||
return request({
|
||||
url: DANYE_BASE_URL,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单页详情
|
||||
* @param id 单页ID
|
||||
* @returns 单页详情
|
||||
*/
|
||||
export function getDanyeById(id: number): Promise<ApiResponse<DanyeDetailResponse>> {
|
||||
return request({
|
||||
url: `${DANYE_BASE_URL}/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单页内容
|
||||
* @param id 单页ID
|
||||
* @param data 更新请求数据
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function updateDanye(id: number, data: DanyeUpdateRequest): Promise<ApiResponse<string>> {
|
||||
return request({
|
||||
url: `${DANYE_BASE_URL}/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换图片优化状态
|
||||
* @param id 单页ID
|
||||
* @param data 图片优化请求数据
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function toggleImageOptimizer(id: number, data: ImageOptimizerRequest): Promise<ApiResponse<string>> {
|
||||
return request({
|
||||
url: `${DANYE_BASE_URL}/${id}/image-optimizer`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import { request, type ApiResponse, type PagedResult } from '@/utils/request'
|
||||
|
||||
// ==================== 悬浮球类型枚举 ====================
|
||||
|
||||
/** 悬浮球类型枚举 */
|
||||
export enum FloatBallType {
|
||||
/** 展示图片 */
|
||||
ShowImage = 1,
|
||||
/** 跳转页面 */
|
||||
JumpPage = 2
|
||||
}
|
||||
|
||||
/** 悬浮球类型标签映射 */
|
||||
export const FloatBallTypeLabels: Record<number, string> = {
|
||||
[FloatBallType.ShowImage]: '展示图片',
|
||||
[FloatBallType.JumpPage]: '跳转页面'
|
||||
}
|
||||
|
||||
/** 悬浮球特效枚举 */
|
||||
export enum FloatBallEffect {
|
||||
/** 无特效 */
|
||||
None = 0,
|
||||
/** 缩放动画 */
|
||||
Scale = 1
|
||||
}
|
||||
|
||||
/** 悬浮球特效标签映射 */
|
||||
export const FloatBallEffectLabels: Record<number, string> = {
|
||||
[FloatBallEffect.None]: '无特效',
|
||||
[FloatBallEffect.Scale]: '缩放动画'
|
||||
}
|
||||
|
||||
// ==================== 悬浮球类型定义 ====================
|
||||
|
||||
/** 悬浮球列表查询请求 */
|
||||
export interface FloatBallListRequest {
|
||||
/** 页码 */
|
||||
page: number
|
||||
/** 每页数量 */
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
/** 悬浮球响应 */
|
||||
export interface FloatBallResponse {
|
||||
/** 主键ID */
|
||||
id: number
|
||||
/** 标题 */
|
||||
title?: string
|
||||
/** 类型: 1展示图片 2跳转页面 */
|
||||
type: number
|
||||
/** 悬浮球图片URL */
|
||||
image: string
|
||||
/** 背景图片URL */
|
||||
imageBj?: string
|
||||
/** 详情图片URL */
|
||||
imageDetails?: string
|
||||
/** 跳转链接 */
|
||||
linkUrl: string
|
||||
/** X轴位置 */
|
||||
positionX: string
|
||||
/** Y轴位置 */
|
||||
positionY: string
|
||||
/** 宽度 */
|
||||
width: string
|
||||
/** 高度 */
|
||||
height: string
|
||||
/** 详情图片X偏移 */
|
||||
imageDetailsX?: string
|
||||
/** 详情图片Y偏移 */
|
||||
imageDetailsY?: string
|
||||
/** 详情图片宽度 */
|
||||
imageDetailsW?: string
|
||||
/** 详情图片高度 */
|
||||
imageDetailsH?: string
|
||||
/** 特效: 0无 1缩放动画 */
|
||||
effect: number
|
||||
/** 状态: 0关闭 1开启 */
|
||||
status: number
|
||||
/** 创建时间 */
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** 悬浮球创建请求 */
|
||||
export interface FloatBallCreateRequest {
|
||||
/** 标题 */
|
||||
title?: string
|
||||
/** 类型: 1展示图片 2跳转页面 */
|
||||
type: number
|
||||
/** 悬浮球图片URL(必填) */
|
||||
image: string
|
||||
/** 背景图片URL */
|
||||
imageBj?: string
|
||||
/** 详情图片URL */
|
||||
imageDetails?: string
|
||||
/** 跳转链接(类型为跳转页面时使用) */
|
||||
linkUrl?: string
|
||||
/** X轴位置(必填) */
|
||||
positionX: string
|
||||
/** Y轴位置(必填) */
|
||||
positionY: string
|
||||
/** 宽度(必填) */
|
||||
width: string
|
||||
/** 高度(必填) */
|
||||
height: string
|
||||
/** 详情图片X偏移 */
|
||||
imageDetailsX?: string
|
||||
/** 详情图片Y偏移 */
|
||||
imageDetailsY?: string
|
||||
/** 详情图片宽度 */
|
||||
imageDetailsW?: string
|
||||
/** 详情图片高度 */
|
||||
imageDetailsH?: string
|
||||
/** 特效: 0无 1缩放动画(必填) */
|
||||
effect: number
|
||||
/** 状态: 0关闭 1开启,默认开启 */
|
||||
status?: number
|
||||
}
|
||||
|
||||
/** 悬浮球更新请求 */
|
||||
export interface FloatBallUpdateRequest {
|
||||
/** 标题 */
|
||||
title?: string
|
||||
/** 类型: 1展示图片 2跳转页面 */
|
||||
type: number
|
||||
/** 悬浮球图片URL(必填) */
|
||||
image: string
|
||||
/** 背景图片URL */
|
||||
imageBj?: string
|
||||
/** 详情图片URL */
|
||||
imageDetails?: string
|
||||
/** 跳转链接(类型为跳转页面时使用) */
|
||||
linkUrl?: string
|
||||
/** X轴位置(必填) */
|
||||
positionX: string
|
||||
/** Y轴位置(必填) */
|
||||
positionY: string
|
||||
/** 宽度(必填) */
|
||||
width: string
|
||||
/** 高度(必填) */
|
||||
height: string
|
||||
/** 详情图片X偏移 */
|
||||
imageDetailsX?: string
|
||||
/** 详情图片Y偏移 */
|
||||
imageDetailsY?: string
|
||||
/** 详情图片宽度 */
|
||||
imageDetailsW?: string
|
||||
/** 详情图片高度 */
|
||||
imageDetailsH?: string
|
||||
/** 特效: 0无 1缩放动画(必填) */
|
||||
effect: number
|
||||
/** 状态: 0关闭 1开启 */
|
||||
status: number
|
||||
}
|
||||
|
||||
/** 悬浮球状态切换请求 */
|
||||
export interface FloatBallStatusRequest {
|
||||
/** 状态: 0关闭 1开启 */
|
||||
status: number
|
||||
}
|
||||
|
||||
// ==================== API 基础路径 ====================
|
||||
|
||||
const FLOATBALL_BASE_URL = '/admin/business/floatball'
|
||||
|
||||
// ==================== 悬浮球配置 API ====================
|
||||
|
||||
/**
|
||||
* 获取悬浮球列表(分页)
|
||||
* @param params 查询参数
|
||||
* @returns 分页悬浮球列表
|
||||
*/
|
||||
export function getFloatBalls(params: FloatBallListRequest): Promise<ApiResponse<PagedResult<FloatBallResponse>>> {
|
||||
return request({
|
||||
url: FLOATBALL_BASE_URL,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取悬浮球详情
|
||||
* @param id 悬浮球ID
|
||||
* @returns 悬浮球详情
|
||||
*/
|
||||
export function getFloatBallById(id: number): Promise<ApiResponse<FloatBallResponse>> {
|
||||
return request({
|
||||
url: `${FLOATBALL_BASE_URL}/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建悬浮球
|
||||
* @param data 创建请求数据
|
||||
* @returns 新悬浮球ID
|
||||
*/
|
||||
export function createFloatBall(data: FloatBallCreateRequest): Promise<ApiResponse<{ id: number }>> {
|
||||
return request({
|
||||
url: FLOATBALL_BASE_URL,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新悬浮球
|
||||
* @param id 悬浮球ID
|
||||
* @param data 更新请求数据
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function updateFloatBall(id: number, data: FloatBallUpdateRequest): Promise<ApiResponse<string>> {
|
||||
return request({
|
||||
url: `${FLOATBALL_BASE_URL}/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除悬浮球
|
||||
* @param id 悬浮球ID
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function deleteFloatBall(id: number): Promise<ApiResponse<string>> {
|
||||
return request({
|
||||
url: `${FLOATBALL_BASE_URL}/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换悬浮球状态
|
||||
* @param id 悬浮球ID
|
||||
* @param data 状态请求数据
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function updateFloatBallStatus(id: number, data: FloatBallStatusRequest): Promise<ApiResponse<string>> {
|
||||
return request({
|
||||
url: `${FLOATBALL_BASE_URL}/${id}/status`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import { request, type ApiResponse, type PagedResult } from '@/utils/request'
|
||||
|
||||
// ==================== 福利屋入口类型定义 ====================
|
||||
|
||||
/** 福利屋入口列表查询请求 */
|
||||
export interface WelfareHouseListRequest {
|
||||
/** 页码 */
|
||||
page: number
|
||||
/** 每页数量 */
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
/** 福利屋入口响应 */
|
||||
export interface WelfareHouseResponse {
|
||||
/** 主键ID */
|
||||
id: number
|
||||
/** 名称 */
|
||||
name: string
|
||||
/** 图片URL */
|
||||
image: string
|
||||
/** 跳转链接 */
|
||||
url: string
|
||||
/** 排序值 */
|
||||
sort: number
|
||||
/** 状态: 0禁用 1启用 */
|
||||
status: number
|
||||
/** 创建时间 */
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/** 福利屋入口创建请求 */
|
||||
export interface WelfareHouseCreateRequest {
|
||||
/** 名称(必填) */
|
||||
name: string
|
||||
/** 图片URL(必填) */
|
||||
image: string
|
||||
/** 跳转链接(必填) */
|
||||
url: string
|
||||
/** 排序值(必填) */
|
||||
sort: number
|
||||
/** 状态: 0禁用 1启用,默认启用 */
|
||||
status?: number
|
||||
}
|
||||
|
||||
/** 福利屋入口更新请求 */
|
||||
export interface WelfareHouseUpdateRequest {
|
||||
/** 名称(必填) */
|
||||
name: string
|
||||
/** 图片URL(必填) */
|
||||
image: string
|
||||
/** 跳转链接(必填) */
|
||||
url: string
|
||||
/** 排序值(必填) */
|
||||
sort: number
|
||||
/** 状态: 0禁用 1启用 */
|
||||
status: number
|
||||
}
|
||||
|
||||
/** 福利屋入口状态切换请求 */
|
||||
export interface WelfareHouseStatusRequest {
|
||||
/** 状态: 0禁用 1启用 */
|
||||
status: number
|
||||
}
|
||||
|
||||
// ==================== API 基础路径 ====================
|
||||
|
||||
const WELFAREHOUSE_BASE_URL = '/admin/business/welfarehouse'
|
||||
|
||||
// ==================== 福利屋入口 API ====================
|
||||
|
||||
/**
|
||||
* 获取福利屋入口列表(分页)
|
||||
* @param params 查询参数
|
||||
* @returns 分页福利屋入口列表
|
||||
*/
|
||||
export function getWelfareHouses(params: WelfareHouseListRequest): Promise<ApiResponse<PagedResult<WelfareHouseResponse>>> {
|
||||
return request({
|
||||
url: WELFAREHOUSE_BASE_URL,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取福利屋入口详情
|
||||
* @param id 福利屋入口ID
|
||||
* @returns 福利屋入口详情
|
||||
*/
|
||||
export function getWelfareHouseById(id: number): Promise<ApiResponse<WelfareHouseResponse>> {
|
||||
return request({
|
||||
url: `${WELFAREHOUSE_BASE_URL}/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建福利屋入口
|
||||
* @param data 创建请求数据
|
||||
* @returns 新福利屋入口ID
|
||||
*/
|
||||
export function createWelfareHouse(data: WelfareHouseCreateRequest): Promise<ApiResponse<{ id: number }>> {
|
||||
return request({
|
||||
url: WELFAREHOUSE_BASE_URL,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新福利屋入口
|
||||
* @param id 福利屋入口ID
|
||||
* @param data 更新请求数据
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function updateWelfareHouse(id: number, data: WelfareHouseUpdateRequest): Promise<ApiResponse<string>> {
|
||||
return request({
|
||||
url: `${WELFAREHOUSE_BASE_URL}/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除福利屋入口
|
||||
* @param id 福利屋入口ID
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function deleteWelfareHouse(id: number): Promise<ApiResponse<string>> {
|
||||
return request({
|
||||
url: `${WELFAREHOUSE_BASE_URL}/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换福利屋入口状态
|
||||
* @param id 福利屋入口ID
|
||||
* @param data 状态请求数据
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function updateWelfareHouseStatus(id: number, data: WelfareHouseStatusRequest): Promise<ApiResponse<string>> {
|
||||
return request({
|
||||
url: `${WELFAREHOUSE_BASE_URL}/${id}/status`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -458,6 +458,47 @@ export const businessRoutes: RouteRecordRaw[] = [
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// 内容管理
|
||||
{
|
||||
path: 'content',
|
||||
redirect: '/business/danye/list',
|
||||
meta: {
|
||||
title: '内容管理',
|
||||
icon: 'Document'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/business/danye/list',
|
||||
name: 'DanyeList',
|
||||
component: () => import('@/views/business/danye/list.vue'),
|
||||
meta: {
|
||||
title: '单页管理',
|
||||
permission: 'danye:list',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/business/floatball/list',
|
||||
name: 'FloatBallList',
|
||||
component: () => import('@/views/business/floatball/list.vue'),
|
||||
meta: {
|
||||
title: '悬浮球配置',
|
||||
permission: 'floatball:list',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/business/welfarehouse/list',
|
||||
name: 'WelfareHouseList',
|
||||
component: () => import('@/views/business/welfarehouse/list.vue'),
|
||||
meta: {
|
||||
title: '福利屋入口',
|
||||
permission: 'welfarehouse:list',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -884,4 +925,63 @@ export const rankPermissions = {
|
|||
delete: 'rank:delete'
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容管理模块路由配置参考
|
||||
*
|
||||
* 后端菜单配置示例:
|
||||
*
|
||||
* 1. 内容管理(目录)
|
||||
* - name: 内容管理
|
||||
* - path: /business/content
|
||||
* - menuType: 1 (目录)
|
||||
* - icon: Document
|
||||
* - sortOrder: 70
|
||||
*
|
||||
* 2. 单页管理(菜单)
|
||||
* - name: 单页管理
|
||||
* - path: /business/danye/list
|
||||
* - component: business/danye/list
|
||||
* - menuType: 2 (菜单)
|
||||
* - permission: danye:list
|
||||
* - sortOrder: 1
|
||||
*
|
||||
* 3. 悬浮球配置(菜单)
|
||||
* - name: 悬浮球配置
|
||||
* - path: /business/floatball/list
|
||||
* - component: business/floatball/list
|
||||
* - menuType: 2 (菜单)
|
||||
* - permission: floatball:list
|
||||
* - sortOrder: 2
|
||||
*
|
||||
* 4. 福利屋入口(菜单)
|
||||
* - name: 福利屋入口
|
||||
* - path: /business/welfarehouse/list
|
||||
* - component: business/welfarehouse/list
|
||||
* - menuType: 2 (菜单)
|
||||
* - permission: welfarehouse:list
|
||||
* - sortOrder: 3
|
||||
*/
|
||||
|
||||
/**
|
||||
* 内容管理模块权限配置
|
||||
* 用于按钮级别权限控制
|
||||
*/
|
||||
export const contentPermissions = {
|
||||
// 单页管理
|
||||
danyeList: 'danye:list',
|
||||
danyeEdit: 'danye:edit',
|
||||
|
||||
// 悬浮球配置
|
||||
floatballList: 'floatball:list',
|
||||
floatballAdd: 'floatball:add',
|
||||
floatballEdit: 'floatball:edit',
|
||||
floatballDelete: 'floatball:delete',
|
||||
|
||||
// 福利屋入口
|
||||
welfareHouseList: 'welfarehouse:list',
|
||||
welfareHouseAdd: 'welfarehouse:add',
|
||||
welfareHouseEdit: 'welfarehouse:edit',
|
||||
welfareHouseDelete: 'welfarehouse:delete'
|
||||
}
|
||||
|
||||
export default businessRoutes
|
||||
|
|
|
|||
|
|
@ -0,0 +1,351 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="编辑单页"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input
|
||||
v-model="formData.title"
|
||||
placeholder="请输入标题"
|
||||
maxlength="100"
|
||||
:disabled="!isTitleEditable"
|
||||
/>
|
||||
<div v-if="!isTitleEditable" class="form-tip">
|
||||
系统预设单页标题不可编辑
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="内容" prop="content">
|
||||
<div class="editor-container">
|
||||
<!-- 简易富文本编辑器 - 使用 textarea 支持 HTML -->
|
||||
<div class="editor-toolbar">
|
||||
<el-button-group>
|
||||
<el-button size="small" @click="insertTag('b')">
|
||||
<strong>B</strong>
|
||||
</el-button>
|
||||
<el-button size="small" @click="insertTag('i')">
|
||||
<em>I</em>
|
||||
</el-button>
|
||||
<el-button size="small" @click="insertTag('u')">
|
||||
<u>U</u>
|
||||
</el-button>
|
||||
<el-button size="small" @click="insertTag('p')">
|
||||
段落
|
||||
</el-button>
|
||||
<el-button size="small" @click="insertTag('br', true)">
|
||||
换行
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-button-group style="margin-left: 8px;">
|
||||
<el-button size="small" @click="insertImage">
|
||||
<el-icon><Picture /></el-icon> 图片
|
||||
</el-button>
|
||||
<el-button size="small" @click="insertLink">
|
||||
<el-icon><Link /></el-icon> 链接
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-button-group style="margin-left: 8px;">
|
||||
<el-button
|
||||
size="small"
|
||||
:type="previewMode ? 'primary' : 'default'"
|
||||
@click="togglePreview"
|
||||
>
|
||||
{{ previewMode ? '编辑' : '预览' }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域 -->
|
||||
<el-input
|
||||
v-if="!previewMode"
|
||||
ref="textareaRef"
|
||||
v-model="formData.content"
|
||||
type="textarea"
|
||||
:rows="15"
|
||||
placeholder="请输入内容(支持HTML格式)"
|
||||
class="editor-textarea"
|
||||
/>
|
||||
|
||||
<!-- 预览区域 -->
|
||||
<div
|
||||
v-else
|
||||
class="editor-preview"
|
||||
v-html="formData.content"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Picture, Link } from '@element-plus/icons-vue'
|
||||
import {
|
||||
updateDanye,
|
||||
type DanyeDetailResponse,
|
||||
type DanyeUpdateRequest
|
||||
} from '@/api/business/danye'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
danye: DanyeDetailResponse | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
// 弹窗显示状态
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>()
|
||||
const textareaRef = ref()
|
||||
const submitLoading = ref(false)
|
||||
const previewMode = ref(false)
|
||||
|
||||
// 标题是否可编辑
|
||||
const isTitleEditable = computed(() => {
|
||||
return props.danye?.isTitleEditable ?? true
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入标题', trigger: 'blur' },
|
||||
{ max: 100, message: '标题不能超过100个字符', trigger: 'blur' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: '请输入内容', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听弹窗打开,初始化数据
|
||||
watch(() => props.modelValue, (visible) => {
|
||||
if (visible && props.danye) {
|
||||
formData.title = props.danye.title
|
||||
formData.content = props.danye.content || ''
|
||||
previewMode.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 插入HTML标签
|
||||
const insertTag = (tag: string, selfClosing = false) => {
|
||||
if (previewMode.value) return
|
||||
|
||||
const textarea = textareaRef.value?.$el?.querySelector('textarea')
|
||||
if (!textarea) return
|
||||
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
const selectedText = formData.content.substring(start, end)
|
||||
|
||||
let insertText: string
|
||||
if (selfClosing) {
|
||||
insertText = `<${tag} />`
|
||||
} else {
|
||||
insertText = `<${tag}>${selectedText}</${tag}>`
|
||||
}
|
||||
|
||||
formData.content =
|
||||
formData.content.substring(0, start) +
|
||||
insertText +
|
||||
formData.content.substring(end)
|
||||
|
||||
// 恢复光标位置
|
||||
nextTick(() => {
|
||||
const newPos = selfClosing
|
||||
? start + insertText.length
|
||||
: start + tag.length + 2 + selectedText.length
|
||||
textarea.setSelectionRange(newPos, newPos)
|
||||
textarea.focus()
|
||||
})
|
||||
}
|
||||
|
||||
// 插入图片
|
||||
const insertImage = async () => {
|
||||
if (previewMode.value) return
|
||||
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('请输入图片URL', '插入图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: 'https://example.com/image.jpg'
|
||||
})
|
||||
|
||||
if (value) {
|
||||
const imgTag = `<img src="${value}" alt="图片" style="max-width: 100%;" />`
|
||||
insertAtCursor(imgTag)
|
||||
}
|
||||
} catch {
|
||||
// 取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 插入链接
|
||||
const insertLink = async () => {
|
||||
if (previewMode.value) return
|
||||
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('请输入链接URL', '插入链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: 'https://example.com'
|
||||
})
|
||||
|
||||
if (value) {
|
||||
const textarea = textareaRef.value?.$el?.querySelector('textarea')
|
||||
const selectedText = textarea
|
||||
? formData.content.substring(textarea.selectionStart, textarea.selectionEnd) || '链接文字'
|
||||
: '链接文字'
|
||||
const linkTag = `<a href="${value}" target="_blank">${selectedText}</a>`
|
||||
insertAtCursor(linkTag)
|
||||
}
|
||||
} catch {
|
||||
// 取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 在光标位置插入文本
|
||||
const insertAtCursor = (text: string) => {
|
||||
const textarea = textareaRef.value?.$el?.querySelector('textarea')
|
||||
if (!textarea) {
|
||||
formData.content += text
|
||||
return
|
||||
}
|
||||
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
|
||||
formData.content =
|
||||
formData.content.substring(0, start) +
|
||||
text +
|
||||
formData.content.substring(end)
|
||||
|
||||
nextTick(() => {
|
||||
const newPos = start + text.length
|
||||
textarea.setSelectionRange(newPos, newPos)
|
||||
textarea.focus()
|
||||
})
|
||||
}
|
||||
|
||||
// 切换预览模式
|
||||
const togglePreview = () => {
|
||||
previewMode.value = !previewMode.value
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.title = ''
|
||||
formData.content = ''
|
||||
previewMode.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value || !props.danye) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const submitData: DanyeUpdateRequest = {
|
||||
content: formData.content
|
||||
}
|
||||
|
||||
// 只有标题可编辑时才提交标题
|
||||
if (isTitleEditable.value) {
|
||||
submitData.title = formData.title
|
||||
}
|
||||
|
||||
await updateDanye(props.danye.id, submitData)
|
||||
ElMessage.success('保存成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-container {
|
||||
width: 100%;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
padding: 8px;
|
||||
background: #f5f7fa;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.editor-textarea :deep(.el-textarea__inner) {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
min-height: 300px !important;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.editor-preview {
|
||||
min-height: 300px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.editor-preview :deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div class="danye-table">
|
||||
<el-table :data="data" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
|
||||
<el-table-column prop="title" label="标题" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="图片优化" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.isImageOptimizer"
|
||||
@change="(val: boolean) => handleToggleOptimizer(row, val)"
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
inline-prompt
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="更新时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.updateTime || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DanyeResponse } from '@/api/business/danye'
|
||||
|
||||
interface Props {
|
||||
data: DanyeResponse[]
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'edit', row: DanyeResponse): void
|
||||
(e: 'toggle-optimizer', row: DanyeResponse, value: boolean): void
|
||||
}>()
|
||||
|
||||
const handleEdit = (row: DanyeResponse) => {
|
||||
emit('edit', row)
|
||||
}
|
||||
|
||||
const handleToggleOptimizer = (row: DanyeResponse, value: boolean) => {
|
||||
emit('toggle-optimizer', row, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.danye-table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>单页管理</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 单页表格 -->
|
||||
<DanyeTable
|
||||
:data="danyeList"
|
||||
:loading="loading"
|
||||
@edit="handleEdit"
|
||||
@toggle-optimizer="handleToggleOptimizer"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<DanyeFormDialog
|
||||
v-model="formDialogVisible"
|
||||
:danye="currentDanye"
|
||||
@success="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import DanyeTable from './components/DanyeTable.vue'
|
||||
import DanyeFormDialog from './components/DanyeFormDialog.vue'
|
||||
import {
|
||||
getDanyeList,
|
||||
getDanyeById,
|
||||
toggleImageOptimizer,
|
||||
type DanyeResponse,
|
||||
type DanyeDetailResponse
|
||||
} from '@/api/business/danye'
|
||||
|
||||
// 列表数据
|
||||
const loading = ref(false)
|
||||
const danyeList = ref<DanyeResponse[]>([])
|
||||
|
||||
// 弹窗控制
|
||||
const formDialogVisible = ref(false)
|
||||
const currentDanye = ref<DanyeDetailResponse | null>(null)
|
||||
|
||||
// 获取单页列表
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDanyeList()
|
||||
danyeList.value = res.data
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑单页
|
||||
const handleEdit = async (row: DanyeResponse) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await getDanyeById(row.id)
|
||||
currentDanye.value = res.data
|
||||
formDialogVisible.value = true
|
||||
} catch {
|
||||
ElMessage.error('获取单页详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换图片优化状态
|
||||
const handleToggleOptimizer = async (row: DanyeResponse, value: boolean) => {
|
||||
try {
|
||||
await toggleImageOptimizer(row.id, { isImageOptimizer: value })
|
||||
ElMessage.success('状态更新成功')
|
||||
// 更新本地数据
|
||||
const item = danyeList.value.find(d => d.id === row.id)
|
||||
if (item) {
|
||||
item.isImageOptimizer = value
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('状态更新失败')
|
||||
// 恢复原状态
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,478 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑悬浮球' : '新增悬浮球'"
|
||||
width="650px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="110px"
|
||||
>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input
|
||||
v-model="formData.title"
|
||||
placeholder="请输入标题(可选)"
|
||||
maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-radio-group v-model="formData.type">
|
||||
<el-radio :value="FloatBallType.ShowImage">展示图片</el-radio>
|
||||
<el-radio :value="FloatBallType.JumpPage">跳转页面</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="悬浮球图片" prop="image">
|
||||
<div class="image-upload-container">
|
||||
<el-input
|
||||
v-model="formData.image"
|
||||
placeholder="请输入图片URL"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<el-image
|
||||
v-if="formData.image"
|
||||
:src="formData.image"
|
||||
fit="cover"
|
||||
class="preview-image"
|
||||
:preview-src-list="[formData.image]"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="背景图片" prop="imageBj">
|
||||
<div class="image-upload-container">
|
||||
<el-input
|
||||
v-model="formData.imageBj"
|
||||
placeholder="请输入背景图片URL(可选)"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<el-image
|
||||
v-if="formData.imageBj"
|
||||
:src="formData.imageBj"
|
||||
fit="cover"
|
||||
class="preview-image"
|
||||
:preview-src-list="[formData.imageBj]"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="详情图片" prop="imageDetails">
|
||||
<div class="image-upload-container">
|
||||
<el-input
|
||||
v-model="formData.imageDetails"
|
||||
placeholder="请输入详情图片URL(可选)"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<el-image
|
||||
v-if="formData.imageDetails"
|
||||
:src="formData.imageDetails"
|
||||
fit="cover"
|
||||
class="preview-image"
|
||||
:preview-src-list="[formData.imageDetails]"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 条件显示:跳转链接(类型为跳转页面时显示) -->
|
||||
<el-form-item
|
||||
v-if="formData.type === FloatBallType.JumpPage"
|
||||
label="跳转链接"
|
||||
prop="linkUrl"
|
||||
>
|
||||
<el-input
|
||||
v-model="formData.linkUrl"
|
||||
placeholder="请输入跳转链接"
|
||||
maxlength="500"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">位置设置</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="X轴位置" prop="positionX">
|
||||
<el-input
|
||||
v-model="formData.positionX"
|
||||
placeholder="请输入X轴位置"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Y轴位置" prop="positionY">
|
||||
<el-input
|
||||
v-model="formData.positionY"
|
||||
placeholder="请输入Y轴位置"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">尺寸设置</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="宽度" prop="width">
|
||||
<el-input
|
||||
v-model="formData.width"
|
||||
placeholder="请输入宽度"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="高度" prop="height">
|
||||
<el-input
|
||||
v-model="formData.height"
|
||||
placeholder="请输入高度"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">详情图位置和尺寸(可选)</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="详情图X偏移" prop="imageDetailsX">
|
||||
<el-input
|
||||
v-model="formData.imageDetailsX"
|
||||
placeholder="请输入X偏移"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="详情图Y偏移" prop="imageDetailsY">
|
||||
<el-input
|
||||
v-model="formData.imageDetailsY"
|
||||
placeholder="请输入Y偏移"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="详情图宽度" prop="imageDetailsW">
|
||||
<el-input
|
||||
v-model="formData.imageDetailsW"
|
||||
placeholder="请输入宽度"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="详情图高度" prop="imageDetailsH">
|
||||
<el-input
|
||||
v-model="formData.imageDetailsH"
|
||||
placeholder="请输入高度"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">其他设置</el-divider>
|
||||
|
||||
<el-form-item label="特效" prop="effect">
|
||||
<el-select v-model="formData.effect" placeholder="请选择特效" style="width: 100%">
|
||||
<el-option
|
||||
v-for="(label, value) in FloatBallEffectLabels"
|
||||
:key="value"
|
||||
:label="label"
|
||||
:value="Number(value)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch
|
||||
v-model="formData.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
inline-prompt
|
||||
active-text="开启"
|
||||
inactive-text="关闭"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import {
|
||||
createFloatBall,
|
||||
updateFloatBall,
|
||||
FloatBallType,
|
||||
FloatBallEffect,
|
||||
FloatBallEffectLabels,
|
||||
type FloatBallResponse,
|
||||
type FloatBallCreateRequest
|
||||
} from '@/api/business/floatball'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
floatBall: FloatBallResponse | null
|
||||
isEdit: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
// 弹窗显示状态
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
interface FormDataType {
|
||||
title: string
|
||||
type: number
|
||||
image: string
|
||||
imageBj: string
|
||||
imageDetails: string
|
||||
linkUrl: string
|
||||
positionX: string
|
||||
positionY: string
|
||||
width: string
|
||||
height: string
|
||||
imageDetailsX: string
|
||||
imageDetailsY: string
|
||||
imageDetailsW: string
|
||||
imageDetailsH: string
|
||||
effect: number
|
||||
status: number
|
||||
}
|
||||
|
||||
const formData = reactive<FormDataType>({
|
||||
title: '',
|
||||
type: FloatBallType.ShowImage,
|
||||
image: '',
|
||||
imageBj: '',
|
||||
imageDetails: '',
|
||||
linkUrl: '',
|
||||
positionX: '',
|
||||
positionY: '',
|
||||
width: '',
|
||||
height: '',
|
||||
imageDetailsX: '',
|
||||
imageDetailsY: '',
|
||||
imageDetailsW: '',
|
||||
imageDetailsH: '',
|
||||
effect: FloatBallEffect.None,
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed<FormRules>(() => ({
|
||||
type: [
|
||||
{ required: true, message: '请选择类型', trigger: 'change' }
|
||||
],
|
||||
image: [
|
||||
{ required: true, message: '请输入悬浮球图片URL', trigger: 'blur' }
|
||||
],
|
||||
positionX: [
|
||||
{ required: true, message: '请输入X轴位置', trigger: 'blur' }
|
||||
],
|
||||
positionY: [
|
||||
{ required: true, message: '请输入Y轴位置', trigger: 'blur' }
|
||||
],
|
||||
width: [
|
||||
{ required: true, message: '请输入宽度', trigger: 'blur' }
|
||||
],
|
||||
height: [
|
||||
{ required: true, message: '请输入高度', trigger: 'blur' }
|
||||
],
|
||||
effect: [
|
||||
{ required: true, message: '请选择特效', trigger: 'change' }
|
||||
],
|
||||
linkUrl: formData.type === FloatBallType.JumpPage
|
||||
? [{ required: true, message: '请输入跳转链接', trigger: 'blur' }]
|
||||
: []
|
||||
}))
|
||||
|
||||
// 监听弹窗打开,初始化数据
|
||||
watch(() => props.modelValue, (visible) => {
|
||||
if (visible) {
|
||||
if (props.isEdit && props.floatBall) {
|
||||
// 编辑模式:回显数据
|
||||
Object.assign(formData, {
|
||||
title: props.floatBall.title || '',
|
||||
type: props.floatBall.type,
|
||||
image: props.floatBall.image,
|
||||
imageBj: props.floatBall.imageBj || '',
|
||||
imageDetails: props.floatBall.imageDetails || '',
|
||||
linkUrl: props.floatBall.linkUrl || '',
|
||||
positionX: props.floatBall.positionX,
|
||||
positionY: props.floatBall.positionY,
|
||||
width: props.floatBall.width,
|
||||
height: props.floatBall.height,
|
||||
imageDetailsX: props.floatBall.imageDetailsX || '',
|
||||
imageDetailsY: props.floatBall.imageDetailsY || '',
|
||||
imageDetailsW: props.floatBall.imageDetailsW || '',
|
||||
imageDetailsH: props.floatBall.imageDetailsH || '',
|
||||
effect: props.floatBall.effect,
|
||||
status: props.floatBall.status
|
||||
})
|
||||
} else {
|
||||
// 新增模式:重置数据
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听类型变化,清空跳转链接
|
||||
watch(() => formData.type, (newType) => {
|
||||
if (newType !== FloatBallType.JumpPage) {
|
||||
formData.linkUrl = ''
|
||||
}
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
title: '',
|
||||
type: FloatBallType.ShowImage,
|
||||
image: '',
|
||||
imageBj: '',
|
||||
imageDetails: '',
|
||||
linkUrl: '',
|
||||
positionX: '',
|
||||
positionY: '',
|
||||
width: '',
|
||||
height: '',
|
||||
imageDetailsX: '',
|
||||
imageDetailsY: '',
|
||||
imageDetailsW: '',
|
||||
imageDetailsH: '',
|
||||
effect: FloatBallEffect.None,
|
||||
status: 1
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const submitData: FloatBallCreateRequest = {
|
||||
title: formData.title || undefined,
|
||||
type: formData.type,
|
||||
image: formData.image,
|
||||
imageBj: formData.imageBj || undefined,
|
||||
imageDetails: formData.imageDetails || undefined,
|
||||
linkUrl: formData.type === FloatBallType.JumpPage ? formData.linkUrl : undefined,
|
||||
positionX: formData.positionX,
|
||||
positionY: formData.positionY,
|
||||
width: formData.width,
|
||||
height: formData.height,
|
||||
imageDetailsX: formData.imageDetailsX || undefined,
|
||||
imageDetailsY: formData.imageDetailsY || undefined,
|
||||
imageDetailsW: formData.imageDetailsW || undefined,
|
||||
imageDetailsH: formData.imageDetailsH || undefined,
|
||||
effect: formData.effect,
|
||||
status: formData.status
|
||||
}
|
||||
|
||||
if (props.isEdit && props.floatBall) {
|
||||
await updateFloatBall(props.floatBall.id, {
|
||||
...submitData,
|
||||
status: formData.status
|
||||
})
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createFloatBall(submitData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
handleClose()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-upload-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(.el-divider__text) {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
<template>
|
||||
<div class="floatball-table">
|
||||
<el-table :data="data" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
|
||||
<el-table-column prop="title" label="标题" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.title || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="悬浮球图片" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.image"
|
||||
:src="row.image"
|
||||
:preview-src-list="[row.image]"
|
||||
fit="cover"
|
||||
class="table-image"
|
||||
preview-teleported
|
||||
lazy
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<span v-else class="no-image">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="背景图" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.imageBj"
|
||||
:src="row.imageBj"
|
||||
:preview-src-list="[row.imageBj]"
|
||||
fit="cover"
|
||||
class="table-image"
|
||||
preview-teleported
|
||||
lazy
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<span v-else class="no-image">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="详情图" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.imageDetails"
|
||||
:src="row.imageDetails"
|
||||
:preview-src-list="[row.imageDetails]"
|
||||
fit="cover"
|
||||
class="table-image"
|
||||
preview-teleported
|
||||
lazy
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<span v-else class="no-image">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === FloatBallType.ShowImage ? 'info' : 'primary'" size="small">
|
||||
{{ FloatBallTypeLabels[row.type] || '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="跳转链接" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.type === FloatBallType.JumpPage && row.linkUrl" class="url-text">
|
||||
{{ row.linkUrl }}
|
||||
</span>
|
||||
<span v-else class="no-image">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="位置" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.positionX }}, {{ row.positionY }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="尺寸" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.width }} × {{ row.height }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="特效" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.effect === FloatBallEffect.Scale ? 'success' : 'info'" size="small">
|
||||
{{ FloatBallEffectLabels[row.effect] || '无特效' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.status === 1"
|
||||
@change="(val: boolean) => handleStatusChange(row, val ? 1 : 0)"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="创建时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.createdAt || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="130" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import {
|
||||
FloatBallType,
|
||||
FloatBallTypeLabels,
|
||||
FloatBallEffect,
|
||||
FloatBallEffectLabels,
|
||||
type FloatBallResponse
|
||||
} from '@/api/business/floatball'
|
||||
|
||||
interface Props {
|
||||
data: FloatBallResponse[]
|
||||
loading: boolean
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'page-change', page: number): void
|
||||
(e: 'size-change', size: number): void
|
||||
(e: 'edit', row: FloatBallResponse): void
|
||||
(e: 'delete', row: FloatBallResponse): void
|
||||
(e: 'status-change', row: FloatBallResponse, status: number): void
|
||||
}>()
|
||||
|
||||
const currentPage = ref(props.page)
|
||||
const currentPageSize = ref(props.pageSize)
|
||||
|
||||
watch(() => props.page, (val) => {
|
||||
currentPage.value = val
|
||||
})
|
||||
|
||||
watch(() => props.pageSize, (val) => {
|
||||
currentPageSize.value = val
|
||||
})
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
emit('page-change', page)
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
emit('size-change', size)
|
||||
}
|
||||
|
||||
const handleEdit = (row: FloatBallResponse) => {
|
||||
emit('edit', row)
|
||||
}
|
||||
|
||||
const handleDelete = (row: FloatBallResponse) => {
|
||||
emit('delete', row)
|
||||
}
|
||||
|
||||
const handleStatusChange = (row: FloatBallResponse, status: number) => {
|
||||
emit('status-change', row, status)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.floatball-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.no-image {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.url-text {
|
||||
color: #409eff;
|
||||
word-break: break-all;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>悬浮球配置</span>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增悬浮球
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 悬浮球表格 -->
|
||||
<FloatBallTable
|
||||
:data="floatBallList"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="queryParams.page"
|
||||
:page-size="queryParams.pageSize"
|
||||
@page-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
@status-change="handleStatusChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<FloatBallFormDialog
|
||||
v-model="formDialogVisible"
|
||||
:float-ball="currentFloatBall"
|
||||
:is-edit="isEdit"
|
||||
@success="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import FloatBallTable from './components/FloatBallTable.vue'
|
||||
import FloatBallFormDialog from './components/FloatBallFormDialog.vue'
|
||||
import {
|
||||
getFloatBalls,
|
||||
deleteFloatBall,
|
||||
updateFloatBallStatus,
|
||||
type FloatBallListRequest,
|
||||
type FloatBallResponse
|
||||
} from '@/api/business/floatball'
|
||||
|
||||
// 列表数据
|
||||
const loading = ref(false)
|
||||
const floatBallList = ref<FloatBallResponse[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive<FloatBallListRequest>({
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
// 弹窗控制
|
||||
const formDialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentFloatBall = ref<FloatBallResponse | null>(null)
|
||||
|
||||
// 获取悬浮球列表
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getFloatBalls(queryParams)
|
||||
floatBallList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handlePageChange = (page: number) => {
|
||||
queryParams.page = page
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
queryParams.pageSize = size
|
||||
queryParams.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 新增悬浮球
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
currentFloatBall.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑悬浮球
|
||||
const handleEdit = (row: FloatBallResponse) => {
|
||||
isEdit.value = true
|
||||
currentFloatBall.value = { ...row }
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除悬浮球
|
||||
const handleDelete = async (row: FloatBallResponse) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除该悬浮球配置吗?删除后不可恢复!`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
await deleteFloatBall(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch {
|
||||
// 取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
const handleStatusChange = async (row: FloatBallResponse, newStatus: number) => {
|
||||
try {
|
||||
await updateFloatBallStatus(row.id, { status: newStatus })
|
||||
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
|
||||
fetchData()
|
||||
} catch {
|
||||
// 恢复原状态
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑福利屋入口' : '新增福利屋入口'"
|
||||
width="550px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入名称"
|
||||
maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片" prop="image">
|
||||
<div class="image-upload-container">
|
||||
<el-input
|
||||
v-model="formData.image"
|
||||
placeholder="请输入图片URL"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<el-image
|
||||
v-if="formData.image"
|
||||
:src="formData.image"
|
||||
fit="cover"
|
||||
class="preview-image"
|
||||
:preview-src-list="[formData.image]"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="跳转链接" prop="url">
|
||||
<el-input
|
||||
v-model="formData.url"
|
||||
placeholder="请输入跳转链接"
|
||||
maxlength="500"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="请输入排序值"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch
|
||||
v-model="formData.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
inline-prompt
|
||||
active-text="开启"
|
||||
inactive-text="关闭"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import {
|
||||
createWelfareHouse,
|
||||
updateWelfareHouse,
|
||||
type WelfareHouseResponse,
|
||||
type WelfareHouseCreateRequest
|
||||
} from '@/api/business/welfarehouse'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
welfareHouse: WelfareHouseResponse | null
|
||||
isEdit: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
// 弹窗显示状态
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
interface FormDataType {
|
||||
name: string
|
||||
image: string
|
||||
url: string
|
||||
sort: number
|
||||
status: number
|
||||
}
|
||||
|
||||
const formData = reactive<FormDataType>({
|
||||
name: '',
|
||||
image: '',
|
||||
url: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: 'blur' }
|
||||
],
|
||||
image: [
|
||||
{ required: true, message: '请输入图片URL', trigger: 'blur' }
|
||||
],
|
||||
url: [
|
||||
{ required: true, message: '请输入跳转链接', trigger: 'blur' }
|
||||
],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序值', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听弹窗打开,初始化数据
|
||||
watch(() => props.modelValue, (visible) => {
|
||||
if (visible) {
|
||||
if (props.isEdit && props.welfareHouse) {
|
||||
// 编辑模式:回显数据
|
||||
Object.assign(formData, {
|
||||
name: props.welfareHouse.name || '',
|
||||
image: props.welfareHouse.image || '',
|
||||
url: props.welfareHouse.url || '',
|
||||
sort: props.welfareHouse.sort || 0,
|
||||
status: props.welfareHouse.status
|
||||
})
|
||||
} else {
|
||||
// 新增模式:重置数据
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
image: '',
|
||||
url: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const submitData: WelfareHouseCreateRequest = {
|
||||
name: formData.name,
|
||||
image: formData.image,
|
||||
url: formData.url,
|
||||
sort: formData.sort,
|
||||
status: formData.status
|
||||
}
|
||||
|
||||
if (props.isEdit && props.welfareHouse) {
|
||||
await updateWelfareHouse(props.welfareHouse.id, {
|
||||
...submitData,
|
||||
status: formData.status
|
||||
})
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createWelfareHouse(submitData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
handleClose()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-upload-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
<template>
|
||||
<div class="welfarehouse-table">
|
||||
<el-table :data="data" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
|
||||
<el-table-column prop="name" label="名称" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="图片" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.image"
|
||||
:src="row.image"
|
||||
:preview-src-list="[row.image]"
|
||||
fit="cover"
|
||||
class="table-image"
|
||||
preview-teleported
|
||||
lazy
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<span v-else class="no-image">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="跳转链接" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.url" class="url-text">{{ row.url }}</span>
|
||||
<span v-else class="no-image">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.status === 1"
|
||||
@change="(val: boolean) => handleStatusChange(row, val ? 1 : 0)"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="创建时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.createTime || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="130" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import type { WelfareHouseResponse } from '@/api/business/welfarehouse'
|
||||
|
||||
interface Props {
|
||||
data: WelfareHouseResponse[]
|
||||
loading: boolean
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'page-change', page: number): void
|
||||
(e: 'size-change', size: number): void
|
||||
(e: 'edit', row: WelfareHouseResponse): void
|
||||
(e: 'delete', row: WelfareHouseResponse): void
|
||||
(e: 'status-change', row: WelfareHouseResponse, status: number): void
|
||||
}>()
|
||||
|
||||
const currentPage = ref(props.page)
|
||||
const currentPageSize = ref(props.pageSize)
|
||||
|
||||
watch(() => props.page, (val) => {
|
||||
currentPage.value = val
|
||||
})
|
||||
|
||||
watch(() => props.pageSize, (val) => {
|
||||
currentPageSize.value = val
|
||||
})
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
emit('page-change', page)
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
emit('size-change', size)
|
||||
}
|
||||
|
||||
const handleEdit = (row: WelfareHouseResponse) => {
|
||||
emit('edit', row)
|
||||
}
|
||||
|
||||
const handleDelete = (row: WelfareHouseResponse) => {
|
||||
emit('delete', row)
|
||||
}
|
||||
|
||||
const handleStatusChange = (row: WelfareHouseResponse, status: number) => {
|
||||
emit('status-change', row, status)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.welfarehouse-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.no-image {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.url-text {
|
||||
color: #409eff;
|
||||
word-break: break-all;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>福利屋入口</span>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增入口
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 福利屋表格 -->
|
||||
<WelfareHouseTable
|
||||
:data="welfareHouseList"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="queryParams.page"
|
||||
:page-size="queryParams.pageSize"
|
||||
@page-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
@status-change="handleStatusChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<WelfareHouseFormDialog
|
||||
v-model="formDialogVisible"
|
||||
:welfare-house="currentWelfareHouse"
|
||||
:is-edit="isEdit"
|
||||
@success="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import WelfareHouseTable from './components/WelfareHouseTable.vue'
|
||||
import WelfareHouseFormDialog from './components/WelfareHouseFormDialog.vue'
|
||||
import {
|
||||
getWelfareHouses,
|
||||
deleteWelfareHouse,
|
||||
updateWelfareHouseStatus,
|
||||
type WelfareHouseListRequest,
|
||||
type WelfareHouseResponse
|
||||
} from '@/api/business/welfarehouse'
|
||||
|
||||
// 列表数据
|
||||
const loading = ref(false)
|
||||
const welfareHouseList = ref<WelfareHouseResponse[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive<WelfareHouseListRequest>({
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
// 弹窗控制
|
||||
const formDialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentWelfareHouse = ref<WelfareHouseResponse | null>(null)
|
||||
|
||||
// 获取福利屋列表
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getWelfareHouses(queryParams)
|
||||
welfareHouseList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handlePageChange = (page: number) => {
|
||||
queryParams.page = page
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
queryParams.pageSize = size
|
||||
queryParams.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 新增福利屋入口
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
currentWelfareHouse.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑福利屋入口
|
||||
const handleEdit = (row: WelfareHouseResponse) => {
|
||||
isEdit.value = true
|
||||
currentWelfareHouse.value = { ...row }
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除福利屋入口
|
||||
const handleDelete = async (row: WelfareHouseResponse) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除福利屋入口"${row.name}"吗?删除后不可恢复!`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
await deleteWelfareHouse(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch {
|
||||
// 取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
const handleStatusChange = async (row: WelfareHouseResponse, newStatus: number) => {
|
||||
try {
|
||||
await updateWelfareHouseStatus(row.id, { status: newStatus })
|
||||
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
|
||||
fetchData()
|
||||
} catch {
|
||||
// 恢复原状态
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user