Initial project
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 370 B |
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const BEFlowerFall = Toolkit.withInstall(view);
|
||||
export default BEFlowerFall;
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="tui-background tui-be-flower-fall">
|
||||
<div :ref="el => fillNodes(el as HTMLElement, i - 1)" class="leaf" v-for="i in leafSize" :key="i"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
import Resizer from "~/utils/Resizer";
|
||||
|
||||
defineOptions({
|
||||
name: "BEFlowerFall"
|
||||
});
|
||||
|
||||
/** 叶子对象 */
|
||||
class Leaf {
|
||||
|
||||
/** 横轴 */
|
||||
x = 0;
|
||||
|
||||
/** 纵轴 */
|
||||
y = 0;
|
||||
|
||||
/** 背景偏移 */
|
||||
bgI = 0;
|
||||
|
||||
/** 跳跃状态 */
|
||||
jump = false;
|
||||
|
||||
/** 透明度 */
|
||||
opacity = 1;
|
||||
}
|
||||
|
||||
const docEl = document.documentElement;
|
||||
const leafF = ref<number>(0);
|
||||
const leafs: Leaf[] = [];
|
||||
const leafEls: HTMLElement[] = [];
|
||||
const leafSize = 16;
|
||||
const leafTimer = ref();
|
||||
const leafPause = ref<boolean>(false);
|
||||
const leafVector = ref<number>(0);
|
||||
|
||||
const fillNodes = (el: HTMLElement, index: number) => {
|
||||
if (el) {
|
||||
leafEls[index] = el;
|
||||
}
|
||||
};
|
||||
|
||||
const onResize = (width: number) => leafPause.value = width < 1200;
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
// 生成叶子
|
||||
for (let i = 0; i < leafSize; i++) {
|
||||
leafs[i] = new Leaf();
|
||||
leafs[i].x = Toolkit.random(0, docEl.clientWidth);
|
||||
leafs[i].y = Toolkit.random(-docEl.clientHeight, -32);
|
||||
}
|
||||
// 掉落叶子
|
||||
clearInterval(leafTimer.value);
|
||||
leafTimer.value = setInterval(() => {
|
||||
// 随机上方位置向下飘落,维护一个向量值作为风向,可以向左向右
|
||||
//
|
||||
// 暂定 2 秒刷新风力向量,向量偏转范围 -10 - +10,分别表示左右向
|
||||
// -5 - +5 有固定 10% 几率增加或减小,同时增减或同时无增减都不改变向量
|
||||
// -10 - -5 有 0%, 2%, 4%... 几率再次减小
|
||||
// +5 - +10 有 8%, 6%, 4%... 几率再次增加
|
||||
// 降低垂直飘落的几率,扩大向量影响范围
|
||||
|
||||
if (!leafPause.value) {
|
||||
if (leafF.value % 10 === 0) {
|
||||
// 刷新风力向量
|
||||
let add = false,
|
||||
sub = false;
|
||||
if (-5 <= leafVector.value) {
|
||||
sub = Math.random() < 0.1;
|
||||
if (5 < leafVector.value) {
|
||||
add = Math.random() < (10 - leafVector.value) / 50;
|
||||
}
|
||||
}
|
||||
if (leafVector.value <= 5) {
|
||||
add = Math.random() < 0.1;
|
||||
if (leafVector.value < -5) {
|
||||
sub = Math.random() < (leafVector.value + 10) / 50;
|
||||
}
|
||||
}
|
||||
if (add !== sub) {
|
||||
leafVector.value = leafVector.value + (add ? 1 : -1);
|
||||
}
|
||||
}
|
||||
// 叶子飘落
|
||||
for (let i = 0; i < leafSize; i++) {
|
||||
const leaf = leafs[i];
|
||||
|
||||
if (leafVector.value < 0) {
|
||||
leaf.x += Toolkit.random(leafVector.value * 4 - 10, -10);
|
||||
} else {
|
||||
if (leafVector.value === 0) {
|
||||
leaf.x += Toolkit.random(-8, 8);
|
||||
} else {
|
||||
leaf.x += Toolkit.random(10, 10 + leafVector.value * 4);
|
||||
}
|
||||
}
|
||||
// 跳跃机制
|
||||
if (leafVector.value !== 0 && leaf.jump) {
|
||||
leaf.y += Toolkit.random(-12, Math.abs(leafVector.value));
|
||||
leaf.jump = Math.random() < 0.4; // 二次跳跃几率
|
||||
} else {
|
||||
leaf.y += Toolkit.random(10 + Math.abs(leafVector.value) * 5, 10);
|
||||
leaf.jump = Math.random() < 0.03; // 跳跃几率
|
||||
}
|
||||
// 操作 DOM
|
||||
const el = leafEls[i];
|
||||
el.style.top = leaf.y + "px";
|
||||
el.style.left = leaf.x + "px";
|
||||
el.style.backgroundPosition = "0 -" + Toolkit.random(0, 3) * 32 + "px";
|
||||
el.style.opacity = leaf.opacity.toString();
|
||||
|
||||
// y 掉出最大值或没有掉出最大值 X 却不在宽度内的,视为掉出屏幕,重新飘落
|
||||
if ((0 < leaf.y && (leaf.x < 0 || docEl.clientWidth < leaf.x)) || docEl.clientHeight < leaf.y) {
|
||||
// 根据风向反向偏移起始 X 轴
|
||||
const offset = -leafVector.value * 100;
|
||||
// 飘落越出屏幕,重新计算位置
|
||||
leafs[i].x = Toolkit.random(offset, docEl.clientWidth + offset);
|
||||
leafs[i].y = Toolkit.random(-docEl.clientHeight * 0.5, -32);
|
||||
// 随机透明度
|
||||
leafs[i].opacity = Toolkit.random(40, 100) * 0.01;
|
||||
}
|
||||
}
|
||||
// 帧循环
|
||||
leafF.value++;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
onResize(Resizer.getWidth());
|
||||
Resizer.addListener("TUI_BE_FLOWER_FALL", onResize);
|
||||
});
|
||||
onUnmounted(() => Resizer.removeListener("TUI_BE_FLOWER_FALL"));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import "../style";
|
||||
|
||||
.tui-be-flower-fall {
|
||||
image-rendering: pixelated;
|
||||
|
||||
.leaf {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
background: url("assets/petal.png") no-repeat;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
.tui-background {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: -1;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const Captcha = Toolkit.withInstall(view);
|
||||
export default Captcha;
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<img
|
||||
class="tui-captcha ir-pixelated"
|
||||
v-if="src"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:src="src"
|
||||
alt="验证码"
|
||||
@click="update()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
defineOptions({
|
||||
name: "Captcha"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
width: number,
|
||||
height: number,
|
||||
from: string,
|
||||
api: string,
|
||||
}>(), {});
|
||||
const { width, height, from, api } = toRefs(props);
|
||||
|
||||
const src = ref("");
|
||||
function update() {
|
||||
src.value = `${api.value}?from=${from.value}&width=${width.value}&height=${height.value}&r=${Toolkit.random(0, 999999)}`;
|
||||
}
|
||||
onMounted(update);
|
||||
|
||||
defineExpose({
|
||||
update
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-captcha {
|
||||
cursor: var(--tui-cur-pointer);
|
||||
border: 1px solid gray;
|
||||
display: block;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 786 B |
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const Copyright = Toolkit.withInstall(view);
|
||||
export default Copyright;
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="tui-copyright">
|
||||
<p>朝朝频顾惜,夜夜不相忘</p>
|
||||
<p v-if="icp" class="selectable">
|
||||
<a href="https://beian.miit.gov.cn/" v-text="icp" :title="icp" target="_blank"></a>
|
||||
</p>
|
||||
<p v-if="author && domain">
|
||||
<span v-text="`Copyright © 2017 - ${new Date().getFullYear()} ${domain}`"></span>
|
||||
<span v-text="`All Rights Reserved ${author} 版权所有`"></span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "Copyright"
|
||||
});
|
||||
|
||||
withDefaults(defineProps<{
|
||||
icp?: string;
|
||||
domain?: string;
|
||||
author?: string;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-copyright {
|
||||
color: #777;
|
||||
padding: 1rem 0 3rem 0;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
background: url("assets/bottom.png") repeat-x left bottom;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const EmptyTips = Toolkit.withInstall(view);
|
||||
export default EmptyTips;
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div v-show="showOn" class="tui-empty-tips gray" v-text="content"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineOptions({
|
||||
name: "EmptyTips"
|
||||
});
|
||||
|
||||
withDefaults(defineProps<{
|
||||
showOn: boolean;
|
||||
content?: string,
|
||||
}>(), {
|
||||
showOn: true,
|
||||
content: "没有更多数据"
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-empty-tips {
|
||||
width: 100%;
|
||||
padding: 1rem 0;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const Icon = Toolkit.withInstall(view);
|
||||
export default Icon;
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<svg
|
||||
v-if="name && d"
|
||||
class="tui-icon"
|
||||
:class="{ 'disable': disable || disabled }"
|
||||
:width="16 * scale"
|
||||
:height="16 * scale"
|
||||
:fill="fill"
|
||||
>
|
||||
<path class="path" :d="d" />
|
||||
</svg>
|
||||
<div v-else class="tui-icon empty"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import IconMapper from "~/utils/IconMapper";
|
||||
|
||||
defineOptions({
|
||||
name: "Icon"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
name?: string,
|
||||
scale?: number,
|
||||
fill?: string,
|
||||
disable?: boolean,
|
||||
disabled?: boolean,
|
||||
}>(), {
|
||||
scale: 1,
|
||||
fill: "#333",
|
||||
});
|
||||
|
||||
const { name, scale, fill, disable, disabled } = toRefs(props);
|
||||
const d = ref();
|
||||
|
||||
function update() {
|
||||
if (name.value) {
|
||||
d.value = IconMapper.getInstance().getSVG(name.value, scale.value);
|
||||
}
|
||||
}
|
||||
|
||||
watch(name, update);
|
||||
watch(scale, update);
|
||||
onMounted(update);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-icon {
|
||||
display: block;
|
||||
transform: rotateX(180deg) translateY(1px);
|
||||
|
||||
&.disable {
|
||||
opacity: .7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
width: v-bind("16 * scale + 'px'");
|
||||
height: v-bind("16 * scale + 'px'");
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
/** 导出所有组件 */
|
||||
import Icon from "./icon";
|
||||
import Popup from "./popup";
|
||||
import Captcha from "./captcha";
|
||||
import Loading from "./loading";
|
||||
import UserLevel from "./user-level";
|
||||
import Copyright from "./copyright";
|
||||
import EmptyTips from "./empty-tips";
|
||||
import MarkdownView from "./markdown-view";
|
||||
import BEFlowerFall from "./background-effect/flower-fall";
|
||||
import MarkdownEditor from "./markdown-editor";
|
||||
|
||||
export default [
|
||||
Icon,
|
||||
Popup,
|
||||
Captcha,
|
||||
Loading,
|
||||
UserLevel,
|
||||
Copyright,
|
||||
EmptyTips,
|
||||
MarkdownView,
|
||||
BEFlowerFall,
|
||||
MarkdownEditor
|
||||
];
|
||||
|
||||
export {
|
||||
Icon,
|
||||
Popup,
|
||||
Captcha,
|
||||
Loading,
|
||||
UserLevel,
|
||||
Copyright,
|
||||
EmptyTips,
|
||||
MarkdownView,
|
||||
BEFlowerFall,
|
||||
MarkdownEditor
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const Loading = Toolkit.withInstall(view);
|
||||
export default Loading;
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div v-if="visible" class="tui-loading" :class="{ 'filled': filled }" :style="style">
|
||||
<div class="icon">
|
||||
<div
|
||||
class="line"
|
||||
:class="lineClass(i)"
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
></div>
|
||||
</div>
|
||||
<div v-if="tips" class="text" v-text="tips"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Toolkit } from "~/index";
|
||||
defineOptions({
|
||||
name: "Loading"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tips?: string | boolean;
|
||||
delay?: number;
|
||||
showOn?: boolean;
|
||||
size?: number;
|
||||
filled?: boolean;
|
||||
}>(), {
|
||||
tips: "加载中..",
|
||||
delay: 260,
|
||||
showOn: true,
|
||||
size: 24,
|
||||
filled: false
|
||||
});
|
||||
const { tips, delay, showOn, size, filled } = toRefs(props);
|
||||
|
||||
// ---------- 尺寸参数 ----------
|
||||
|
||||
const style = computed(() => {
|
||||
const base = Math.max(8, size.value);
|
||||
const height = Math.round(base);
|
||||
const width = Math.round(base * (8 / 24));
|
||||
const gap = Math.round(base * (3 / 24));
|
||||
return {
|
||||
"--loading-height": `${height}px`,
|
||||
"--rect-width": `${width}px`,
|
||||
"--rect-gap": `${gap}px`
|
||||
};
|
||||
});
|
||||
|
||||
// ---------- 显示控制 ----------
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
watch(showOn, async () => await start());
|
||||
|
||||
// ---------- 动画 ----------
|
||||
|
||||
const activeI = ref<number>(0);
|
||||
const timer = ref();
|
||||
const lineClass = (i: number): string => {
|
||||
let clazz = "i" + i;
|
||||
if (activeI.value === i) {
|
||||
clazz += " active";
|
||||
}
|
||||
return clazz;
|
||||
};
|
||||
const start = async () => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
}
|
||||
if (showOn.value) {
|
||||
if (0 < delay.value) {
|
||||
await Toolkit.sleep(delay.value);
|
||||
}
|
||||
if (!showOn.value) {
|
||||
return;
|
||||
}
|
||||
visible.value = true;
|
||||
|
||||
// 动画
|
||||
let direction = 1;
|
||||
timer.value = setInterval(() => {
|
||||
activeI.value = activeI.value + direction;
|
||||
if (4 < activeI.value || activeI.value < 1) {
|
||||
direction *= -1;
|
||||
}
|
||||
}, 120);
|
||||
} else {
|
||||
visible.value = false;
|
||||
}
|
||||
};
|
||||
onMounted(start);
|
||||
onUnmounted(() => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-loading {
|
||||
--h1: calc(var(--loading-height) * (9 / 24));
|
||||
--h2: calc(var(--loading-height) * (14 / 24));
|
||||
--h3: calc(var(--loading-height) * (19 / 24));
|
||||
--h4: var(--loading-height);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
&.filled {
|
||||
color: #FFF;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, .3);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.line {
|
||||
width: var(--rect-width);
|
||||
background: #BBB;
|
||||
margin-right: var(--rect-gap);
|
||||
|
||||
&.i1 {
|
||||
height: calc(var(--h1))
|
||||
}
|
||||
|
||||
&.i2 {
|
||||
height: calc(var(--h2))
|
||||
}
|
||||
|
||||
&.i3 {
|
||||
height: calc(var(--h3))
|
||||
}
|
||||
|
||||
&.i4 {
|
||||
height: var(--h4)
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #53BD93;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
// from element ui
|
||||
// https://github.com/ElemeFE/element/blob/dev/packages/input/src/calcTextareaHeight.js
|
||||
|
||||
let tempTextArea: HTMLTextAreaElement | null;
|
||||
|
||||
const HIDDEN_STYLE = `
|
||||
height:0 !important;
|
||||
visibility:hidden !important;
|
||||
overflow:hidden !important;
|
||||
position:absolute !important;
|
||||
z-index:-1000 !important;
|
||||
top:0 !important;
|
||||
right:0 !important
|
||||
`;
|
||||
|
||||
const CONTEXT_STYLE = [
|
||||
"letter-spacing",
|
||||
"line-height",
|
||||
"padding-top",
|
||||
"padding-bottom",
|
||||
"font-family",
|
||||
"font-weight",
|
||||
"font-size",
|
||||
"text-rendering",
|
||||
"text-transform",
|
||||
"width",
|
||||
"text-indent",
|
||||
"padding-left",
|
||||
"padding-right",
|
||||
"border-width",
|
||||
"box-sizing"
|
||||
];
|
||||
|
||||
type NodeStyling = {
|
||||
contextStyle: string;
|
||||
paddingSize: number;
|
||||
borderSize: number;
|
||||
boxSizing: string;
|
||||
}
|
||||
|
||||
export type Result = {
|
||||
height: number;
|
||||
minHeight: number;
|
||||
}
|
||||
|
||||
function calculateNodeStyling(targetElement: HTMLTextAreaElement): NodeStyling {
|
||||
const style = window.getComputedStyle(targetElement);
|
||||
|
||||
const boxSizing = style.getPropertyValue("box-sizing");
|
||||
|
||||
const paddingSize = (
|
||||
parseFloat(style.getPropertyValue("padding-bottom")) +
|
||||
parseFloat(style.getPropertyValue("padding-top"))
|
||||
);
|
||||
|
||||
const borderSize = (
|
||||
parseFloat(style.getPropertyValue("border-bottom-width")) +
|
||||
parseFloat(style.getPropertyValue("border-top-width"))
|
||||
);
|
||||
|
||||
const contextStyle = CONTEXT_STYLE
|
||||
.map(name => `${name}:${style.getPropertyValue(name)}`)
|
||||
.join(";");
|
||||
|
||||
return {contextStyle, paddingSize, borderSize, boxSizing};
|
||||
}
|
||||
|
||||
export default function calc(el: HTMLTextAreaElement, minRows = 1, maxRows?: number) {
|
||||
if (!tempTextArea) {
|
||||
tempTextArea = document.createElement("textarea") as HTMLTextAreaElement;
|
||||
document.body.appendChild(tempTextArea);
|
||||
}
|
||||
const {paddingSize, borderSize, boxSizing, contextStyle} = calculateNodeStyling(el);
|
||||
|
||||
tempTextArea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`);
|
||||
tempTextArea.value = el.value || el.placeholder || "";
|
||||
|
||||
let height = tempTextArea.scrollHeight;
|
||||
const result: Result = {
|
||||
height: 0,
|
||||
minHeight: 0
|
||||
};
|
||||
|
||||
if (boxSizing === "border-box") {
|
||||
height = height + borderSize;
|
||||
} else if (boxSizing === "content-box") {
|
||||
height = height - paddingSize;
|
||||
}
|
||||
|
||||
tempTextArea.value = "";
|
||||
const singleRowHeight = tempTextArea.scrollHeight - paddingSize;
|
||||
|
||||
if (minRows) {
|
||||
let minHeight = singleRowHeight * minRows;
|
||||
if (boxSizing === "border-box") {
|
||||
minHeight = minHeight + paddingSize + borderSize;
|
||||
}
|
||||
height = Math.max(minHeight, height);
|
||||
result.minHeight = minHeight;
|
||||
}
|
||||
if (maxRows) {
|
||||
let maxHeight = singleRowHeight * maxRows;
|
||||
if (boxSizing === "border-box") {
|
||||
maxHeight = maxHeight + paddingSize + borderSize;
|
||||
}
|
||||
height = Math.min(maxHeight, height);
|
||||
}
|
||||
result.height = height;
|
||||
tempTextArea.parentNode && tempTextArea.parentNode.removeChild(tempTextArea);
|
||||
tempTextArea = null;
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const MarkdownEditor = Toolkit.withInstall(view);
|
||||
export default MarkdownEditor;
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
class="tui-markdown-editor diselect"
|
||||
:class="{ 'fold': isFold }"
|
||||
>
|
||||
<div class="editor">
|
||||
<div class="header">
|
||||
<icon class="icon" name="WRITING" />
|
||||
<slot name="editorHeader">
|
||||
<h4 class="title">
|
||||
<span>源码</span>
|
||||
<span class="light-gray word-space">Markdown</span>
|
||||
</h4>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="editor">
|
||||
<textarea ref="textArea" class="text-area" v-model="_data"></textarea>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="preview" :class="{ 'showing': showingPreview }">
|
||||
<div class="header">
|
||||
<icon
|
||||
v-if="isFold"
|
||||
class="icon cur-pointer"
|
||||
:name="showingPreview ? 'ARROW_1_E' : 'ARROW_1_W'"
|
||||
@click="showingPreview = !showingPreview"
|
||||
/>
|
||||
<slot name="previewHeader">
|
||||
<h4 class="title">预览</h4>
|
||||
</slot>
|
||||
</div>
|
||||
<div ref="previewContent" class="content">
|
||||
<markdown-view :content="_data" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Icon, MarkdownView } from "~/index";
|
||||
import calcHeight from "./CalcTextareaHeight";
|
||||
|
||||
defineOptions({
|
||||
name: "MarkdownEditor"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
data?: string,
|
||||
minRows?: number,
|
||||
maxRows?: number
|
||||
}>(), {
|
||||
data: "",
|
||||
minRows: 8,
|
||||
maxRows: 32
|
||||
});
|
||||
const { data, minRows, maxRows } = toRefs(props);
|
||||
|
||||
const _data = ref(data.value);
|
||||
const textArea = ref<HTMLTextAreaElement>();
|
||||
const textAreaHeight = ref(30);
|
||||
const previewContent = ref<HTMLDivElement>();
|
||||
|
||||
const emit = defineEmits(["update:data"]);
|
||||
|
||||
watch(_data, () => {
|
||||
emit("update:data", _data.value);
|
||||
calcTextAreaHeight();
|
||||
});
|
||||
watch(data, () => {
|
||||
_data.value = data.value;
|
||||
calcTextAreaHeight();
|
||||
});
|
||||
|
||||
// 自适应折叠预览
|
||||
const root = ref<HTMLDivElement>();
|
||||
const isFold = ref(false);
|
||||
const showingPreview = ref(false);
|
||||
onMounted(() => {
|
||||
if (root.value) {
|
||||
const foldObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
if (entries && 0 < entries.length) {
|
||||
isFold.value = entries[0].contentRect.width < 650;
|
||||
}
|
||||
});
|
||||
foldObserver.observe(root.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 自适应高度
|
||||
const calcTextAreaHeight = () => {
|
||||
if (textArea.value) {
|
||||
textAreaHeight.value = calcHeight(textArea.value, minRows.value, maxRows.value).height;
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
calcTextAreaHeight();
|
||||
if (textArea.value) {
|
||||
const textareaHeightObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
if (entries && 0 < entries.length) {
|
||||
if (previewContent.value) {
|
||||
previewContent.value.style.height = entries[0].contentRect.height + "px";
|
||||
}
|
||||
}
|
||||
});
|
||||
textareaHeightObserver.observe(textArea.value);
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
textArea
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-markdown-editor {
|
||||
width: 100%;
|
||||
border: var(--tui-border);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--tui-dark-white);
|
||||
|
||||
.icon {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
line-height: 30px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
border-right: var(--tui-border);
|
||||
flex-direction: column;
|
||||
|
||||
.text-area {
|
||||
width: calc(100% - .5rem * 2);
|
||||
height: v-bind("textAreaHeight + 'px'");
|
||||
resize: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: .5rem;
|
||||
font-size: 14px;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 50%;
|
||||
background: #FFF;
|
||||
|
||||
.content {
|
||||
padding: .5rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.fold {
|
||||
|
||||
.editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview {
|
||||
left: calc(100% - 5rem);
|
||||
width: calc(100% - 2rem);
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
border-left: var(--tui-border);
|
||||
transition: left .5s var(--tui-bezier);
|
||||
box-shadow: -2px 0 0 var(--tui-shadow-color);
|
||||
|
||||
&.showing {
|
||||
left: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
import "./style.less";
|
||||
|
||||
export const MarkdownView = Toolkit.withInstall(view);
|
||||
export default MarkdownView;
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div
|
||||
class="tui-markdown-view selectable break-all line-numbers"
|
||||
v-html="markdownHTML"
|
||||
:data-max-height="maxHeight"
|
||||
></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Prism from "prismjs";
|
||||
import Markdown from "~/utils/Markdown";
|
||||
|
||||
defineOptions({
|
||||
name: "MarkdownView"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
content?: string;
|
||||
showCodeBorder?: boolean;
|
||||
maxHeight?: string;
|
||||
}>(), {
|
||||
content: "",
|
||||
showCodeBorder: true,
|
||||
maxHeight: "400px"
|
||||
});
|
||||
const { content } = toRefs(props);
|
||||
const markdownHTML = ref("");
|
||||
|
||||
const doRender = async () => {
|
||||
markdownHTML.value = await Markdown.getInstance().toHTML(content.value);
|
||||
await nextTick();
|
||||
Prism.highlightAll();
|
||||
};
|
||||
|
||||
watch(() => props.content, doRender);
|
||||
onMounted(doRender);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.tui-markdown-view {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,341 @@
|
||||
@import url(~/assets/style/variable);
|
||||
|
||||
.tui-markdown-view {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 2rem 0 .5rem 0;
|
||||
padding: .25rem 1rem;
|
||||
position: relative;
|
||||
border-left: .4rem solid var(--tui-light-blue);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p:first-child,
|
||||
h1:first-child,
|
||||
h2:first-child,
|
||||
h3:first-child,
|
||||
h4:first-child,
|
||||
h5:first-child,
|
||||
h6:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1:hover a.anchor,
|
||||
h2:hover a.anchor,
|
||||
h3:hover a.anchor,
|
||||
h4:hover a.anchor,
|
||||
h5:hover a.anchor,
|
||||
h6:hover a.anchor {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h2 a,
|
||||
h3 a {
|
||||
color: #34495E;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
color: #777;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
dl,
|
||||
table,
|
||||
blockquote {
|
||||
margin: .5rem 0;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
p {
|
||||
text-indent: 2em;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: .2em .5em;
|
||||
background: #EEE;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
li > ol,
|
||||
li > ul {
|
||||
margin: 0 0;
|
||||
}
|
||||
|
||||
li > p {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 2px;
|
||||
margin: 16px 0;
|
||||
border: 0 none;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #E7E7E7;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
h1 p,
|
||||
h2 p,
|
||||
h3 p,
|
||||
h4 p,
|
||||
h5 p,
|
||||
h6 p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
li p.first {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
ul:first-child,
|
||||
ol:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul:last-child,
|
||||
ol:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 2px 10px;
|
||||
background: rgba(153, 153, 153, .1);
|
||||
border-left: 4px solid #EDEDED;
|
||||
}
|
||||
|
||||
table {
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
min-width: 240px;
|
||||
max-width: 100%;
|
||||
word-break: initial;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background: #F2F2F2;
|
||||
}
|
||||
|
||||
table tr {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-top: 1px solid #DFE2E5;
|
||||
}
|
||||
|
||||
table tr:nth-child(2n) {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
table tr th {
|
||||
border: 1px solid #DFE2E5;
|
||||
margin: 0;
|
||||
padding: 2px 13px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
border: 1px solid #DFE2E5;
|
||||
margin: 0;
|
||||
padding: 0 13px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
table tr th:first-child,
|
||||
table tr td:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
table tr th:last-child,
|
||||
table tr td:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
tt {
|
||||
color: #e96900;
|
||||
padding: 2px 4px;
|
||||
font-size: 0.92rem;
|
||||
background: #F8F8F8;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
tt {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid #525870;
|
||||
}
|
||||
|
||||
.media {
|
||||
margin: .5rem auto;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
|
||||
+ .media-tips {
|
||||
color: #777;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 代码
|
||||
pre[class*="language-"] {
|
||||
border: 1px solid #B8BBC9;
|
||||
padding: 0 !important;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
max-height: var(data-max-height);
|
||||
transition: max-height .5s var(--tui-bezier);
|
||||
font-family: var(--td-font-family);
|
||||
line-height: 1;
|
||||
border-radius: 0;
|
||||
|
||||
code {
|
||||
color: #333;
|
||||
background: transparent;
|
||||
text-shadow: none !important;
|
||||
font-family: var(--td-font-family);
|
||||
|
||||
.line-numbers-rows {
|
||||
left: 0;
|
||||
float: left;
|
||||
z-index: 1;
|
||||
position: sticky;
|
||||
background: rgba(242, 242, 242, .9);
|
||||
letter-spacing: 1px;
|
||||
|
||||
>span::before {
|
||||
padding-right: .2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.codes {
|
||||
position: absolute;
|
||||
min-width: calc(100% - 4.6em);
|
||||
padding-right: .5em;
|
||||
|
||||
.token.namespace {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.token.constant {
|
||||
color: #FF7A9B;
|
||||
}
|
||||
|
||||
.token.annotation {
|
||||
color: purple;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.token.class-name {
|
||||
color: #FF461F;
|
||||
}
|
||||
|
||||
.token.generics .class-name {
|
||||
color: #895532;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.comment {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token.string {
|
||||
color: #55AA55;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #EB9354;
|
||||
}
|
||||
|
||||
.token.keyword,
|
||||
.token.boolean {
|
||||
color: #177CB0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const Popup = Toolkit.withInstall(view);
|
||||
export default Popup;
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div id="tui-popup"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({
|
||||
name: "Popup"
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#tui-popup {
|
||||
border: var(--tui-border);
|
||||
z-index: 20;
|
||||
position: fixed;
|
||||
font-size: 13px;
|
||||
visibility: hidden;
|
||||
box-shadow: 2px 2px 0 rgba(50, 50, 50, .2);
|
||||
background: #F4F4F4;
|
||||
|
||||
:deep(.text) {
|
||||
padding: 3px 6px 5px 6px;
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 745 B |
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const UserLevel = Toolkit.withInstall(view);
|
||||
export default UserLevel;
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="tui-user-level" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({
|
||||
name: "UserLevel"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
value: number;
|
||||
}>(), {});
|
||||
const { value } = toRefs(props);
|
||||
|
||||
const offset = computed(() => -9 * value.value + "px");
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-user-level {
|
||||
width: 19px;
|
||||
height: 9px;
|
||||
background: url("./icon.png");
|
||||
background-position-y: v-bind(offset);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user