新增:开发者中心功能、md编辑器等。
This commit is contained in:
@@ -12,7 +12,7 @@ const config = useConfigInfo();
|
||||
<div class="text-base text-gray-400 hover:text-gray-200 py-3 font-bold">{{ item.title }}</div>
|
||||
<div class="sub-menu flex flex-col">
|
||||
<template v-for="(sub,subIndex) in item.children">
|
||||
<nuxt-link :to="navTo(sub)" :target="sub.target" class="py-1 text-sm"><span class="text-gray-400 hover:text-gray-200">{{ sub.title }}</span></nuxt-link>
|
||||
<nuxt-link :to="navTo(sub)" class="py-1 text-sm"><span class="text-gray-400 hover:text-gray-200">{{ sub.title }}</span></nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
<el-dropdown @command="handleCommand">
|
||||
<el-space class="flex items-center cursor-pointer">
|
||||
<el-avatar class="cursor-pointer" :src="user?.avatar" :size="30" />
|
||||
<span>{{ user?.tenantName }}</span>
|
||||
<span>{{ user?.nickname }}</span>
|
||||
</el-space>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="user"><nuxt-link to="/user">用户中心</nuxt-link></el-dropdown-item>
|
||||
<!-- <el-dropdown-item command="password"><nuxt-link to="/user/password">修改密码</nuxt-link></el-dropdown-item>-->
|
||||
<el-dropdown-item command="password"><nuxt-link to="/user/password">修改密码</nuxt-link></el-dropdown-item>
|
||||
<el-dropdown-item command="auth"><nuxt-link to="/user/auth">实名认证</nuxt-link></el-dropdown-item>
|
||||
<el-dropdown-item divided command="order"><nuxt-link to="/user/order">我的订单</nuxt-link></el-dropdown-item>
|
||||
<el-dropdown-item divided command="logOut"><nuxt-link to="/user/logout">退出登录</nuxt-link>
|
||||
|
||||
109
components/ByteMdEditor/index.vue
Normal file
109
components/ByteMdEditor/index.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<!-- markdown 编辑器 -->
|
||||
<template>
|
||||
<div ref="rootRef" class="ele-bytemd-wrap"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { Editor } from 'bytemd';
|
||||
import type { BytemdPlugin, BytemdLocale, ViewerProps } from 'bytemd';
|
||||
import 'bytemd/dist/index.min.css';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: string;
|
||||
plugins?: BytemdPlugin[];
|
||||
sanitize?: (schema: any) => any;
|
||||
mode?: 'split' | 'tab' | 'auto';
|
||||
previewDebounce?: number;
|
||||
placeholder?: string;
|
||||
editorConfig?: Record<string, any>;
|
||||
locale?: Partial<BytemdLocale>;
|
||||
uploadImages?: (
|
||||
files: File[]
|
||||
) => Promise<Pick<any, 'url' | 'alt' | 'title'>[]>;
|
||||
overridePreview?: (el: HTMLElement, props: ViewerProps) => void;
|
||||
maxLength?: number;
|
||||
height?: string;
|
||||
fullZIndex?: number;
|
||||
}>(),
|
||||
{
|
||||
fullZIndex: 999
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value?: string): void;
|
||||
(e: 'change', value?: string): void;
|
||||
}>();
|
||||
|
||||
const rootRef = ref<HTMLElement | null>(null);
|
||||
const editor = ref<InstanceType<typeof Editor> | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
editor.value = new Editor({
|
||||
target: rootRef.value as HTMLElement,
|
||||
props
|
||||
});
|
||||
editor.value.$on('change', (e: any) => {
|
||||
emit('update:value', e.detail.value);
|
||||
emit('change', e.detail.value);
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
() => props.value,
|
||||
() => props.plugins,
|
||||
() => props.sanitize,
|
||||
() => props.mode,
|
||||
() => props.previewDebounce,
|
||||
() => props.placeholder,
|
||||
() => props.editorConfig,
|
||||
() => props.locale,
|
||||
() => props.uploadImages,
|
||||
() => props.maxLength
|
||||
],
|
||||
() => {
|
||||
const option = { ...props };
|
||||
for (let key in option) {
|
||||
if (typeof option[key] === 'undefined') {
|
||||
delete option[key];
|
||||
}
|
||||
}
|
||||
editor.value?.$set(option);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 修改编辑器高度
|
||||
.ele-bytemd-wrap :deep(.bytemd) {
|
||||
height: v-bind(height);
|
||||
|
||||
// 修改全屏的 zIndex
|
||||
&.bytemd-fullscreen {
|
||||
z-index: v-bind(fullZIndex);
|
||||
}
|
||||
|
||||
// 去掉默认的最大宽度限制
|
||||
.CodeMirror .CodeMirror-lines {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre.CodeMirror-line,
|
||||
pre.CodeMirror-line-like {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
max-width: 100%;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
// 去掉 github 图标
|
||||
.bytemd-toolbar-right > .bytemd-toolbar-icon:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,98 +0,0 @@
|
||||
<template>
|
||||
<div class="xl:w-screen-xl m-auto py-4">
|
||||
<div v-if="title" class="text-center flex flex-col items-center z-100 relative">
|
||||
<h2 class="text-4xl font-bold tracking-tight text-gray-500 dark:text-white">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<div class="sub-title">
|
||||
<p class="text-gray-500 dark:text-gray-400 py-3">
|
||||
{{ comments }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-row :gutter="24" id="container" class="clearfix">
|
||||
<el-col v-for="(item,index) in list" :key="index" :span="6" class="left mb-8">
|
||||
<el-card shadow="hover" :body-style="{ padding: '0px' }" class=" hover:bg-gray-50 cursor-pointer" @click="navigateTo(`/market/${item.websiteId}.html`)">
|
||||
<div class="flex-1 px-4 py-5 sm:p-4 !p-4">
|
||||
<div class="text-gray-700 dark:text-white text-base font-semibold flex gap-1.5">
|
||||
<el-avatar
|
||||
:src="item.websiteLogo" shape="square" :size="55" style="background-color: white;"/>
|
||||
<div class="flex-1 text-lg cursor-pointer flex flex-col">
|
||||
{{ item.websiteName }}
|
||||
<div class="flex justify-between items-center">
|
||||
<sapn class="text-xs text-gray-400 font-normal line-clamp-1">{{ item.comments || '暂无描述' }}</sapn>
|
||||
<el-button size="small" round>获取</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-image pt-3">
|
||||
<el-image v-if="item.files" :src="`${JSON.parse(item.files)[0].url}`" class="w-full h-1/2 max-h-[220px]" />
|
||||
<el-image v-else class="w-full h-[220px]" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-row :gutter="24" class="flex w-full">
|
||||
<el-col v-for="(item,index) in list" :key="index" :span="6" :xs="24" class="mb-6">
|
||||
<nuxt-link :to="item.path">
|
||||
<el-card shadow="hover" :body-style="{ padding: '0px' }" class="hover:bg-green-50 cursor-pointer h-[200px] flex items-center justify-center">
|
||||
<div class="flex-1 px-4 py-5 sm:p-6 !p-4">
|
||||
<div class="text-gray-700 dark:text-white text-base font-semibold flex flex-col gap-1.5">
|
||||
<el-space class="flex-1 cursor-pointer flex items-center flex-col">
|
||||
<el-image v-if="item.icon" :src="item.icon" class="w-[40px]" />
|
||||
<span class="py-3 text-lg">{{ item.title }}</span>
|
||||
</el-space>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</nuxt-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div v-if="disabled" class="px-1 text-center text-gray-500 min-h-xs">
|
||||
没有更多了
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {loginAdminByToken} from "~/utils/common";
|
||||
import {useServerRequest} from "~/composables/useServerRequest";
|
||||
import type {ApiResult, PageResult} from "~/api";
|
||||
import type {Company, CompanyParam} from "~/api/system/company/model";
|
||||
import {pageCompanyAll} from "~/api/system/company";
|
||||
import {listCmsNavigation} from "~/api/cms/cmsNavigation";
|
||||
import type {CmsNavigation} from "~/api/cms/cmsNavigation/model";
|
||||
import {navigateTo} from "#imports";
|
||||
import type {CmsWebsite} from "~/api/cms/cmsWebsite/model";
|
||||
import {listWebsite} from "~/api/system/website";
|
||||
import {listCmsWebsite, pageCmsWebsiteAll} from "~/api/cms/cmsWebsite";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
param?: CompanyParam;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
comments?: string;
|
||||
fit?: any;
|
||||
}>(),
|
||||
{
|
||||
fit: 'cover'
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
const list = ref<CmsWebsite[]>([]);
|
||||
|
||||
// 请求数据
|
||||
const reload = async () => {
|
||||
pageCmsWebsiteAll({
|
||||
}).then(data => {
|
||||
list.value = data?.list || [];
|
||||
})
|
||||
}
|
||||
reload();
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<header class="clearfix flex justify-between items-center w-full">
|
||||
<div class="logo" :class="logo?.style">
|
||||
<nuxt-link :to="`/m`" target="_top"><img :src="`${logo?.value }`" /></nuxt-link>
|
||||
<nuxt-link :to="`/m`" target="_top"><el-image :src="logo?.value" class=" rounded-sm rounded-sm w-[107px] h-[40px]"/></nuxt-link>
|
||||
</div>
|
||||
<div class="sub-menu">
|
||||
<el-icon color="black" :size="20" @click="drawer = true"><Fold /></el-icon>
|
||||
@@ -49,14 +49,14 @@
|
||||
<script setup lang="ts">
|
||||
// 引入所需的图标
|
||||
import {
|
||||
useConfigInfo,
|
||||
useConfigInfo, useLogo,
|
||||
useMenu,
|
||||
useToken,
|
||||
useUser, useWebsite
|
||||
} from "~/composables/configState";
|
||||
import {getPath, isMobileDevice, navTo} from "#imports";
|
||||
import { Search, Expand, Fold } from '@element-plus/icons-vue'
|
||||
import {configWebsiteField} from "~/api/cms/cmsWebsiteField";
|
||||
import {configWebsiteField, listCmsWebsiteField} from "~/api/cms/cmsWebsiteField";
|
||||
import {getSiteInfo, getUserInfo} from "~/api/layout";
|
||||
import {listCmsLangLog} from "~/api/cms/cmsLangLog";
|
||||
import type {CmsLangLog} from "~/api/cms/cmsLangLog/model";
|
||||
@@ -71,7 +71,7 @@ const i18n = useI18n();
|
||||
const isMobile = ref<boolean>(false);
|
||||
const langList = ref<CmsLangLog[]>([]);
|
||||
const drawer = ref<boolean>(false);
|
||||
const logo = ref<CmsWebsiteField>();
|
||||
const logo = useLogo()
|
||||
const keyword = ref();
|
||||
|
||||
const setNav = () => {
|
||||
@@ -102,9 +102,12 @@ const reload = async () => {
|
||||
langList.value = list;
|
||||
})
|
||||
|
||||
// TODO 读取logo
|
||||
getWebsiteField(13686).then(data => {
|
||||
logo.value = data;
|
||||
listCmsWebsiteField({
|
||||
name: 'siteLogo'
|
||||
}).then(data => {
|
||||
if(data[0]){
|
||||
logo.value = data[0]
|
||||
}
|
||||
})
|
||||
|
||||
// TODO 是否跳转H5版
|
||||
|
||||
83
components/SiteList.vue
Normal file
83
components/SiteList.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="xl:w-screen-xl m-auto py-4">
|
||||
<div v-if="title" class="text-center flex flex-col items-center z-100 relative">
|
||||
<h2 class="text-4xl font-bold tracking-tight text-gray-500 dark:text-white">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<div class="sub-title">
|
||||
<p class="text-gray-500 dark:text-gray-400 py-3">
|
||||
{{ comments }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-row :gutter="24" id="container" class="clearfix">
|
||||
<el-col v-for="(item,index) in list" :key="index" :span="6" class="left mb-8">
|
||||
<el-card shadow="hover" :body-style="{ padding: '0px' }" class="h-[200px] items-center flex justify-center" @mouseover="showMenu(item)" @mouseleave="hideMenu">
|
||||
<div class="flex-1 px-4 py-5 sm:p-4 !p-4">
|
||||
<div class="text-gray-700 dark:text-white text-base font-semibold flex flex-col items-center gap-1.5">
|
||||
<el-avatar
|
||||
:src="item.websiteLogo" shape="square" :size="55" style="background-color: white;"/>
|
||||
<div class="flex-1 cursor-pointer flex flex-col text-center">
|
||||
<nuxt-link :to="`https://${item.websiteCode}.websoft.top`" class="text-lg">{{ item.websiteName }}</nuxt-link>
|
||||
<div v-if="id == item.websiteId" class="flex text-gray-400 text-sm font-normal py2 justify-between items-center">
|
||||
<div>
|
||||
<nuxt-link :to="`https://websoft.top/docs?appid=${item.websiteId}`"><span class="text-gray-400 hover:text-green-700">开发文档</span></nuxt-link>
|
||||
<el-divider direction="vertical" />
|
||||
<nuxt-link :to="`https://websoft.top/docs?appid=${item.websiteId}`"><span class="text-gray-400 hover:text-green-700">社区</span></nuxt-link>
|
||||
<el-divider direction="vertical" />
|
||||
<nuxt-link :to="`https://websoft.top/market?appid=${item.websiteId}`"><span class="text-gray-400 hover:text-green-700">插件市场</span></nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div v-if="disabled" class="px-1 text-center text-gray-500 min-h-xs">
|
||||
没有更多了
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {navigateTo} from "#imports";
|
||||
import type {CmsWebsite, CmsWebsiteParam} from "~/api/cms/cmsWebsite/model";
|
||||
import {pageCmsWebsiteAll} from "~/api/cms/cmsWebsite";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
param?: CmsWebsiteParam;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
comments?: string;
|
||||
fit?: any;
|
||||
}>(),
|
||||
{
|
||||
fit: 'cover'
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
const list = ref<CmsWebsite[]>([]);
|
||||
const id = ref<number>(0);
|
||||
|
||||
const showMenu = (item: CmsWebsite) => {
|
||||
id.value = Number(item.websiteId);
|
||||
};
|
||||
|
||||
const hideMenu = () => {
|
||||
id.value = 0;
|
||||
};
|
||||
|
||||
// 请求数据
|
||||
const reload = async () => {
|
||||
pageCmsWebsiteAll(props.param).then(data => {
|
||||
list.value = data?.list || [];
|
||||
})
|
||||
}
|
||||
reload();
|
||||
</script>
|
||||
242
components/TinymceEditor/index.vue
Normal file
242
components/TinymceEditor/index.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<!-- 富文本编辑器 -->
|
||||
<template>
|
||||
<component v-if="inlineEditor" :is="tagName" :id="elementId" />
|
||||
<textarea v-else :id="elementId"></textarea>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
watch,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
nextTick,
|
||||
useAttrs
|
||||
} from 'vue';
|
||||
import tinymce from 'tinymce/tinymce';
|
||||
import type {
|
||||
Editor as TinyMCEEditor,
|
||||
EditorEvent,
|
||||
RawEditorSettings
|
||||
} from 'tinymce';
|
||||
import 'tinymce/themes/silver';
|
||||
import 'tinymce/icons/default';
|
||||
import 'tinymce/plugins/code';
|
||||
import 'tinymce/plugins/preview';
|
||||
import 'tinymce/plugins/fullscreen';
|
||||
import 'tinymce/plugins/paste';
|
||||
import 'tinymce/plugins/searchreplace';
|
||||
import 'tinymce/plugins/save';
|
||||
import 'tinymce/plugins/autosave';
|
||||
import 'tinymce/plugins/link';
|
||||
import 'tinymce/plugins/autolink';
|
||||
import 'tinymce/plugins/image';
|
||||
import 'tinymce/plugins/media';
|
||||
import 'tinymce/plugins/table';
|
||||
import 'tinymce/plugins/codesample';
|
||||
import 'tinymce/plugins/lists';
|
||||
import 'tinymce/plugins/advlist';
|
||||
import 'tinymce/plugins/hr';
|
||||
import 'tinymce/plugins/charmap';
|
||||
import 'tinymce/plugins/emoticons';
|
||||
import 'tinymce/plugins/anchor';
|
||||
import 'tinymce/plugins/directionality';
|
||||
import 'tinymce/plugins/pagebreak';
|
||||
import 'tinymce/plugins/quickbars';
|
||||
import 'tinymce/plugins/nonbreaking';
|
||||
import 'tinymce/plugins/visualblocks';
|
||||
import 'tinymce/plugins/visualchars';
|
||||
import 'tinymce/plugins/wordcount';
|
||||
import 'tinymce/plugins/emoticons/js/emojis';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
DARK_CONFIG,
|
||||
uuid,
|
||||
bindHandlers,
|
||||
openAlert
|
||||
} from './util';
|
||||
import type { AlertOption } from './util';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 编辑器唯一 id
|
||||
id?: string;
|
||||
// v-model
|
||||
value?: string;
|
||||
// 编辑器配置
|
||||
init?: RawEditorSettings;
|
||||
// 是否内联模式
|
||||
inline?: boolean;
|
||||
// model events
|
||||
modelEvents?: string;
|
||||
// 内联模式标签名
|
||||
tagName?: string;
|
||||
// 是否禁用
|
||||
disabled?: boolean;
|
||||
// 是否跟随框架主题
|
||||
autoTheme?: boolean;
|
||||
// 不跟随框架主题时是否使用暗黑主题
|
||||
darkTheme?: boolean;
|
||||
}>(),
|
||||
{
|
||||
inline: false,
|
||||
modelEvents: 'change input undo redo',
|
||||
tagName: 'div',
|
||||
autoTheme: true
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string): void;
|
||||
}>();
|
||||
|
||||
const attrs = useAttrs();
|
||||
const themeStore = useThemeStore();
|
||||
const { darkMode } = storeToRefs(themeStore);
|
||||
|
||||
// 编辑器唯一 id
|
||||
const elementId: string = props.id || uuid('tiny-vue');
|
||||
|
||||
// 编辑器实例
|
||||
let editorIns: TinyMCEEditor | null = null;
|
||||
|
||||
// 是否内联模式
|
||||
const inlineEditor: boolean = props.init?.inline || props.inline;
|
||||
|
||||
/* 更新 value */
|
||||
const updateValue = (value: string) => {
|
||||
emit('update:value', value);
|
||||
};
|
||||
|
||||
/* 修改内容 */
|
||||
const setContent = (value?: string) => {
|
||||
if (
|
||||
editorIns &&
|
||||
typeof value === 'string' &&
|
||||
value !== editorIns.getContent()
|
||||
) {
|
||||
editorIns.setContent(value);
|
||||
}
|
||||
};
|
||||
|
||||
/* 渲染编辑器 */
|
||||
const render = () => {
|
||||
const isDark = props.autoTheme ? darkMode.value : props.darkTheme;
|
||||
tinymce.init({
|
||||
...DEFAULT_CONFIG,
|
||||
...(isDark ? DARK_CONFIG : {}),
|
||||
...props.init,
|
||||
selector: `#${elementId}`,
|
||||
readonly: props.disabled,
|
||||
inline: inlineEditor,
|
||||
setup: (editor: TinyMCEEditor) => {
|
||||
editorIns = editor;
|
||||
editor.on('init', (e: EditorEvent<any>) => {
|
||||
// 回显初始值
|
||||
if (props.value) {
|
||||
setContent(props.value);
|
||||
}
|
||||
// v-model
|
||||
editor.on(props.modelEvents, () => {
|
||||
updateValue(editor.getContent());
|
||||
});
|
||||
// valid events
|
||||
bindHandlers(e, attrs, editor);
|
||||
});
|
||||
if (typeof props.init?.setup === 'function') {
|
||||
props.init.setup(editor);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 销毁编辑器 */
|
||||
const destory = () => {
|
||||
if (tinymce != null && editorIns != null) {
|
||||
tinymce.remove(editorIns as any);
|
||||
editorIns = null;
|
||||
}
|
||||
};
|
||||
|
||||
/* 弹出提示框 */
|
||||
const alert = (option?: AlertOption) => {
|
||||
openAlert(editorIns, option);
|
||||
};
|
||||
|
||||
defineExpose({ editorIns, alert });
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(val: string, prevVal: string) => {
|
||||
if (val !== prevVal) {
|
||||
setContent(val);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(disable) => {
|
||||
if (editorIns !== null) {
|
||||
if (typeof editorIns.mode?.set === 'function') {
|
||||
editorIns.mode.set(disable ? 'readonly' : 'design');
|
||||
} else {
|
||||
editorIns.setMode(disable ? 'readonly' : 'design');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.tagName,
|
||||
() => {
|
||||
destory();
|
||||
nextTick(() => {
|
||||
render();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
watch(darkMode, () => {
|
||||
if (props.autoTheme) {
|
||||
destory();
|
||||
nextTick(() => {
|
||||
render();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destory();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
destory();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body .tox-tinymce-aux {
|
||||
z-index: 19990000;
|
||||
}
|
||||
|
||||
textarea[id^='tiny-vue'] {
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
248
components/TinymceEditor/util.ts
Normal file
248
components/TinymceEditor/util.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import type {
|
||||
Editor as TinyMCEEditor,
|
||||
EditorEvent,
|
||||
RawEditorSettings
|
||||
} from 'tinymce';
|
||||
const BASE_URL = import.meta.env.BASE_URL;
|
||||
|
||||
// 默认加载插件
|
||||
const PLUGINS: string = [
|
||||
'code',
|
||||
'preview',
|
||||
'fullscreen',
|
||||
'paste',
|
||||
'searchreplace',
|
||||
'save',
|
||||
'autosave',
|
||||
'link',
|
||||
'autolink',
|
||||
'image',
|
||||
'media',
|
||||
'table',
|
||||
'codesample',
|
||||
'lists',
|
||||
'advlist',
|
||||
'hr',
|
||||
'charmap',
|
||||
'emoticons',
|
||||
'anchor',
|
||||
'directionality',
|
||||
'pagebreak',
|
||||
'quickbars',
|
||||
'nonbreaking',
|
||||
'visualblocks',
|
||||
'visualchars',
|
||||
'wordcount'
|
||||
].join(' ');
|
||||
|
||||
// 默认工具栏布局
|
||||
const TOOLBAR: string = [
|
||||
'fullscreen',
|
||||
'preview',
|
||||
'code',
|
||||
'codesample',
|
||||
'emoticons',
|
||||
'image',
|
||||
'media',
|
||||
'|',
|
||||
'undo',
|
||||
'redo',
|
||||
'|',
|
||||
'forecolor',
|
||||
'backcolor',
|
||||
'|',
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'strikethrough',
|
||||
'|',
|
||||
'alignleft',
|
||||
'aligncenter',
|
||||
'alignright',
|
||||
'alignjustify',
|
||||
'|',
|
||||
'outdent',
|
||||
'indent',
|
||||
'|',
|
||||
'numlist',
|
||||
'bullist',
|
||||
'|',
|
||||
'formatselect',
|
||||
'fontselect',
|
||||
'fontsizeselect',
|
||||
'|',
|
||||
'link',
|
||||
'charmap',
|
||||
'anchor',
|
||||
'pagebreak',
|
||||
'|',
|
||||
'ltr',
|
||||
'rtl'
|
||||
].join(' ');
|
||||
|
||||
// 默认配置
|
||||
export const DEFAULT_CONFIG: RawEditorSettings = {
|
||||
height: 300,
|
||||
branding: false,
|
||||
skin_url: BASE_URL + 'tinymce/skins/ui/oxide',
|
||||
content_css: BASE_URL + 'tinymce/skins/content/default/content.min.css',
|
||||
language_url: BASE_URL + 'tinymce/langs/zh_CN.js',
|
||||
language: 'zh_CN',
|
||||
plugins: PLUGINS,
|
||||
toolbar: TOOLBAR,
|
||||
draggable_modal: true,
|
||||
toolbar_mode: 'sliding',
|
||||
quickbars_insert_toolbar: '',
|
||||
images_upload_handler: (blobInfo: any, success: any, error: any) => {
|
||||
if (blobInfo.blob().size / 1024 > 400) {
|
||||
error('大小不能超过 400KB');
|
||||
return;
|
||||
}
|
||||
success('data:image/jpeg;base64,' + blobInfo.base64());
|
||||
},
|
||||
file_picker_types: 'media',
|
||||
file_picker_callback: () => {}
|
||||
};
|
||||
|
||||
// 暗黑主题配置
|
||||
export const DARK_CONFIG: RawEditorSettings = {
|
||||
skin_url: BASE_URL + 'tinymce/skins/ui/oxide-dark',
|
||||
content_css: BASE_URL + 'tinymce/skins/content/dark/content.min.css'
|
||||
};
|
||||
|
||||
// 支持监听的事件
|
||||
export const VALID_EVENTS = [
|
||||
'onActivate',
|
||||
'onAddUndo',
|
||||
'onBeforeAddUndo',
|
||||
'onBeforeExecCommand',
|
||||
'onBeforeGetContent',
|
||||
'onBeforeRenderUI',
|
||||
'onBeforeSetContent',
|
||||
'onBeforePaste',
|
||||
'onBlur',
|
||||
'onChange',
|
||||
'onClearUndos',
|
||||
'onClick',
|
||||
'onContextMenu',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDblclick',
|
||||
'onDeactivate',
|
||||
'onDirty',
|
||||
'onDrag',
|
||||
'onDragDrop',
|
||||
'onDragEnd',
|
||||
'onDragGesture',
|
||||
'onDragOver',
|
||||
'onDrop',
|
||||
'onExecCommand',
|
||||
'onFocus',
|
||||
'onFocusIn',
|
||||
'onFocusOut',
|
||||
'onGetContent',
|
||||
'onHide',
|
||||
'onInit',
|
||||
'onKeyDown',
|
||||
'onKeyPress',
|
||||
'onKeyUp',
|
||||
'onLoadContent',
|
||||
'onMouseDown',
|
||||
'onMouseEnter',
|
||||
'onMouseLeave',
|
||||
'onMouseMove',
|
||||
'onMouseOut',
|
||||
'onMouseOver',
|
||||
'onMouseUp',
|
||||
'onNodeChange',
|
||||
'onObjectResizeStart',
|
||||
'onObjectResized',
|
||||
'onObjectSelected',
|
||||
'onPaste',
|
||||
'onPostProcess',
|
||||
'onPostRender',
|
||||
'onPreProcess',
|
||||
'onProgressState',
|
||||
'onRedo',
|
||||
'onRemove',
|
||||
'onReset',
|
||||
'onSaveContent',
|
||||
'onSelectionChange',
|
||||
'onSetAttrib',
|
||||
'onSetContent',
|
||||
'onShow',
|
||||
'onSubmit',
|
||||
'onUndo',
|
||||
'onVisualAid'
|
||||
];
|
||||
|
||||
let unique = 0;
|
||||
|
||||
/**
|
||||
* 生成编辑器 id
|
||||
*/
|
||||
export function uuid(prefix: string): string {
|
||||
const time = Date.now();
|
||||
const random = Math.floor(Math.random() * 1000000000);
|
||||
unique++;
|
||||
return prefix + '_' + random + unique + String(time);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定事件
|
||||
*/
|
||||
export function bindHandlers(
|
||||
initEvent: EditorEvent<any>,
|
||||
listeners: Record<string, any>,
|
||||
editor: TinyMCEEditor
|
||||
): void {
|
||||
const validEvents = VALID_EVENTS.map((event) => event.toLowerCase());
|
||||
Object.keys(listeners)
|
||||
.filter((key: string) => validEvents.includes(key.toLowerCase()))
|
||||
.forEach((key: string) => {
|
||||
const handler = listeners[key];
|
||||
if (typeof handler === 'function') {
|
||||
if (key === 'onInit') {
|
||||
handler(initEvent, editor);
|
||||
} else {
|
||||
editor.on(key.substring(2), (e: EditorEvent<any>) =>
|
||||
handler(e, editor)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出提示框
|
||||
*/
|
||||
export function openAlert(
|
||||
editor: TinyMCEEditor | null,
|
||||
option: AlertOption = {}
|
||||
) {
|
||||
editor?.windowManager?.open({
|
||||
title: option.title ?? '提示',
|
||||
body: {
|
||||
type: 'panel',
|
||||
items: [
|
||||
{
|
||||
type: 'htmlpanel',
|
||||
html: `<p>${option.content ?? ''}</p>`
|
||||
}
|
||||
]
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
type: 'cancel',
|
||||
name: 'closeButton',
|
||||
text: '确定',
|
||||
primary: true
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
export interface AlertOption {
|
||||
title?: string;
|
||||
content?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user