测试新编辑器

This commit is contained in:
Guarp 2025-02-22 19:21:25 +08:00
parent 11c7873760
commit eda2d71cb6
8 changed files with 509 additions and 46 deletions

View File

@ -1,29 +0,0 @@
<template>
<div>
<textarea v-model="latexInput" placeholder="输入数学公式" rows="5" cols="50"></textarea>
<div v-html="formattedLatex"></div>
</div>
</template>
<script setup>
import {ref, watch, onMounted, nextTick} from 'vue';
const latexInput = ref(''); // LaTeX
const formattedLatex = ref(''); // LaTeX
onMounted(() => {
// MathJax
if (window.MathJax) {
MathJax.typeset();
}
});
// latexInput
watch(latexInput, async (newValue) => {
formattedLatex.value = newValue; //
await nextTick(); // DOM
if (window.MathJax) {
MathJax.typeset(); // MathJax
}
});
</script>

View File

@ -0,0 +1,87 @@
<template>
<div v-html="formattedLatex" class="renderer-container"></div>
</template>
<script setup>
import { ref, watch, onMounted, nextTick, defineProps } from 'vue';
import { marked } from "marked";
// prop `contentInput`
const props = defineProps({
contentInput: {
type: String,
default: ''
}
});
// textarea
const localInput = ref(props.contentInput);
const formattedLatex = ref('');
//
watch(() => props.contentInput, (newValue) => {
localInput.value = newValue;
});
// localInput
watch(localInput, async (newValue) => {
formattedLatex.value = marked.parse(newValue);
await nextTick(); // DOM
if (window.MathJax) {
MathJax.typeset(); // MathJax
}
});
//
const is2WindowMode = ref(true);
const checkWindowSize = () => {
is2WindowMode.value = window.innerWidth < 1200;
};
onMounted(async () => {
checkWindowSize();
window.addEventListener('resize', checkWindowSize);
formattedLatex.value = marked.parse(localInput.value);
await nextTick();
if (window.MathJax) {
MathJax.typeset();
}
});
</script>
<style scoped>
.renderer-container {
font-family: Arial, sans-serif;
line-height: 1.6;
}
.renderer-container h1,
.renderer-container h2,
.renderer-container h3 {
color: #333;
}
.renderer-container p {
margin-bottom: 1rem;
}
.renderer-container code {
background-color: #f4f4f4;
padding: 0.2rem 0.5rem;
border-radius: 4px;
}
.renderer-container pre {
background-color: #f4f4f4;
padding: 1rem;
border-radius: 4px;
overflow: auto;
}
.renderer-container hr {
width: 80%;
height: 1px;
background: linear-gradient(to bottom, rgba(217, 217, 217, 0.6), rgba(114, 114, 114, 0.6));
margin: 20px 0;
border-radius: 2px;
}
</style>

View File

@ -319,17 +319,21 @@ const toggleTheme = () => {
/* 移动端下拉菜单 */
.mobile-nav-items {
display: flex;
flex-direction: column;
position: absolute;
backdrop-filter: blur(10px);
top: 45px;
word-break: keep-all;
list-style: none;
padding: 1rem;
margin: 0;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
text-align: center;
}
.mobile-nav-items li {
display: flex;
justify-content: center;
margin: 0.5rem;
padding: 5px;
border-radius: 4px;

View File

@ -23,16 +23,7 @@ const funcButtons = ref([
{name: 'url', func: '[[cur]](https://example.com)'},
{name: 'img', func: '![图片说明](https://example.com/img1.png)'},
])
function getFormattedTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // 01
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
function clickFuncBtn(func) {
const textarea = document.querySelector('textarea'); // textarea
const startPos = textarea.selectionStart; //
@ -194,6 +185,7 @@ onUnmounted(() => {
width: 70%;
}
button {
color: white;
background: #363636;

View File

@ -1,5 +1,5 @@
<script setup>
import {ref} from 'vue'
import {ref, watch} from 'vue'
import AuthService from "../../../services/auth.js";
import router from "../../router/index.js";
import store from "../../store/index.js";
@ -18,6 +18,8 @@ const userIntro = ref('')
const tempIntro = ref(userIntro.value)
const fileInput = ref(null);
const autoSaveInterval = ref(store.state.editAutoSave.interval);
const openFileDialog = () => {
fileInput.value.click();
}
@ -91,6 +93,10 @@ function saveIntro() {
userIntro.value = tempIntro.value
isEditingIntro.value = false
}
watch(autoSaveInterval, () => {
store.commit('setAutoSaveTime', autoSaveInterval.value);
});
</script>
<template>
@ -184,6 +190,23 @@ function saveIntro() {
<label class="color-mode-box">
<input type="radio" v-model="store.state.theme" value="dark">深色模式
</label>
<div class="halving-line" />
<div class="auto-save-setting">
<label class="checkbox-label">
<input type="checkbox" @click="store.commit('toggleAutoSave')" :checked="store.state.editAutoSave.on">
</label>
<span>自动保存</span>
<select v-model="autoSaveInterval">
<option value="114514">动态更新</option>
<option value="15000">15</option>
<option value="30000">30</option>
<option value="45000">45</option>
<option value="60000">60</option>
<option value="100000">100</option>
</select>
</div>
<!-- -->
<!-- <div class="intro-box">-->
@ -312,7 +335,7 @@ input, textarea {
.halving-line {
width: 80%;
height: 1px;
height: 0.1rem;
background: linear-gradient(to bottom, rgba(217, 217, 217, 0.6), rgba(114, 114, 114, 0.6));
margin: 20px 0;
border-radius: 2px;
@ -392,6 +415,33 @@ input, textarea {
accent-color: #ffb74d;
}
.auto-save-setting {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
width: 80%;
gap: 20px;
}
.checkbox-label input {
width: 20px;
height: 20px;
accent-color: #007bff;
transition: transform 0.2s ease;
}
.checkbox-label input:checked {
transform: scale(1.2);
}
.theme-light .checkbox-label input {
accent-color: #ffb74d;
border: 2px solid #ddd;
}
.theme-light .checkbox-label input:checked {
border-color: #ffb74d;
}
/* ========== 个人简介区 ========== */
.intro-box {
width: 80%;

View File

@ -1,16 +1,359 @@
<script setup>
import GeneralEditor from "../components/GeneralEditor.vue";
import GeneralRenderer from "../components/GeneralRenderer.vue";
import {onMounted, onUnmounted, ref, watch} from "vue";
import store from "../store/index.js";
import swal from "../utils/sweetalert.js";
import getCurrentTime from "../utils/getCurrentTime.js";
const contentInput = ref(store.state.editStore.blog || '');
const titleInput = ref(store.state.editStore.blogTitle || '')
const portMode = ref('both');
const windowWidth = ref(0);
const isMobileMode = ref(false);
const isMenuOpen = ref(false);
const funcButtons = ref([
{name: 'h1', func: '# [cur]'},
{name: 'h2', func: '## [cur]'},
{name: 'h3', func: '### [cur]'},
{name: '<s>abc</s>', func: '~~[cur]~~'},
{name: '<b>abc</b>', func: '**[cur]**'},
{name: '<i>abc</i>', func: '*[cur]*'},
{name: '<code>abc</code>', func: '\`[cur]\`'},
{name: '●', func: '- '},
{name: 'url', func: '[[cur]](https://example.com)'},
{name: 'img', func: '![图片说明](https://example.com/img1.png)'},
{name: 'mth', func: '$[cur]$'},
{name: 'Mth', func: '$$[cur]$$'},
])
function clickFuncBtn(func) {
const textarea = document.querySelector('textarea'); // textarea
const startPos = textarea.selectionStart; //
const endPos = textarea.selectionEnd; //
const selectedText = textarea.value.slice(startPos, endPos); //
let newText = '';
if (selectedText) {
newText = func.replace('[cur]', selectedText);
} else {
newText = func.replace('[cur]', '请在此填写内容');
}
textarea.setRangeText(newText, startPos, endPos, 'select'); //
contentInput.value = textarea.value;
const curPos = textarea.value.indexOf('[cur]');
textarea.selectionStart = curPos;
textarea.selectionEnd = curPos;
textarea.focus();
}
const checkWindowSize = () => {
windowWidth.value = window.innerWidth;
if (windowWidth.value < 705) {
isMobileMode.value = true;
} else {
isMobileMode.value = false;
isMenuOpen.value = false;
}
};
const saveDocument = () => {
store.commit('saveEdit', {
blog: contentInput.value,
blogTitle: titleInput.value,
blogSaveTime: getCurrentTime()
});
swal.tip('success', '保存成功')
};
const handleKeydown = (event) => {
// Ctrl + S
if (event.ctrlKey && event.key === 's') {
event.preventDefault(); //
saveDocument(); //
}
};
watch(portMode, async () => {
setTimeout(() => {
contentInput.value = contentInput.value + ' ';
}, 1)
setTimeout(() => {
contentInput.value = contentInput.value.slice(0, -1);
}, 2)
});
watch(contentInput, () => {
if (store.state.editAutoSave.on && store.state.editAutoSave.interval === 114514) {
store.commit('saveEdit', {
blog: contentInput.value,
blogTitle: titleInput.value,
blogSaveTime: getCurrentTime()
});
}
})
onMounted(() => {
checkWindowSize();
window.addEventListener('resize', checkWindowSize);
window.addEventListener('keydown', handleKeydown);
autoSave = setInterval(()=>{
if (! store.state.editAutoSave.on || store.state.editAutoSave.interval === 114514) {
return;
}
store.commit('saveEdit', {
blog: contentInput.value,
blogTitle: titleInput.value,
blogSaveTime: getCurrentTime()
});
}, store.state.editAutoSave.interval);
});
let autoSave
onUnmounted(() => {
clearInterval(autoSave);
window.removeEventListener('keydown', handleKeydown);
});
</script>
<template>
<div class="container">
<h1>测试页面</h1>
<GeneralEditor></GeneralEditor>
<div class="header">
<input placeholder="输入标题" v-model="titleInput">
</div>
<div class="top">
<div class="function-btn">
<button v-if="! isMobileMode" v-for="btn in funcButtons" v-html="btn.name" @click="clickFuncBtn(btn.func)"/>
<button v-if="isMobileMode" @click="isMenuOpen = ! isMenuOpen"></button>
<div v-if="isMobileMode && isMenuOpen" class="function-btn-menu">
<button v-for="btn in funcButtons" v-html="btn.name" @click="clickFuncBtn(btn.func)"/>
</div>
</div>
<div class="port-btn">
<button @click="portMode = 'both'" :class="{onMode: portMode === 'both'}"></button>
<button @click="portMode = 'edit'" :class="{onMode: portMode === 'edit'}"></button>
<button @click="portMode = 'view'" :class="{onMode: portMode === 'view'}"></button>
</div>
<div class="doc-btn">
<button @click="saveDocument">保存</button>
<button>提交</button>
</div>
</div>
<div class="middle">
<div v-if="portMode !== 'view'" class="left">
<textarea v-model="contentInput"></textarea>
</div>
<div v-if="portMode !== 'edit'" class="right">
<GeneralRenderer :content-input="contentInput"/>
</div>
</div>
<div class="bottom">
<div class="characters">总字符数: {{ contentInput.length }}</div>
<div class="auto-save-switch" @click="store.commit('toggleAutoSave')">自动保存: {{ store.state.editAutoSave.on ? '' : '' }} </div>
<div class="save-time-display">上次保存 [{{ store.state.editStore.blogSaveTime }}]</div>
</div>
</div>
</template>
<style scoped>
.container {
background: #131313;
width: calc(100% - 40px);
height: calc(100vh - 100px);
padding: 20px;
max-width: none;
display: flex;
flex-direction: column;
align-items: center;
}
.theme-light .container {
background: #e0e0e0;
}
.header {
flex: 0 0 60px;
width: 100%;
background: white;
display: flex;
align-items: center;
}
.header input {
border: none;
outline: none;
padding: 15px;
width: 100%;
height: calc(100% - 30px);
font-size: initial;
background: #2a2a2a;
color: white;
}
.theme-light .header input {
background: white;
color: black;
}
.top {
flex: 0 0 40px;
width: 100%;
background: #3d3d3d;
display: flex;
flex-direction: row;
gap: 10px;
align-content: space-between;
}
.theme-light .top {
background: #f1f1f1;
}
.function-btn {
flex: 2;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
margin-left: 5px;
gap: 5px;
}
.function-btn-menu {
position: absolute;
display: flex;
flex-direction: row;
flex-wrap: wrap;
max-height: 100px;
gap: 3px;
padding: 3px;
top: 190px;
left: 30px;
//width: 100px;
//height: 50px;
background: rgba(0, 0, 0, 0.12);
}
.port-btn {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 5px;
}
.doc-btn {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
margin-right: 5px;
gap: 5px;
}
.doc-btn button {
width: auto !important;
}
.top button {
color: white;
background: #131313;
border: #007bff solid 2px;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
overflow: hidden;
}
.top button.onMode {
background: #007bff;
border: #007bff solid 2px;
}
.theme-light .top button {
color: black;
background: white;
border: #ffb74d solid 2px;
}
.theme-light .top button.onMode {
background: #ffb74d;
border: #ffb74d solid 2px;
}
.bottom {
flex: 0 0 25px;
width: 100%;
background: #2a2a2a;
color: gray;
font-size: small;
display: flex;
align-items: center;
justify-content: space-between;
}
.theme-light .bottom {
background: white;
outline: gray solid 1px;
}
.bottom .characters {
margin: 0 5px;
}
.bottom .save-time-display {
margin: 0 5px;
}
.bottom .auto-save-switch:hover {
background: rgba(128, 128, 128, 0.1);
cursor: pointer;
}
.middle {
width: 100%;
max-height: calc(100% - 125px);
flex: 1;
display: flex;
align-items: center;
background: white;
}
.middle .left {
flex: 1;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
border: rgba(128, 128, 128, 0.5) solid 1px;
}
.left textarea {
width: calc(100% - 2 * (15px));
height: calc(100% - 2 * (15px));
resize: none;
padding: 15px;
font-size: 17px;
font-family: sans-serif,serif;
border: none;
transition: all 0.2s ease;
}
.left textarea {
background: #1a1a1a;
color: white;
}
.theme-light .left textarea {
background: white;
color: black;
}
.left textarea:focus {
outline: #007bff solid 1px;
}
.theme-light .left textarea:focus {
outline: #ffb74d solid 1px;
}
.middle .right {
flex: 1;
width: calc(100% - 2 * (15px));
height: 100%;
background: #1a1a1a;
border: rgba(128, 128, 128, 0.5) solid 1px;
color: white;
overflow: auto;
padding: 0 15px;
transition: all 0.2s ease;
}
.theme-light .right {
background: white;
color: black;
}
</style>

View File

@ -1,6 +1,5 @@
import { createStore } from 'vuex';
import createPersistedStatePlugin from '../plugins/vuexLocalStorage';
import api from "../utils/axios.js";
import {getDomain} from "../utils/getDomain.js";
const store = createStore({
@ -10,6 +9,7 @@ const store = createStore({
token: null,
userInfo: {},
editStore: {},
editAutoSave: {on: true, interval: 30000},
demos: {},
},
mutations: {
@ -42,6 +42,12 @@ const store = createStore({
},
setLogTemp(state, arr) {
state.log = arr;
},
toggleAutoSave(state) {
state.editAutoSave.on = ! state.editAutoSave.on;
},
setAutoSaveTime(state, ms) {
state.editAutoSave.interval = Number(ms) || 30000;
}
},
getters: {
@ -74,7 +80,7 @@ const store = createStore({
},
plugins: [createPersistedStatePlugin({
key: 'cyberStorage',
whitelist: ['theme', 'userInfo', 'demos', 'editStore'],
whitelist: ['theme', 'userInfo', 'demos', 'editStore', 'editAutoSave'],
})]
})

View File

@ -0,0 +1,10 @@
export default function getCurrentTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始需要加1
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}