Initial project

This commit is contained in:
Timi
2025-07-08 16:33:11 +08:00
parent 1a5a16be74
commit f862530142
80 changed files with 8301 additions and 129 deletions
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;
}
+5
View File
@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "~/utils/Toolkit";
export const Captcha = Toolkit.withInstall(view);
export default Captcha;
+46
View File
@@ -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

+5
View File
@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "~/utils/Toolkit";
export const Copyright = Toolkit.withInstall(view);
export default Copyright;
+45
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "~/utils/Toolkit";
export const EmptyTips = Toolkit.withInstall(view);
export default EmptyTips;
+27
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "~/utils/Toolkit";
export const Icon = Toolkit.withInstall(view);
export default Icon;
+62
View File
@@ -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>
+37
View File
@@ -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
};
+5
View File
@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "~/utils/Toolkit";
export const Loading = Toolkit.withInstall(view);
export default Loading;
+153
View File
@@ -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;
}
+5
View File
@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "~/utils/Toolkit";
export const MarkdownEditor = Toolkit.withInstall(view);
export default MarkdownEditor;
+192
View File
@@ -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>
+6
View File
@@ -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;
+42
View File
@@ -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>
+341
View File
@@ -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;
}
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "~/utils/Toolkit";
export const Popup = Toolkit.withInstall(view);
export default Popup;
+26
View File
@@ -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

+5
View File
@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "~/utils/Toolkit";
export const UserLevel = Toolkit.withInstall(view);
export default UserLevel;
+25
View File
@@ -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>