feat(公共): 富文本组件评论Mention功能扩展
This commit is contained in:
parent
e56239cb5a
commit
15b744be34
|
@ -55,6 +55,22 @@ export default defineConfig({
|
|||
find: 'vue',
|
||||
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'],
|
||||
},
|
||||
|
|
|
@ -40,7 +40,10 @@
|
|||
"@arco-design/web-vue": "^2.53.3",
|
||||
"@arco-themes/vue-ms-theme-default": "^0.0.30",
|
||||
"@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",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"ace-builds": "^1.24.2",
|
||||
|
@ -60,6 +63,7 @@
|
|||
"query-string": "^8.1.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"vue": "^3.3.4",
|
||||
"vue-dompurify-html": "^4.1.4",
|
||||
"vue-draggable-plus": "^0.2.7",
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
GetFileIsUpdateUrl,
|
||||
GetRecycleCaseListUrl,
|
||||
GetRecycleCaseModulesCountUrl,
|
||||
GetReviewerListUrl,
|
||||
GetSearchCustomFieldsUrl,
|
||||
getTransferTreeUrl,
|
||||
GetTrashCaseModuleTreeUrl,
|
||||
|
@ -64,6 +65,7 @@ import type {
|
|||
UpdateModule,
|
||||
} from '@/models/caseManagement/featureCase';
|
||||
import type { CommonList, MoveModules, TableQueryParams } from '@/models/common';
|
||||
import type { UserListItem } from '@/models/setting/user';
|
||||
// 获取模块树
|
||||
export function getCaseModuleTree(projectId: string) {
|
||||
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 });
|
||||
}
|
||||
|
||||
// 创建评论
|
||||
export function UpdateCommentList(data: CreateOrUpdate) {
|
||||
export function updateCommentList(data: CreateOrUpdate) {
|
||||
return MSR.post({ url: UpdateCommentItemUrl, data });
|
||||
}
|
||||
|
||||
// 删除评论
|
||||
export function DeleteCommentList(commentId: string) {
|
||||
export function deleteCommentList(commentId: string) {
|
||||
return MSR.post({ url: `${DeleteCommentItemUrl}/${commentId}` });
|
||||
}
|
||||
|
||||
|
@ -269,4 +271,9 @@ export function getDetailCaseReviewPage(data: TableQueryParams) {
|
|||
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 {};
|
||||
|
|
|
@ -100,5 +100,7 @@ export const UpdateCommentItemUrl = '/functional/case/comment/update';
|
|||
export const DeleteCommentItemUrl = '/functional/case/comment/delete';
|
||||
// 获取详情用例评审
|
||||
export const GetDetailCaseReviewUrl = '/functional/case/review/page';
|
||||
// 获取有权限的评审人
|
||||
export const GetReviewerListUrl = '/case/review/user-option';
|
||||
|
||||
export default {};
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import useLocale from '@/locale/useLocale';
|
||||
|
||||
import '@halo-dev/richtext-editor/dist/style.css';
|
||||
import suggestion from './extensions/mention/suggestion';
|
||||
import {
|
||||
ExtensionAudio,
|
||||
ExtensionBlockquote,
|
||||
|
@ -58,6 +59,7 @@
|
|||
RichTextEditor,
|
||||
useEditor,
|
||||
} from '@halo-dev/richtext-editor';
|
||||
import Mention from '@tiptap/extension-mention';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
|
@ -79,6 +81,7 @@
|
|||
class: 'dropcursor',
|
||||
color: 'skyblue',
|
||||
}),
|
||||
ExtensionCommands,
|
||||
ExtensionGapcursor,
|
||||
ExtensionHardBreak,
|
||||
ExtensionHeading,
|
||||
|
@ -113,7 +116,6 @@
|
|||
ExtensionHighlight,
|
||||
ExtensionVideo,
|
||||
ExtensionAudio,
|
||||
ExtensionCommands,
|
||||
ExtensionCodeBlock.configure({
|
||||
lowlight,
|
||||
}),
|
||||
|
@ -126,6 +128,12 @@
|
|||
ExtensionColumn,
|
||||
ExtensionNodeSelected,
|
||||
ExtensionTrailingNode,
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestion,
|
||||
}),
|
||||
],
|
||||
onUpdate: () => {
|
||||
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>
|
||||
|
|
|
@ -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.newSuccess': 'Added successfully',
|
||||
'common.publish': 'Publish',
|
||||
'common.publishSuccessfully': 'Published successfully',
|
||||
};
|
||||
|
|
|
@ -81,4 +81,5 @@ export default {
|
|||
'common.new': '新增',
|
||||
'common.newSuccess': '新增成功',
|
||||
'common.publish': '发布',
|
||||
'common.publishSuccessfully': '发布成功',
|
||||
};
|
||||
|
|
|
@ -209,7 +209,7 @@
|
|||
import TabTestPlan from './tabContent/tabTestPlan.vue';
|
||||
|
||||
import {
|
||||
CreateCommentList,
|
||||
createCommentList,
|
||||
deleteCaseRequest,
|
||||
followerCaseRequest,
|
||||
getCaseDetail,
|
||||
|
@ -479,16 +479,23 @@
|
|||
const content = ref('');
|
||||
|
||||
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 {
|
||||
const params = {
|
||||
caseId: detailInfo.value.id,
|
||||
notifier: '',
|
||||
notifier: notifiers,
|
||||
replyUser: '',
|
||||
parentId: '',
|
||||
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) {
|
||||
console.log(error);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,15 @@
|
|||
</a-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <MsComment :comment-list="commentList" /> -->
|
||||
<div>
|
||||
<!-- TODO -->
|
||||
<!-- <MsComment
|
||||
:current-user-id="currentUserId"
|
||||
:comment-list="commentList"
|
||||
@update-or-add="handleUpdateOrAdd"
|
||||
@delete="handleDelete"
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -34,9 +42,6 @@
|
|||
|
||||
const commentList = ref<CommentItem[]>([]);
|
||||
|
||||
// 发布评论
|
||||
function publishHandler() {}
|
||||
|
||||
// 初始化评论列表
|
||||
async function initCommentList() {
|
||||
try {
|
||||
|
@ -46,6 +51,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
const currentUserId = ref('');
|
||||
|
||||
// 添加或者更新评论
|
||||
function handleUpdateOrAdd() {}
|
||||
|
||||
// 删除评论
|
||||
function handleDelete() {}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initCommentList();
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue