Compare commits

...

3 Commits

Author SHA1 Message Date
Libin YANG
ea0a42dffb
fix: update button (#477)
All checks were successful
Build and Deploy / build-and-deploy (push) Has been skipped
2024-12-18 12:59:50 +08:00
Libin YANG
3442a94c51
fix: update button (#476) 2024-12-18 12:39:55 +08:00
YangFong
71de06633e
feat: init post list (#475) 2024-12-18 09:53:56 +08:00
21 changed files with 281 additions and 36 deletions

View File

@ -33,7 +33,7 @@ import {
} from '@/config'
import { useDisplayStore, useStore } from '@/stores'
import { mergeCss, solveWeChatImage } from '@/utils'
import { Moon, Paintbrush, Sun } from 'lucide-vue-next'
import { Moon, Paintbrush, PanelLeftClose, PanelLeftOpen, Sun } from 'lucide-vue-next'
import { storeToRefs } from 'pinia'
import { nextTick, ref, useTemplateRef } from 'vue'
import PickColors from 'vue-pick-colors'
@ -122,10 +122,10 @@ function copy() {
// position translateY
.replace(/top:(.*?)em/g, `transform: translateY($1em)`)
//
.replaceAll(`hsl(var(--foreground))`, `#3f3f3f`)
.replaceAll(`var(--blockquote-background)`, `#f7f7f7`)
.replaceAll(`var(--md-primary-color)`, primaryColor.value)
.replaceAll(/--md-primary-color:.+?;/g, ``)
.replace(/hsl\(var\(--foreground\)\)/g, `#3f3f3f`)
.replace(/var\(--blockquote-background\)/g, `#f7f7f7`)
.replace(/var\(--md-primary-color\)/g, primaryColor.value)
.replace(/--md-primary-color:.+?;/g, ``)
.replace(/<span class="nodeLabel"([^>]*)><p[^>]*>(.*?)<\/p><\/span>/g, `<span class="nodeLabel"$1>$2</span>`)
clipboardDiv.focus()
@ -219,6 +219,12 @@ const formatOptions = ref<Format[]>([`rgb`, `hex`, `hsl`, `hsv`])
<HelpDropdown />
</Menubar>
<Button v-if="!store.isOpenPostSlider" variant="outline" class="mr-2" @click="store.isOpenPostSlider = true">
<PanelLeftOpen class="size-4" />
</Button>
<Button v-else variant="outline" class="mr-2" @click="store.isOpenPostSlider = false">
<PanelLeftClose class="size-4" />
</Button>
<Popover>
<PopoverTrigger>
<Button variant="outline">

View File

@ -0,0 +1,189 @@
<script setup lang="ts">
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import { useStore } from '@/stores'
import { Edit3, Ellipsis, Plus, Trash } from 'lucide-vue-next'
import { ref, watch } from 'vue'
import { toast } from 'vue-sonner'
const store = useStore()
const isOpen = ref(false)
const addPostInputVal = ref(``)
watch(isOpen, () => {
if (isOpen.value) {
addPostInputVal.value = ``
}
})
function addPost() {
if (addPostInputVal.value === ``) {
toast.error(`文章标题不可为空`)
return
}
store.addPost(addPostInputVal.value)
isOpen.value = false
toast.success(`文章新增成功`)
}
const editTarget = ref(-1)
const isOpenEditDialog = ref(false)
const renamePostInputVal = ref(``)
function startRenamePost(index: number) {
editTarget.value = index
renamePostInputVal.value = store.posts[index].title
isOpenEditDialog.value = true
}
function renamePost() {
if (renamePostInputVal.value === ``) {
toast.error(`文章标题不可为空`)
return
}
store.renamePost(editTarget.value, renamePostInputVal.value)
isOpenEditDialog.value = false
toast.success(`文章更名成功`)
}
const isOpenDelPostConfirmDialog = ref(false)
function startDelPost(index: number) {
editTarget.value = index
isOpenDelPostConfirmDialog.value = true
}
function delPost() {
store.delPost(editTarget.value)
isOpenDelPostConfirmDialog.value = false
toast.success(`文章删除成功`)
}
</script>
<template>
<div
class="overflow-hidden border-r bg-gray/20 transition-width dark:bg-gray/40"
:class="{
'w-0': !store.isOpenPostSlider,
'w-50': store.isOpenPostSlider,
}"
>
<nav class="space-y-1 h-full overflow-auto p-2">
<Dialog v-model:open="isOpen">
<DialogTrigger as-child>
<Button variant="outline" class="w-full" size="xs">
<Plus /> 新增文章
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>新增文章</DialogTitle>
<DialogDescription>
请输入文章名称
</DialogDescription>
</DialogHeader>
<Input v-model="addPostInputVal" />
<DialogFooter>
<Button @click="addPost()">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<a
v-for="(post, index) in store.posts"
:key="post.title"
href="#"
:class="{
'bg-primary text-primary-foreground': store.currentPostIndex === index,
}"
class="hover:bg-primary/90 hover:text-primary-foreground dark:bg-muted dark:hover:bg-muted h-8 w-full inline-flex items-center justify-start gap-2 whitespace-nowrap rounded px-2 text-sm transition-colors dark:text-white dark:hover:text-white"
@click="store.currentPostIndex = index"
>
<span class="line-clamp-1">{{ post.title }}</span>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button size="xs" variant="ghost" class="ml-auto px-1.5">
<Ellipsis class="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem @click.stop="startRenamePost(index)">
<Edit3 class="mr-2 size-4" />
更名
</DropdownMenuItem>
<DropdownMenuItem @click.stop="startDelPost(index)">
<Trash class="mr-2 size-4" />
删除
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</a>
<!-- 重命名弹窗 -->
<Dialog v-model:open="isOpenEditDialog">
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>编辑文章名称</DialogTitle>
<DialogDescription>
请输入新的文章名称
</DialogDescription>
</DialogHeader>
<Input v-model="renamePostInputVal" />
<DialogFooter>
<Button variant="outline" @click="isOpenEditDialog = false">
取消
</Button>
<Button @click="renamePost()">
保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog v-model:open="isOpenDelPostConfirmDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>提示</AlertDialogTitle>
<AlertDialogDescription>
此操作将删除该文章是否继续
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction @click="delPost()">
确认
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</nav>
</div>
</template>
<style scoped lang="less">
</style>

View File

@ -601,6 +601,7 @@ function onDrop(e: DragEvent) {
<Button
variant="link"
class="p-0"
as="a"
href="https://developer.qiniu.com/kodo"
target="_blank"
>
@ -642,6 +643,7 @@ function onDrop(e: DragEvent) {
<Button
variant="link"
class="p-0"
as="a"
href="http://docs.minio.org.cn/docs/master/minio-client-complete-guide"
target="_blank"
>
@ -680,6 +682,7 @@ function onDrop(e: DragEvent) {
<Button
variant="link"
class="p-0"
as="a"
href="https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html"
target="_blank"
>
@ -688,6 +691,7 @@ function onDrop(e: DragEvent) {
<Button
variant="link"
class="p-0"
as="a"
href="https://mpmd.pages.dev/tutorial/"
target="_blank"
>

View File

@ -10,7 +10,7 @@ const props = defineProps<{
<template>
<div>
<Label class="flex items-center">
<span class="mr-4 w-[150px] text-right font-bold min-h-4 flex-shrink-0" :class="{ required: props.required }">
<span class="mr-4 min-h-4 w-[150px] flex-shrink-0 text-right font-bold" :class="{ required: props.required }">
{{ props.label }}
</span>
<slot />

View File

@ -4,8 +4,8 @@ import { cn } from '@/lib/utils'
import { type AlertVariants, alertVariants } from '.'
const props = defineProps<{
class?: HTMLAttributes['class']
variant?: AlertVariants['variant']
class?: HTMLAttributes[`class`]
variant?: AlertVariants[`variant`]
}>()
</script>

View File

@ -3,7 +3,7 @@ import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
class?: HTMLAttributes[`class`]
}>()
</script>

View File

@ -3,7 +3,7 @@ import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
class?: HTMLAttributes[`class`]
}>()
</script>

View File

@ -5,17 +5,17 @@ export { default as AlertDescription } from './AlertDescription.vue'
export { default as AlertTitle } from './AlertTitle.vue'
export const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
`relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground`,
{
variants: {
variant: {
default: 'bg-background text-foreground',
default: `bg-background text-foreground`,
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
`border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive`,
},
},
defaultVariants: {
variant: 'default',
variant: `default`,
},
},
)

View File

@ -3,7 +3,7 @@ import { cn } from '@/lib/utils'
import { Label, type LabelProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<LabelProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

View File

@ -4,7 +4,7 @@ import { cn } from '@/lib/utils'
import { NumberFieldRoot, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<NumberFieldRootProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<NumberFieldRootProps & { class?: HTMLAttributes[`class`] }>()
const emits = defineEmits<NumberFieldRootEmits>()
const delegatedProps = computed(() => {

View File

@ -3,7 +3,7 @@ import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
class?: HTMLAttributes[`class`]
}>()
</script>

View File

@ -5,7 +5,7 @@ import { Minus } from 'lucide-vue-next'
import { NumberFieldDecrement, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<NumberFieldDecrementProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<NumberFieldDecrementProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

View File

@ -5,7 +5,7 @@ import { Plus } from 'lucide-vue-next'
import { NumberFieldIncrement, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<NumberFieldIncrementProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<NumberFieldIncrementProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

View File

@ -4,7 +4,7 @@ import { cn } from '@/lib/utils'
import { NumberFieldInput } from 'radix-vue'
const props = defineProps<{
class?: HTMLAttributes['class']
class?: HTMLAttributes[`class`]
}>()
</script>

View File

@ -9,7 +9,7 @@ import {
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes[`class`] }>()
const emits = defineEmits<SwitchRootEmits>()

View File

@ -3,7 +3,7 @@ import { cn } from '@/lib/utils'
import { TabsContent, type TabsContentProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<TabsContentProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

View File

@ -3,7 +3,7 @@ import { cn } from '@/lib/utils'
import { TabsList, type TabsListProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<TabsListProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<TabsListProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

View File

@ -3,7 +3,7 @@ import { cn } from '@/lib/utils'
import { TabsTrigger, type TabsTriggerProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

View File

@ -4,16 +4,16 @@ import { cn } from '@/lib/utils'
import { useVModel } from '@vueuse/core'
const props = defineProps<{
class?: HTMLAttributes['class']
class?: HTMLAttributes[`class`]
defaultValue?: string | number
modelValue?: string | number
}>()
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
(e: `update:modelValue`, payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
const modelValue = useVModel(props, `modelValue`, emits, {
passive: true,
defaultValue: props.defaultValue,
})

View File

@ -52,12 +52,50 @@ export const useStore = defineStore(`store`, () => {
// 内容编辑器编辑器
const editor = ref<CodeMirror.EditorFromTextArea | null>(null)
// 编辑区域内容
// 预备弃用
const editorContent = useStorage(`__editor_content`, DEFAULT_CONTENT)
const isOpenPostSlider = useStorage(addPrefix(`is_open_post_slider`), false)
// 文章列表
const posts = useStorage(addPrefix(`posts`), [{
title: `文章1`,
content: DEFAULT_CONTENT,
}])
// 当前文章
const currentPostIndex = useStorage(addPrefix(`current_post_index`), 0)
const addPost = (title: string) => {
currentPostIndex.value = posts.value.push({
title,
content: DEFAULT_CONTENT,
}) - 1
}
const renamePost = (index: number, title: string) => {
posts.value[index].title = title
}
const delPost = (index: number) => {
posts.value.splice(index, 1)
currentPostIndex.value = 0
}
watch(currentPostIndex, () => {
toRaw(editor.value!).setValue(posts.value[currentPostIndex.value].content)
})
onMounted(() => {
// 迁移阶段,兼容之前的方案
if (editorContent.value !== DEFAULT_CONTENT) {
posts.value[currentPostIndex.value].content = editorContent.value
editorContent.value = DEFAULT_CONTENT
}
})
// 格式化文档
const formatContent = () => {
formatDoc((editor.value!).getValue()).then((doc) => {
editorContent.value = doc
posts.value[currentPostIndex.value].content = doc
toRaw(editor.value!).setValue(doc)
})
}
@ -421,7 +459,6 @@ export const useStore = defineStore(`store`, () => {
isOpenConfirmDialog,
resetStyleConfirm,
resetStyle,
editorContent,
cssContentConfig,
addCssContentTab,
@ -429,6 +466,12 @@ export const useStore = defineStore(`store`, () => {
setCssEditorValue,
tabChanged,
renameTab,
posts,
currentPostIndex,
addPost,
renamePost,
delPost,
isOpenPostSlider,
}
})

View File

@ -3,6 +3,7 @@ import type { ComponentPublicInstance } from 'vue'
import CssEditor from '@/components/CodemirrorEditor/CssEditor.vue'
import EditorHeader from '@/components/CodemirrorEditor/EditorHeader/index.vue'
import InsertFormDialog from '@/components/CodemirrorEditor/InsertFormDialog.vue'
import PostSlider from '@/components/CodemirrorEditor/PostSlider.vue'
import UploadImgDialog from '@/components/CodemirrorEditor/UploadImgDialog.vue'
import RunLoading from '@/components/RunLoading.vue'
import {
@ -15,6 +16,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import {
ContextMenu,
ContextMenuContent,
@ -38,7 +40,7 @@ import { toast } from 'vue-sonner'
const store = useStore()
const displayStore = useDisplayStore()
const { isDark, output, editor, editorContent } = storeToRefs(store)
const { isDark, output, editor } = storeToRefs(store)
const { isShowCssEditor } = storeToRefs(displayStore)
const {
@ -201,7 +203,7 @@ function initEditor() {
const editorDom = document.querySelector<HTMLTextAreaElement>(`#editor`)!
if (!editorDom.value) {
editorDom.value = editorContent.value
editorDom.value = store.posts[store.currentPostIndex].content
}
editor.value = CodeMirror.fromTextArea(editorDom, {
mode: `text/x-markdown`,
@ -248,7 +250,7 @@ function initEditor() {
clearTimeout(changeTimer.value)
changeTimer.value = setTimeout(() => {
onEditorRefresh()
editorContent.value = e.getValue()
store.posts[store.currentPostIndex].content = e.getValue()
}, 300)
})
@ -396,10 +398,11 @@ onMounted(() => {
@end-copy="endCopy"
/>
<main class="container-main flex-1">
<div class="container-main-section grid h-full border-1" :class="isShowCssEditor ? 'grid-cols-3' : 'grid-cols-2'">
<div class="container-main-section h-full flex border-1">
<PostSlider />
<div
ref="codeMirrorWrapper"
class="codeMirror-wrapper border-r-1"
class="codeMirror-wrapper flex-1 border-r-1"
:class="{
'order-1': !store.isEditOnLeft,
}"
@ -443,7 +446,7 @@ onMounted(() => {
id="preview"
ref="preview"
:span="isShowCssEditor ? 8 : 12"
class="preview-wrapper p-5"
class="preview-wrapper flex-1 p-5"
>
<div id="output-wrapper" :class="{ output_night: !backLight }">
<div class="preview border shadow-xl">
@ -457,7 +460,7 @@ onMounted(() => {
</div>
</div>
</div>
<CssEditor />
<CssEditor class="flex-1" />
</div>
</main>