新增前端vue

This commit is contained in:
2025-11-26 13:55:01 +08:00
parent ae391f1b94
commit ffd5a6ad66
781 changed files with 83348 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import { withInstall } from '@jeesite/core/utils';
import markdown from './src/Markdown.vue';
import markdownViewer from './src/MarkdownViewer.vue';
export const Markdown = withInstall(markdown);
export const MarkdownViewer = withInstall(markdownViewer);
export * from './src/typing';

View File

@@ -0,0 +1,216 @@
<template>
<div ref="wrapRef"></div>
</template>
<script lang="ts" setup>
import './adapter.js';
import type { Ref } from 'vue';
import { ref, unref, nextTick, computed, watch, onBeforeUnmount, onDeactivated, useAttrs } from 'vue';
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import { useLocale } from '@jeesite/core/locales/useLocale';
import { useModalContext } from '../../Modal';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import { onMountedOrActivated } from '@jeesite/core/hooks/core/onMountedOrActivated';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { getToken } from '@jeesite/core/utils/auth';
import { getTheme } from './getTheme';
import { buildUUID } from '@jeesite/core/utils/uuid';
type Lang = 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' | undefined;
defineOptions({ inheritAttrs: false });
const props = defineProps({
height: { type: Number, default: 360 },
value: { type: String, default: '' },
bizKey: {
type: [String, Number] as PropType<string | number>,
default: '',
},
bizType: {
type: String as PropType<string>,
default: '',
},
});
const emit = defineEmits(['change', 'get', 'update:value']);
const attrs = useAttrs();
const wrapRef = ref(null);
const vditorRef = ref(null) as Ref<Vditor | null>;
const initedRef = ref(false);
const modalFn = useModalContext();
const { getLocale } = useLocale();
const { getDarkMode } = useRootSetting();
const valueRef = ref(props.value || '');
watch(
[() => getDarkMode.value, () => initedRef.value],
([val, inited]) => {
if (!inited) {
return;
}
instance.getVditor()?.setTheme(getTheme(val) as any, getTheme(val, 'content'), getTheme(val, 'code'));
},
{
immediate: true,
flush: 'post',
},
);
watch(
() => props.value,
(v) => {
if (v !== valueRef.value) {
instance.getVditor()?.setValue(v);
}
valueRef.value = v;
},
);
const getCurrentLang = computed((): 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' => {
let lang: Lang;
switch (unref(getLocale)) {
case 'en':
lang = 'en_US';
break;
case 'ja':
lang = 'ja_JP';
break;
case 'ko':
lang = 'ko_KR';
break;
default:
lang = 'zh_CN';
}
return lang;
});
const { ctxAdminPath } = useGlobSetting();
function init() {
const wrapEl = unref(wrapRef);
if (!wrapEl) return;
const bindValue = { ...attrs, ...props };
const insEditor = new Vditor(wrapEl, {
theme: getTheme(getDarkMode.value) as any,
lang: unref(getCurrentLang),
mode: 'ir', // 编辑模式wysiwyg 所见即所得、ir 即时渲染、sv 分屏预览
fullscreen: {
index: 520,
},
preview: {
theme: {
current: getTheme(getDarkMode.value, 'content'),
},
hljs: {
style: getTheme(getDarkMode.value, 'code'),
},
actions: [],
},
toolbar: [
'emoji',
'headings',
'bold',
'italic',
'strike',
'link',
'|',
'list',
'ordered-list',
'check',
'outdent',
'indent',
'|',
'quote',
'line',
'code',
'inline-code',
'insert-before',
'insert-after',
'|',
'upload',
'record',
'table',
'|',
'undo',
'redo',
'|',
'fullscreen',
'edit-mode',
{
name: 'more',
toolbar: ['both', 'code-theme', 'content-theme', 'export', 'outline', 'preview'],
},
],
input: (v) => {
valueRef.value = v;
emit('update:value', v);
emit('change', v);
},
after: () => {
nextTick(() => {
modalFn?.redoModalHeight?.();
insEditor.setValue(valueRef.value);
vditorRef.value = insEditor;
initedRef.value = true;
emit('get', instance);
});
},
blur: () => {
//unref(vditorRef)?.setValue(props.value);
},
upload: {
max: 10 * 1024 * 1024, // 10M
url: ctxAdminPath + '/file/vditor/upload',
fieldName: 'files',
extraData: {
bizKey: String(props.bizKey),
bizType: props.bizType + '_markdown',
uploadType: 'all',
},
setHeaders: () => {
const token = getToken();
return { 'x-token': String(token || '') };
},
file: async (files: File[]) => {
const md5s: string[] = [];
for (const file of files) {
md5s.push(buildUUID()); // 专业版支持 MD5 校验
}
if (insEditor.vditor.options.upload && insEditor.vditor.options.upload.extraData) {
insEditor.vditor.options.upload.extraData.md5s = md5s.join(',');
}
return files;
},
},
...bindValue,
cache: {
enable: false,
},
});
}
const instance = {
getVditor: (): Vditor => vditorRef.value!,
};
function destroy() {
const vditorInstance = unref(vditorRef);
if (!vditorInstance) return;
try {
vditorInstance?.destroy?.();
} catch (error) {
//
}
vditorRef.value = null;
initedRef.value = false;
}
onMountedOrActivated(init);
onBeforeUnmount(destroy);
onDeactivated(destroy);
</script>

View File

@@ -0,0 +1,62 @@
<template>
<div ref="viewerRef" id="markdownViewer" :class="$props.class"></div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onDeactivated, Ref, ref, unref, watch } from 'vue';
import { onMountedOrActivated } from '@jeesite/core/hooks/core/onMountedOrActivated';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import VditorPreview from 'vditor/dist/method.min';
import { getTheme } from './getTheme';
const props = defineProps({
value: { type: String },
class: { type: String },
});
const viewerRef = ref(null);
const vditorPreviewRef = ref(null) as Ref<VditorPreview | null>;
const { getDarkMode } = useRootSetting();
function init() {
const viewerEl = unref(viewerRef);
vditorPreviewRef.value = VditorPreview.preview(viewerEl, props.value, {
mode: getTheme(getDarkMode.value, 'content'),
theme: {
current: getTheme(getDarkMode.value, 'content'),
},
hljs: {
style: getTheme(getDarkMode.value, 'code'),
},
});
}
watch(
() => getDarkMode.value,
(val) => {
VditorPreview.setContentTheme(getTheme(val, 'content'));
VditorPreview.setCodeTheme(getTheme(val, 'code'));
init();
},
);
watch(
() => props.value,
(v, oldValue) => {
v !== oldValue && init();
},
);
function destroy() {
const vditorInstance = unref(vditorPreviewRef);
if (!vditorInstance) return;
try {
vditorInstance?.destroy?.();
} catch (error) {
//
}
vditorPreviewRef.value = null;
}
onMountedOrActivated(init);
onBeforeUnmount(destroy);
onDeactivated(destroy);
</script>

View File

@@ -0,0 +1,24 @@
// https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
// 推荐使用处理了约束的 adapter.js polyfill 来替代。
// 浏览器过老或过新都可能不存在
if (!navigator.mediaDevices) {
navigator.mediaDevices = {};
// 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
// 因为这样可能会覆盖已有的属性。这里我们只会在没有 getUserMedia 属性的时候添加它。
if (!navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia = function (constraints) {
// 首先,如果有 getUserMedia 的话,就获得它
const getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
// 一些浏览器根本没实现它 - 那么就返回一个 error 到 promise 的 reject 来保持一个统一的接口
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
}
// 否则,为老的 navigator.getUserMedia 方法包裹一个 Promise
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
};
}
}

View File

@@ -0,0 +1,19 @@
/**
* 获取主题类型 深色浅色模式 对应的值
* @param darkModeVal 深色模式值
* @param themeMode 主题类型——外观(默认), 内容, 代码块
*/
export const getTheme = (
darkModeVal: 'light' | 'dark' | string,
themeMode: 'default' | 'content' | 'code' = 'default',
) => {
const isDark = darkModeVal === 'dark';
switch (themeMode) {
case 'default':
return isDark ? 'dark' : 'classic';
case 'content':
return isDark ? 'dark' : 'light';
case 'code':
return isDark ? 'dracula' : 'github';
}
};

View File

@@ -0,0 +1,5 @@
import Vditor from 'vditor';
export interface MarkDownActionType {
getVditor: () => Vditor;
}