搜索结果列表页面开发

This commit is contained in:
付民康
2025-03-12 18:41:20 +08:00
commit 7cebe8fc00
739 changed files with 88149 additions and 0 deletions

View 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/)

View 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; // 仓库 issuemr 图片上传走单独接口
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>