4 Commity c2fcff4fe7 ... 1eeccce43f

Autor SHA1 Wiadomość Data
  Sakulin 1eeccce43f Merge branch 'main' of http://8.130.126.103:10880/liuqianpan2008/bk-vue 2 miesięcy temu
  Sakulin b56c4a7f82 feat(编辑器): 增强文章编辑页面功能并添加样式 2 miesięcy temu
  Sakulin 7103d6a45c feat(主题): 添加暗色主题样式文件 2 miesięcy temu
  Sakulin ab0d99ffe0 feat(组件): 添加BetterSelect下拉选择组件 2 miesięcy temu

+ 103 - 0
src/assets/editordark.css

@@ -0,0 +1,103 @@
+.v-md-textarea-editor pre,
+.v-md-textarea-editor textarea {
+  background-color: var(--background-color) !important;
+}
+
+.v-md-editor__menu {
+  background-color: var(--background-color) !important;
+}
+
+.v-md-editor {
+  background-color: var(--background-color) !important;
+}
+
+.v-md-editor__toolbar-item--active,
+.v-md-editor__toolbar-item--active:hover {
+  background: var(--secondary-background-color) !important;
+}
+
+.v-md-editor__tooltip {
+  background-color: var(--secondary-background-color) !important;
+}
+
+.v-md-editor__menu-item:hover {
+  background-color: var(--secondary-background-color); /* 菜单项悬停背景改为暗色 */
+}
+.v-md-editor__toolbar-item:hover {
+  background: var(--secondary-background-color); /* 工具栏项悬停背景改为暗色 */
+}
+.v-md-editor__toolbar {
+  border-bottom: 1px solid var(--secondary-background-color); /* 工具栏底部边框颜色改为暗色 */
+}
+
+.v-md-editor__toolbar-divider:before {
+  border-left: 1px solid var(--secondary-background-color); /* 工具栏分隔线颜色改为暗色 */
+}
+
+.v-md-editor__left-area {
+  border-right: 1px solid var(--secondary-background-color); /* 左侧区域右边框颜色改为暗色 */
+}
+
+.v-md-editor__left-area-title:after {
+  border-bottom: 1px solid var(--secondary-background-color); /* 左侧区域标题底部边框颜色改为暗色 */
+}
+
+.v-md-editor--left-area-reverse .v-md-editor__left-area {
+  border-left: 1px solid var(--secondary-background-color); /* 反转左侧区域左边框颜色改为暗色 */
+}
+
+.v-md-editor--editable .v-md-editor__editor-wrapper {
+  border-right: 1px solid var(--secondary-background-color); /* 可编辑区域右边框颜色改为暗色 */
+}
+
+@media (prefers-color-scheme: dark) {
+  /* Dark For V Md Editor */
+  .v-md-textarea-editor pre,
+  .v-md-textarea-editor textarea {
+    color: #e0e0e0;
+  }
+
+  .v-md-textarea-editor textarea::-webkit-input-placeholder {
+    color: #707070; /* 输入提示颜色调暗 */
+  }
+
+  .v-md-textarea-editor textarea::placeholder {
+    color: #707070; /* 输入提示颜色调暗 */
+  }
+
+  .v-md-editor__tooltip {
+    color: #ffffff;
+  }
+
+  .v-md-editor__menu::-webkit-scrollbar-thumb {
+    background-color: rgba(80, 80, 80, 0.3); /* 滚动条拇指颜色调暗 */
+  }
+
+  .v-md-editor__menu::-webkit-scrollbar-thumb:hover {
+    background-color: rgba(80, 80, 80, 0.5); /* 滚动条拇指悬停颜色调暗 */
+  }
+
+  .v-md-editor__menu-item {
+    color: #e0e0e0; /* 菜单项文字改为较亮的颜色 */
+  }
+
+  .v-md-editor__toolbar-item {
+    color: #e0e0e0; /* 工具栏项文字改为较亮的颜色 */
+  }
+
+  .v-md-editor__left-area-title {
+    color: #e0e0e0; /* 左侧区域标题文字改为较亮的颜色 */
+  }
+
+  .scrollbar__thumb {
+    background-color: rgba(80, 80, 80, 0.3); /* 滚动条拇指颜色调暗 */
+  }
+
+  .scrollbar__thumb:hover {
+    background-color: rgba(80, 80, 80, 0.5); /* 滚动条拇指悬停颜色调暗 */
+  }
+
+  .v-md-editor__toc-nav-item {
+    color: #e0e0e0; /* 目录导航项文字改为较亮的颜色 */
+  }
+}

+ 178 - 0
src/assets/vuepressdark.css

@@ -0,0 +1,178 @@
+.vuepress-markdown-body {
+  background-color: var(--background-color);
+}
+
+@media (prefers-color-scheme: dark) {
+  .vuepress-markdown-body code[class*='v-md-prism-'],
+  .vuepress-markdown-body pre[class*='v-md-prism-'] {
+    color: #ddd;
+    background: none;
+  }
+  .vuepress-markdown-body :not(pre) > code[class*='v-md-prism-'],
+  .vuepress-markdown-body pre[class*='v-md-prism-'] {
+    background: #1a1a1a;
+  }
+  .vuepress-markdown-body .token.block-comment,
+  .vuepress-markdown-body .token.cdata,
+  .vuepress-markdown-body .token.comment,
+  .vuepress-markdown-body .token.doctype,
+  .vuepress-markdown-body .token.prolog {
+    color: #888;
+  }
+  .vuepress-markdown-body .token.punctuation {
+    color: #ddd;
+  }
+  .vuepress-markdown-body .token.attr-name,
+  .vuepress-markdown-body .token.deleted,
+  .vuepress-markdown-body .token.namespace,
+  .vuepress-markdown-body .token.tag {
+    color: #ff6b6e;
+  }
+  .vuepress-markdown-body .token.function-name {
+    color: #4a90e2;
+  }
+  .vuepress-markdown-body .token.boolean,
+  .vuepress-markdown-body .token.function,
+  .vuepress-markdown-body .token.number {
+    color: #ff9f43;
+  }
+  .vuepress-markdown-body .token.class-name,
+  .vuepress-markdown-body .token.constant,
+  .vuepress-markdown-body .token.property,
+  .vuepress-markdown-body .token.symbol {
+    color: #ffd700;
+  }
+  .vuepress-markdown-body .token.atrule,
+  .vuepress-markdown-body .token.builtin,
+  .vuepress-markdown-body .token.important,
+  .vuepress-markdown-body .token.keyword,
+  .vuepress-markdown-body .token.selector {
+    color: #ba55d3;
+  }
+  .vuepress-markdown-body .token.attr-value,
+  .vuepress-markdown-body .token.char,
+  .vuepress-markdown-body .token.regex,
+  .vuepress-markdown-body .token.string,
+  .vuepress-markdown-body .token.variable {
+    color: #50fa7b;
+  }
+  .vuepress-markdown-body .token.entity,
+  .vuepress-markdown-body .token.operator,
+  .vuepress-markdown-body .token.url {
+    color: #8be9fd;
+  }
+  .vuepress-markdown-body .token.inserted {
+    color: #00ff00;
+  }
+  .vuepress-markdown-body code {
+    color: #a0a0a0;
+    background-color: #2d2d2d;
+  }
+  .vuepress-markdown-body code .token.deleted {
+    color: #ff4d4f;
+  }
+  .vuepress-markdown-body code .token.inserted {
+    color: #52c41a;
+  }
+  .vuepress-markdown-body pre,
+  .vuepress-markdown-body pre[class*='v-md-prism-'] {
+    background-color: #1e1e1e;
+  }
+  .vuepress-markdown-body pre[class*='v-md-prism-'] code,
+  .vuepress-markdown-body pre code {
+    color: #fff;
+    background-color: initial;
+  }
+  .vuepress-markdown-body div[class*='v-md-pre-wrapper-'] {
+    background-color: #1e1e1e;
+  }
+  .vuepress-markdown-body div[class*='v-md-pre-wrapper-']:before {
+    color: hsla(0, 0%, 100%, 0.5);
+  }
+  .vuepress-markdown-body div[class*='v-md-pre-wrapper-'].line-numbers-mode .line-numbers-wrapper {
+    color: hsla(0, 0%, 100%, 0.4);
+  }
+  .vuepress-markdown-body div[class*='v-md-pre-wrapper-'].line-numbers-mode:after {
+    background-color: #1e1e1e;
+    border-right: 1px solid rgba(255, 255, 255, 0.1);
+  }
+  .vuepress-markdown-body .arrow.up {
+    border-bottom: 6px solid #ddd;
+  }
+  .vuepress-markdown-body .arrow.down {
+    border-top: 6px solid #ddd;
+  }
+  .vuepress-markdown-body .arrow.right {
+    border-left: 6px solid #ddd;
+  }
+  .vuepress-markdown-body .arrow.left {
+    border-right: 6px solid #ddd;
+  }
+  .vuepress-markdown-body {
+    color: #e0e0e0;
+  }
+  .vuepress-markdown-body:not(.custom) p.demo {
+    border: 1px solid #333;
+  }
+  .vuepress-markdown-body kbd {
+    background: #333;
+    border: 0.15rem solid #444;
+    border-bottom: 0.25rem solid #444;
+  }
+  .vuepress-markdown-body blockquote {
+    color: #888;
+    border-left: 0.2rem solid #333;
+  }
+  .vuepress-markdown-body h2 {
+    border-bottom: 1px solid #333;
+  }
+  .vuepress-markdown-body hr {
+    border-top: 1px solid #333;
+  }
+  .vuepress-markdown-body tr {
+    border-top: 1px solid #333;
+  }
+  .vuepress-markdown-body tr:nth-child(2n) {
+    background-color: #1a1a1a;
+  }
+  .vuepress-markdown-body td,
+  .vuepress-markdown-body th {
+    border: 1px solid #333;
+  }
+  .vuepress-markdown-body .v-md-svg-outbound {
+    color: #888;
+  }
+  .v-md-plugin-tip.tip {
+    background-color: #1a1a1a;
+    border-color: #42b983;
+  }
+  .v-md-plugin-tip.warning {
+    color: #ffd700;
+    background-color: rgba(255, 215, 0, 0.1);
+    border-color: #ffd700;
+  }
+  .v-md-plugin-tip.warning .v-md-plugin-tip-title {
+    color: #ffd700;
+  }
+  .v-md-plugin-tip.warning a {
+    color: #e0e0e0;
+  }
+  .v-md-plugin-tip.danger {
+    color: #ff4d4f;
+    background-color: rgba(255, 77, 79, 0.1);
+    border-color: #ff4d4f;
+  }
+  .v-md-plugin-tip.danger .v-md-plugin-tip-title {
+    color: #ff4d4f;
+  }
+  .v-md-plugin-tip.danger a {
+    color: #e0e0e0;
+  }
+  .v-md-plugin-tip.details {
+    background-color: #1a1a1a;
+  }
+  .vuepress-markdown-body a,
+  .vuepress-markdown-body p a code {
+    color: #61dafb;
+  }
+}

+ 162 - 0
src/components/BetterSelect.vue

@@ -0,0 +1,162 @@
+<script setup lang="ts">
+import { ref, computed, onMounted, ref as elementRef, onUnmounted, watch } from 'vue';
+
+const props = defineProps<{
+  options: { value: string; label: string }[];
+  modelValue: string;
+  placeholder: string;
+  width: string;
+}>();
+
+const emit = defineEmits(['update:modelValue']);
+
+const showOptions = ref(false);
+const selectedValueRef = elementRef<HTMLElement | null>(null);
+const optionsListRef = elementRef<HTMLElement | null>(null);
+
+const handleClickOutside = (event: MouseEvent) => {
+  if (selectedValueRef.value && optionsListRef.value && showOptions.value) {
+    const isClickInside = selectedValueRef.value.contains(event.target as Node) || optionsListRef.value.contains(event.target as Node);
+    if (!isClickInside) {
+      showOptions.value = false;
+    }
+  }
+};
+
+const handleSelect = (option: { value: string; label: string }) => {
+  emit('update:modelValue', option.value);
+  showOptions.value = false;
+};
+
+const selectedOption = computed(() => {
+  return props.options.find(option => option.value === props.modelValue);
+});
+
+let updateTimer: number | null = null;
+
+const updateOptionsListPosition = () => {
+  if (updateTimer) {
+    clearTimeout(updateTimer);
+  }
+
+  updateTimer = setTimeout(() => {
+    if (selectedValueRef.value && optionsListRef.value && showOptions.value) {
+      optionsListRef.value.style.top = selectedValueRef.value.getBoundingClientRect().bottom + 'px';
+      optionsListRef.value.style.left = selectedValueRef.value.getBoundingClientRect().left + 'px';
+      optionsListRef.value.style.width = selectedValueRef.value.offsetWidth + 'px';
+    }
+    updateTimer = null;
+  }, 300);
+};
+
+onMounted(() => {
+  document.addEventListener('click', handleClickOutside);
+  window.addEventListener('resize', updateOptionsListPosition);
+});
+
+onUnmounted(() => {
+  document.removeEventListener('click', handleClickOutside);
+  window.removeEventListener('resize', updateOptionsListPosition);
+});
+
+watch(showOptions, (newValue) => {
+  if (newValue) {
+    updateOptionsListPosition();
+  }
+});
+
+</script>
+
+<template>
+  <div class="better-select" :class="showOptions ? ['showing-select'] : []">
+    <div ref="selectedValueRef" :class="selectedOption ? [] : ['placeholder-showing']" class="selected-value" @click="showOptions = !showOptions">
+      {{ selectedOption?.label || props.placeholder }}
+    </div>
+    <ul
+      ref="optionsListRef"
+      class="options-list"
+      :class="showOptions ? ['showing-options-list'] : []"
+      :style="{
+        position: 'fixed',
+        top: selectedValueRef?.getBoundingClientRect().bottom + 'px',
+        left: selectedValueRef?.getBoundingClientRect().left + 'px',
+        width: selectedValueRef?.offsetWidth + 'px'
+      }"
+    >
+      <li
+        v-for="option in props.options"
+        :key="option.value"
+        :class="option.value == selectedOption?.value ? ['active'] : []"
+        @click="handleSelect(option)"
+      >
+        {{ option.label }}
+      </li>
+    </ul>
+  </div>
+</template>
+
+<style scoped>
+.better-select {
+  position: relative;
+  text-align: center;
+  width: v-bind("props.width");
+}
+
+.selected-value:hover {
+  background-size: 100% 2px;
+}
+
+.showing-select .selected-value {
+  background-size: 100% 2px;
+}
+
+.selected-value {
+  padding: 2px;
+  cursor: pointer;
+  background: linear-gradient(to right, var(--text-color), var(--text-color)) no-repeat left bottom;
+  color: var(--text-color);
+  background-size: 0 2px;
+  transition: all .3s;
+}
+
+.placeholder-showing {
+  color: var(--secondary-text-color);
+}
+
+.options-list {
+  margin: 0;
+  padding: 0;
+  opacity: 0;
+  list-style-type: none;
+  border-top: none;
+  border-radius: 0 0 4px 4px;
+  max-height: 200px;
+  overflow-y: hidden;
+  background-color: var(--background-color);
+  z-index: 1000;
+  transform-origin: top;
+  transform: scaleY(0%);
+  transition: all .3s;
+}
+
+.showing-options-list {
+  opacity: 100%;
+  transform: scaleY(100%);
+  box-shadow: var(--muted-text-color) 0 0 4px;
+}
+
+.options-list li {
+  padding: 4px 0;
+  cursor: pointer;
+  text-align: center;
+  transition: all .3s;
+}
+
+.options-list .active {
+  background-color: var(--secondary-background-color);
+}
+
+.options-list li:hover {
+  background-color: var(--secondary-background-color);
+}
+</style>

+ 9 - 8
src/main.ts

@@ -6,17 +6,18 @@ import { createPinia } from 'pinia'
 import App from './App.vue'
 import { router } from './router'
 
-import VMdEditor from '@kangc/v-md-editor';
-import '@kangc/v-md-editor/lib/style/base-editor.css';
+import VMdEditor from '@kangc/v-md-editor'
+import '@kangc/v-md-editor/lib/style/base-editor.css'
 
-import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
-import '@kangc/v-md-editor/lib/theme/style/vuepress.css';
+import githubTheme from '@kangc/v-md-editor/lib/theme/vuepress.js'
+import '@kangc/v-md-editor/lib/theme/style/vuepress.css'
 
-import Prism from 'prismjs';
+import './assets/editordark.css'
+import './assets/vuepressdark.css'
 
-VMdEditor.use(vuepressTheme, {
-  Prism
-});
+import Prism from 'prismjs'
+
+VMdEditor.use(githubTheme, { Prism })
 
 const app = createApp(App)
 

+ 101 - 3
src/views/PostEditView.vue

@@ -1,6 +1,13 @@
 <script setup lang="ts">
+import { computed, ref } from 'vue'
+import BetterSelect from '@/components/BetterSelect.vue'
 
-import { ref } from 'vue'
+const editorConfig = {
+  leftToolbar: 'undo redo | image',
+  rightToolbar: 'preview fullscreen',
+}
+
+const title = ref('')
 
 const text = ref(`
 # 这是一个标题
@@ -13,10 +20,101 @@ print('Hello, World!')
 
 `)
 
+const titleShadowRadius = computed(() => (title.value.trim().length > 0 ? '8px' : ''))
+
+const selected = ref('')
+
+const options = [
+  {label: "技术", value: "技术"},
+  {label: "日志", value: "日志"},
+  {label: "随笔", value: "随笔"},
+  {label: "其他", value: "其他"},
+]
+
 </script>
 
 <template>
-  <v-md-editor v-model="text" height="400px"/>
+  <div>
+    <div class="post-item" style="padding: 12px 30px; overflow: hidden">
+      <div>
+        <input class="p-input" type="text" placeholder="博文标题" v-model="title" />
+      </div>
+      <div style="display: flex; align-items: center; gap: 12px">
+        <better-select :options="options" v-model="selected" placeholder="类型" width="64px"/>
+      </div>
+    </div>
+    <div class="post-item" style="padding: 0; overflow: hidden">
+      <v-md-editor
+        v-model="text"
+        height="400px"
+        :left-toolbar="editorConfig.leftToolbar"
+        :right-toolbar="editorConfig.rightToolbar"
+      />
+    </div>
+  </div>
 </template>
 
-<style scoped></style>
+<style scoped>
+.p-input {
+  width: 100%;
+  border: none;
+  font-size: 20px;
+  margin-top: 20px;
+  margin-bottom: 10px;
+  padding-bottom: 8px;
+  background: linear-gradient(to right, var(--text-color), var(--text-color)) no-repeat left bottom;
+  background-size: 0 2px;
+  color: var(--text-color);
+  transition: all 0.3s;
+
+  font-weight: bold;
+}
+
+.p-input::placeholder {
+  color: var(--muted-text-color);
+}
+
+
+.p-input:focus {
+  outline: none;
+  background-size: 100% 2px;
+  text-shadow: var(--secondary-text-color) 0 0 v-bind(titleShadowRadius);
+}
+
+.p-select {
+  width: 64px;
+  border: none;
+  font-size: 16px;
+  margin-top: 10px;
+  margin-bottom: 10px;
+  padding: 8px;
+  background: linear-gradient(to right, var(--text-color), var(--text-color)) no-repeat left bottom;
+  background-size: 0 2px;
+  color: var(--text-color);
+  transition: all 0.3s;
+  appearance: none;
+  text-align: center;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+}
+
+.p-select:hover {
+  background-size: 100% 2px;
+}
+
+.p-select option {
+  background-color: var(--background-color);
+  color: var(--text-color);
+  text-align: center;
+}
+
+.p-select option:checked {
+  background-color: var(--muted-text-color);
+}
+
+.p-select option:hover {
+  background-color: var(--muted-text-color);
+}
+
+
+</style>