diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementCreateInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementCreateInput.cs new file mode 100644 index 00000000..31460cbe --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementCreateInput.cs @@ -0,0 +1,59 @@ +using System.ComponentModel.DataAnnotations; +using Yi.Framework.AiHub.Domain.Shared.Enums; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement; + +/// +/// 创建公告输入 +/// +public class AnnouncementCreateInput +{ + /// + /// 标题 + /// + [Required(ErrorMessage = "标题不能为空")] + [StringLength(200, ErrorMessage = "标题不能超过200个字符")] + public string Title { get; set; } + + /// + /// 内容列表 + /// + [Required(ErrorMessage = "内容不能为空")] + [MinLength(1, ErrorMessage = "至少需要一条内容")] + public List Content { get; set; } = new List(); + + /// + /// 备注 + /// + [StringLength(500, ErrorMessage = "备注不能超过500个字符")] + public string? Remark { get; set; } + + /// + /// 图片url + /// + [StringLength(500, ErrorMessage = "图片URL不能超过500个字符")] + public string? ImageUrl { get; set; } + + /// + /// 开始时间 + /// + [Required(ErrorMessage = "开始时间不能为空")] + public DateTime StartTime { get; set; } + + /// + /// 活动结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 公告类型 + /// + [Required(ErrorMessage = "公告类型不能为空")] + public AnnouncementTypeEnum Type { get; set; } + + /// + /// 跳转链接 + /// + [StringLength(500, ErrorMessage = "跳转链接不能超过500个字符")] + public string? Url { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementDto.cs new file mode 100644 index 00000000..625b0f87 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementDto.cs @@ -0,0 +1,59 @@ +using Yi.Framework.AiHub.Domain.Shared.Enums; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement; + +/// +/// 公告 DTO(后台管理使用) +/// +public class AnnouncementDto +{ + /// + /// 公告ID + /// + public Guid Id { get; set; } + + /// + /// 标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 内容列表 + /// + public List Content { get; set; } = new List(); + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 图片url + /// + public string? ImageUrl { get; set; } + + /// + /// 开始时间 + /// + public DateTime StartTime { get; set; } + + /// + /// 活动结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 公告类型 + /// + public AnnouncementTypeEnum Type { get; set; } + + /// + /// 跳转链接 + /// + public string? Url { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreationTime { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementGetListInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementGetListInput.cs new file mode 100644 index 00000000..75b478c3 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementGetListInput.cs @@ -0,0 +1,29 @@ +using Yi.Framework.AiHub.Domain.Shared.Enums; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement; + +/// +/// 获取公告列表输入 +/// +public class AnnouncementGetListInput +{ + /// + /// 搜索关键字 + /// + public string? SearchKey { get; set; } + + /// + /// 跳过数量 + /// + public int SkipCount { get; set; } = 0; + + /// + /// 最大结果数量 + /// + public int MaxResultCount { get; set; } = 10; + + /// + /// 公告类型 + /// + public AnnouncementTypeEnum? Type { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementUpdateInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementUpdateInput.cs new file mode 100644 index 00000000..23b58b1d --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementUpdateInput.cs @@ -0,0 +1,65 @@ +using System.ComponentModel.DataAnnotations; +using Yi.Framework.AiHub.Domain.Shared.Enums; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement; + +/// +/// 更新公告输入 +/// +public class AnnouncementUpdateInput +{ + /// + /// 公告ID + /// + [Required(ErrorMessage = "公告ID不能为空")] + public Guid Id { get; set; } + + /// + /// 标题 + /// + [Required(ErrorMessage = "标题不能为空")] + [StringLength(200, ErrorMessage = "标题不能超过200个字符")] + public string Title { get; set; } + + /// + /// 内容列表 + /// + [Required(ErrorMessage = "内容不能为空")] + [MinLength(1, ErrorMessage = "至少需要一条内容")] + public List Content { get; set; } = new List(); + + /// + /// 备注 + /// + [StringLength(500, ErrorMessage = "备注不能超过500个字符")] + public string? Remark { get; set; } + + /// + /// 图片url + /// + [StringLength(500, ErrorMessage = "图片URL不能超过500个字符")] + public string? ImageUrl { get; set; } + + /// + /// 开始时间 + /// + [Required(ErrorMessage = "开始时间不能为空")] + public DateTime StartTime { get; set; } + + /// + /// 活动结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 公告类型 + /// + [Required(ErrorMessage = "公告类型不能为空")] + public AnnouncementTypeEnum Type { get; set; } + + /// + /// 跳转链接 + /// + [StringLength(500, ErrorMessage = "跳转链接不能超过500个字符")] + public string? Url { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs index acdda549..87cc4dc5 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs @@ -1,3 +1,4 @@ +using Volo.Abp.Application.Dtos; using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement; namespace Yi.Framework.AiHub.Application.Contracts.IServices; @@ -8,8 +9,42 @@ namespace Yi.Framework.AiHub.Application.Contracts.IServices; public interface IAnnouncementService { /// - /// 获取公告信息 + /// 获取公告信息(前端首页使用) /// /// 公告信息 Task> GetAsync(); + + /// + /// 获取公告列表(后台管理使用) + /// + /// 查询参数 + /// 分页公告列表 + Task> GetListAsync(AnnouncementGetListInput input); + + /// + /// 根据ID获取公告 + /// + /// 公告ID + /// 公告详情 + Task GetByIdAsync(Guid id); + + /// + /// 创建公告 + /// + /// 创建输入 + /// 创建的公告 + Task CreateAsync(AnnouncementCreateInput input); + + /// + /// 更新公告 + /// + /// 更新输入 + /// 更新后的公告 + Task UpdateAsync(AnnouncementUpdateInput input); + + /// + /// 删除公告 + /// + /// 公告ID + Task DeleteAsync(Guid id); } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AnnouncementService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AnnouncementService.cs index a1001148..f86a6244 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AnnouncementService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AnnouncementService.cs @@ -1,6 +1,10 @@ using Mapster; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; +using SqlSugar; +using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Caching; using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement; @@ -31,8 +35,9 @@ public class AnnouncementService : ApplicationService, IAnnouncementService } /// - /// 获取公告信息 + /// 获取公告信息(前端首页使用,允许匿名访问) /// + [AllowAnonymous] public async Task> GetAsync() { // 使用 GetOrAddAsync 从缓存获取或添加数据,缓存1小时 @@ -48,18 +53,124 @@ public class AnnouncementService : ApplicationService, IAnnouncementService return cacheData?.Logs ?? new List(); } + /// + /// 获取公告列表(后台管理使用) + /// + [Authorize(Roles = "admin")] + [HttpGet("announcement/list")] + public async Task> GetListAsync(AnnouncementGetListInput input) + { + var query = _announcementRepository._DbQueryable + .WhereIF(!string.IsNullOrWhiteSpace(input.SearchKey), + x => x.Title.Contains(input.SearchKey!) || (x.Remark != null && x.Remark.Contains(input.SearchKey!))) + .WhereIF(input.Type.HasValue, x => x.Type == input.Type!.Value) + .OrderByDescending(x => x.StartTime); + + var totalCount = await query.CountAsync(); + var items = await query + .Skip(input.SkipCount) + .Take(input.MaxResultCount) + .ToListAsync(); + + return new PagedResultDto( + totalCount, + items.Adapt>() + ); + } + + /// + /// 根据ID获取公告 + /// + [Authorize(Roles = "admin")] + [HttpGet("{id}")] + public async Task GetByIdAsync(Guid id) + { + var entity = await _announcementRepository.GetByIdAsync(id); + if (entity == null) + { + throw new Exception("公告不存在"); + } + return entity.Adapt(); + } + + /// + /// 创建公告 + /// + [Authorize(Roles = "admin")] + [HttpPost] + public async Task CreateAsync(AnnouncementCreateInput input) + { + var entity = input.Adapt(); + await _announcementRepository.InsertAsync(entity); + + // 清除缓存 + await _announcementCache.RemoveAsync(AnnouncementCacheKey); + + return entity.Adapt(); + } + + /// + /// 更新公告 + /// + [Authorize(Roles = "admin")] + [HttpPut] + public async Task UpdateAsync(AnnouncementUpdateInput input) + { + var entity = await _announcementRepository.GetByIdAsync(input.Id); + if (entity == null) + { + throw new Exception("公告不存在"); + } + + // 更新字段 + entity.Title = input.Title; + entity.Content = input.Content; + entity.Remark = input.Remark; + entity.ImageUrl = input.ImageUrl; + entity.StartTime = input.StartTime; + entity.EndTime = input.EndTime; + entity.Type = input.Type; + entity.Url = input.Url; + + await _announcementRepository.UpdateAsync(entity); + + // 清除缓存 + await _announcementCache.RemoveAsync(AnnouncementCacheKey); + + return entity.Adapt(); + } + + /// + /// 删除公告 + /// + [Authorize(Roles = "admin")] + [HttpDelete("announcement/{id}")] + public async Task DeleteAsync(Guid id) + { + var entity = await _announcementRepository.GetByIdAsync(id); + if (entity == null) + { + throw new Exception("公告不存在"); + } + + await _announcementRepository.DeleteAsync(entity); + + // 清除缓存 + await _announcementCache.RemoveAsync(AnnouncementCacheKey); + } + /// /// 从数据库加载公告数据 /// private async Task LoadAnnouncementDataAsync() { - // 1️⃣ 一次性查出全部公告(不排序) + // 一次性查出全部公告(不排序) var logs = await _announcementRepository._DbQueryable .ToListAsync(); var now = DateTime.Now; - // 2️⃣ 内存中处理排序 + // 内存中处理排序 var orderedLogs = logs .OrderByDescending(x => x.StartTime <= now && diff --git a/Yi.Ai.Vue3/src/api/announcement/index.ts b/Yi.Ai.Vue3/src/api/announcement/index.ts index a8ae2870..8d86fac7 100644 --- a/Yi.Ai.Vue3/src/api/announcement/index.ts +++ b/Yi.Ai.Vue3/src/api/announcement/index.ts @@ -1,5 +1,14 @@ -import type { AnnouncementLogDto } from './types'; -import { get } from '@/utils/request'; +import type { + AnnouncementLogDto, + AnnouncementDto, + AnnouncementCreateInput, + AnnouncementUpdateInput, + AnnouncementGetListInput, + PagedResultDto, +} from './types'; +import { del, get, post, put } from '@/utils/request'; + +// ==================== 前端首页用 ==================== /** * 获取系统公告和活动数据 @@ -9,4 +18,49 @@ import { get } from '@/utils/request'; export function getSystemAnnouncements() { return get('/announcement').json(); } + +// ==================== 后台管理用 ==================== + +// 获取公告列表 +export function getList(params?: AnnouncementGetListInput) { + const queryParams = new URLSearchParams(); + if (params?.searchKey) { + queryParams.append('SearchKey', params.searchKey); + } + if (params?.skipCount !== undefined) { + queryParams.append('SkipCount', params.skipCount.toString()); + } + if (params?.maxResultCount !== undefined) { + queryParams.append('MaxResultCount', params.maxResultCount.toString()); + } + if (params?.type !== undefined) { + queryParams.append('Type', params.type.toString()); + } + + const queryString = queryParams.toString(); + const url = queryString ? `/announcement/list?${queryString}` : '/announcement/list'; + + return get>(url).json(); +} + +// 根据ID获取公告 +export function getById(id: string) { + return get(`/announcement/${id}`).json(); +} + +// 创建公告 +export function create(data: AnnouncementCreateInput) { + return post('/announcement', data).json(); +} + +// 更新公告 +export function update(data: AnnouncementUpdateInput) { + return put('/announcement', data).json(); +} + +// 删除公告 +export function deleteById(id: string) { + return del(`/announcement/${id}`).json(); +} + export * from './types'; diff --git a/Yi.Ai.Vue3/src/api/announcement/types.ts b/Yi.Ai.Vue3/src/api/announcement/types.ts index c05a030a..beb99136 100644 --- a/Yi.Ai.Vue3/src/api/announcement/types.ts +++ b/Yi.Ai.Vue3/src/api/announcement/types.ts @@ -1,4 +1,10 @@ -// 公告类型(对应后端 AnnouncementTypeEnum) +// 公告类型枚举(对应后端 AnnouncementTypeEnum) +export enum AnnouncementTypeEnum { + Activity = 1, + System = 2, +} + +// 公告类型(兼容旧代码) export type AnnouncementType = 'Activity' | 'System' // 公告DTO(对应后端 AnnouncementLogDto) @@ -16,3 +22,58 @@ export interface AnnouncementLogDto { /** 公告类型(系统、活动) */ type: AnnouncementType } + +// ==================== 后台管理用 DTO ==================== + +// 公告 DTO(后台管理列表) +export interface AnnouncementDto { + id: string; + title: string; + content: string[]; + remark?: string; + imageUrl?: string; + startTime: string; + endTime?: string; + type: AnnouncementTypeEnum; + url?: string; + creationTime: string; +} + +// 创建公告输入 +export interface AnnouncementCreateInput { + title: string; + content: string[]; + remark?: string; + imageUrl?: string; + startTime: string; + endTime?: string; + type: AnnouncementTypeEnum; + url?: string; +} + +// 更新公告输入 +export interface AnnouncementUpdateInput { + id: string; + title: string; + content: string[]; + remark?: string; + imageUrl?: string; + startTime: string; + endTime?: string; + type: AnnouncementTypeEnum; + url?: string; +} + +// 获取公告列表输入 +export interface AnnouncementGetListInput { + searchKey?: string; + skipCount?: number; + maxResultCount?: number; + type?: AnnouncementTypeEnum; +} + +// 分页结果 +export interface PagedResultDto { + items: T[]; + totalCount: number; +} diff --git a/Yi.Ai.Vue3/src/config/permission.ts b/Yi.Ai.Vue3/src/config/permission.ts index e658fe28..7715a034 100644 --- a/Yi.Ai.Vue3/src/config/permission.ts +++ b/Yi.Ai.Vue3/src/config/permission.ts @@ -30,6 +30,11 @@ export const PAGE_PERMISSIONS: PermissionConfig[] = [ allowedUsers: ['cc', 'Guo'], description: '系统统计页面 - 仅限cc和Guo用户访问', }, + { + path: '/console/announcement', + allowedUsers: ['cc', 'Guo'], + description: '公告管理页面 - 仅限cc和Guo用户访问', + }, // 可以在这里继续添加其他需要权限控制的页面 // { // path: '/console/admin', diff --git a/Yi.Ai.Vue3/src/pages/console/announcement/index.vue b/Yi.Ai.Vue3/src/pages/console/announcement/index.vue new file mode 100644 index 00000000..2f72dcb2 --- /dev/null +++ b/Yi.Ai.Vue3/src/pages/console/announcement/index.vue @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + 新建公告 + + + + + + + + + + {{ row.content?.join(' / ') || '-' }} + + + + + {{ row.remark || '-' }} + + + + + + {{ row.endTime || '-' }} + + + + + + + 编辑 + + + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + + 添加内容项 + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + + + 保存 + + + + + + + diff --git a/Yi.Ai.Vue3/src/pages/console/index.vue b/Yi.Ai.Vue3/src/pages/console/index.vue index 21f3fb33..b57e3534 100644 --- a/Yi.Ai.Vue3/src/pages/console/index.vue +++ b/Yi.Ai.Vue3/src/pages/console/index.vue @@ -21,6 +21,7 @@ const userName = userStore.userInfo?.user?.userName; const hasChannelPermission = checkPagePermission('/console/channel', userName); const hasSystemStatisticsPermission = checkPagePermission('/console/system-statistics', userName); +const hasAnnouncementPermission = checkPagePermission('/console/announcement', userName); // 菜单项配置 @@ -47,6 +48,10 @@ if (hasSystemStatisticsPermission) { navItems.push({ name: 'system-statistics', label: '系统统计', icon: 'DataAnalysis', path: '/console/system-statistics' }); } +if (hasAnnouncementPermission) { + navItems.push({ name: 'announcement', label: '公告管理', icon: 'Bell', path: '/console/announcement' }); +} + // 当前激活的菜单 const activeNav = computed(() => { const path = route.path; diff --git a/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts b/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts index d6bfdbde..99d72ee6 100644 --- a/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts +++ b/Yi.Ai.Vue3/src/routers/modules/staticRouter.ts @@ -231,6 +231,14 @@ export const layoutRouter: RouteRecordRaw[] = [ title: '意心Ai-系统统计', }, }, + { + path: 'announcement', + name: 'consoleAnnouncement', + component: () => import('@/pages/console/announcement/index.vue'), + meta: { + title: '意心Ai-公告管理', + }, + }, ], }, ], diff --git a/Yi.Ai.Vue3/types/import_meta.d.ts b/Yi.Ai.Vue3/types/import_meta.d.ts index c98d612e..8f2a798b 100644 --- a/Yi.Ai.Vue3/types/import_meta.d.ts +++ b/Yi.Ai.Vue3/types/import_meta.d.ts @@ -7,7 +7,6 @@ interface ImportMetaEnv { readonly VITE_WEB_BASE_API: string; readonly VITE_API_URL: string; readonly VITE_FILE_UPLOAD_API: string; - readonly VITE_BUILD_COMPRESS: string; readonly VITE_SSO_SEVER_URL: string; readonly VITE_APP_VERSION: string; }