u
This commit is contained in:
commit
c570ec514c
550 files changed
+152597
No files matched your search
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="layout-main">
|
||||
<div class="layout-sidebar">
|
||||
<slot name="sidebar"></slot>
|
||||
</div>
|
||||
<div class="layout-content">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style scoped>
|
||||
.layout-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
.layout-sidebar {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
z-index: 2;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.layout-sidebar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.layout-content {
|
||||
flex: 1;
|
||||
background-color: #ffffff;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div>
|
||||
<HugerteEditor ref="editorRef" :init="init" v-model="modelValue" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import HugerteEditor from "@/components/hugerte-editor/index";
|
||||
import { uploadTempFile } from "@/service/email";
|
||||
const editorRef = ref<any>(null);
|
||||
const modelValue = defineModel();
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: Number,
|
||||
default: 600
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请输入内容..."
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(["upload-image"]);
|
||||
|
||||
function getPlainText() {
|
||||
const editor = editorRef.value?.getEditor?.();
|
||||
return editor?.getContent({ format: "text" }) ?? "";
|
||||
}
|
||||
|
||||
function getHtml() {
|
||||
const editor = editorRef.value?.getEditor?.();
|
||||
return editor?.getContent() ?? "";
|
||||
}
|
||||
function clear() {
|
||||
const editor = editorRef.value?.getEditor?.();
|
||||
if (editor) {
|
||||
editor.setContent(""); // 清空内容
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getHtml,
|
||||
getPlainText,
|
||||
clear
|
||||
});
|
||||
|
||||
const init = {
|
||||
language: "zh_CN",
|
||||
height: props.height,
|
||||
placeholder: props.placeholder,
|
||||
menubar: false,
|
||||
statusbar: false,
|
||||
plugins: "accordion advlist anchor formatpainter fullscreen image link lists preview table searchreplace ",
|
||||
font_size_formats: "12px 14px 16px 18px 20px 22px 24px 26px 28px 30px 32px",
|
||||
font_family_formats:
|
||||
"PingFang SC;微软雅黑='微软雅黑';宋体='宋体';黑体='黑体';仿宋='仿宋';楷体='楷体';隶书='隶书';幼圆='幼圆';Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,;sans-serif",
|
||||
toolbar:
|
||||
"undo redo formatpainter removeformat | " +
|
||||
"blocks fontfamily fontsize | " +
|
||||
"bold italic underline strikethrough superscript subscript | forecolor backcolor |hr blockquote | " +
|
||||
"align lineheight | bullist numlist indent outdent table|" +
|
||||
"link customImage| searchreplace preview fullscreen print",
|
||||
formats: {
|
||||
blockquote: {
|
||||
block: "blockquote",
|
||||
styles: {
|
||||
"border-left": "2px solid #ccc",
|
||||
margin: "1em 0",
|
||||
"padding-left": "1em"
|
||||
}
|
||||
}
|
||||
},
|
||||
body_class: "my-editor-body",
|
||||
toolbar_mode: "wrap",
|
||||
setup: function (editor: any) {
|
||||
function applyTableInlineStyles() {
|
||||
const body = editor.getBody();
|
||||
if (!body) return;
|
||||
|
||||
body.querySelectorAll("table").forEach((table: HTMLTableElement) => {
|
||||
table.style.width = "100%";
|
||||
table.style.borderCollapse = "collapse";
|
||||
table.style.border = "1px solid #ccc";
|
||||
});
|
||||
|
||||
body.querySelectorAll("th").forEach((th: HTMLTableElement) => {
|
||||
th.style.border = "1px solid #ccc";
|
||||
th.style.padding = "8px";
|
||||
th.style.backgroundColor = "#f2f2f2";
|
||||
th.style.textAlign = "left";
|
||||
});
|
||||
|
||||
body.querySelectorAll("td").forEach((td: HTMLTableElement) => {
|
||||
td.style.border = "1px solid #ccc";
|
||||
td.style.padding = "8px";
|
||||
td.style.wordBreak = "break-word"; // 更合理
|
||||
td.style.whiteSpace = "normal";
|
||||
});
|
||||
}
|
||||
|
||||
// 只在执行“插入表格”命令时添加样式
|
||||
editor.on("ExecCommand", (e: any) => {
|
||||
if (e.command === "mceInsertTable") {
|
||||
// 延迟一下让表格插入完毕
|
||||
setTimeout(() => {
|
||||
applyTableInlineStyles();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
editor.ui.registry.addButton("customImage", {
|
||||
icon: "image",
|
||||
tooltip: "上传图片",
|
||||
onAction: () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.click();
|
||||
|
||||
input.onchange = async () => {
|
||||
const files = input.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
|
||||
try {
|
||||
const res = await uploadTempFile(file);
|
||||
|
||||
emit("upload-image", res.data);
|
||||
|
||||
editor.insertContent(`<img src="${res.data.fileUrl}" />`);
|
||||
} catch (err) {
|
||||
console.error("图片上传失败", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-iframe {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<SvgIcon :local-icon="localIcon" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
email: String
|
||||
});
|
||||
|
||||
const localIcon = computed(() => {
|
||||
const emailType = props.email?.split("@")[1] || "";
|
||||
switch (emailType) {
|
||||
case "163.com":
|
||||
return "email-163";
|
||||
case "126.com":
|
||||
return "email-126";
|
||||
case "qq.com":
|
||||
return "email-qq";
|
||||
case "gmail.com":
|
||||
return "email-gmail";
|
||||
case "outlook.com":
|
||||
return "email-outlook";
|
||||
case "sina.com":
|
||||
return "email-sina";
|
||||
default:
|
||||
return "email";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div v-if="visible" class="init-overlay">
|
||||
<div class="init-modal">
|
||||
<div class="init-title">{{ title }}</div>
|
||||
<div class="init-message">{{ content }}</div>
|
||||
<div class="progress-container">
|
||||
<a-progress :percent="percent" size="large" :status="status" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String as PropType<"normal" | "success" | "warning" | "danger">,
|
||||
default: "normal"
|
||||
},
|
||||
percent: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
// 当模态框关闭时,重置百分比或执行其他操作
|
||||
console.log("模态框关闭");
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.init-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9;
|
||||
animation: fadeIn 0.5s ease-in-out; /* 添加渐现动画 */
|
||||
}
|
||||
|
||||
.init-modal {
|
||||
background-color: white;
|
||||
padding: 30px 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
width: 600px;
|
||||
max-width: 90%;
|
||||
text-align: center;
|
||||
animation: scaleUp 0.5s ease-in-out; /* 添加缩放动画 */
|
||||
}
|
||||
|
||||
/* 渐现动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 缩放动画 */
|
||||
@keyframes scaleUp {
|
||||
from {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.init-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.init-message {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 14px;
|
||||
min-height: 24px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.init-status {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div ref="sandboxRef" class="email-renderer"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from "vue";
|
||||
import { useRouterStore } from "@/store/modules/router";
|
||||
|
||||
const routerStore = useRouterStore();
|
||||
|
||||
const props = defineProps<{
|
||||
html: string;
|
||||
}>();
|
||||
|
||||
const sandboxRef = ref<HTMLDivElement | null>(null);
|
||||
let shadowRoot: ShadowRoot | null = null;
|
||||
|
||||
function sanitizeHtml(html: string): string {
|
||||
// 简单去除 <script> 标签及其内容
|
||||
return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.html,
|
||||
async (newHtml) => {
|
||||
if (!sandboxRef.value) return;
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (!shadowRoot) {
|
||||
shadowRoot = sandboxRef.value.attachShadow({ mode: "open" });
|
||||
|
||||
// 监听shadowRoot里的点击事件,事件委托拦截a标签
|
||||
shadowRoot.addEventListener("click", (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const anchor = target.closest("a");
|
||||
if (anchor && anchor.getAttribute("href")?.startsWith("mailto:")) {
|
||||
e.preventDefault();
|
||||
const email = anchor.getAttribute("href")!.replace("mailto:", "");
|
||||
if (email) {
|
||||
routerStore.navigateTo({
|
||||
path: "/write",
|
||||
query: {
|
||||
toEmail: email
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 先清空
|
||||
shadowRoot.innerHTML = "";
|
||||
|
||||
// 安全过滤
|
||||
const safeHtml = sanitizeHtml(newHtml);
|
||||
|
||||
// 直接插入,保留里面的 <style> 标签
|
||||
shadowRoot.innerHTML = `
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
table {
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
<div class="wrapper">
|
||||
${safeHtml}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.email-renderer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
// utils/ErrorModalService.ts
|
||||
import { createApp, h } from "vue";
|
||||
import ErrorModal from "./index.vue";
|
||||
|
||||
/** 异常详情信息接口 */
|
||||
export interface ErrorModalOptions {
|
||||
/** 异常记录的唯一ID */
|
||||
id?: number;
|
||||
|
||||
/** 异常发生时间 */
|
||||
time?: Date;
|
||||
|
||||
/** 请求的URI地址 */
|
||||
uri?: string;
|
||||
|
||||
/** HTTP请求方法,如GET、POST等 */
|
||||
httpMethod?: string;
|
||||
|
||||
/** 抛出异常的类名 */
|
||||
className?: string;
|
||||
|
||||
/** 抛出异常的方法名 */
|
||||
methodName?: string;
|
||||
|
||||
/** 抛出异常的文件名 */
|
||||
fileName?: string;
|
||||
|
||||
/** 抛出异常的代码行号 */
|
||||
lineNumber?: number;
|
||||
|
||||
/** 异常的简短信息 */
|
||||
summary?: string;
|
||||
|
||||
/** 异常的详细堆栈信息 */
|
||||
detailMessage?: string;
|
||||
|
||||
/** 请求参数(可能为JSON字符串等) */
|
||||
params?: string;
|
||||
|
||||
/** 是否展示详细异常信息(用于前端展示控制) */
|
||||
showDetail?: boolean;
|
||||
|
||||
/** 异常状态码,方便分类或识别 */
|
||||
code?: number;
|
||||
}
|
||||
|
||||
export const ErrorModalService = {
|
||||
show(responseData: IResData<ErrorModalOptions>) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(ErrorModal, {
|
||||
responseData: responseData,
|
||||
onClose: () => {
|
||||
app.unmount();
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.mount(container);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<div class="error-modal-wrapper" @click.self="handleClose">
|
||||
<div v-if="visible" class="error-modal" @click.stop>
|
||||
<div class="error-modal-header">
|
||||
<span class="error-modal-title">⚠️ 系统异常提示 - {{ props.responseData.message }} - {{ props.responseData.code || 500 }}</span>
|
||||
<div class="error-modal-close-btn" @click="handleClose" role="button" tabindex="0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-modal-body">
|
||||
<div class="log-item" v-for="item in logList" :key="item.label">
|
||||
<div class="log-label">{{ item.label }}</div>
|
||||
<div class="log-value">
|
||||
<pre v-if="item.label === '堆栈跟踪' || item.label === '请求参数'" class="code-block">{{ item.value }}</pre>
|
||||
<div v-else>{{ item.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-modal-footer">
|
||||
<div class="error-modal-tip">
|
||||
<svg width="24" height="24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="16" x2="12" y2="12" />
|
||||
<line x1="12" y1="8" x2="12.01" y2="8" />
|
||||
</svg>
|
||||
<span>请将错误信息 导出 或 复制 发送给管理员!</span>
|
||||
</div>
|
||||
<div class="error-modal-actions">
|
||||
<div class="btn txt-btn" role="button" tabindex="0" @click="exportAsTxt">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" />
|
||||
<path d="M14 2v6h6m-9 4H9m3 3H9m6-9h-1" />
|
||||
<path d="M12 18v-6m2 3h-4" />
|
||||
</svg>
|
||||
<span>导出TXT</span>
|
||||
</div>
|
||||
|
||||
<div class="btn data-btn" role="button" tabindex="0" @click="copyInfo">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
<span>复制信息</span>
|
||||
</div>
|
||||
|
||||
<div class="btn cancel-btn" role="button" tabindex="0" @click="handleClose">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M15 9l-6 6m0-6l6 6" />
|
||||
</svg>
|
||||
<span>取消</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from "vue";
|
||||
import { ErrorModalOptions } from ".";
|
||||
import { copyText } from "@/utils/common";
|
||||
|
||||
interface Props {
|
||||
responseData: IResData<ErrorModalOptions>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
visible.value = true;
|
||||
});
|
||||
|
||||
const logList = computed(() => [
|
||||
{ label: "ID 唯一标识", value: props.responseData.data.id ?? "" },
|
||||
{ label: "发生时间", value: props.responseData.data.time?.toString() ?? "" },
|
||||
{ label: "请求路径", value: props.responseData.data.uri ?? "" },
|
||||
{ label: "HTTP 方法", value: props.responseData.data.httpMethod ?? "" },
|
||||
{ label: "类 - 方法", value: `${props.responseData.data.className ?? ""} - ${props.responseData.data.methodName ?? ""}` },
|
||||
{ label: "代码位置", value: `${props.responseData.data.fileName ?? ""} - ${props.responseData.data.lineNumber ?? ""}` },
|
||||
{ label: "错误主要信息", value: props.responseData.data.summary ?? "" },
|
||||
{ label: "堆栈跟踪", value: props.responseData.data.detailMessage ?? "" },
|
||||
{ label: "请求参数", value: props.responseData.data.params ? JSON.stringify(JSON.parse(props.responseData.data.params), null, 2) : "" }
|
||||
]);
|
||||
|
||||
function handleClose() {
|
||||
if (visible.value) {
|
||||
visible.value = false;
|
||||
props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function exportAsTxt() {
|
||||
const content = logList.value.map((item) => `${item.label}:\n${item.value}\n`).join("\n");
|
||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "错误信息日志.txt";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function copyInfo() {
|
||||
copyText(JSON.stringify(props.responseData));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@keyframes modalFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
.error-modal-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 99;
|
||||
|
||||
.error-modal {
|
||||
animation: modalFadeIn 0.3s forwards;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 4px;
|
||||
width: 1000px;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.error-modal-header {
|
||||
height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid rgb(229, 230, 235);
|
||||
line-height: 20px;
|
||||
background-color: #e04848;
|
||||
.error-modal-title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.error-modal-close-btn {
|
||||
padding: 4px;
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
|
||||
color: #fff;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.error-modal-close-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
.error-modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
padding: 16px 0px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box; /* 包含内边距 */
|
||||
|
||||
.log-item {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 16px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.log-label {
|
||||
color: #2b2b2b;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.log-value {
|
||||
word-break: break-all;
|
||||
.code-block {
|
||||
background: #f1f1f1;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
max-height: 120px;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap; /* 保留换行和缩进空格 */
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.error-modal-footer {
|
||||
border-top: 1px solid rgb(229, 230, 235);
|
||||
padding: 16px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.error-modal-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #ff7d00;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.error-modal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-left: auto;
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid #1976d2;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 导出按钮样式 */
|
||||
&.txt-btn {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
&:active {
|
||||
background: #0d47a1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 复制按钮样式 */
|
||||
&.data-btn {
|
||||
color: #1976d2;
|
||||
border-color: #90caf9;
|
||||
|
||||
&:active {
|
||||
background: #90caf9;
|
||||
}
|
||||
}
|
||||
|
||||
/* 取消按钮样式 */
|
||||
&.cancel-btn {
|
||||
background: #f5f5f5;
|
||||
color: #757575;
|
||||
|
||||
&:hover {
|
||||
background: #eeeeee;
|
||||
}
|
||||
&:active {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<SvgIcon :icon="iconName" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
fileName: string | null;
|
||||
}>();
|
||||
|
||||
// 纯函数放组件里也可以,简单方便
|
||||
function getFileIcon(fileName: string | null): string {
|
||||
if (!fileName) return "material-symbols:file-copy-outline";
|
||||
const ext = fileName.toLowerCase().split(".").pop();
|
||||
switch (ext) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "png":
|
||||
case "gif":
|
||||
case "bmp":
|
||||
case "svg":
|
||||
return "material-symbols:imagesmode-outline";
|
||||
case "mp4":
|
||||
case "mkv":
|
||||
case "avi":
|
||||
case "mov":
|
||||
return "material-symbols:camera-video-outline";
|
||||
case "mp3":
|
||||
case "wav":
|
||||
case "flac":
|
||||
case "ogg":
|
||||
return "material-symbols:audio-file-outline";
|
||||
case "pdf":
|
||||
return "material-symbols:picture-as-pdf-outline";
|
||||
default:
|
||||
return "material-symbols:file-copy-outline";
|
||||
}
|
||||
}
|
||||
|
||||
const iconName = computed(() => getFileIcon(props.fileName));
|
||||
</script>
|
||||
@@ -0,0 +1 @@
|
||||
export const getHugeRTE = () => (globalThis as any).hugerte ?? null;
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2018-present, Ephox, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { Ref, SetupContext } from "vue";
|
||||
import { watch } from "vue";
|
||||
import type { IPropTypes } from "./modules/EditorPropTypes";
|
||||
import type { Editor as HugeRTEEditor, EditorEvent } from "hugerte";
|
||||
import { validEvents } from "./utils/index";
|
||||
|
||||
const isValidKey = (key: string) => validEvents.map((event) => "on" + event.toLowerCase()).indexOf(key.toLowerCase()) !== -1;
|
||||
|
||||
const bindHandlers = (initEvent: EditorEvent<any>, listeners: Record<string, any>, editor: HugeRTEEditor): void => {
|
||||
Object.keys(listeners)
|
||||
.filter(isValidKey)
|
||||
.forEach((key: string) => {
|
||||
const handler = listeners[key];
|
||||
if (typeof handler === "function") {
|
||||
if (key === "Init") {
|
||||
handler(initEvent, editor);
|
||||
} else {
|
||||
editor.on(key, (e: EditorEvent<any>) => handler(e, editor));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const bindModelHandlers = (props: IPropTypes, ctx: SetupContext, editor: HugeRTEEditor, modelValue: Ref<any>) => {
|
||||
const modelEvents = props.modelEvents ? props.modelEvents : null;
|
||||
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(" ") : modelEvents;
|
||||
|
||||
watch(modelValue, (val: string, prevVal: string) => {
|
||||
if (editor && typeof val === "string" && val !== prevVal && val !== editor.getContent({ format: props.outputFormat })) {
|
||||
editor.setContent(val);
|
||||
}
|
||||
});
|
||||
|
||||
editor.on(normalizedEvents ? normalizedEvents : "change input undo redo", () => {
|
||||
ctx.emit("update:modelValue", editor.getContent({ format: props.outputFormat }));
|
||||
});
|
||||
};
|
||||
|
||||
const initEditor = (initEvent: EditorEvent<any>, props: IPropTypes, ctx: SetupContext, editor: HugeRTEEditor, modelValue: Ref<any>, content: () => string) => {
|
||||
editor.setContent(content());
|
||||
if (ctx.attrs["onUpdate:modelValue"]) {
|
||||
bindModelHandlers(props, ctx, editor, modelValue);
|
||||
}
|
||||
bindHandlers(initEvent, ctx.attrs, editor);
|
||||
};
|
||||
|
||||
export {
|
||||
bindHandlers,
|
||||
initEditor,
|
||||
isValidKey // TODO: Seems to only be used in a test, consider removing
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2018-present, Ephox, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import { Editor } from "./modules/Editor";
|
||||
|
||||
export default Editor;
|
||||
@@ -0,0 +1,126 @@
|
||||
import { ScriptLoader, mergePlugins, isTextarea, uuid } from "../utils/index";
|
||||
import { getHugeRTE } from "../HugeRTE";
|
||||
import { initEditor } from "../Utils";
|
||||
import { editorProps } from "./EditorPropTypes";
|
||||
import type { IPropTypes } from "./EditorPropTypes";
|
||||
import type { Ref } from "vue";
|
||||
import { h, defineComponent, onMounted, ref, toRefs, nextTick, watch, onBeforeUnmount, onActivated, onDeactivated } from "vue";
|
||||
import type { Editor as HugeRTEEditor, EditorEvent, HugeRTE } from "hugerte";
|
||||
|
||||
type EditorOptions = Parameters<HugeRTE["init"]>[0];
|
||||
|
||||
const renderInline = (ce: any, id: string, elementRef: Ref<Element | null>, tagName?: string) =>
|
||||
ce(tagName ? tagName : "div", {
|
||||
id,
|
||||
ref: elementRef
|
||||
});
|
||||
|
||||
const renderIframe = (ce: any, id: string, elementRef: Ref<Element | null>) =>
|
||||
ce("textarea", {
|
||||
id,
|
||||
visibility: "hidden",
|
||||
ref: elementRef
|
||||
});
|
||||
|
||||
const defaultInitValues = { selector: undefined, target: undefined };
|
||||
|
||||
export const Editor = defineComponent({
|
||||
props: editorProps,
|
||||
setup: (props: IPropTypes, ctx) => {
|
||||
let conf = props.init ? { ...props.init, ...defaultInitValues } : { ...defaultInitValues };
|
||||
const { disabled, modelValue, tagName } = toRefs(props);
|
||||
const element: Ref<Element | null> = ref(null);
|
||||
let vueEditor: any = null;
|
||||
const elementId: string = props.id || uuid("hugerte-vue");
|
||||
const inlineEditor: boolean = (props.init && props.init.inline) || props.inline;
|
||||
const modelBind = !!ctx.attrs["onUpdate:modelValue"];
|
||||
let mounting = true;
|
||||
const initialValue: string = props.initialValue ? props.initialValue : "";
|
||||
let cache = "";
|
||||
|
||||
const getContent = (isMounting: boolean): (() => string) => (modelBind ? () => (modelValue?.value ? modelValue.value : "") : () => (isMounting ? initialValue : cache));
|
||||
|
||||
const initWrapper = (): void => {
|
||||
const content = getContent(mounting);
|
||||
const finalInit = {
|
||||
...conf,
|
||||
readonly: props.disabled,
|
||||
target: element.value,
|
||||
plugins: mergePlugins(conf.plugins, props.plugins),
|
||||
toolbar: props.toolbar || conf.toolbar,
|
||||
inline: inlineEditor,
|
||||
setup: (editor: HugeRTEEditor) => {
|
||||
vueEditor = editor;
|
||||
editor.on("init", (e: EditorEvent<any>) => initEditor(e, props, ctx, editor, modelValue, content));
|
||||
if (typeof conf.setup === "function") {
|
||||
// TODO: add that to other framework integrations, especially svelte
|
||||
conf.setup(editor);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (isTextarea(element.value)) {
|
||||
element.value.style.visibility = "";
|
||||
}
|
||||
getHugeRTE().init(finalInit);
|
||||
mounting = false;
|
||||
};
|
||||
watch(disabled, (disable) => {
|
||||
if (vueEditor !== null) {
|
||||
if (typeof vueEditor.mode?.set === "function") {
|
||||
vueEditor.mode.set(disable ? "readonly" : "design");
|
||||
} else {
|
||||
vueEditor.setMode(disable ? "readonly" : "design");
|
||||
}
|
||||
}
|
||||
});
|
||||
watch(tagName, (_) => {
|
||||
if (!modelBind) {
|
||||
cache = vueEditor.getContent();
|
||||
}
|
||||
getHugeRTE()?.remove(vueEditor);
|
||||
// TODO
|
||||
|
||||
nextTick(() => initWrapper());
|
||||
});
|
||||
onMounted(() => {
|
||||
if (getHugeRTE() !== null) {
|
||||
initWrapper();
|
||||
} else if (element.value && element.value.ownerDocument) {
|
||||
const version = props.cdnVersion ?? "1";
|
||||
const scriptSrc: string = props.hugerteScriptSrc ?? `https://cdn.jsdelivr.net/npm/hugerte@${version}/hugerte.min.js`;
|
||||
ScriptLoader.load(element.value.ownerDocument, scriptSrc, initWrapper);
|
||||
}
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (getHugeRTE() !== null) {
|
||||
getHugeRTE().remove(vueEditor);
|
||||
}
|
||||
});
|
||||
if (!inlineEditor) {
|
||||
onActivated(() => {
|
||||
if (!mounting) {
|
||||
initWrapper();
|
||||
}
|
||||
});
|
||||
onDeactivated(() => {
|
||||
if (!modelBind) {
|
||||
cache = vueEditor.getContent();
|
||||
}
|
||||
getHugeRTE()?.remove(vueEditor);
|
||||
});
|
||||
}
|
||||
const rerender = (init: EditorOptions) => {
|
||||
cache = vueEditor.getContent();
|
||||
getHugeRTE()?.remove(vueEditor);
|
||||
conf = { ...conf, ...init, ...defaultInitValues };
|
||||
// TODO
|
||||
|
||||
nextTick(() => initWrapper());
|
||||
};
|
||||
ctx.expose({
|
||||
rerender,
|
||||
getEditor: () => vueEditor
|
||||
});
|
||||
return () => (inlineEditor ? renderInline(h, elementId, element, props.tagName) : renderIframe(h, elementId, element));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2018-present, Ephox, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import type { HugeRTE } from "hugerte";
|
||||
|
||||
type EditorOptions = Parameters<HugeRTE["init"]>[0];
|
||||
|
||||
export type CopyProps<T> = { [P in keyof T]: any };
|
||||
|
||||
export interface IPropTypes {
|
||||
cdnVersion: string;
|
||||
id: string;
|
||||
init: EditorOptions & { selector?: undefined; target?: undefined };
|
||||
initialValue: string;
|
||||
outputFormat: "html" | "text";
|
||||
inline: boolean;
|
||||
modelEvents: string[] | string;
|
||||
plugins: string[] | string;
|
||||
tagName: string;
|
||||
toolbar: string[] | string;
|
||||
modelValue: string;
|
||||
disabled: boolean;
|
||||
hugerteScriptSrc: string;
|
||||
}
|
||||
|
||||
export const editorProps: CopyProps<IPropTypes> = {
|
||||
cdnVersion: String,
|
||||
id: String,
|
||||
init: Object,
|
||||
initialValue: String,
|
||||
inline: Boolean,
|
||||
modelEvents: [String, Array],
|
||||
plugins: [String, Array],
|
||||
tagName: String,
|
||||
toolbar: [String, Array],
|
||||
modelValue: String,
|
||||
disabled: Boolean,
|
||||
hugerteScriptSrc: String,
|
||||
outputFormat: {
|
||||
type: String,
|
||||
validator: (prop: string) => prop === "html" || prop === "text"
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export declare const pLoadVersion: (version: string) => Promise<void>;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { cleanupGlobalHugeRTE } from "./TestHelpers";
|
||||
/* Based on code from TinyMCE, MODIFIED */
|
||||
/* See LICENSE.txt for the original license information */
|
||||
const loadScript = (url, success, failure) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = url;
|
||||
const onLoad = () => {
|
||||
script.removeEventListener("load", onLoad);
|
||||
script.removeEventListener("error", onError);
|
||||
success();
|
||||
};
|
||||
const onError = () => {
|
||||
script.removeEventListener("error", onError);
|
||||
script.removeEventListener("load", onLoad);
|
||||
failure(new Error(`Failed to load script: ${url}`));
|
||||
};
|
||||
script.addEventListener("load", onLoad);
|
||||
script.addEventListener("error", onError);
|
||||
document.body.appendChild(script);
|
||||
};
|
||||
const getHugeRTE = () => globalThis.hugerte;
|
||||
const setHugeRTEBaseUrl = (hugerte, baseUrl) => {
|
||||
const prefix = document.location.protocol + "//" + document.location.host;
|
||||
hugerte.baseURL = baseUrl.indexOf("://") === -1 ? prefix + baseUrl : baseUrl;
|
||||
hugerte.baseURI = new hugerte.util.URI(hugerte.baseURL);
|
||||
};
|
||||
const updateHugeRTEUrls = (packageName) => {
|
||||
const hugerte = getHugeRTE();
|
||||
if (hugerte) {
|
||||
setHugeRTEBaseUrl(hugerte, `/project/node_modules/${packageName}`);
|
||||
}
|
||||
};
|
||||
const versionToPackageName = (version) => (version === "latest" ? "hugerte" : `hugerte-${version}`);
|
||||
const unload = () => {
|
||||
const hugerte = getHugeRTE();
|
||||
if (hugerte) {
|
||||
hugerte.remove();
|
||||
}
|
||||
cleanupGlobalHugeRTE();
|
||||
};
|
||||
const load = (version, success, failure) => {
|
||||
const packageName = versionToPackageName(version);
|
||||
unload();
|
||||
loadScript(
|
||||
`/project/node_modules/${packageName}/hugerte.min.js`,
|
||||
() => {
|
||||
updateHugeRTEUrls(versionToPackageName(version));
|
||||
success();
|
||||
},
|
||||
failure
|
||||
);
|
||||
};
|
||||
export const pLoadVersion = (version) =>
|
||||
new Promise((resolve, reject) => {
|
||||
load(version, resolve, reject);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
export type CallbackFn = () => void;
|
||||
export interface IStateObj {
|
||||
listeners: CallbackFn[];
|
||||
scriptId: string;
|
||||
scriptLoaded: boolean;
|
||||
}
|
||||
interface ScriptLoader {
|
||||
load: (doc: Document, url: string, callback: CallbackFn) => void;
|
||||
reinitialize: () => void;
|
||||
}
|
||||
declare const ScriptLoader: ScriptLoader;
|
||||
export { ScriptLoader };
|
||||
@@ -0,0 +1,47 @@
|
||||
import { uuid } from "./Utils";
|
||||
const createState = () => ({
|
||||
listeners: [],
|
||||
scriptId: uuid("hugerte-script"),
|
||||
scriptLoaded: false
|
||||
});
|
||||
const CreateScriptLoader = () => {
|
||||
let state = createState();
|
||||
const injectScriptTag = (scriptId, doc, url, callback) => {
|
||||
const scriptTag = doc.createElement("script");
|
||||
scriptTag.referrerPolicy = "origin";
|
||||
scriptTag.type = "application/javascript";
|
||||
scriptTag.id = scriptId;
|
||||
scriptTag.src = url;
|
||||
const handler = () => {
|
||||
scriptTag.removeEventListener("load", handler);
|
||||
callback();
|
||||
};
|
||||
scriptTag.addEventListener("load", handler);
|
||||
if (doc.head) {
|
||||
doc.head.appendChild(scriptTag);
|
||||
}
|
||||
};
|
||||
const load = (doc, url, callback) => {
|
||||
if (state.scriptLoaded) {
|
||||
callback();
|
||||
} else {
|
||||
state.listeners.push(callback);
|
||||
if (!doc.getElementById(state.scriptId)) {
|
||||
injectScriptTag(state.scriptId, doc, url, () => {
|
||||
state.listeners.forEach((fn) => fn());
|
||||
state.scriptLoaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
// Only to be used by tests.
|
||||
const reinitialize = () => {
|
||||
state = createState();
|
||||
};
|
||||
return {
|
||||
load,
|
||||
reinitialize
|
||||
};
|
||||
};
|
||||
const ScriptLoader = CreateScriptLoader();
|
||||
export { ScriptLoader };
|
||||
@@ -0,0 +1,3 @@
|
||||
/** Function to clean up and remove HugeRTE-related scripts and links from the document */
|
||||
declare const cleanupGlobalHugeRTE: () => void;
|
||||
export { cleanupGlobalHugeRTE };
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ScriptLoader } from "./ScriptLoader";
|
||||
/** Function to clean up and remove HugeRTE-related scripts and links from the document */
|
||||
const cleanupGlobalHugeRTE = () => {
|
||||
ScriptLoader.reinitialize();
|
||||
delete globalThis.hugerte;
|
||||
delete globalThis.hugeRTE;
|
||||
/** Helper function to check if an element has a HugeRTE-related URI in a specific attribute */
|
||||
const hasHugeRTEUri = (attrName) => (elm) => {
|
||||
const src = elm.getAttribute(attrName);
|
||||
return src != null && src.includes("hugerte");
|
||||
};
|
||||
// Find all script and link elements that have a HugeRTE-related URI
|
||||
[...Array.from(document.querySelectorAll("script")).filter(hasHugeRTEUri("src")), ...Array.from(document.querySelectorAll("link")).filter(hasHugeRTEUri("href"))].forEach((elm) => elm.remove());
|
||||
};
|
||||
export { cleanupGlobalHugeRTE };
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface FakeHugeRTE {
|
||||
majorVersion: string;
|
||||
minorVersion: string;
|
||||
}
|
||||
export interface HugeRTE extends FakeHugeRTE {
|
||||
baseURL: string;
|
||||
baseURI: any;
|
||||
PluginManager: any;
|
||||
util: {
|
||||
URI: any;
|
||||
};
|
||||
remove(): void;
|
||||
remove(selector: string | Editor): Editor | void;
|
||||
}
|
||||
export interface Editor {
|
||||
editorManager: any;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,5 @@
|
||||
export declare const uuid: (prefix: string) => string;
|
||||
export declare const validEvents: string[];
|
||||
export declare const isTextarea: (element?: Element | null) => element is HTMLTextAreaElement;
|
||||
export declare const normalizePluginArray: (plugins?: string | string[]) => string[];
|
||||
export declare const mergePlugins: (initPlugins?: string | string[], inputPlugins?: string | string[]) => string[];
|
||||
@@ -0,0 +1,85 @@
|
||||
let unique = 0;
|
||||
export const uuid = (prefix) => {
|
||||
const time = Date.now();
|
||||
const random = Math.floor(Math.random() * 1000000000);
|
||||
unique++;
|
||||
return prefix + "_" + random + unique + String(time);
|
||||
};
|
||||
export const validEvents = [
|
||||
"Activate",
|
||||
"AddUndo",
|
||||
"BeforeAddUndo",
|
||||
"BeforeExecCommand",
|
||||
"BeforeGetContent",
|
||||
"BeforeRenderUI",
|
||||
"BeforeSetContent",
|
||||
"BeforePaste",
|
||||
"Blur",
|
||||
"Change",
|
||||
"ClearUndos",
|
||||
"Click",
|
||||
"CommentChange", // TODO has not been in angular
|
||||
"CompositionEnd",
|
||||
"CompositionStart",
|
||||
"CompositionUpdate",
|
||||
"ContextMenu",
|
||||
"Copy",
|
||||
"Cut",
|
||||
"Dblclick",
|
||||
"Deactivate",
|
||||
"Dirty",
|
||||
"Drag",
|
||||
"DragDrop",
|
||||
"DragEnd",
|
||||
"DragGesture",
|
||||
"DragOver",
|
||||
"Drop",
|
||||
"ExecCommand",
|
||||
"Focus",
|
||||
"FocusIn",
|
||||
"FocusOut",
|
||||
"GetContent",
|
||||
"Hide",
|
||||
"Init",
|
||||
"Input",
|
||||
"KeyDown",
|
||||
"KeyPress",
|
||||
"KeyUp",
|
||||
"LoadContent",
|
||||
"MouseDown",
|
||||
"MouseEnter",
|
||||
"MouseLeave",
|
||||
"MouseMove",
|
||||
"MouseOut",
|
||||
"MouseOver",
|
||||
"MouseUp",
|
||||
"NodeChange",
|
||||
"ObjectResizeStart",
|
||||
"ObjectResized",
|
||||
"ObjectSelected",
|
||||
"Paste",
|
||||
"PostProcess",
|
||||
"PostRender",
|
||||
"PreProcess",
|
||||
"ProgressState",
|
||||
"Redo",
|
||||
"Remove",
|
||||
"Reset",
|
||||
"ResizeEditor", // TODO only in svelte and angular, not in Vue – add to changelog at least. also check react.
|
||||
"SaveContent",
|
||||
"SelectionChange",
|
||||
"SetAttrib",
|
||||
"SetContent",
|
||||
"Show",
|
||||
"Submit",
|
||||
"Undo",
|
||||
"VisualAid"
|
||||
];
|
||||
export const isTextarea = (element) => (element === null || element === void 0 ? void 0 : element.tagName.toLowerCase()) === "textarea";
|
||||
export const normalizePluginArray = (plugins) => {
|
||||
if (typeof plugins === "undefined" || plugins === "") {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(plugins) ? plugins : plugins.split(/[ ,]/);
|
||||
};
|
||||
export const mergePlugins = (initPlugins, inputPlugins) => normalizePluginArray(initPlugins).concat(normalizePluginArray(inputPlugins));
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./Loader";
|
||||
export * from "./ScriptLoader";
|
||||
export * from "./TestHelpers";
|
||||
export * from "./Types";
|
||||
export * from "./Utils";
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./Loader";
|
||||
export * from "./ScriptLoader";
|
||||
export * from "./TestHelpers";
|
||||
export * from "./Types";
|
||||
export * from "./Utils";
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<a-modal title="新增账户" v-bind="$attrs" :visible="true">
|
||||
<a-form :model="form" auto-label-width>
|
||||
<a-form-item field="email" label="Email地址">
|
||||
<template #extra>
|
||||
<div>系统默认使用 IMAP 和 SMTP 协议,请确保邮箱已开启对应服务。</div>
|
||||
</template>
|
||||
<a-input v-model="form.email" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="password" label="密码">
|
||||
<template #extra>
|
||||
<div>请填写邮箱的授权码,而非登录密码。</div>
|
||||
</template>
|
||||
<a-input v-model="form.password" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
|
||||
const form = reactive({
|
||||
email: "",
|
||||
password: ""
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
|
||||
defineOptions({ name: "SvgIcon", inheritAttrs: false });
|
||||
|
||||
/**
|
||||
* Props
|
||||
*
|
||||
* - Support iconify and local svg icon
|
||||
* - If icon and localIcon are passed at the same time, localIcon will be rendered first
|
||||
*/
|
||||
interface Props {
|
||||
/** Iconify icon name */
|
||||
icon?: string;
|
||||
/** Local svg icon name */
|
||||
localIcon?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
const bindAttrs = computed<{ class: string; style: string }>(() => ({
|
||||
class: (attrs.class as string) || "",
|
||||
style: (attrs.style as string) || ""
|
||||
}));
|
||||
|
||||
const symbolId = computed(() => {
|
||||
const { VITE_ICON_LOCAL_PREFIX: prefix } = import.meta.env;
|
||||
|
||||
const defaultLocalIcon = "no-icon";
|
||||
|
||||
const icon = props.localIcon || defaultLocalIcon;
|
||||
|
||||
return `#${prefix}-${icon}`;
|
||||
});
|
||||
|
||||
/** If localIcon is passed, render localIcon first */
|
||||
const renderLocalIcon = computed(() => props.localIcon || !props.icon);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="renderLocalIcon">
|
||||
<svg aria-hidden="true" width="1em" height="1em" v-bind="bindAttrs">
|
||||
<use :xlink:href="symbolId" fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Icon v-if="icon" :icon="icon" v-bind="bindAttrs" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in new issue
Block a user