feat(公共): 富文本组件评论Mention功能扩展
This commit is contained in:
parent
e56239cb5a
commit
15b744be34
|
@ -55,6 +55,22 @@ export default defineConfig({
|
||||||
find: 'vue',
|
find: 'vue',
|
||||||
replacement: 'vue/dist/vue.esm-bundler.js', // compile template
|
replacement: 'vue/dist/vue.esm-bundler.js', // compile template
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
find: '@tiptap/pm/state',
|
||||||
|
replacement: '@halo-dev/richtext-editor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: '@tiptap/pm/model',
|
||||||
|
replacement: '@halo-dev/richtext-editor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: '@tiptap/core',
|
||||||
|
replacement: '@halo-dev/richtext-editor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: '@tiptap/pm/view',
|
||||||
|
replacement: '@halo-dev/richtext-editor',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
extensions: ['.ts', '.js'],
|
extensions: ['.ts', '.js'],
|
||||||
},
|
},
|
||||||
|
|
|
@ -40,7 +40,10 @@
|
||||||
"@arco-design/web-vue": "^2.53.3",
|
"@arco-design/web-vue": "^2.53.3",
|
||||||
"@arco-themes/vue-ms-theme-default": "^0.0.30",
|
"@arco-themes/vue-ms-theme-default": "^0.0.30",
|
||||||
"@form-create/arco-design": "^3.1.23",
|
"@form-create/arco-design": "^3.1.23",
|
||||||
"@halo-dev/richtext-editor": "0.0.0-alpha.32",
|
"@halo-dev/richtext-editor": "0.0.0-alpha.33",
|
||||||
|
"@tiptap/extension-mention": "^2.1.13",
|
||||||
|
"@tiptap/suggestion": "^2.1.13",
|
||||||
|
"@tiptap/vue-3": "^2.1.13",
|
||||||
"@types/color": "^3.0.4",
|
"@types/color": "^3.0.4",
|
||||||
"@vueuse/core": "^10.4.1",
|
"@vueuse/core": "^10.4.1",
|
||||||
"ace-builds": "^1.24.2",
|
"ace-builds": "^1.24.2",
|
||||||
|
@ -60,6 +63,7 @@
|
||||||
"query-string": "^8.1.0",
|
"query-string": "^8.1.0",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
|
"tippy.js": "^6.3.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-dompurify-html": "^4.1.4",
|
"vue-dompurify-html": "^4.1.4",
|
||||||
"vue-draggable-plus": "^0.2.7",
|
"vue-draggable-plus": "^0.2.7",
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
GetFileIsUpdateUrl,
|
GetFileIsUpdateUrl,
|
||||||
GetRecycleCaseListUrl,
|
GetRecycleCaseListUrl,
|
||||||
GetRecycleCaseModulesCountUrl,
|
GetRecycleCaseModulesCountUrl,
|
||||||
|
GetReviewerListUrl,
|
||||||
GetSearchCustomFieldsUrl,
|
GetSearchCustomFieldsUrl,
|
||||||
getTransferTreeUrl,
|
getTransferTreeUrl,
|
||||||
GetTrashCaseModuleTreeUrl,
|
GetTrashCaseModuleTreeUrl,
|
||||||
|
@ -64,6 +65,7 @@ import type {
|
||||||
UpdateModule,
|
UpdateModule,
|
||||||
} from '@/models/caseManagement/featureCase';
|
} from '@/models/caseManagement/featureCase';
|
||||||
import type { CommonList, MoveModules, TableQueryParams } from '@/models/common';
|
import type { CommonList, MoveModules, TableQueryParams } from '@/models/common';
|
||||||
|
import type { UserListItem } from '@/models/setting/user';
|
||||||
// 获取模块树
|
// 获取模块树
|
||||||
export function getCaseModuleTree(projectId: string) {
|
export function getCaseModuleTree(projectId: string) {
|
||||||
return MSR.get<ModulesTreeType[]>({ url: `${GetCaseModuleTreeUrl}/${projectId}` });
|
return MSR.get<ModulesTreeType[]>({ url: `${GetCaseModuleTreeUrl}/${projectId}` });
|
||||||
|
@ -250,17 +252,17 @@ export function getCommentList(caseId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建评论
|
// 创建评论
|
||||||
export function CreateCommentList(data: CreateOrUpdate) {
|
export function createCommentList(data: CreateOrUpdate) {
|
||||||
return MSR.post({ url: CreateCommentItemUrl, data });
|
return MSR.post({ url: CreateCommentItemUrl, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建评论
|
// 创建评论
|
||||||
export function UpdateCommentList(data: CreateOrUpdate) {
|
export function updateCommentList(data: CreateOrUpdate) {
|
||||||
return MSR.post({ url: UpdateCommentItemUrl, data });
|
return MSR.post({ url: UpdateCommentItemUrl, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除评论
|
// 删除评论
|
||||||
export function DeleteCommentList(commentId: string) {
|
export function deleteCommentList(commentId: string) {
|
||||||
return MSR.post({ url: `${DeleteCommentItemUrl}/${commentId}` });
|
return MSR.post({ url: `${DeleteCommentItemUrl}/${commentId}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,4 +271,9 @@ export function getDetailCaseReviewPage(data: TableQueryParams) {
|
||||||
return MSR.post<CommonList<CaseManagementTable>>({ url: GetDetailCaseReviewUrl, data });
|
return MSR.post<CommonList<CaseManagementTable>>({ url: GetDetailCaseReviewUrl, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取评审人列表
|
||||||
|
export function getReviewerList(projectId: string, keyword: string) {
|
||||||
|
return MSR.get<UserListItem[]>({ url: `${GetReviewerListUrl}/${projectId}`, params: { keyword } });
|
||||||
|
}
|
||||||
|
|
||||||
export default {};
|
export default {};
|
||||||
|
|
|
@ -100,5 +100,7 @@ export const UpdateCommentItemUrl = '/functional/case/comment/update';
|
||||||
export const DeleteCommentItemUrl = '/functional/case/comment/delete';
|
export const DeleteCommentItemUrl = '/functional/case/comment/delete';
|
||||||
// 获取详情用例评审
|
// 获取详情用例评审
|
||||||
export const GetDetailCaseReviewUrl = '/functional/case/review/page';
|
export const GetDetailCaseReviewUrl = '/functional/case/review/page';
|
||||||
|
// 获取有权限的评审人
|
||||||
|
export const GetReviewerListUrl = '/case/review/user-option';
|
||||||
|
|
||||||
export default {};
|
export default {};
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import useLocale from '@/locale/useLocale';
|
import useLocale from '@/locale/useLocale';
|
||||||
|
|
||||||
import '@halo-dev/richtext-editor/dist/style.css';
|
import '@halo-dev/richtext-editor/dist/style.css';
|
||||||
|
import suggestion from './extensions/mention/suggestion';
|
||||||
import {
|
import {
|
||||||
ExtensionAudio,
|
ExtensionAudio,
|
||||||
ExtensionBlockquote,
|
ExtensionBlockquote,
|
||||||
|
@ -58,6 +59,7 @@
|
||||||
RichTextEditor,
|
RichTextEditor,
|
||||||
useEditor,
|
useEditor,
|
||||||
} from '@halo-dev/richtext-editor';
|
} from '@halo-dev/richtext-editor';
|
||||||
|
import Mention from '@tiptap/extension-mention';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
|
@ -79,6 +81,7 @@
|
||||||
class: 'dropcursor',
|
class: 'dropcursor',
|
||||||
color: 'skyblue',
|
color: 'skyblue',
|
||||||
}),
|
}),
|
||||||
|
ExtensionCommands,
|
||||||
ExtensionGapcursor,
|
ExtensionGapcursor,
|
||||||
ExtensionHardBreak,
|
ExtensionHardBreak,
|
||||||
ExtensionHeading,
|
ExtensionHeading,
|
||||||
|
@ -113,7 +116,6 @@
|
||||||
ExtensionHighlight,
|
ExtensionHighlight,
|
||||||
ExtensionVideo,
|
ExtensionVideo,
|
||||||
ExtensionAudio,
|
ExtensionAudio,
|
||||||
ExtensionCommands,
|
|
||||||
ExtensionCodeBlock.configure({
|
ExtensionCodeBlock.configure({
|
||||||
lowlight,
|
lowlight,
|
||||||
}),
|
}),
|
||||||
|
@ -126,6 +128,12 @@
|
||||||
ExtensionColumn,
|
ExtensionColumn,
|
||||||
ExtensionNodeSelected,
|
ExtensionNodeSelected,
|
||||||
ExtensionTrailingNode,
|
ExtensionTrailingNode,
|
||||||
|
Mention.configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'mention',
|
||||||
|
},
|
||||||
|
suggestion,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
onUpdate: () => {
|
onUpdate: () => {
|
||||||
content.value = `${editor.value?.getHTML()}`;
|
content.value = `${editor.value?.getHTML()}`;
|
||||||
|
@ -167,4 +175,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
:deep(.editor-header) {
|
||||||
|
svg {
|
||||||
|
color: var(--color-text-3) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 修改滚动条
|
||||||
|
:deep(.editor-header + div > div) {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px !important;
|
||||||
|
height: 4px !important;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
background: var(--color-text-input-border) !important;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a1a7b0 !important;
|
||||||
|
}
|
||||||
|
&&::-webkit-scrollbar-track {
|
||||||
|
@apply bg-white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
<template>
|
||||||
|
<div class="items">
|
||||||
|
<template v-if="items.length">
|
||||||
|
<button
|
||||||
|
v-for="(item, index) in items"
|
||||||
|
:key="index"
|
||||||
|
class="item one-line-text"
|
||||||
|
:class="{ 'is-selected': index === selectedIndex }"
|
||||||
|
@click="selectItem(index)"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<div v-else class="item"> No result </div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { UserListItem } from '@/models/setting/user';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
items: UserListItem[];
|
||||||
|
command: (item: UserListItem) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedIndex = ref(0);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.items,
|
||||||
|
() => {
|
||||||
|
selectedIndex.value = 0;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleKeyUp() {
|
||||||
|
selectedIndex.value = (selectedIndex.value + props.items.length - 1) % props.items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown() {
|
||||||
|
selectedIndex.value = (selectedIndex.value + 1) % props.items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectItem(index: number) {
|
||||||
|
const item = props.items[index];
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
props.command({ id: `${item.name}(${item.email})` } as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyEnter() {
|
||||||
|
handleSelectItem(selectedIndex.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown({ event }: { event: KeyboardEvent }) {
|
||||||
|
if (event.key === 'ArrowUp' || (event.key === 'k' && event.ctrlKey)) {
|
||||||
|
handleKeyUp();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown' || (event.key === 'j' && event.ctrlKey)) {
|
||||||
|
handleKeyDown();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
handleKeyEnter();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectItem(index: any) {
|
||||||
|
const item = props.items[index];
|
||||||
|
if (item) {
|
||||||
|
props.command({ id: `${item.name}(${item.email})` } as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
onKeyDown,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.items {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: block;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0 0 0 / 5%), 0 10px 20px rgba(0 0 0 / 10%);
|
||||||
|
@apply bg-white;
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
&.is-selected {
|
||||||
|
background: var(--color-bg-3);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-bg-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,88 @@
|
||||||
|
import MentionList from './MentionList.vue';
|
||||||
|
|
||||||
|
import { getReviewerList } from '@/api/modules/case-management/featureCase';
|
||||||
|
import useAppStore from '@/store/modules/app';
|
||||||
|
|
||||||
|
import type { UserListItem } from '@/models/setting/user';
|
||||||
|
|
||||||
|
import { VueRenderer } from '@halo-dev/richtext-editor';
|
||||||
|
import type { Instance } from 'tippy.js';
|
||||||
|
import tippy from 'tippy.js';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const projectMember = ref<UserListItem[]>([]);
|
||||||
|
|
||||||
|
async function getMembersToolBar(query: string) {
|
||||||
|
const params = {
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
keyword: query,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
projectMember.value = await getReviewerList(params.projectId, params.keyword);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
items: async ({ query }: any) => {
|
||||||
|
await getMembersToolBar(query);
|
||||||
|
return projectMember.value.filter((item: UserListItem) => item.name.toLowerCase().startsWith(query.toLowerCase()));
|
||||||
|
},
|
||||||
|
|
||||||
|
render: () => {
|
||||||
|
let component: VueRenderer;
|
||||||
|
let popup: Instance[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
onStart: (props: Record<string, any>) => {
|
||||||
|
debugger;
|
||||||
|
component = new VueRenderer(MentionList, {
|
||||||
|
props,
|
||||||
|
editor: props.editor,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!props.clientRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
popup = tippy('body', {
|
||||||
|
getReferenceClientRect: props.clientRect,
|
||||||
|
appendTo: () => document.body,
|
||||||
|
content: component.element,
|
||||||
|
showOnCreate: true,
|
||||||
|
interactive: true,
|
||||||
|
trigger: 'manual',
|
||||||
|
placement: 'bottom-start',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdate(props: Record<string, any>) {
|
||||||
|
component.updateProps(props);
|
||||||
|
|
||||||
|
if (!props.clientRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
popup[0].setProps({
|
||||||
|
getReferenceClientRect: props.clientRect,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown(props: Record<string, any>) {
|
||||||
|
if (props.event.key === 'Escape') {
|
||||||
|
popup[0].hide();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return component.ref?.onKeyDown(props);
|
||||||
|
},
|
||||||
|
|
||||||
|
onExit() {
|
||||||
|
popup[0].destroy();
|
||||||
|
component.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
|
@ -79,4 +79,5 @@ export default {
|
||||||
'common.new': 'New',
|
'common.new': 'New',
|
||||||
'common.newSuccess': 'Added successfully',
|
'common.newSuccess': 'Added successfully',
|
||||||
'common.publish': 'Publish',
|
'common.publish': 'Publish',
|
||||||
|
'common.publishSuccessfully': 'Published successfully',
|
||||||
};
|
};
|
||||||
|
|
|
@ -81,4 +81,5 @@ export default {
|
||||||
'common.new': '新增',
|
'common.new': '新增',
|
||||||
'common.newSuccess': '新增成功',
|
'common.newSuccess': '新增成功',
|
||||||
'common.publish': '发布',
|
'common.publish': '发布',
|
||||||
|
'common.publishSuccessfully': '发布成功',
|
||||||
};
|
};
|
||||||
|
|
|
@ -209,7 +209,7 @@
|
||||||
import TabTestPlan from './tabContent/tabTestPlan.vue';
|
import TabTestPlan from './tabContent/tabTestPlan.vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateCommentList,
|
createCommentList,
|
||||||
deleteCaseRequest,
|
deleteCaseRequest,
|
||||||
followerCaseRequest,
|
followerCaseRequest,
|
||||||
getCaseDetail,
|
getCaseDetail,
|
||||||
|
@ -479,16 +479,23 @@
|
||||||
const content = ref('');
|
const content = ref('');
|
||||||
|
|
||||||
async function publishHandler(currentContent: string) {
|
async function publishHandler(currentContent: string) {
|
||||||
|
const regex = /data-id="([^"]*)"/g;
|
||||||
|
const matchesNotifier = currentContent.match(regex);
|
||||||
|
let notifiers = '';
|
||||||
|
if (matchesNotifier?.length) {
|
||||||
|
notifiers = matchesNotifier.map((match) => match.replace('data-id="', '').replace('"', '')).join(';');
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
caseId: detailInfo.value.id,
|
caseId: detailInfo.value.id,
|
||||||
notifier: '',
|
notifier: notifiers,
|
||||||
replyUser: '',
|
replyUser: '',
|
||||||
parentId: '',
|
parentId: '',
|
||||||
content: currentContent,
|
content: currentContent,
|
||||||
event: 'COMMENT', // 任务事件(仅评论: ’COMMENT‘; 评论并@: ’AT‘; 回复评论/回复并@: ’REPLAY‘;)
|
event: notifiers ? 'AT' : 'COMMENT', // 任务事件(仅评论: ’COMMENT‘; 评论并@: ’AT‘; 回复评论/回复并@: ’REPLAY‘;)
|
||||||
};
|
};
|
||||||
await CreateCommentList(params);
|
await createCommentList(params);
|
||||||
|
Message.success(t('common.publishSuccessfully'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,15 @@
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <MsComment :comment-list="commentList" /> -->
|
<div>
|
||||||
|
<!-- TODO -->
|
||||||
|
<!-- <MsComment
|
||||||
|
:current-user-id="currentUserId"
|
||||||
|
:comment-list="commentList"
|
||||||
|
@update-or-add="handleUpdateOrAdd"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/> -->
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -34,9 +42,6 @@
|
||||||
|
|
||||||
const commentList = ref<CommentItem[]>([]);
|
const commentList = ref<CommentItem[]>([]);
|
||||||
|
|
||||||
// 发布评论
|
|
||||||
function publishHandler() {}
|
|
||||||
|
|
||||||
// 初始化评论列表
|
// 初始化评论列表
|
||||||
async function initCommentList() {
|
async function initCommentList() {
|
||||||
try {
|
try {
|
||||||
|
@ -46,6 +51,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentUserId = ref('');
|
||||||
|
|
||||||
|
// 添加或者更新评论
|
||||||
|
function handleUpdateOrAdd() {}
|
||||||
|
|
||||||
|
// 删除评论
|
||||||
|
function handleDelete() {}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
initCommentList();
|
initCommentList();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue