搜索结果列表页面开发
This commit is contained in:
10
src/components/MdEditor/README.md
Normal file
10
src/components/MdEditor/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# md单栏显示组件
|
||||
|
||||
### 说明
|
||||
``` js
|
||||
// template
|
||||
<md-editor v-model="content" @content-change="mdChange"><md-editor>
|
||||
|
||||
```
|
||||
### Props
|
||||
+ 更多透传参数可参考markdown组件api文档添加:[eidtor-md 文档](https://vue-devui.github.io/components/editor-md/)
|
||||
418
src/components/MdEditor/index.vue
Normal file
418
src/components/MdEditor/index.vue
Normal file
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<div class="g-md-container" :class="{ 'g-border': border, 'g-border-light': borderLight }" v-loading="loading">
|
||||
<!-- <d-upload
|
||||
v-if="projectId"
|
||||
:upload-options="uploadOptions"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="onSuccess"
|
||||
:on-error="onError"
|
||||
:disabled="mode === 'readonly'"
|
||||
>
|
||||
<d-popover content="文件上传" trigger="hover" :position="['bottom']">
|
||||
<span class="g-md-upload-btn"
|
||||
><d-icon :disabled="mode === 'readonly'" name="icon-op-upload" size="16px" operable>
|
||||
</d-icon></span
|
||||
></d-popover>
|
||||
</d-upload> -->
|
||||
<Button v-if="~['editonly', 'readonly'].indexOf(mode)" class="g-md-preview-btn" :size="'md'" @click="previewClick">
|
||||
{{ mode === 'editonly' ? '预览' : '返回' }}
|
||||
</Button>
|
||||
<EditorMd
|
||||
v-model="vModel"
|
||||
:mode="mode"
|
||||
:options="options"
|
||||
:style="mdStyle"
|
||||
:breaks="false"
|
||||
:md-plugins="plugins"
|
||||
:md-rules="mdRules"
|
||||
image-upload-to-server
|
||||
:custom-xss-rules="customXssRules"
|
||||
:placeholder="placeholder"
|
||||
:hint-config="_hintConfig"
|
||||
:before-show-hint="beforeShowHint"
|
||||
@content-change="contentChange"
|
||||
@image-upload="imageUpload"
|
||||
@after-editor-init="afterInit"
|
||||
@checked-change="onCheckedEvent"
|
||||
@preview-content-change="previewContentChange"
|
||||
>
|
||||
<template #hintTemplate>
|
||||
<ul class="list-menu" v-if="hintList && hintList.length" v-loading="hintLoading">
|
||||
<MemberItemMini v-for="(item) of hintList" :item="item" :key="item.username" @click="hintItemClick(item)" />
|
||||
</ul>
|
||||
</template>
|
||||
</EditorMd>
|
||||
</div>
|
||||
<div v-if="showCount && !maxLength" class="flex flex-row-reverse">
|
||||
<span>{{ vModels.length }}</span>
|
||||
</div>
|
||||
<div v-if="showCount && maxLength" class="flex flex-row-reverse">
|
||||
<span>{{ vModels.length }}/{{ maxLength }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'GMdEditor' });
|
||||
import { ref, watch, reactive, onMounted, computed } from 'vue';
|
||||
import { useModel } from '@/utils/hooks/useModel';
|
||||
import { uploadFile } from '@/api/user';
|
||||
import { uploadRepoImage, getDiscussionPreview } from '@/api/repo';
|
||||
import { imageToBase64 } from '@/utils';
|
||||
import { checkbox, EditorMd } from 'vue-devui/editor-md';
|
||||
import { Button } from 'vue-devui/button';
|
||||
import { type EditorConfiguration } from 'codemirror';
|
||||
import type { CustomPlugin } from '@/components/MdRender/mdPlugins';
|
||||
import { useDefinePlugin } from '@/components/MdRender/mdPlugins';
|
||||
import MemberItemMini from '@/components/MemberItemMini/index.vue';
|
||||
import katexPlugin from '@iktakahiro/markdown-it-katex';
|
||||
import PlantUml from 'markdown-it-plantuml';
|
||||
import { Message } from 'vue-devui/message';
|
||||
import keys from 'lodash/keys';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
interface IProps {
|
||||
modelValue?: string;
|
||||
mdStyle?: object;
|
||||
mode?: string;
|
||||
options?: EditorConfiguration;
|
||||
projectId?: string;
|
||||
toggleRepoPermission?: boolean; // 仓库 issue,mr 图片上传走单独接口
|
||||
customPlugins?: CustomPlugin[]; // 自带的插件
|
||||
border?: boolean; // 加黑色边框
|
||||
borderLight?: boolean; // 加浅色边框
|
||||
placeholder?: string;
|
||||
showCount?: boolean;
|
||||
maxLength?: number;
|
||||
hintConfig?: object;
|
||||
}
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
modelValue: '',
|
||||
mode: 'editonly',
|
||||
projectId: '',
|
||||
toggleRepoPermission: false,
|
||||
customPlugins: () => [],
|
||||
options: () => ({}),
|
||||
placeholder: '',
|
||||
hintConfig: () => ({})
|
||||
});
|
||||
|
||||
const customXssRules = ref([
|
||||
{
|
||||
key: 'kbd',
|
||||
value: ['id', 'class', 'style'] // 为空表示过滤所有属性,放开属性则添加对应项,如['id', 'style']
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
value: ['cellspacing', 'cellpadding']
|
||||
},
|
||||
{
|
||||
key: 'td',
|
||||
value: ['align', 'valign', 'colspan', 'rowspan']
|
||||
},
|
||||
{
|
||||
key: 'div',
|
||||
value: ['align']
|
||||
},
|
||||
{
|
||||
key: 'p',
|
||||
value: ['align']
|
||||
},
|
||||
{
|
||||
key: 'img',
|
||||
value: ['align', 'src', 'alt', 'width', 'height']
|
||||
}
|
||||
]);
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'contentChange', 'previewContentChange']);
|
||||
const { vModels } = useModel(props, emits);
|
||||
const mode = ref(props.mode || 'editonly');
|
||||
|
||||
const mdPreview = ref('');
|
||||
|
||||
const vModel = computed({
|
||||
get() {
|
||||
if (mode.value === 'readonly') {
|
||||
return mdPreview.value;
|
||||
}
|
||||
return vModels.value;
|
||||
},
|
||||
set(val) {
|
||||
if (mode.value === 'editonly' || mode.value === 'normal') {
|
||||
emits('update:modelValue', val);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => vModels.value,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
mdPreview.value = val;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const mdRules = reactive({
|
||||
linkify: {
|
||||
fuzzyLink: false
|
||||
}
|
||||
});
|
||||
const plugins = reactive([
|
||||
{
|
||||
plugin: checkbox,
|
||||
opts: {
|
||||
idPrefix: 'devui',
|
||||
disable: false
|
||||
}
|
||||
},
|
||||
{ plugin: PlantUml },
|
||||
{ plugin: katexPlugin },
|
||||
...props.customPlugins.map(({ pluginName, opts }) => useDefinePlugin(pluginName, opts))
|
||||
]);
|
||||
const instance = ref({}); // CodeMirror 实例
|
||||
const loading = ref(false);
|
||||
const hintLoading = ref(false);
|
||||
const hintDataSource = ref([]);
|
||||
const hintList = ref([]);
|
||||
const fileBaseURL = (import.meta as any).env.VITE_FILE_HOST;
|
||||
const fileDownloadBaseURL = (import.meta as any).env.VITE_DOWNLOAD_HOST;
|
||||
const afterInit = (obj: Object) => {
|
||||
instance.value = obj;
|
||||
};
|
||||
const hintItemClick = (item) => {
|
||||
hintCallback.value && hintCallback.value(item.insertText || item.itemText);
|
||||
};
|
||||
|
||||
const hintCallback = ref();
|
||||
const _hintConfig = {
|
||||
throttleTime: 200
|
||||
};
|
||||
|
||||
keys(props.hintConfig || {}).forEach((key) => {
|
||||
_hintConfig[key] = {
|
||||
handler: async (evt) => {
|
||||
try {
|
||||
const { callback, cursorHint } = evt;
|
||||
const cb = props.hintConfig[key];
|
||||
if (isFunction(cb)) {
|
||||
hintLoading.value = true;
|
||||
const loadData = debounce(async () => {
|
||||
if (!hintDataSource.value.length) {
|
||||
const dataList = await cb();
|
||||
hintDataSource.value = dataList;
|
||||
}
|
||||
hintList.value = hintDataSource.value
|
||||
?.filter((item) => {
|
||||
return (
|
||||
item.itemText?.toLowerCase().indexOf(cursorHint.toLowerCase()) !== -1 ||
|
||||
item.nick_name?.toLowerCase().indexOf(cursorHint.toLowerCase()) !== -1
|
||||
);
|
||||
})
|
||||
?.slice(0, 5);
|
||||
hintCallback.value = callback;
|
||||
}, 200);
|
||||
loadData();
|
||||
}
|
||||
} finally {
|
||||
hintLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 仓库开启上传文件功能
|
||||
const uploadOptions = ref({
|
||||
uri: `${fileBaseURL}/${props.projectId}/uploads/attachment_file`
|
||||
// withCredentials: true
|
||||
});
|
||||
watch(
|
||||
() => props.projectId,
|
||||
() => {
|
||||
uploadOptions.value.uri = `${fileBaseURL}/${props.projectId}/uploads/attachment_file`;
|
||||
}
|
||||
);
|
||||
|
||||
const beforeUpload = async (file: any) => {
|
||||
loading.value = true;
|
||||
const currentFile = file[0].file;
|
||||
const fileSize = currentFile.size;
|
||||
if (fileSize / (1024 * 1024) > 10) {
|
||||
Message.warning('上传的文件不能超过 10M');
|
||||
loading.value = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
interface resultItemType {
|
||||
file: File;
|
||||
response: string;
|
||||
}
|
||||
const onSuccess = (result: resultItemType[]) => {
|
||||
const { name } = result[0].file;
|
||||
const response = JSON.parse(result[0].response);
|
||||
const { path } = response;
|
||||
if (path) {
|
||||
const url = `${fileDownloadBaseURL}/${props.projectId}/attachment/${path}`;
|
||||
vModels.value = `${vModels.value}${vModels.value && '\n'}[${name}](${url})`;
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const onError = (error: any) => {
|
||||
console.error('upload error', error);
|
||||
Message.error('文件上传失败');
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const discussionPreview = async () => {
|
||||
const res = await getDiscussionPreview({
|
||||
discussion: vModels.value
|
||||
});
|
||||
if (!res.error) {
|
||||
mdPreview.value = res.data?.data?.body;
|
||||
}
|
||||
};
|
||||
|
||||
const previewClick = async () => {
|
||||
if (mode.value === 'editonly') {
|
||||
mode.value = 'readonly';
|
||||
await discussionPreview();
|
||||
} else {
|
||||
mode.value = 'editonly';
|
||||
vModel.value = vModels.value;
|
||||
}
|
||||
document.activeElement.blur();
|
||||
};
|
||||
const onCheckedEvent = (val: string) => {
|
||||
vModels.value = val;
|
||||
};
|
||||
const contentChange = (value: string) => {
|
||||
emits('contentChange', value);
|
||||
};
|
||||
/**
|
||||
* 预览内容变化(html字符串)
|
||||
* @param value
|
||||
*/
|
||||
const previewContentChange = (value: string) => {
|
||||
emits('previewContentChange', value);
|
||||
};
|
||||
|
||||
const imageUpload = async ({ file, callback }) => {
|
||||
let message = '';
|
||||
const rFilter = /^(image\/bmp|image\/gif|image\/jpge|image\/jpeg|image\/jpg|image\/png|image\/tiff)$/i;
|
||||
if (!rFilter.test(file.type)) {
|
||||
message = '请选择bmp/jpg/jpge/png/gif/tiff图片格式上传';
|
||||
} else if (file.size / (1024 * 1024) > 2) {
|
||||
message = '选择的图片大小不能超过2M';
|
||||
}
|
||||
if (message) {
|
||||
Message({
|
||||
message,
|
||||
type: 'warning'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
let resImg;
|
||||
if (props.toggleRepoPermission) {
|
||||
const imgBase64 = (await imageToBase64(file)) as string;
|
||||
const res = await uploadRepoImage({
|
||||
project_id: props.projectId,
|
||||
attach: imgBase64.replace(/^data:image\/\w+;base64,/, ''),
|
||||
file_name: file.name
|
||||
});
|
||||
if (!res.error) {
|
||||
const resData = res.data.data;
|
||||
resImg = `${fileDownloadBaseURL}/${props.projectId}/attachment/${resData.path}`;
|
||||
}
|
||||
} else {
|
||||
resImg = await uploadFile([file], false, file.type);
|
||||
}
|
||||
|
||||
if (!resImg || resImg.includes('Error')) {
|
||||
Message({
|
||||
message: '上传图片失败',
|
||||
type: 'error'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
callback({ name: file.name, imgUrl: resImg, title: file.name });
|
||||
};
|
||||
|
||||
const beforeShowHint = (val) => {
|
||||
return val.startsWith('>')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
instance
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 通过属性限制输入长度,目前只支持字符输入个数限制
|
||||
if (props.maxLength) {
|
||||
setTimeout(() => {
|
||||
const dd = document.querySelector('.dp-md-editor');
|
||||
const form_ele = dd.getElementsByTagName('textarea');
|
||||
if (form_ele?.length) {
|
||||
for (let i = 0; i < form_ele.length; i++) {
|
||||
form_ele[i].setAttribute('maxLength', props.maxLength + '');
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/css/devui_mdpreview.scss';
|
||||
.g-md-container {
|
||||
z-index: 1;
|
||||
:deep(.md-toolbar-container) {
|
||||
padding-right: 88px;
|
||||
}
|
||||
:deep(.dp-editor-md-preview-container.dp-md-view summary) {
|
||||
cursor: pointer;
|
||||
}
|
||||
position: relative;
|
||||
.g-md-preview-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
color: var(--color-lighter);
|
||||
&:hover {
|
||||
:deep(.button-content) {
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.g-md-upload-btn {
|
||||
position: absolute;
|
||||
left: 710px;
|
||||
top: 10px;
|
||||
cursor: pointer;
|
||||
:deep(.devui-icon__container) {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
:deep(.icon) {
|
||||
color: var(--devui-icon-text, #71757f);
|
||||
}
|
||||
}
|
||||
:deep(.devui-fullscreen) {
|
||||
z-index: 999 !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.dp-md-container) {
|
||||
border: none;
|
||||
.CodeMirror {
|
||||
border-radius: 3px;
|
||||
pre {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user