xiongzhu 2 лет назад
Родитель
Сommit
3676fad5fe
4 измененных файлов с 231 добавлено и 1 удалено
  1. 2 0
      package.json
  2. 196 0
      src/components/common/OutlineEditor.vue
  3. 20 0
      src/views/page/PaperGen.vue
  4. 13 1
      yarn.lock

+ 2 - 0
package.json

@@ -24,6 +24,8 @@
     "common:prepare": "husky install"
   },
   "dependencies": {
+    "@doonce/num2chn": "^1.0.0",
+    "@siakhooi/number-to-chinese-words": "^1.12.4",
     "@traptitech/markdown-it-katex": "^3.6.0",
     "@vicons/tabler": "^0.12.0",
     "@vueuse/core": "^9.13.0",

+ 196 - 0
src/components/common/OutlineEditor.vue

@@ -0,0 +1,196 @@
+<template>
+    <NElement
+        tag="div"
+        class="et-outline m-4 p-2 rounded-lg border outline-none text-left overflow-auto"
+        @keydown="onKeyDown"
+        @keypress="onKeyPressed"
+        @keyup="onKeyUp"
+        contenteditable
+        style="width: 200px; height: 300px"
+        ref="el"
+    >
+        <p data-level="0" data-title="第一章"></p>
+    </NElement>
+</template>
+<script setup lang="ts">
+import { Ref, computed, onMounted, reactive, ref, watch, defineEmits } from 'vue'
+import { useMutationObserver, useDebounceFn } from '@vueuse/core'
+import { NConfigProvider, NElement, useThemeVars } from 'naive-ui'
+// @ts-ignore
+import * as NumberToChinese from '@siakhooi/number-to-chinese-words'
+
+const props = defineProps({
+    modelValue: {
+        type: Array<any>,
+        default: (): any[] => []
+    }
+})
+watch(props.modelValue, v => {
+    if (JSON.stringify(v) !== JSON.stringify(props.modelValue)) {
+        update(props.modelValue)
+    }
+})
+const themeVars = useThemeVars()
+const el: Ref<any> = ref(null)
+const et = computed(() => {
+    return el.value?.$el as HTMLElement
+})
+
+onMounted(() => {
+    useMutationObserver(
+        et.value,
+        mutations => {
+            if (mutations.flatMap(m => Array.from(m.addedNodes).concat(Array.from(m.removedNodes))).length > 0) {
+                arrange()
+            } else if (mutations.filter(m => m.type === 'characterData').length > 0) {
+                arrange()
+            }
+            // arrange()
+        },
+        {
+            attributes: true,
+            childList: true,
+            subtree: true,
+            characterData: true
+        }
+    )
+    update(props.modelValue)
+})
+
+const chapters: any[] = reactive([])
+
+const emit = defineEmits(['update:modelValue'])
+watch(chapters, v => {
+    emit('update:modelValue', v)
+})
+const arrange = useDebounceFn(() => {
+    const levels = [0, 0, 0, 0]
+    chapters.splice(0, chapters.length)
+    for (const child of Array.from(et.value?.children || [])) {
+        const e = child as Element
+        const level = parseInt(e.getAttribute('data-level') || '0')
+        levels[level] = levels[level] + 1
+        for (let i = level + 1; i < levels.length; i++) {
+            levels[i] = 0
+        }
+        if (level === 0) {
+            e.setAttribute(
+                'data-title',
+                '第' + NumberToChinese.convertNumber(levels[0], { removeLeadingOne: levels[level] > 1 }) + '章'
+            )
+        } else {
+            e.setAttribute('data-title', levels.slice(0, level + 1).join('.'))
+        }
+
+        let node: any
+        let parent = chapters
+        levels.slice(0, level + 1).forEach((v: number) => {
+            node = parent[v - 1]
+            if (!node) {
+                node = {
+                    children: []
+                }
+                parent[v - 1] = node
+            }
+            parent = node.children
+        })
+        if (node) node.title = e.textContent
+    }
+}, 10)
+
+function update(chaptersData: any) {
+    console.log('update')
+    chaptersData = chaptersData || []
+    const list: any[] = []
+    function _update(_data: any, level: number) {
+        _data.forEach((e: any) => {
+            list.push({
+                title: e.title,
+                level
+            })
+            _update(e.children, level + 1)
+        })
+    }
+    _update(chaptersData, 0)
+    et.value?.childNodes.forEach((e: any) => {
+        et.value?.removeChild(e)
+    })
+    list.forEach((e, i) => {
+        const p = document.createElement('p')
+        p.setAttribute('data-level', e.level.toString())
+        p.textContent = e.title
+        et.value?.appendChild(p)
+    })
+    arrange()
+    console.log(list)
+}
+
+function onKeyDown(e: KeyboardEvent) {
+    const sel = window.getSelection()!
+    let node = sel!.anchorNode!
+    if (node?.nodeName !== 'P') {
+        node = node.parentNode!
+    }
+    const idx = Array.from(node?.parentNode?.children || []).indexOf(node as any)
+
+    if (e.key === 'Enter') {
+        if (e.shiftKey) {
+            e.preventDefault()
+            return
+        }
+        // e.preventDefault()
+        // const p = document.createElement('p')
+        // et.value?.insertBefore(p, node!.parentNode!.children[idx + 1])
+        // let prev
+        // if (idx === 0) {
+        //     prev = node
+        // } else {
+        //     prev = node!.parentNode!.children[idx - 1]
+        // }
+        // p.setAttribute('data-title', '第二章')
+        // p.setAttribute('data-level', (prev as Element)?.getAttribute('data-level') || '0')
+        // range.setStart(p, 0)
+        // range.collapse(true)
+        // sel?.removeAllRanges()
+        // sel?.addRange(range)
+    } else if (e.key === 'Tab') {
+        e.preventDefault()
+        if (idx === 0) return
+        let level = parseInt((node as Element).getAttribute('data-level') || '0')
+        level += e.shiftKey ? -1 : 1
+        level = Math.min(Math.max(0, level), 3)
+        ;(node as Element).setAttribute('data-level', level.toString())
+        arrange()
+        console.log(e)
+    } else if (e.key === 'Backspace') {
+        if (!node.textContent && node.parentNode?.children.length === 1) {
+            e.preventDefault()
+        }
+        //
+    }
+}
+function onKeyPressed(e: KeyboardEvent) {}
+function onKeyUp(e: KeyboardEvent) {}
+</script>
+<style lang="less" scoped>
+.et-outline {
+    &:focus {
+        border-color: var(--primary-color);
+    }
+    :deep(p) {
+        &::before {
+            content: attr(data-title) ' ';
+            opacity: 0.6;
+        }
+        &[data-level='1'] {
+            padding-left: 16px;
+        }
+        &[data-level='2'] {
+            padding-left: 32px;
+        }
+        &[data-level='3'] {
+            padding-left: 48px;
+        }
+    }
+}
+</style>

+ 20 - 0
src/views/page/PaperGen.vue

@@ -1,4 +1,5 @@
 <template>
+    <OutlineEditor v-model="chapters" />
     <NLayout class="h-full">
         <NLayoutHeader class="flex items-center px-8" style="height: 64px">
             <span class="text-lg flex-1">论文助手</span>
@@ -128,6 +129,7 @@ import hljs from 'highlight.js'
 import { t } from '@/locales'
 import Vditor from 'vditor'
 import 'vditor/src/assets/less/index.less'
+import OutlineEditor from '@/components/common/OutlineEditor.vue'
 
 const message = useMessage()
 const showLogin = ref(false)
@@ -495,4 +497,22 @@ function onEditorShow() {
         height: '100%'
     })
 }
+
+const chapters: Ref<any[]> = ref([
+    { children: [], title: 'asdf' },
+    { children: [], title: 'asdf' },
+    {
+        children: [
+            { children: [], title: 'werwefwsdfasdf' },
+            {
+                children: [
+                    { children: [], title: 'sdf' },
+                    { children: [], title: '' }
+                ],
+                title: 'asdf'
+            }
+        ],
+        title: 'asdf'
+    }
+])
 </script>

+ 13 - 1
yarn.lock

@@ -1157,6 +1157,11 @@
   resolved "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.12.tgz"
   integrity sha512-AQLGhhaE0F+rwybRCkKUdzBdTEM/5PZBYy+fSYe1T9z9+yxMuV/k7ZRqa4M69X+EI1W8pa4kc9Iq2VjQkZx4rg==
 
+"@doonce/num2chn@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/@doonce/num2chn/-/num2chn-1.0.0.tgz#d547e8ec69f4028555a6cdffdb261501a955f665"
+  integrity sha512-xwxWPeLl4KP8MBQHB/g7pZosyGlXVkcsY/qrcwQsCaEOy7y/RWbRCfGN8/goHCB+N2ra/AZFecMjoQV3ptYovw==
+
 "@emotion/hash@~0.8.0":
   version "0.8.0"
   resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz"
@@ -1532,6 +1537,13 @@
   resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz"
   integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==
 
+"@siakhooi/number-to-chinese-words@^1.12.4":
+  version "1.12.4"
+  resolved "https://registry.npmmirror.com/@siakhooi/number-to-chinese-words/-/number-to-chinese-words-1.12.4.tgz#5cfc758427281f8c2197551aaa1707516f655da4"
+  integrity sha512-/94LlVgPqm3tlMmdGGchmJ9cD7FrZ/maKbpzBeoXv4TyasCDFu1wnA5uO/uDTzeGxJrrkvWmSiR53S7w+cRZ5w==
+  dependencies:
+    ts-node "^10.9.1"
+
 "@sindresorhus/is@^0.7.0":
   version "0.7.0"
   resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz"
@@ -7691,7 +7703,7 @@ ts-interface-checker@^0.1.9:
   resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
   integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
 
-ts-node@^10.8.1:
+ts-node@^10.8.1, ts-node@^10.9.1:
   version "10.9.1"
   resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz"
   integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==