消息修复

This commit is contained in:
2026-07-27 16:06:02 +08:00 Unverified
parent 06d40a7bf1
commit 14ac49115c
4 changed files with 184 additions and 4 deletions
@@ -872,8 +872,8 @@ public sealed class NotificationsController(
} }
public sealed record SendMessageRequest( public sealed record SendMessageRequest(
[property: Required, StringLength(200)] string Title, [Required, StringLength(200)] string Title,
[property: Required, StringLength(20000)] string Content, [Required, StringLength(20000)] string Content,
Guid? TeachingTaskId = null, Guid? TeachingTaskId = null,
MessageRecipientMode RecipientMode = MessageRecipientMode.Scope, MessageRecipientMode RecipientMode = MessageRecipientMode.Scope,
MessageRecipientFilter? RecipientFilter = null, MessageRecipientFilter? RecipientFilter = null,
@@ -3,14 +3,50 @@ using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Jiaowu.Api.Tests; namespace Jiaowu.Api.Tests;
public sealed class NotificationsControllerTests public sealed class NotificationsControllerTests
{ {
[Fact]
public void Send_message_request_validation_metadata_is_compatible_with_mvc_record_binding()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddControllers()
.AddApplicationPart(typeof(NotificationsController).Assembly);
using var serviceProvider = services.BuildServiceProvider();
var objectValidator = serviceProvider.GetRequiredService<IObjectModelValidator>();
var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
var actionContext = new ActionContext(
httpContext,
new RouteData(),
new ActionDescriptor(),
new ModelStateDictionary());
var request = new SendMessageRequest(
"校内通知",
"<p>这是一条用于验证请求模型绑定的通知。</p>");
var exception = Record.Exception(() =>
objectValidator.Validate(actionContext, null, string.Empty, request));
Assert.Null(exception);
Assert.True(actionContext.ModelState.IsValid);
}
[Fact] [Fact]
public async Task College_administrator_can_only_send_to_enabled_users_in_own_college() public async Task College_administrator_can_only_send_to_enabled_users_in_own_college()
{ {
+86 -1
View File
@@ -11,8 +11,14 @@ const safeHtml = computed(() => DOMPurify.sanitize(props.content, {
ALLOWED_TAGS: [ ALLOWED_TAGS: [
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's',
'h2', 'h3', 'h4', 'ul', 'ol', 'li', 'blockquote', 'a', 'h2', 'h3', 'h4', 'ul', 'ol', 'li', 'blockquote', 'a',
'figure', 'figcaption', 'img',
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'colgroup', 'col',
],
ALLOWED_ATTR: [
'href', 'target', 'rel',
'src', 'alt', 'title', 'width', 'height', 'class', 'style',
'colspan', 'rowspan', 'scope',
], ],
ALLOWED_ATTR: ['href', 'target', 'rel'],
ALLOW_DATA_ATTR: false, ALLOW_DATA_ATTR: false,
})) }))
</script> </script>
@@ -35,6 +41,12 @@ const safeHtml = computed(() => DOMPurify.sanitize(props.content, {
line-height: 1.75; line-height: 1.75;
} }
.rich-message-content::after {
display: block;
clear: both;
content: '';
}
.rich-message-content :deep(p), .rich-message-content :deep(p),
.rich-message-content :deep(ul), .rich-message-content :deep(ul),
.rich-message-content :deep(ol), .rich-message-content :deep(ol),
@@ -77,4 +89,77 @@ const safeHtml = computed(() => DOMPurify.sanitize(props.content, {
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px; text-underline-offset: 2px;
} }
.rich-message-content :deep(figure) {
margin: .9em auto;
}
.rich-message-content :deep(figure.image) {
max-width: 100%;
}
.rich-message-content :deep(img) {
display: block;
max-width: 100%;
height: auto;
margin: 0 auto;
border-radius: 6px;
}
.rich-message-content :deep(figcaption) {
margin-top: .4em;
color: var(--muted);
font-size: .86em;
line-height: 1.5;
text-align: center;
}
.rich-message-content :deep(.image-style-align-left),
.rich-message-content :deep(.image-style-block-align-left) {
margin-right: 1.25em;
margin-left: 0;
}
.rich-message-content :deep(.image-style-align-right),
.rich-message-content :deep(.image-style-block-align-right),
.rich-message-content :deep(.image-style-side) {
margin-right: 0;
margin-left: auto;
}
.rich-message-content :deep(.image-style-wrap-text.image-style-align-left) {
float: left;
}
.rich-message-content :deep(.image-style-wrap-text.image-style-align-right),
.rich-message-content :deep(.image-style-side) {
float: right;
margin-left: 1.25em;
}
.rich-message-content :deep(figure.table) {
max-width: 100%;
overflow-x: auto;
}
.rich-message-content :deep(table) {
width: 100%;
border-collapse: collapse;
background: #fff;
}
.rich-message-content :deep(th),
.rich-message-content :deep(td) {
min-width: 4.5em;
padding: .55em .7em;
border: 1px solid var(--line);
vertical-align: top;
}
.rich-message-content :deep(th) {
background: #f3f5f8;
color: var(--ink);
font-weight: 650;
text-align: left;
}
</style> </style>
+60 -1
View File
@@ -13,15 +13,30 @@ import {
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { Ckeditor } from '@ckeditor/ckeditor5-vue' import { Ckeditor } from '@ckeditor/ckeditor5-vue'
import { import {
AutoLink,
BlockQuote, BlockQuote,
Bold, Bold,
ClassicEditor, ClassicEditor,
Essentials, Essentials,
Heading, Heading,
Image,
ImageCaption,
ImageInsert,
ImageInsertViaUrl,
ImageResize,
ImageStyle,
ImageToolbar,
Italic, Italic,
Link, Link,
LinkImage,
List, List,
Paragraph, Paragraph,
Table,
TableCaption,
TableCellProperties,
TableColumnResize,
TableProperties,
TableToolbar,
type EditorConfig, type EditorConfig,
} from 'ckeditor5' } from 'ckeditor5'
import 'ckeditor5/ckeditor5.css' import 'ckeditor5/ckeditor5.css'
@@ -84,8 +99,23 @@ const editorConfig: EditorConfig = {
Bold, Bold,
Italic, Italic,
Link, Link,
AutoLink,
List, List,
BlockQuote, BlockQuote,
Image,
ImageCaption,
ImageInsert,
ImageInsertViaUrl,
ImageResize,
ImageStyle,
ImageToolbar,
LinkImage,
Table,
TableCaption,
TableCellProperties,
TableColumnResize,
TableProperties,
TableToolbar,
], ],
toolbar: { toolbar: {
items: [ items: [
@@ -95,6 +125,9 @@ const editorConfig: EditorConfig = {
'italic', 'italic',
'link', 'link',
'|', '|',
'insertTable',
'insertImage',
'|',
'bulletedList', 'bulletedList',
'numberedList', 'numberedList',
'blockQuote', 'blockQuote',
@@ -115,6 +148,32 @@ const editorConfig: EditorConfig = {
addTargetToExternalLinks: true, addTargetToExternalLinks: true,
defaultProtocol: 'https://', defaultProtocol: 'https://',
}, },
image: {
insert: {
integrations: ['insertImageViaUrl'],
},
toolbar: [
'toggleImageCaption',
'imageTextAlternative',
'|',
'imageStyle:inline',
'imageStyle:wrapText',
'imageStyle:breakText',
'|',
'resizeImage',
'linkImage',
],
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells',
'toggleTableCaption',
'tableProperties',
'tableCellProperties',
],
},
} }
const categories: Record<string, { label: string; className: string }> = { const categories: Record<string, { label: string; className: string }> = {
@@ -808,7 +867,7 @@ onMounted(() => load())
:config="editorConfig" :config="editorConfig"
/> />
<div class="editor-footnote"> <div class="editor-footnote">
<span>支持标题列表引用和链接不支持上传附件或图片</span> <span>支持表格超链接和外链图片图片请填写 HTTPS 地址不上传本地文件</span>
<b :class="{ warning: messageForm.content.length > 18000 }"> <b :class="{ warning: messageForm.content.length > 18000 }">
{{ editorTextLength }} · 源码 {{ messageForm.content.length }}/20000 {{ editorTextLength }} · 源码 {{ messageForm.content.length }}/20000
</b> </b>