其他管理

This commit is contained in:
gpu 2026-01-18 11:18:09 +08:00
parent 5ede4b87a7
commit 5242fae158
30 changed files with 7001 additions and 0 deletions

View 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`

View 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

View 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`

View 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'内容与辅助模块菜单初始化完成';

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
}

View File

@ -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
})
}

View File

@ -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
})
}

View File

@ -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
})
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>