消息修复
This commit is contained in:
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user