feat: 新增公告管理
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建公告输入
|
||||||
|
/// </summary>
|
||||||
|
public class AnnouncementCreateInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 标题
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "标题不能为空")]
|
||||||
|
[StringLength(200, ErrorMessage = "标题不能超过200个字符")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 内容列表
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "内容不能为空")]
|
||||||
|
[MinLength(1, ErrorMessage = "至少需要一条内容")]
|
||||||
|
public List<string> Content { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(500, ErrorMessage = "备注不能超过500个字符")]
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图片url
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(500, ErrorMessage = "图片URL不能超过500个字符")]
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始时间
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "开始时间不能为空")]
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 活动结束时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公告类型
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "公告类型不能为空")]
|
||||||
|
public AnnouncementTypeEnum Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 跳转链接
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(500, ErrorMessage = "跳转链接不能超过500个字符")]
|
||||||
|
public string? Url { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公告 DTO(后台管理使用)
|
||||||
|
/// </summary>
|
||||||
|
public class AnnouncementDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 公告ID
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标题
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 内容列表
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Content { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图片url
|
||||||
|
/// </summary>
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 活动结束时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公告类型
|
||||||
|
/// </summary>
|
||||||
|
public AnnouncementTypeEnum Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 跳转链接
|
||||||
|
/// </summary>
|
||||||
|
public string? Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreationTime { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取公告列表输入
|
||||||
|
/// </summary>
|
||||||
|
public class AnnouncementGetListInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 搜索关键字
|
||||||
|
/// </summary>
|
||||||
|
public string? SearchKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 跳过数量
|
||||||
|
/// </summary>
|
||||||
|
public int SkipCount { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大结果数量
|
||||||
|
/// </summary>
|
||||||
|
public int MaxResultCount { get; set; } = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公告类型
|
||||||
|
/// </summary>
|
||||||
|
public AnnouncementTypeEnum? Type { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新公告输入
|
||||||
|
/// </summary>
|
||||||
|
public class AnnouncementUpdateInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 公告ID
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "公告ID不能为空")]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标题
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "标题不能为空")]
|
||||||
|
[StringLength(200, ErrorMessage = "标题不能超过200个字符")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 内容列表
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "内容不能为空")]
|
||||||
|
[MinLength(1, ErrorMessage = "至少需要一条内容")]
|
||||||
|
public List<string> Content { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(500, ErrorMessage = "备注不能超过500个字符")]
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图片url
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(500, ErrorMessage = "图片URL不能超过500个字符")]
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始时间
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "开始时间不能为空")]
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 活动结束时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公告类型
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "公告类型不能为空")]
|
||||||
|
public AnnouncementTypeEnum Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 跳转链接
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(500, ErrorMessage = "跳转链接不能超过500个字符")]
|
||||||
|
public string? Url { get; set; }
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Volo.Abp.Application.Dtos;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
|
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||||
@@ -8,8 +9,42 @@ namespace Yi.Framework.AiHub.Application.Contracts.IServices;
|
|||||||
public interface IAnnouncementService
|
public interface IAnnouncementService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取公告信息
|
/// 获取公告信息(前端首页使用)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>公告信息</returns>
|
/// <returns>公告信息</returns>
|
||||||
Task<List<AnnouncementLogDto>> GetAsync();
|
Task<List<AnnouncementLogDto>> GetAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取公告列表(后台管理使用)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询参数</param>
|
||||||
|
/// <returns>分页公告列表</returns>
|
||||||
|
Task<PagedResultDto<AnnouncementDto>> GetListAsync(AnnouncementGetListInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据ID获取公告
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">公告ID</param>
|
||||||
|
/// <returns>公告详情</returns>
|
||||||
|
Task<AnnouncementDto> GetByIdAsync(Guid id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建公告
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">创建输入</param>
|
||||||
|
/// <returns>创建的公告</returns>
|
||||||
|
Task<AnnouncementDto> CreateAsync(AnnouncementCreateInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新公告
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">更新输入</param>
|
||||||
|
/// <returns>更新后的公告</returns>
|
||||||
|
Task<AnnouncementDto> UpdateAsync(AnnouncementUpdateInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除公告
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">公告ID</param>
|
||||||
|
Task DeleteAsync(Guid id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
using Mapster;
|
using Mapster;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using SqlSugar;
|
||||||
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Caching;
|
using Volo.Abp.Caching;
|
||||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||||
@@ -31,8 +35,9 @@ public class AnnouncementService : ApplicationService, IAnnouncementService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取公告信息
|
/// 获取公告信息(前端首页使用,允许匿名访问)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
public async Task<List<AnnouncementLogDto>> GetAsync()
|
public async Task<List<AnnouncementLogDto>> GetAsync()
|
||||||
{
|
{
|
||||||
// 使用 GetOrAddAsync 从缓存获取或添加数据,缓存1小时
|
// 使用 GetOrAddAsync 从缓存获取或添加数据,缓存1小时
|
||||||
@@ -48,18 +53,124 @@ public class AnnouncementService : ApplicationService, IAnnouncementService
|
|||||||
return cacheData?.Logs ?? new List<AnnouncementLogDto>();
|
return cacheData?.Logs ?? new List<AnnouncementLogDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取公告列表(后台管理使用)
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = "admin")]
|
||||||
|
[HttpGet("announcement/list")]
|
||||||
|
public async Task<PagedResultDto<AnnouncementDto>> 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<AnnouncementDto>(
|
||||||
|
totalCount,
|
||||||
|
items.Adapt<List<AnnouncementDto>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据ID获取公告
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = "admin")]
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<AnnouncementDto> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
var entity = await _announcementRepository.GetByIdAsync(id);
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
throw new Exception("公告不存在");
|
||||||
|
}
|
||||||
|
return entity.Adapt<AnnouncementDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建公告
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = "admin")]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<AnnouncementDto> CreateAsync(AnnouncementCreateInput input)
|
||||||
|
{
|
||||||
|
var entity = input.Adapt<AnnouncementAggregateRoot>();
|
||||||
|
await _announcementRepository.InsertAsync(entity);
|
||||||
|
|
||||||
|
// 清除缓存
|
||||||
|
await _announcementCache.RemoveAsync(AnnouncementCacheKey);
|
||||||
|
|
||||||
|
return entity.Adapt<AnnouncementDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新公告
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = "admin")]
|
||||||
|
[HttpPut]
|
||||||
|
public async Task<AnnouncementDto> 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<AnnouncementDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除公告
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从数据库加载公告数据
|
/// 从数据库加载公告数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<AnnouncementCacheDto> LoadAnnouncementDataAsync()
|
private async Task<AnnouncementCacheDto> LoadAnnouncementDataAsync()
|
||||||
{
|
{
|
||||||
// 1️⃣ 一次性查出全部公告(不排序)
|
// 一次性查出全部公告(不排序)
|
||||||
var logs = await _announcementRepository._DbQueryable
|
var logs = await _announcementRepository._DbQueryable
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
|
|
||||||
// 2️⃣ 内存中处理排序
|
// 内存中处理排序
|
||||||
var orderedLogs = logs
|
var orderedLogs = logs
|
||||||
.OrderByDescending(x =>
|
.OrderByDescending(x =>
|
||||||
x.StartTime <= now &&
|
x.StartTime <= now &&
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import type { AnnouncementLogDto } from './types';
|
import type {
|
||||||
import { get } from '@/utils/request';
|
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() {
|
export function getSystemAnnouncements() {
|
||||||
return get<AnnouncementLogDto[]>('/announcement').json();
|
return get<AnnouncementLogDto[]>('/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<PagedResultDto<AnnouncementDto>>(url).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据ID获取公告
|
||||||
|
export function getById(id: string) {
|
||||||
|
return get<AnnouncementDto>(`/announcement/${id}`).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建公告
|
||||||
|
export function create(data: AnnouncementCreateInput) {
|
||||||
|
return post<AnnouncementDto>('/announcement', data).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新公告
|
||||||
|
export function update(data: AnnouncementUpdateInput) {
|
||||||
|
return put<AnnouncementDto>('/announcement', data).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除公告
|
||||||
|
export function deleteById(id: string) {
|
||||||
|
return del(`/announcement/${id}`).json();
|
||||||
|
}
|
||||||
|
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
// 公告类型(对应后端 AnnouncementTypeEnum)
|
// 公告类型枚举(对应后端 AnnouncementTypeEnum)
|
||||||
|
export enum AnnouncementTypeEnum {
|
||||||
|
Activity = 1,
|
||||||
|
System = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公告类型(兼容旧代码)
|
||||||
export type AnnouncementType = 'Activity' | 'System'
|
export type AnnouncementType = 'Activity' | 'System'
|
||||||
|
|
||||||
// 公告DTO(对应后端 AnnouncementLogDto)
|
// 公告DTO(对应后端 AnnouncementLogDto)
|
||||||
@@ -16,3 +22,58 @@ export interface AnnouncementLogDto {
|
|||||||
/** 公告类型(系统、活动) */
|
/** 公告类型(系统、活动) */
|
||||||
type: AnnouncementType
|
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<T> {
|
||||||
|
items: T[];
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ export const PAGE_PERMISSIONS: PermissionConfig[] = [
|
|||||||
allowedUsers: ['cc', 'Guo'],
|
allowedUsers: ['cc', 'Guo'],
|
||||||
description: '系统统计页面 - 仅限cc和Guo用户访问',
|
description: '系统统计页面 - 仅限cc和Guo用户访问',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/console/announcement',
|
||||||
|
allowedUsers: ['cc', 'Guo'],
|
||||||
|
description: '公告管理页面 - 仅限cc和Guo用户访问',
|
||||||
|
},
|
||||||
// 可以在这里继续添加其他需要权限控制的页面
|
// 可以在这里继续添加其他需要权限控制的页面
|
||||||
// {
|
// {
|
||||||
// path: '/console/admin',
|
// path: '/console/admin',
|
||||||
|
|||||||
404
Yi.Ai.Vue3/src/pages/console/announcement/index.vue
Normal file
404
Yi.Ai.Vue3/src/pages/console/announcement/index.vue
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AnnouncementDto } from '@/api/announcement/types';
|
||||||
|
import { Delete, Edit, Plus, Refresh } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import {
|
||||||
|
create,
|
||||||
|
deleteById,
|
||||||
|
getList,
|
||||||
|
update,
|
||||||
|
} from '@/api/announcement';
|
||||||
|
import { AnnouncementTypeEnum } from '@/api/announcement/types';
|
||||||
|
|
||||||
|
// ==================== Tab 切换 ====================
|
||||||
|
const activeTab = ref<'activity' | 'system'>('system');
|
||||||
|
|
||||||
|
// Tab 切换时重新加载数据
|
||||||
|
function handleTabChange() {
|
||||||
|
currentPage.value = 1;
|
||||||
|
fetchList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前 Tab 对应的类型枚举值
|
||||||
|
function getCurrentTypeEnum(): AnnouncementTypeEnum {
|
||||||
|
return activeTab.value === 'activity' ? AnnouncementTypeEnum.Activity : AnnouncementTypeEnum.System;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 公告列表管理 ====================
|
||||||
|
const announcementList = ref<AnnouncementDto[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const searchKey = ref('');
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
// 公告对话框
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const dialogTitle = ref('');
|
||||||
|
const form = ref<Partial<AnnouncementDto>>({});
|
||||||
|
|
||||||
|
// 获取公告列表
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getList({
|
||||||
|
searchKey: searchKey.value,
|
||||||
|
skipCount: (currentPage.value - 1) * pageSize.value,
|
||||||
|
maxResultCount: pageSize.value,
|
||||||
|
type: getCurrentTypeEnum(),
|
||||||
|
});
|
||||||
|
announcementList.value = res.data.items;
|
||||||
|
total.value = res.data.totalCount;
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
ElMessage.error(error.message || '获取公告列表失败');
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开对话框
|
||||||
|
function openDialog(type: 'create' | 'edit', row?: AnnouncementDto) {
|
||||||
|
dialogTitle.value = type === 'create' ? '创建公告' : '编辑公告';
|
||||||
|
if (type === 'create') {
|
||||||
|
form.value = {
|
||||||
|
title: '',
|
||||||
|
content: [''],
|
||||||
|
remark: '',
|
||||||
|
imageUrl: '',
|
||||||
|
startTime: new Date().toISOString().slice(0, 19),
|
||||||
|
endTime: '',
|
||||||
|
type: getCurrentTypeEnum(),
|
||||||
|
url: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
form.value = {
|
||||||
|
...row,
|
||||||
|
startTime: row.startTime ? new Date(row.startTime).toISOString().slice(0, 19) : '',
|
||||||
|
endTime: row.endTime ? new Date(row.endTime).toISOString().slice(0, 19) : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加内容项
|
||||||
|
function addContentItem() {
|
||||||
|
if (form.value.content && form.value.content.length < 10) {
|
||||||
|
form.value.content.push('');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ElMessage.warning('最多只能添加10条内容');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除内容项
|
||||||
|
function removeContentItem(index: number) {
|
||||||
|
if (form.value.content && form.value.content.length > 1) {
|
||||||
|
form.value.content.splice(index, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ElMessage.warning('至少需要保留一条内容');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
async function save() {
|
||||||
|
if (!form.value.title || !form.value.content || form.value.content.some(c => !c)) {
|
||||||
|
ElMessage.warning('请填写标题和所有内容项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
...form.value,
|
||||||
|
content: form.value.content?.filter(c => c.trim()) || [],
|
||||||
|
remark: form.value.remark || null,
|
||||||
|
imageUrl: form.value.imageUrl || null,
|
||||||
|
endTime: form.value.endTime || null,
|
||||||
|
url: form.value.url || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (form.value.id) {
|
||||||
|
await update(data as any);
|
||||||
|
ElMessage.success('更新成功');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await create(data as any);
|
||||||
|
ElMessage.success('创建成功');
|
||||||
|
}
|
||||||
|
dialogVisible.value = false;
|
||||||
|
fetchList();
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
ElMessage.error(error.message || '保存失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
async function handleDelete(row: AnnouncementDto) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该公告吗?', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
await deleteById(row.id);
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
fetchList();
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error(error.message || '删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页改变
|
||||||
|
function handleCurrentChange(page: number) {
|
||||||
|
currentPage.value = page;
|
||||||
|
fetchList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSizeChange(size: number) {
|
||||||
|
pageSize.value = size;
|
||||||
|
currentPage.value = 1;
|
||||||
|
fetchList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
fetchList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="announcement-management">
|
||||||
|
<div class="management-container">
|
||||||
|
<!-- Tab 切换 -->
|
||||||
|
<el-tabs v-model="activeTab" class="announcement-tabs" @tab-change="handleTabChange">
|
||||||
|
<el-tab-pane label="系统公告" name="system" />
|
||||||
|
<el-tab-pane label="活动公告" name="activity" />
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKey"
|
||||||
|
placeholder="搜索标题或备注"
|
||||||
|
clearable
|
||||||
|
style="width: 250px; margin-right: 10px"
|
||||||
|
@keyup.enter="fetchList"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button :icon="Refresh" @click="fetchList" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openDialog('create')">
|
||||||
|
新建公告
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="announcementList"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="title" label="标题" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="content" label="内容预览" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.content?.join(' / ') || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.remark || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="startTime" label="开始时间" width="160" />
|
||||||
|
<el-table-column prop="endTime" label="结束时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.endTime || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="creationTime" label="创建时间" width="160" />
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" size="small" @click="openDialog('edit', row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" size="small" @click="handleDelete(row)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
:hide-on-single-page="false"
|
||||||
|
background
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑对话框 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
|
||||||
|
<el-form :model="form" label-width="100px">
|
||||||
|
<el-form-item label="标题" required>
|
||||||
|
<el-input v-model="form.title" placeholder="请输入公告标题" maxlength="200" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="内容" required>
|
||||||
|
<div style="width: 100%">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in form.content"
|
||||||
|
:key="index"
|
||||||
|
style="display: flex; gap: 8px; margin-bottom: 8px"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="form.content![index]"
|
||||||
|
placeholder="请输入内容"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
v-if="form.content && form.content.length > 1"
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
@click="removeContentItem(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
v-if="form.content && form.content.length < 10"
|
||||||
|
type="primary"
|
||||||
|
:icon="Plus"
|
||||||
|
size="small"
|
||||||
|
@click="addContentItem"
|
||||||
|
>
|
||||||
|
添加内容项
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入备注(可选)"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
:rows="2"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="图片URL">
|
||||||
|
<el-input v-model="form.imageUrl" placeholder="请输入图片URL(可选)" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="跳转链接">
|
||||||
|
<el-input v-model="form.url" placeholder="请输入跳转链接(可选)" maxlength="500" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="开始时间" required>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.startTime"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="选择开始时间"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="结束时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.endTime"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="选择结束时间(可选)"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="save">
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.announcement-management {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f5f7fa;
|
||||||
|
|
||||||
|
.management-container {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-tabs {
|
||||||
|
padding: 0 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
:deep(.el-tabs__header) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,6 +21,7 @@ const userName = userStore.userInfo?.user?.userName;
|
|||||||
|
|
||||||
const hasChannelPermission = checkPagePermission('/console/channel', userName);
|
const hasChannelPermission = checkPagePermission('/console/channel', userName);
|
||||||
const hasSystemStatisticsPermission = checkPagePermission('/console/system-statistics', 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' });
|
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 activeNav = computed(() => {
|
||||||
const path = route.path;
|
const path = route.path;
|
||||||
|
|||||||
@@ -231,6 +231,14 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
title: '意心Ai-系统统计',
|
title: '意心Ai-系统统计',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'announcement',
|
||||||
|
name: 'consoleAnnouncement',
|
||||||
|
component: () => import('@/pages/console/announcement/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '意心Ai-公告管理',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
@@ -7,7 +7,6 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_WEB_BASE_API: string;
|
readonly VITE_WEB_BASE_API: string;
|
||||||
readonly VITE_API_URL: string;
|
readonly VITE_API_URL: string;
|
||||||
readonly VITE_FILE_UPLOAD_API: string;
|
readonly VITE_FILE_UPLOAD_API: string;
|
||||||
readonly VITE_BUILD_COMPRESS: string;
|
|
||||||
readonly VITE_SSO_SEVER_URL: string;
|
readonly VITE_SSO_SEVER_URL: string;
|
||||||
readonly VITE_APP_VERSION: string;
|
readonly VITE_APP_VERSION: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user