style: update dialog (#402)

This commit is contained in:
YangFong 2024-09-16 00:01:54 +08:00 committed by GitHub
parent 442beb8053
commit d3a7d08f9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1525 additions and 862 deletions

View File

@ -5,66 +5,63 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 222.2 84% 4.9%; --foreground: 0 0% 3.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%; --card-foreground: 0 0% 3.9%;
--border: 214.3 31.8% 91.4%; --popover: 0 0% 100%;
--input: 214.3 31.8% 91.4%; --popover-foreground: 0 0% 3.9%;
--primary: 222.2 47.4% 11.2%; --primary: 0 0% 9%;
--primary-foreground: 210 40% 98%; --primary-foreground: 0 0% 98%;
--secondary: 210 40% 96.1%; --secondary: 0 0% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 0 0% 9%;
--accent: 210 40% 96.1%; --muted: 0 0% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%; --muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 0 0% 98%;
--ring: 222.2 84% 4.9%;
--border:0 0% 89.8%;
--input:0 0% 89.8%;
--ring:0 0% 3.9%;
--radius: 0.5rem; --radius: 0.5rem;
} }
.dark { .dark {
--background: 222.2 84% 4.9%; --background:0 0% 3.9%;
--foreground: 210 40% 98%; --foreground:0 0% 98%;
--muted: 217.2 32.6% 17.5%; --card:0 0% 3.9%;
--muted-foreground: 215 20.2% 65.1%; --card-foreground:0 0% 98%;
--popover: 222.2 84% 4.9%; --popover:0 0% 3.9%;
--popover-foreground: 210 40% 98%; --popover-foreground:0 0% 98%;
--card: 222.2 84% 4.9%; --primary:0 0% 98%;
--card-foreground: 210 40% 98%; --primary-foreground:0 0% 9%;
--border: 217.2 32.6% 17.5%; --secondary:0 0% 14.9%;
--input: 217.2 32.6% 17.5%; --secondary-foreground:0 0% 98%;
--primary: 210 40% 98%; --muted:0 0% 14.9%;
--primary-foreground: 222.2 47.4% 11.2%; --muted-foreground:0 0% 63.9%;
--secondary: 217.2 32.6% 17.5%; --accent:0 0% 14.9%;
--secondary-foreground: 210 40% 98%; --accent-foreground:0 0% 98%;
--accent: 217.2 32.6% 17.5%; --destructive:0 62.8% 30.6%;
--accent-foreground: 210 40% 98%; --destructive-foreground:0 0% 98%;
--destructive: 0 62.8% 30.6%; --border:0 0% 14.9%;
--destructive-foreground: 210 40% 98%; --input:0 0% 14.9%;
--ring:0 0% 83.1%;
--ring: 212.7 26.8% 83.9%;
} }
} }

View File

@ -71,7 +71,6 @@ section {
justify-content: center; justify-content: center;
padding: 0; padding: 0;
overflow-y: scroll; overflow-y: scroll;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
} }
.hint { .hint {
@ -87,8 +86,6 @@ section {
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
outline: none; outline: none;
color: var(--el-text-color-regular);
box-shadow: var(--el-box-shadow);
} }
.preview table { .preview table {

View File

@ -1,35 +1,13 @@
@nightBgColor: #333333; @nightPreviewColor: #191919;
@nightPreviewColor: #1e1e1e; @nightCodeMirrorColor: #191919;
@nightHeaderColor: #3c3c3c;
@nightCodeMirrorColor: #1e1e1e;
@nightActiveCodeMirrorColor: gray; @nightActiveCodeMirrorColor: gray;
@nightFontColor: gray; @nightFontColor: gray;
@nightLinkColor: #8e9eb9; @nightLinkColor: #8e9eb9;
@nightLinkTextColor: #84868b; @nightLinkTextColor: #84868b;
@nightWhiteColor: #f0f0f0;
@nightButtonBg: #1e1e1e;
@nightButtonHoverColor: #84868b;
@nightLineColor: #84868b; @nightLineColor: #84868b;
.dark { .dark {
.container { .container {
background-color: @nightBgColor;
.CodeMirror {
caret-color: @nightFontColor;
color: @nightFontColor;
background-color: @nightCodeMirrorColor;
box-shadow: inset 0 0 0 1px rgba(100, 37, 37, 0.102);
.CodeMirror-cursor {
border-left: 1px solid @nightLineColor;
}
.CodeMirror-activeline-background {
background-color: #3e3e3e!important;
}
}
.output_night { .output_night {
.preview { .preview {
background-color: @nightPreviewColor; background-color: @nightPreviewColor;
@ -46,20 +24,6 @@
} }
} }
.cm-s-xq-light span.cm-variable-2,
.cm-s-xq-light .CodeMirror-activeline-background {
background-color: transparent;
}
.cm-s-xq-light span.cm-string {
color: @nightLinkColor;
}
.cm-s-xq-light span.cm-link {
color: @nightLinkTextColor;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
background-color: @nightCodeMirrorColor; background-color: @nightCodeMirrorColor;
} }
@ -69,7 +33,6 @@
.CodeMirror { .CodeMirror {
padding-bottom: 0; padding-bottom: 0;
height: 100% !important; height: 100% !important;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
font-size: 14px; font-size: 14px;
font-family: 'PingFang SC', BlinkMacSystemFont, Roboto, 'Helvetica Neue', font-family: 'PingFang SC', BlinkMacSystemFont, Roboto, 'Helvetica Neue',
sans-serif !important; sans-serif !important;

View File

@ -67,7 +67,7 @@ function handleTabsEdit(targetName, action) {
<template> <template>
<transition enter-active-class="bounceInRight"> <transition enter-active-class="bounceInRight">
<el-col v-show="store.isShowCssEditor" :span="8" class="cssEditor-wrapper order-1 h-full flex flex-col"> <el-col v-show="store.isShowCssEditor" :span="8" class="cssEditor-wrapper order-1 h-full flex flex-col border-l-1">
<el-tabs <el-tabs
v-model="store.cssContentConfig.active" v-model="store.cssContentConfig.active"
type="border-card" type="border-card"

View File

@ -1,4 +1,12 @@
<script setup> <script setup>
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
const props = defineProps({ const props = defineProps({
visible: { visible: {
type: Boolean, type: Boolean,
@ -8,6 +16,12 @@ const props = defineProps({
const emit = defineEmits([`close`]) const emit = defineEmits([`close`])
function onUpdate(val) {
if (!val) {
emit(`close`)
}
}
const links = [ const links = [
{ label: `GitHub 仓库`, url: `https://github.com/doocs/md` }, { label: `GitHub 仓库`, url: `https://github.com/doocs/md` },
{ label: `Gitee 仓库`, url: `https://gitee.com/doocs/md` }, { label: `Gitee 仓库`, url: `https://gitee.com/doocs/md` },
@ -20,34 +34,30 @@ function onRedirect(url) {
</script> </script>
<template> <template>
<el-dialog <Dialog :open="props.visible" @update:open="onUpdate">
title="关于" <DialogContent>
class="about__dialog" <DialogHeader>
:model-value="props.visible" <DialogTitle>关于</DialogTitle>
width="520" </DialogHeader>
center <div class="text-center">
@close="emit('close')" <h3>一款高度简洁的微信 Markdown 编辑器</h3>
> <p>扫码关注公众号 Doocs原创技术文章第一时间推送</p>
<div class="text-center"> <img
<h3>一款高度简洁的微信 Markdown 编辑器</h3> class="mx-auto my-5"
<p>扫码关注公众号 Doocs原创技术文章第一时间推送</p> src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png"
<img alt="Doocs Markdown 编辑器"
class="mx-auto my-5" style="width: 40%"
src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png" >
alt="Doocs Markdown 编辑器" </div>
style="width: 40%" <DialogFooter class="sm:justify-evenly">
> <Button
</div> v-for="link in links"
<template #footer> :key="link.url"
<el-button @click="onRedirect(link.url)"
v-for="link in links" >
:key="link.url" {{ link.label }}
type="primary" </Button>
plain </DialogFooter>
@click="onRedirect(link.url)" </DialogContent>
> </Dialog>
{{ link.label }}
</el-button>
</template>
</el-dialog>
</template> </template>

View File

@ -1,49 +1,27 @@
<script setup> <script setup>
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useStore } from '@/stores' import { useStore } from '@/stores'
const props = defineProps([`isOpen`, `clickTrigger`, `openDropdown`, `updateOpen`])
const store = useStore() const store = useStore()
const { const { toggleShowInsertFormDialog, toggleShowUploadImgDialog } = store
toggleShowInsertFormDialog,
toggleShowUploadImgDialog,
} = store
</script> </script>
<template> <template>
<DropdownMenu :open="props.isOpen" @update:open="props.updateOpen"> <MenubarMenu>
<DropdownMenuTrigger <MenubarTrigger> 编辑 </MenubarTrigger>
class="flex items-center p-2 px-4 hover:bg-gray-2 dark:hover:bg-stone-9" <MenubarContent align="start">
:class="{ <MenubarItem @click="toggleShowUploadImgDialog()">
'bg-gray-2': props.isOpen,
'dark:bg-stone-9': props.isOpen,
}"
@click="props.clickTrigger()"
@mouseenter="props.openDropdown()"
>
编辑
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem @click="toggleShowUploadImgDialog()">
<el-icon class="mr-2 h-4 w-4"> <el-icon class="mr-2 h-4 w-4">
<ElIconUpload /> <ElIconUpload />
</el-icon> </el-icon>
上传图片 上传图片
</DropdownMenuItem> </MenubarItem>
<DropdownMenuItem @click="toggleShowInsertFormDialog()"> <MenubarItem @click="toggleShowInsertFormDialog()">
<el-icon class="mr-2 h-4 w-4"> <el-icon class="mr-2 h-4 w-4">
<ElIconGrid /> <ElIconGrid />
</el-icon> </el-icon>
插入表格 插入表格
</DropdownMenuItem> </MenubarItem>
</DropdownMenuContent> </MenubarContent>
</DropdownMenu> </MenubarMenu>
</template> </template>

View File

@ -1,18 +1,8 @@
<script setup> <script setup>
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useStore } from '@/stores' import { useStore } from '@/stores'
const props = defineProps([`isOpen`, `clickTrigger`, `openDropdown`, `updateOpen`])
const store = useStore() const store = useStore()
const { const {
@ -30,51 +20,43 @@ const {
</script> </script>
<template> <template>
<DropdownMenu :open="props.isOpen" @update:open="props.updateOpen"> <MenubarMenu>
<DropdownMenuTrigger <MenubarTrigger>
class="flex items-center p-2 px-4 hover:bg-gray-2 dark:hover:bg-stone-9"
:class="{
'bg-gray-2': props.isOpen,
'dark:bg-stone-9': props.isOpen,
}"
@click="props.clickTrigger()"
@mouseenter="props.openDropdown()"
>
文件 文件
</DropdownMenuTrigger> </MenubarTrigger>
<DropdownMenuContent align="start"> <MenubarContent align="start">
<DropdownMenuItem @click="importMarkdownContent()"> <MenubarItem @click="importMarkdownContent()">
<el-icon class="mr-2 h-4 w-4"> <el-icon class="mr-2 h-4 w-4">
<ElIconUpload /> <ElIconUpload />
</el-icon> </el-icon>
导入 .md 导入 .md
</DropdownMenuItem> </MenubarItem>
<DropdownMenuItem @click="exportEditorContent2MD()"> <MenubarItem @click="exportEditorContent2MD()">
<el-icon class="mr-2 h-4 w-4"> <el-icon class="mr-2 h-4 w-4">
<ElIconDownload /> <ElIconDownload />
</el-icon> </el-icon>
导出 .md 导出 .md
</DropdownMenuItem> </MenubarItem>
<DropdownMenuItem @click="exportEditorContent2HTML()"> <MenubarItem @click="exportEditorContent2HTML()">
<el-icon class="mr-2 h-4 w-4"> <el-icon class="mr-2 h-4 w-4">
<ElIconDocument /> <ElIconDocument />
</el-icon> </el-icon>
导出 .html 导出 .html
</DropdownMenuItem> </MenubarItem>
<DropdownMenuSeparator /> <MenubarSeparator />
<DropdownMenuItem @click="toggleDark()"> <MenubarItem @click="toggleDark()">
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isDark }"> <el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isDark }">
<ElIconCheck /> <ElIconCheck />
</el-icon> </el-icon>
深色模式 深色模式
</DropdownMenuItem> </MenubarItem>
<DropdownMenuSeparator /> <MenubarSeparator />
<DropdownMenuItem @click="toggleEditOnLeft()"> <MenubarItem @click="toggleEditOnLeft()">
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isEditOnLeft }"> <el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isEditOnLeft }">
<ElIconCheck /> <ElIconCheck />
</el-icon> </el-icon>
左侧编辑 左侧编辑
</DropdownMenuItem> </MenubarItem>
</DropdownMenuContent> </MenubarContent>
</DropdownMenu> </MenubarMenu>
</template> </template>

View File

@ -3,41 +3,21 @@ import { ref } from 'vue'
import AboutDialog from './AboutDialog.vue' import AboutDialog from './AboutDialog.vue'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
const props = defineProps([`isOpen`, `clickTrigger`, `openDropdown`, `updateOpen`])
const aboutDialogVisible = ref(false) const aboutDialogVisible = ref(false)
</script> </script>
<template> <template>
<DropdownMenu :open="props.isOpen" @update:open="props.updateOpen"> <MenubarMenu>
<DropdownMenuTrigger <MenubarTrigger>
class="flex items-center p-2 px-4 hover:bg-gray-2 dark:hover:bg-stone-9"
:class="{
'bg-gray-2': props.isOpen,
'dark:bg-stone-9': props.isOpen,
}"
@click="props.clickTrigger()"
@mouseenter="props.openDropdown()"
>
帮助 帮助
</DropdownMenuTrigger> </MenubarTrigger>
<DropdownMenuContent align="start"> <MenubarContent align="start">
<DropdownMenuItem @click="aboutDialogVisible = true"> <MenubarItem @click="aboutDialogVisible = true">
<el-icon class="mr-2 h-4 w-4" /> <el-icon class="mr-2 h-4 w-4" />
<span>关于</span> <span>关于</span>
</DropdownMenuItem> </MenubarItem>
</DropdownMenuContent> </MenubarContent>
</DropdownMenu> </MenubarMenu>
<AboutDialog <AboutDialog :visible="aboutDialogVisible" @close="aboutDialogVisible = false" />
:visible="aboutDialogVisible"
@close="aboutDialogVisible = false"
/>
</template> </template>

View File

@ -1,6 +1,13 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { useStore } from '@/stores' import { useStore } from '@/stores'
@ -48,6 +55,12 @@ function post() {
content: form.value.content || form.value.auto.content, content: form.value.content || form.value.auto.content,
}) })
} }
function onUpdate(val) {
if (!val) {
dialogVisible.value = false
}
}
</script> </script>
<template> <template>
@ -55,51 +68,43 @@ function post() {
发布 发布
</Button> </Button>
<el-dialog <Dialog :open="dialogVisible" @update:open="onUpdate">
title="发布" <DialogContent>
:model-value="dialogVisible" <DialogHeader>
@close="dialogVisible = false" <DialogTitle>发布</DialogTitle>
> </DialogHeader>
<el-alert
class="mb-4"
title="注:此功能由第三方浏览器插件支持,本平台不保证安全性。"
type="info"
show-icon
/>
<el-form
class="postInfo"
label-width="50"
:model="form"
>
<el-form-item label="封面">
<el-input
v-model="form.thumb"
placeholder="自动提取第一张图"
/>
</el-form-item>
<el-form-item label="标题">
<el-input
v-model="form.title"
placeholder="自动提取第一个标题"
/>
</el-form-item>
<el-form-item label="描述">
<el-input
v-model="form.desc"
type="textarea"
:rows="4"
placeholder="自动提取第一个段落"
/>
</el-form-item>
</el-form>
<template #footer> <el-alert
<el-button @click="dialogVisible = false"> class="mb-4"
title="注:此功能由第三方浏览器插件支持,本平台不保证安全性。"
</el-button> type="info"
<el-button type="primary" @click="post"> show-icon
/>
</el-button> <el-form class="postInfo" label-width="50" :model="form">
</template> <el-form-item label="封面">
</el-dialog> <el-input v-model="form.thumb" placeholder="自动提取第一张图" />
</el-form-item>
<el-form-item label="标题">
<el-input v-model="form.title" placeholder="自动提取第一个标题" />
</el-form-item>
<el-form-item label="描述">
<el-input
v-model="form.desc"
type="textarea"
:rows="4"
placeholder="自动提取第一个段落"
/>
</el-form-item>
</el-form>
<DialogFooter>
<Button variant="outline" @click="dialogVisible = false">
</Button>
<Button @click="post">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template> </template>

View File

@ -4,25 +4,22 @@ import { storeToRefs } from 'pinia'
import StyleOptionMenu from './StyleOptionMenu.vue' import StyleOptionMenu from './StyleOptionMenu.vue'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { import {
HoverCard, HoverCard,
HoverCardContent, HoverCardContent,
HoverCardTrigger, HoverCardTrigger,
} from '@/components/ui/hover-card' } from '@/components/ui/hover-card'
import { codeBlockThemeOptions, colorOptions, fontFamilyOptions, fontSizeOptions, legendOptions, themeOptions } from '@/config' import {
codeBlockThemeOptions,
colorOptions,
fontFamilyOptions,
fontSizeOptions,
legendOptions,
themeOptions,
} from '@/config'
import { useStore } from '@/stores' import { useStore } from '@/stores'
const props = defineProps([`isOpen`, `clickTrigger`, `openDropdown`, `updateOpen`])
const store = useStore() const store = useStore()
const { const {
@ -69,23 +66,28 @@ function customStyle() {
</script> </script>
<template> <template>
<DropdownMenu :open="props.isOpen" @update:open="props.updateOpen"> <MenubarMenu>
<DropdownMenuTrigger <MenubarTrigger> 样式 </MenubarTrigger>
class="flex items-center p-2 px-4 hover:bg-gray-2 dark:hover:bg-stone-9" <MenubarContent class="w-56" align="start">
:class="{ <StyleOptionMenu
'bg-gray-2': props.isOpen, title="主题"
'dark:bg-stone-9': props.isOpen, :options="themeOptions"
}" :current="theme"
@click="props.clickTrigger()" :change="themeChanged"
@mouseenter="props.openDropdown()" />
> <MenubarSeparator />
样式 <StyleOptionMenu
</DropdownMenuTrigger> title="字体"
<DropdownMenuContent class="w-56" align="start"> :options="fontFamilyOptions"
<StyleOptionMenu title="主题" :options="themeOptions" :current="theme" :change="themeChanged" /> :current="fontFamily"
<DropdownMenuSeparator /> :change="fontChanged"
<StyleOptionMenu title="字体" :options="fontFamilyOptions" :current="fontFamily" :change="fontChanged" /> />
<StyleOptionMenu title="字号" :options="fontSizeOptions" :current="fontSize" :change="sizeChanged" /> <StyleOptionMenu
title="字号"
:options="fontSizeOptions"
:current="fontSize"
:change="sizeChanged"
/>
<StyleOptionMenu <StyleOptionMenu
title="主题色" title="主题色"
:options="colorOptions" :options="colorOptions"
@ -98,9 +100,14 @@ function customStyle() {
:current="codeBlockTheme" :current="codeBlockTheme"
:change="codeBlockThemeChanged" :change="codeBlockThemeChanged"
/> />
<StyleOptionMenu title="图注格式" :options="legendOptions" :current="legend" :change="legendChanged" /> <StyleOptionMenu
<DropdownMenuSeparator /> title="图注格式"
<DropdownMenuItem @click.self.prevent="showPicker"> :options="legendOptions"
:current="legend"
:change="legendChanged"
/>
<MenubarSeparator />
<MenubarItem @click.self.prevent="showPicker">
<HoverCard :open-delay="100"> <HoverCard :open-delay="100">
<HoverCardTrigger class="w-full flex"> <HoverCardTrigger class="w-full flex">
<el-icon class="mr-2 h-4 w-4" /> <el-icon class="mr-2 h-4 w-4" />
@ -131,23 +138,23 @@ function customStyle() {
@change="colorChanged" @change="colorChanged"
@click="showPicker" @click="showPicker"
/> --> /> -->
</DropdownMenuItem> </MenubarItem>
<DropdownMenuItem @click="customStyle"> <MenubarItem @click="customStyle">
<el-icon class="mr-2 h-4 w-4" /> <el-icon class="mr-2 h-4 w-4" />
自定义 CSS 自定义 CSS
</DropdownMenuItem> </MenubarItem>
<DropdownMenuSeparator /> <MenubarSeparator />
<DropdownMenuItem @click="macCodeBlockChanged"> <MenubarItem @click="macCodeBlockChanged">
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isMacCodeBlock }"> <el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isMacCodeBlock }">
<ElIconCheck /> <ElIconCheck />
</el-icon> </el-icon>
Mac 代码块 Mac 代码块
</DropdownMenuItem> </MenubarItem>
<DropdownMenuSeparator /> <MenubarSeparator />
<DropdownMenuItem divided @click="resetStyleConfirm"> <MenubarItem divided @click="resetStyleConfirm">
<el-icon class="mr-2 h-4 w-4" /> <el-icon class="mr-2 h-4 w-4" />
重置 重置
</DropdownMenuItem> </MenubarItem>
</DropdownMenuContent> </MenubarContent>
</DropdownMenu> </MenubarMenu>
</template> </template>

View File

@ -1,12 +1,10 @@
<script setup> <script setup>
import { import {
DropdownMenuItem, MenubarItem,
DropdownMenuPortal, MenubarSub,
DropdownMenuShortcut, MenubarSubContent,
DropdownMenuSub, MenubarSubTrigger,
DropdownMenuSubContent, } from '@/components/ui/menubar'
DropdownMenuSubTrigger,
} from '@/components/ui/dropdown-menu'
const props = defineProps({ const props = defineProps({
title: { title: {
@ -42,30 +40,28 @@ function setStyle(title, value) {
</script> </script>
<template> <template>
<DropdownMenuSub> <MenubarSub>
<DropdownMenuSubTrigger> <MenubarSubTrigger>
<el-icon class="mr-2 h-4 w-4" /> <el-icon class="mr-2 h-4 w-4" />
<span>{{ props.title }}</span> <span>{{ props.title }}</span>
</DropdownMenuSubTrigger> </MenubarSubTrigger>
<DropdownMenuPortal> <MenubarSubContent class="max-h-56 overflow-auto">
<DropdownMenuSubContent class="max-h-56 overflow-auto"> <MenubarItem
<DropdownMenuItem v-for="{ label, value, desc } in options"
v-for="{ label, value, desc } in options" :key="value"
:key="value" :label="label"
:label="label" :model-value="value"
:model-value="value" class="w-50"
class="w-50" @click="change(value)"
@click="change(value)" >
> <el-icon class="mr-2 h-4 w-4" :style="{ opacity: +(current === value) }">
<el-icon class="mr-2 h-4 w-4" :style="{ opacity: +(current === value) }"> <ElIconCheck />
<ElIconCheck /> </el-icon>
</el-icon> {{ label }}
{{ label }} <DropdownMenuShortcut :style="setStyle(title, value)">
<DropdownMenuShortcut :style="setStyle(title, value)"> {{ desc }}
{{ desc }} </DropdownMenuShortcut>
</DropdownMenuShortcut> </MenubarItem>
</DropdownMenuItem> </MenubarSubContent>
</DropdownMenuSubContent> </MenubarSub>
</DropdownMenuPortal>
</DropdownMenuSub>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { nextTick, reactive, ref } from 'vue' import { nextTick } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { ElNotification } from 'element-plus' import { ElNotification } from 'element-plus'
import { Moon, Paintbrush, Sun } from 'lucide-vue-next' import { Moon, Paintbrush, Sun } from 'lucide-vue-next'
@ -9,7 +9,18 @@ import FileDropdown from './FileDropdown.vue'
import HelpDropdown from './HelpDropdown.vue' import HelpDropdown from './HelpDropdown.vue'
import StyleDropdown from './StyleDropdown.vue' import StyleDropdown from './StyleDropdown.vue'
import EditDropdown from './EditDropdown.vue' import EditDropdown from './EditDropdown.vue'
import { altSign, codeBlockThemeOptions, colorOptions, ctrlKey, ctrlSign, fontFamilyOptions, fontSizeOptions, legendOptions, shiftSign, themeOptions } from '@/config' import {
altSign,
codeBlockThemeOptions,
colorOptions,
ctrlKey,
ctrlSign,
fontFamilyOptions,
fontSizeOptions,
legendOptions,
shiftSign,
themeOptions,
} from '@/config'
import { import {
Select, Select,
@ -19,29 +30,22 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { import {
DropdownMenu, Menubar,
DropdownMenuContent, MenubarContent,
DropdownMenuItem, MenubarItem,
DropdownMenuSeparator, MenubarMenu,
DropdownMenuShortcut, MenubarSeparator,
DropdownMenuTrigger, MenubarShortcut,
} from '@/components/ui/dropdown-menu' MenubarTrigger,
import { } from '@/components/ui/menubar'
Popover,
PopoverContent, import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
PopoverTrigger,
} from '@/components/ui/popover'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { mergeCss, solveWeChatImage } from '@/utils' import { mergeCss, solveWeChatImage } from '@/utils'
import { useStore } from '@/stores' import { useStore } from '@/stores'
const emit = defineEmits([ const emit = defineEmits([`addFormat`, `formatContent`, `startCopy`, `endCopy`])
`addFormat`,
`formatContent`,
`startCopy`,
`endCopy`,
])
const formatItems = [ const formatItems = [
{ {
@ -78,18 +82,9 @@ const formatItems = [
const store = useStore() const store = useStore()
const { const { isDark, isCiteStatus, output, primaryColor } = storeToRefs(store)
isDark,
isCiteStatus,
output,
primaryColor,
} = storeToRefs(store)
const { const { toggleDark, editorRefresh, citeStatusChanged } = store
toggleDark,
editorRefresh,
citeStatusChanged,
} = store
// //
function copy() { function copy() {
@ -103,10 +98,7 @@ function copy() {
const originalItems = tempDiv.querySelectorAll(`li > ul, li > ol`) const originalItems = tempDiv.querySelectorAll(`li > ul, li > ol`)
originalItems.forEach((originalItem) => { originalItems.forEach((originalItem) => {
originalItem.parentElement.insertAdjacentElement( originalItem.parentElement.insertAdjacentElement(`afterend`, originalItem)
`afterend`,
originalItem,
)
}) })
// HTML // HTML
@ -161,76 +153,43 @@ function copy() {
}) })
}, 350) }, 350)
} }
const isClickTrigger = ref(false)
const isOpenList = reactive(Array.from({ length: 5 }).fill(false))
function clickTrigger() {
isClickTrigger.value = !isClickTrigger.value
}
function openDropdown(index) {
return () => {
isOpenList.fill(false)
isOpenList[index] = true
}
}
function updateOpen(isOpen) {
if (!isOpen) {
isClickTrigger.value = false
}
}
</script> </script>
<template> <template>
<div class="header-container"> <header class="header-container h-15 flex items-center px-5">
<div class="dropdowns flex flex-auto"> <Menubar class="menubar mr-auto">
<FileDropdown <FileDropdown />
:is-open="isClickTrigger && isOpenList[0]" :click-trigger="clickTrigger"
:open-dropdown="openDropdown(0)" :update-open="updateOpen"
/>
<DropdownMenu :open="isClickTrigger && isOpenList[1]" @update:open="updateOpen"> <MenubarMenu>
<DropdownMenuTrigger <MenubarTrigger> 格式 </MenubarTrigger>
class="flex items-center p-2 px-4 hover:bg-gray-2 dark:hover:bg-stone-9" :class="{ <MenubarContent class="w-60" align="start">
'bg-gray-2': isClickTrigger && isOpenList[1], <MenubarItem
'dark:bg-stone-9': isClickTrigger && isOpenList[1], v-for="{ label, kbd, emitArgs } in formatItems"
}" @click="clickTrigger()" @mouseenter="openDropdown(1)()" :key="kbd"
> @click="$emit(...emitArgs)"
格式 >
</DropdownMenuTrigger>
<DropdownMenuContent class="w-60" align="start">
<DropdownMenuItem v-for="{ label, kbd, emitArgs } in formatItems" :key="kbd" @click="$emit(...emitArgs);">
<el-icon class="mr-2 h-4 w-4" /> <el-icon class="mr-2 h-4 w-4" />
{{ label }} {{ label }}
<DropdownMenuShortcut> <MenubarShortcut>
<kbd v-for="item in kbd" :key="item" class="mx-1 bg-gray-2 dark:bg-stone-9"> <kbd v-for="item in kbd" :key="item" class="mx-1 bg-gray-2 dark:bg-stone-9">
{{ item }} {{ item }}
</kbd> </kbd>
</DropdownMenuShortcut> </MenubarShortcut>
</DropdownMenuItem> </MenubarItem>
<DropdownMenuSeparator /> <MenubarSeparator />
<DropdownMenuItem @click="citeStatusChanged()"> <MenubarItem @click="citeStatusChanged()">
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isCiteStatus }"> <el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isCiteStatus }">
<ElIconCheck /> <ElIconCheck />
</el-icon> </el-icon>
微信外链转底部引用 微信外链转底部引用
</DropdownMenuItem> </MenubarItem>
</DropdownMenuContent> </MenubarContent>
</DropdownMenu> </MenubarMenu>
<EditDropdown <EditDropdown />
:is-open="isClickTrigger && isOpenList[2]" :click-trigger="clickTrigger" <StyleDropdown />
:open-dropdown="openDropdown(2)" :update-open="updateOpen" <HelpDropdown />
/> </Menubar>
<StyleDropdown
:is-open="isClickTrigger && isOpenList[3]" :click-trigger="clickTrigger"
:open-dropdown="openDropdown(3)" :update-open="updateOpen"
/>
<HelpDropdown
:is-open="isClickTrigger && isOpenList[4]" :click-trigger="clickTrigger"
:open-dropdown="openDropdown(4)" :update-open="updateOpen"
/>
</div>
<Popover> <Popover>
<PopoverTrigger> <PopoverTrigger>
<Button variant="outline"> <Button variant="outline">
@ -240,25 +199,24 @@ function updateOpen(isOpen) {
<PopoverContent class="h-100 w-100 overflow-auto px-6" align="end"> <PopoverContent class="h-100 w-100 overflow-auto px-6" align="end">
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>主题</h2>
主题
</h2>
<div class="grid grid-cols-3 justify-items-center gap-2"> <div class="grid grid-cols-3 justify-items-center gap-2">
<Button <Button
v-for="{ label, value } in themeOptions" v-for="{ label, value } in themeOptions"
:key="value" :key="value"
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': store.theme === value }" @click="store.themeChanged(value)" :class="{
'border-black dark:border-white': store.theme === value,
}"
@click="store.themeChanged(value)"
> >
{{ label }} {{ label }}
</Button> </Button>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>字体</h2>
字体
</h2>
<div class="grid grid-cols-3 justify-items-center gap-2"> <div class="grid grid-cols-3 justify-items-center gap-2">
<Button <Button
v-for="{ label, value } in fontFamilyOptions" v-for="{ label, value } in fontFamilyOptions"
@ -273,9 +231,7 @@ function updateOpen(isOpen) {
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>字号</h2>
字号
</h2>
<div class="grid grid-cols-5 justify-items-center gap-2"> <div class="grid grid-cols-5 justify-items-center gap-2">
<Button <Button
v-for="{ value, desc } in fontSizeOptions" v-for="{ value, desc } in fontSizeOptions"
@ -283,26 +239,30 @@ function updateOpen(isOpen) {
variant="outline" variant="outline"
class="w-full" class="w-full"
:class="{ :class="{
'border-black dark:border-white': store.fontSize === value }" @click="store.sizeChanged(value)" 'border-black dark:border-white': store.fontSize === value,
}"
@click="store.sizeChanged(value)"
> >
{{ desc }} {{ desc }}
</Button> </Button>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>主题色</h2>
主题色
</h2>
<div class="grid grid-cols-3 justify-items-center gap-2"> <div class="grid grid-cols-3 justify-items-center gap-2">
<Button <Button
v-for="{ label, value } in colorOptions" v-for="{ label, value } in colorOptions"
:key="value" :key="value"
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': store.primaryColor === value }" @click="store.colorChanged(value)" :class="{
'border-black dark:border-white': store.primaryColor === value,
}"
@click="store.colorChanged(value)"
> >
<span <span
class="mr-2 inline-block h-4 w-4 rounded-full" :style="{ class="mr-2 inline-block h-4 w-4 rounded-full"
:style="{
background: value, background: value,
}" }"
/> />
@ -311,9 +271,7 @@ function updateOpen(isOpen) {
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>自定义主题色</h2>
自定义主题色
</h2>
<div> <div>
<el-color-picker <el-color-picker
v-model="primaryColor" v-model="primaryColor"
@ -324,16 +282,21 @@ function updateOpen(isOpen) {
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>代码块主题</h2>
代码块主题
</h2>
<div> <div>
<Select v-model="store.codeBlockTheme" @update:model-value="store.codeBlockThemeChanged"> <Select
v-model="store.codeBlockTheme"
@update:model-value="store.codeBlockThemeChanged"
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a fruit" /> <SelectValue placeholder="Select a fruit" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem v-for="{ label, value } in codeBlockThemeOptions" :key="label" :value="value"> <SelectItem
v-for="{ label, value } in codeBlockThemeOptions"
:key="label"
:value="value"
>
{{ label }} {{ label }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -341,16 +304,17 @@ function updateOpen(isOpen) {
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>图注格式</h2>
图注格式
</h2>
<div class="grid grid-cols-3 justify-items-center gap-2"> <div class="grid grid-cols-3 justify-items-center gap-2">
<Button <Button
v-for="{ label, value } in legendOptions" v-for="{ label, value } in legendOptions"
:key="value" :key="value"
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': store.legend === value }" @click="store.legendChanged(value)" :class="{
'border-black dark:border-white': store.legend === value,
}"
@click="store.legendChanged(value)"
> >
{{ label }} {{ label }}
</Button> </Button>
@ -358,84 +322,100 @@ function updateOpen(isOpen) {
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>Mac 代码块</h2>
Mac 代码块
</h2>
<div class="grid grid-cols-5 justify-items-center gap-2"> <div class="grid grid-cols-5 justify-items-center gap-2">
<Button <Button
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': store.isMacCodeBlock }" @click="!store.isMacCodeBlock && store.macCodeBlockChanged()" :class="{
'border-black dark:border-white': store.isMacCodeBlock,
}"
@click="!store.isMacCodeBlock && store.macCodeBlockChanged()"
> >
开启 开启
</Button> </Button>
<Button <Button
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': !store.isMacCodeBlock }" @click="store.isMacCodeBlock && store.macCodeBlockChanged()" :class="{
'border-black dark:border-white': !store.isMacCodeBlock,
}"
@click="store.isMacCodeBlock && store.macCodeBlockChanged()"
> >
关闭 关闭
</Button> </Button>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>微信外链转底部引用</h2>
微信外链转底部引用
</h2>
<div class="grid grid-cols-5 justify-items-center gap-2"> <div class="grid grid-cols-5 justify-items-center gap-2">
<Button <Button
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': store.isCiteStatus }" @click="!store.isCiteStatus && store.citeStatusChanged()" :class="{
'border-black dark:border-white': store.isCiteStatus,
}"
@click="!store.isCiteStatus && store.citeStatusChanged()"
> >
开启 开启
</Button> </Button>
<Button <Button
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': !store.isCiteStatus }" @click="store.isCiteStatus && store.citeStatusChanged()" :class="{
'border-black dark:border-white': !store.isCiteStatus,
}"
@click="store.isCiteStatus && store.citeStatusChanged()"
> >
关闭 关闭
</Button> </Button>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>编辑区位置</h2>
编辑区位置
</h2>
<div class="grid grid-cols-5 justify-items-center gap-2"> <div class="grid grid-cols-5 justify-items-center gap-2">
<Button <Button
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': store.isEditOnLeft }" @click="!store.isEditOnLeft && store.toggleEditOnLeft()" :class="{
'border-black dark:border-white': store.isEditOnLeft,
}"
@click="!store.isEditOnLeft && store.toggleEditOnLeft()"
> >
左侧 左侧
</Button> </Button>
<Button <Button
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': !store.isEditOnLeft }" @click="store.isEditOnLeft && store.toggleEditOnLeft()" :class="{
'border-black dark:border-white': !store.isEditOnLeft,
}"
@click="store.isEditOnLeft && store.toggleEditOnLeft()"
> >
右侧 右侧
</Button> </Button>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2> <h2>模式</h2>
模式
</h2>
<div class="grid grid-cols-5 justify-items-center gap-2"> <div class="grid grid-cols-5 justify-items-center gap-2">
<Button <Button
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': !isDark }" @click="store.toggleDark(false)" :class="{
'border-black dark:border-white': !isDark,
}"
@click="store.toggleDark(false)"
> >
<Sun class="h-4 w-4" /> <Sun class="h-4 w-4" />
</Button> </Button>
<Button <Button
class="w-full" class="w-full"
variant="outline" :class="{ variant="outline"
'border-black dark:border-white': isDark }" @click="store.toggleDark(true)" :class="{
'border-black dark:border-white': isDark,
}"
@click="store.toggleDark(true)"
> >
<Moon class="h-4 w-4" /> <Moon class="h-4 w-4" />
</Button> </Button>
@ -449,18 +429,11 @@ function updateOpen(isOpen) {
</Button> </Button>
<PostInfo /> <PostInfo />
</div> </header>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.header-container { .menubar {
display: flex;
align-items: center;
height: 100%;
padding: 0 20px;
}
.dropdowns {
user-select: none; user-select: none;
} }

View File

@ -1,5 +1,13 @@
<script setup> <script setup>
import { ref, toRaw } from 'vue' import { ref, toRaw } from 'vue'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { useStore } from '@/stores' import { useStore } from '@/stores'
import { createTable } from '@/utils' import { createTable } from '@/utils'
@ -28,72 +36,67 @@ function insertTable() {
resetVal() resetVal()
toggleShowInsertFormDialog() toggleShowInsertFormDialog()
} }
function onUpdate(val) {
if (!val) {
toggleShowInsertFormDialog(false)
}
}
</script> </script>
<template> <template>
<el-dialog <Dialog :open="store.isShowInsertFormDialog" @update:open="onUpdate">
title="插入表格" <DialogContent>
class="insert__dialog" <DialogHeader>
:model-value="store.isShowInsertFormDialog" <DialogTitle>插入表格</DialogTitle>
@close="toggleShowInsertFormDialog(false)" </DialogHeader>
> <el-row class="tb-options" type="flex" align="middle" :gutter="10">
<el-row class="tb-options" type="flex" align="middle" :gutter="10"> <el-col :span="12">
<el-col :span="12"> 行数
行数 <el-input-number
<el-input-number v-model="rowNum"
v-model="rowNum" controls-position="right"
controls-position="right" :min="1"
:min="1" :max="100"
:max="100" size="small"
size="small"
/>
</el-col>
<el-col :span="12">
列数
<el-input-number
v-model="colNum"
controls-position="right"
:min="1"
:max="100"
size="small"
/>
</el-col>
</el-row>
<table style="border-collapse: collapse" class="input-table">
<tr
v-for="row in rowNum + 1"
:key="row"
:class="{ 'head-style': row === 1 }"
>
<td v-for="col in colNum" :key="col">
<el-input
v-model="tableData[`k_${row - 1}_${col - 1}`]"
align="center"
:placeholder="row === 1 ? '表头' : ''"
/> />
</td> </el-col>
</tr> <el-col :span="12">
</table> 列数
<template #footer> <el-input-number
<div class="dialog-footer"> v-model="colNum"
<el-button plain @click="toggleShowInsertFormDialog(false)"> controls-position="right"
:min="1"
:max="100"
size="small"
/>
</el-col>
</el-row>
<table style="border-collapse: collapse" class="input-table">
<tr v-for="row in rowNum + 1" :key="row" :class="{ 'head-style': row === 1 }">
<td v-for="col in colNum" :key="col">
<el-input
v-model="tableData[`k_${row - 1}_${col - 1}`]"
align="center"
:placeholder="row === 1 ? '表头' : ''"
/>
</td>
</tr>
</table>
<DialogFooter>
<Button variant="outline" @click="toggleShowInsertFormDialog(false)">
</el-button> </Button>
<el-button type="primary" plain @click="insertTable"> <Button @click="insertTable">
</el-button> </Button>
</div> </DialogFooter>
</template> </DialogContent>
</el-dialog> </Dialog>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
:deep(.el-dialog) {
width: 55%;
min-height: 375px;
min-width: 440px;
}
.tb-options { .tb-options {
margin-bottom: 20px; margin-bottom: 20px;
} }

View File

@ -4,6 +4,8 @@ import CodeMirror from 'codemirror/lib/codemirror'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue' import { UploadFilled } from '@element-plus/icons-vue'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { checkImage, removeLeft } from '@/utils' import { checkImage, removeLeft } from '@/utils'
import { useStore } from '@/stores' import { useStore } from '@/stores'
@ -117,22 +119,25 @@ const imgHost = ref(`default`)
const formCustomElInput = ref(null) const formCustomElInput = ref(null)
const activeName = ref(`upload`) const activeName = ref(`upload`)
watch(activeName, async (val) => { watch(
if (val === `formCustom`) { activeName,
nextTick(() => { async (val) => {
const textarea if (val === `formCustom`) {
= formCustomElInput.value.$el.querySelector(`textarea`) nextTick(() => {
formCustom.value.editor const textarea = formCustomElInput.value.$el.querySelector(`textarea`)
= formCustom.value.editor formCustom.value.editor
|| CodeMirror.fromTextArea(textarea, { = formCustom.value.editor
mode: `javascript`, || CodeMirror.fromTextArea(textarea, {
}) mode: `javascript`,
// formCustom.value.editor.setValue(formCustom.value.code) })
}) // formCustom.value.editor.setValue(formCustom.value.code)
} })
}, { }
immediate: true, },
}) {
immediate: true,
},
)
onBeforeMount(() => { onBeforeMount(() => {
if (localStorage.getItem(`githubConfig`)) { if (localStorage.getItem(`githubConfig`)) {
@ -278,26 +283,48 @@ function uploadImage(params) {
</script> </script>
<template> <template>
<el-dialog title="本地上传" class="upload__dialog" :model-value="store.isShowUploadImgDialog" @close="store.toggleShowUploadImgDialog(false)"> <Dialog v-model:open="store.isShowUploadImgDialog">
<el-tabs v-model="activeName"> <DialogContent class="max-w-max">
<el-tab-pane class="upload-panel" label="选择上传" name="upload"> <DialogHeader>
<el-select v-model="imgHost" placeholder="请选择" size="small" @change="changeImgHost"> <DialogTitle>本地上传</DialogTitle>
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> </DialogHeader>
</el-select>
<el-upload <el-tabs v-model="activeName">
drag multiple action="" :headers="{ 'Content-Type': 'multipart/form-data' }" :show-file-list="false" <el-tab-pane class="upload-panel" label="选择上传" name="upload">
accept=".jpg, .jpeg, .png, .gif" name="file" :before-upload="beforeImageUpload" :http-request="uploadImage" <el-select
> v-model="imgHost"
<el-icon class="el-icon--upload"> placeholder="请选择"
<UploadFilled /> size="small"
</el-icon> @change="changeImgHost"
<div class="el-upload__text"> >
将图片拖到此处 <el-option
<em>点击上传</em> v-for="item in options"
</div> :key="item.value"
</el-upload> :label="item.label"
</el-tab-pane> :value="item.value"
<!-- <el-tab-pane class="github-panel" label="Gitee 图床" name="gitee"> />
</el-select>
<el-upload
drag
multiple
action=""
:headers="{ 'Content-Type': 'multipart/form-data' }"
:show-file-list="false"
accept=".jpg, .jpeg, .png, .gif"
name="file"
:before-upload="beforeImageUpload"
:http-request="uploadImage"
>
<el-icon class="el-icon--upload">
<UploadFilled />
</el-icon>
<div class="el-upload__text">
将图片拖到此处
<em>点击上传</em>
</div>
</el-upload>
</el-tab-pane>
<!-- <el-tab-pane class="github-panel" label="Gitee 图床" name="gitee">
<el-form <el-form
class="setting-form" class="setting-form"
:model="formGitee" :model="formGitee"
@ -336,203 +363,290 @@ function uploadImage(params) {
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-tab-pane> --> </el-tab-pane> -->
<el-tab-pane class="github-panel" label="GitHub 图床" name="github"> <el-tab-pane class="github-panel" label="GitHub 图床" name="github">
<el-form class="setting-form" :model="formGitHub" label-position="right" label-width="150px"> <el-form
<el-form-item label="GitHub 仓库" :required="true"> class="setting-form"
<el-input v-model.trim="formGitHub.repo" placeholder="如github.com/yanglbme/resource" /> :model="formGitHub"
</el-form-item> label-position="right"
<el-form-item label="分支"> label-width="150px"
<el-input v-model.trim="formGitHub.branch" placeholder="如release可不填默认 master" /> >
</el-form-item> <el-form-item label="GitHub 仓库" :required="true">
<el-form-item label="Token" :required="true"> <el-input
<el-input v-model.trim="formGitHub.repo"
v-model.trim="formGitHub.accessToken" show-password placeholder="如github.com/yanglbme/resource"
placeholder="如cc1d0c1426d0fd0902bd2d7184b14da61b8abc46" />
/> </el-form-item>
<el-link <el-form-item label="分支">
type="primary" <el-input
href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token" v-model.trim="formGitHub.branch"
target="_blank" placeholder="如release可不填默认 master"
> />
如何获取 GitHub Token </el-form-item>
</el-link> <el-form-item label="Token" :required="true">
</el-form-item> <el-input
<el-form-item> v-model.trim="formGitHub.accessToken"
<el-button type="primary" @click="saveGitHubConfiguration"> show-password
保存配置 placeholder="如cc1d0c1426d0fd0902bd2d7184b14da61b8abc46"
</el-button> />
</el-form-item> <el-link
</el-form> type="primary"
</el-tab-pane> href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token"
<el-tab-pane class="github-panel" label="阿里云 OSS" name="aliOSS"> target="_blank"
<el-form class="setting-form" :model="formAliOSS" label-position="right" label-width="150px"> >
<el-form-item label="AccessKey ID" :required="true"> 如何获取 GitHub Token
<el-input v-model.trim="formAliOSS.accessKeyId" placeholder="如LTAI4GdoocsmdoxUf13ylbaNHk" /> </el-link>
</el-form-item> </el-form-item>
<el-form-item label="AccessKey Secret" :required="true"> <el-form-item>
<el-input <el-button type="primary" @click="saveGitHubConfiguration">
v-model.trim="formAliOSS.accessKeySecret" show-password 保存配置
placeholder="如cc1d0c142doocs0902bd2d7md4b14da6ylbabc46" </el-button>
/> </el-form-item>
</el-form-item> </el-form>
<el-form-item label="Bucket" :required="true"> </el-tab-pane>
<el-input v-model.trim="formAliOSS.bucket" placeholder="如doocs" /> <el-tab-pane class="github-panel" label="阿里云 OSS" name="aliOSS">
</el-form-item> <el-form
<el-form-item label="Bucket 所在区域" :required="true"> class="setting-form"
<el-input v-model.trim="formAliOSS.region" placeholder="如oss-cn-shenzhen" /> :model="formAliOSS"
</el-form-item> label-position="right"
<el-form-item label="UseSSL" :required="true"> label-width="150px"
<el-switch v-model="formAliOSS.useSSL" active-text="" inactive-text="" /> >
</el-form-item> <el-form-item label="AccessKey ID" :required="true">
<el-form-item label="自定义 CDN 域名" :required="false"> <el-input
<el-input v-model.trim="formAliOSS.cdnHost" placeholder="如https://imagecdn.alidaodao.com可不填" /> v-model.trim="formAliOSS.accessKeyId"
</el-form-item> placeholder="如LTAI4GdoocsmdoxUf13ylbaNHk"
<el-form-item label="存储路径"> />
<el-input v-model.trim="formAliOSS.path" placeholder="如img可不填默认为根目录" /> </el-form-item>
<el-link type="primary" href="https://help.aliyun.com/document_detail/31883.html" target="_blank"> <el-form-item label="AccessKey Secret" :required="true">
如何使用阿里云 OSS <el-input
</el-link> v-model.trim="formAliOSS.accessKeySecret"
</el-form-item> show-password
<el-form-item> placeholder="如cc1d0c142doocs0902bd2d7md4b14da6ylbabc46"
<el-button type="primary" @click="saveAliOSSConfiguration"> />
保存配置 </el-form-item>
</el-button> <el-form-item label="Bucket" :required="true">
</el-form-item> <el-input v-model.trim="formAliOSS.bucket" placeholder="如doocs" />
</el-form> </el-form-item>
</el-tab-pane> <el-form-item label="Bucket 所在区域" :required="true">
<el-tab-pane class="github-panel" label="腾讯云 COS" name="txCOS"> <el-input
<el-form class="setting-form" :model="formTxCOS" label-position="right" label-width="150px"> v-model.trim="formAliOSS.region"
<el-form-item label="SecretId" :required="true"> placeholder="如oss-cn-shenzhen"
<el-input v-model.trim="formTxCOS.secretId" placeholder="如AKIDnQp1w3DOOCSs8F5MDp9tdoocsmdUPonW3" /> />
</el-form-item> </el-form-item>
<el-form-item label="SecretKey" :required="true"> <el-form-item label="UseSSL" :required="true">
<el-input <el-switch
v-model.trim="formTxCOS.secretKey" show-password v-model="formAliOSS.useSSL"
placeholder="如ukLmdtEJ9271f3DOocsMDsCXdS3YlbW0" active-text="是"
/> inactive-text="否"
</el-form-item> />
<el-form-item label="Bucket" :required="true"> </el-form-item>
<el-input v-model.trim="formTxCOS.bucket" placeholder="如doocs-3212520134" /> <el-form-item label="自定义 CDN 域名" :required="false">
</el-form-item> <el-input
<el-form-item label="Bucket 所在区域" :required="true"> v-model.trim="formAliOSS.cdnHost"
<el-input v-model.trim="formTxCOS.region" placeholder="如ap-guangzhou" /> placeholder="如https://imagecdn.alidaodao.com可不填"
</el-form-item> />
<el-form-item label="自定义 CDN 域名" :required="false"> </el-form-item>
<el-input v-model.trim="formTxCOS.cdnHost" placeholder="如https://imagecdn.alidaodao.com可不填" /> <el-form-item label="存储路径">
</el-form-item> <el-input
<el-form-item label="存储路径"> v-model.trim="formAliOSS.path"
<el-input v-model.trim="formTxCOS.path" placeholder="如img可不填默认根目录" /> placeholder="如img可不填默认为根目录"
<el-link type="primary" href="https://cloud.tencent.com/document/product/436/38484" target="_blank"> />
如何使用腾讯云 COS <el-link
</el-link> type="primary"
</el-form-item> href="https://help.aliyun.com/document_detail/31883.html"
<el-form-item> target="_blank"
<el-button type="primary" @click="saveTxCOSConfiguration"> >
保存配置 如何使用阿里云 OSS
</el-button> </el-link>
</el-form-item> </el-form-item>
</el-form> <el-form-item>
</el-tab-pane> <el-button type="primary" @click="saveAliOSSConfiguration">
<el-tab-pane class="github-panel" label="七牛云 Kodo" name="qiniu"> 保存配置
<el-form class="setting-form" :model="formQiniu" label-position="right" label-width="150px"> </el-button>
<el-form-item label="AccessKey" :required="true"> </el-form-item>
<el-input v-model.trim="formQiniu.accessKey" placeholder="如6DD3VaLJ_SQgOdoocsyTV_YWaDmdnL2n8EGx7kG" /> </el-form>
</el-form-item> </el-tab-pane>
<el-form-item label="SecretKey" :required="true"> <el-tab-pane class="github-panel" label="腾讯云 COS" name="txCOS">
<el-input <el-form
v-model.trim="formQiniu.secretKey" show-password class="setting-form"
placeholder="如qgZa5qrvDOOcsmdKStD1oCjZ9nB7MDvJUs_34SIm" :model="formTxCOS"
/> label-position="right"
</el-form-item> label-width="150px"
<el-form-item label="Bucket" :required="true"> >
<el-input v-model.trim="formQiniu.bucket" placeholder="如md" /> <el-form-item label="SecretId" :required="true">
</el-form-item> <el-input
<el-form-item label="Bucket 对应域名" :required="true"> v-model.trim="formTxCOS.secretId"
<el-input v-model.trim="formQiniu.domain" placeholder="如https://images.123ylb.cn" /> placeholder="如AKIDnQp1w3DOOCSs8F5MDp9tdoocsmdUPonW3"
</el-form-item> />
<el-form-item label="存储区域" :required="false"> </el-form-item>
<el-input v-model.trim="formQiniu.region" placeholder="如z2可不填" /> <el-form-item label="SecretKey" :required="true">
</el-form-item> <el-input
<el-form-item label="存储路径" :required="false"> v-model.trim="formTxCOS.secretKey"
<el-input v-model.trim="formQiniu.path" placeholder="如img可不填默认为根目录" /> show-password
<el-link type="primary" href="https://developer.qiniu.com/kodo" target="_blank"> placeholder="如ukLmdtEJ9271f3DOocsMDsCXdS3YlbW0"
如何使用七牛云 Kodo />
</el-link> </el-form-item>
</el-form-item> <el-form-item label="Bucket" :required="true">
<el-form-item> <el-input
<el-button type="primary" @click="saveQiniuConfiguration"> v-model.trim="formTxCOS.bucket"
保存配置 placeholder="如doocs-3212520134"
</el-button> />
</el-form-item> </el-form-item>
</el-form> <el-form-item label="Bucket 所在区域" :required="true">
</el-tab-pane> <el-input v-model.trim="formTxCOS.region" placeholder="如ap-guangzhou" />
<el-tab-pane class="github-panel" label="MinIO" name="minio"> </el-form-item>
<el-form class="setting-form" :model="minioOSS" label-position="right" label-width="150px"> <el-form-item label="自定义 CDN 域名" :required="false">
<el-form-item label="Endpoint" :required="true"> <el-input
<el-input v-model.trim="minioOSS.endpoint" placeholder="如play.min.io" /> v-model.trim="formTxCOS.cdnHost"
</el-form-item> placeholder="如https://imagecdn.alidaodao.com可不填"
<el-form-item label="Port" :required="false"> />
<el-input v-model.trim="minioOSS.port" type="number" placeholder="如9000可不填http 默认为 80https 默认为 443" /> </el-form-item>
</el-form-item> <el-form-item label="存储路径">
<el-form-item label="UseSSL" :required="true"> <el-input
<el-switch v-model="minioOSS.useSSL" active-text="" inactive-text="" /> v-model.trim="formTxCOS.path"
</el-form-item> placeholder="如img可不填默认根目录"
<el-form-item label="Bucket" :required="true"> />
<el-input v-model.trim="minioOSS.bucket" placeholder="如doocs" /> <el-link
</el-form-item> type="primary"
<el-form-item label="AccessKey" :required="true"> href="https://cloud.tencent.com/document/product/436/38484"
<el-input v-model.trim="minioOSS.accessKey" placeholder="如zhangsan" /> target="_blank"
</el-form-item> >
<el-form-item label="SecretKey" :required="true"> 如何使用腾讯云 COS
<el-input v-model.trim="minioOSS.secretKey" placeholder="如asdasdasd" /> </el-link>
<el-link </el-form-item>
type="primary" href="http://docs.minio.org.cn/docs/master/minio-client-complete-guide" <el-form-item>
target="_blank" <el-button type="primary" @click="saveTxCOSConfiguration">
> 保存配置
如何使用 MinIO </el-button>
</el-link> </el-form-item>
</el-form-item> </el-form>
<el-form-item> </el-tab-pane>
<el-button type="primary" @click="saveMinioOSSConfiguration"> <el-tab-pane class="github-panel" label="七牛云 Kodo" name="qiniu">
保存配置 <el-form
</el-button> class="setting-form"
</el-form-item> :model="formQiniu"
</el-form> label-position="right"
</el-tab-pane> label-width="150px"
<el-tab-pane class="github-panel formCustom" label="自定义代码" name="formCustom"> >
<el-form class="setting-form" :model="formCustom" label-position="right"> <el-form-item label="AccessKey" :required="true">
<el-form-item label="" :required="true"> <el-input
<el-input v-model.trim="formQiniu.accessKey"
ref="formCustomElInput" v-model="formCustom.code" class="formCustomElInput" type="textarea" placeholder="如6DD3VaLJ_SQgOdoocsyTV_YWaDmdnL2n8EGx7kG"
resize="none" placeholder="Your custom code here." />
/> </el-form-item>
<el-link type="primary" href="https://github.com/doocs/md#自定义上传逻辑" target="_blank"> <el-form-item label="SecretKey" :required="true">
参数详情 <el-input
</el-link> v-model.trim="formQiniu.secretKey"
</el-form-item> show-password
<el-form-item> placeholder="如qgZa5qrvDOOcsmdKStD1oCjZ9nB7MDvJUs_34SIm"
<el-button type="primary" @click="formCustomSave"> />
保存配置 </el-form-item>
</el-button> <el-form-item label="Bucket" :required="true">
</el-form-item> <el-input v-model.trim="formQiniu.bucket" placeholder="如md" />
</el-form> </el-form-item>
</el-tab-pane> <el-form-item label="Bucket 对应域名" :required="true">
</el-tabs> <el-input
</el-dialog> v-model.trim="formQiniu.domain"
placeholder="如https://images.123ylb.cn"
/>
</el-form-item>
<el-form-item label="存储区域" :required="false">
<el-input v-model.trim="formQiniu.region" placeholder="如z2可不填" />
</el-form-item>
<el-form-item label="存储路径" :required="false">
<el-input
v-model.trim="formQiniu.path"
placeholder="如img可不填默认为根目录"
/>
<el-link
type="primary"
href="https://developer.qiniu.com/kodo"
target="_blank"
>
如何使用七牛云 Kodo
</el-link>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveQiniuConfiguration">
保存配置
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane class="github-panel" label="MinIO" name="minio">
<el-form
class="setting-form"
:model="minioOSS"
label-position="right"
label-width="150px"
>
<el-form-item label="Endpoint" :required="true">
<el-input v-model.trim="minioOSS.endpoint" placeholder="如play.min.io" />
</el-form-item>
<el-form-item label="Port" :required="false">
<el-input
v-model.trim="minioOSS.port"
type="number"
placeholder="如9000可不填http 默认为 80https 默认为 443"
/>
</el-form-item>
<el-form-item label="UseSSL" :required="true">
<el-switch v-model="minioOSS.useSSL" active-text="" inactive-text="" />
</el-form-item>
<el-form-item label="Bucket" :required="true">
<el-input v-model.trim="minioOSS.bucket" placeholder="如doocs" />
</el-form-item>
<el-form-item label="AccessKey" :required="true">
<el-input v-model.trim="minioOSS.accessKey" placeholder="如zhangsan" />
</el-form-item>
<el-form-item label="SecretKey" :required="true">
<el-input v-model.trim="minioOSS.secretKey" placeholder="如asdasdasd" />
<el-link
type="primary"
href="http://docs.minio.org.cn/docs/master/minio-client-complete-guide"
target="_blank"
>
如何使用 MinIO
</el-link>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveMinioOSSConfiguration">
保存配置
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane class="github-panel formCustom" label="自定义代码" name="formCustom">
<el-form class="setting-form" :model="formCustom" label-position="right">
<el-form-item label="" :required="true">
<el-input
ref="formCustomElInput"
v-model="formCustom.code"
class="formCustomElInput"
type="textarea"
resize="none"
placeholder="Your custom code here."
/>
<el-link
type="primary"
href="https://github.com/doocs/md#自定义上传逻辑"
target="_blank"
>
参数详情
</el-link>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="formCustomSave">
保存配置
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</DialogContent>
</Dialog>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.upload__dialog {
display: flex;
}
:deep(.el-dialog) {
width: 55%;
min-width: 640px;
min-height: 615px;
margin: auto !important;
}
:deep(.el-upload-dragger) { :deep(.el-upload-dragger) {
display: flex; display: flex;
flex-flow: column; flex-flow: column;

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'radix-vue'
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot />
</DialogRoot>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } from 'radix-vue'
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose v-bind="props">
<slot />
</DialogClose>
</template>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue'
import { X } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes[`class`] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
/>
<DialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)"
>
<slot />
<DialogClose
class="data-[state=open]:bg-accent ring-offset-background data-[state=open]:text-muted-foreground focus:ring-ring absolute right-4 top-4 rounded-sm opacity-70 transition-opacity disabled:pointer-events-none hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2"
>
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogDescription
v-bind="forwardedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</DialogDescription>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{ class?: HTMLAttributes[`class`] }>()
</script>
<template>
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes[`class`]
}>()
</script>
<template>
<div
:class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue'
import { X } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes[`class`] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80"
>
<DialogContent
:class="
cn(
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class,
)
"
v-bind="forwarded"
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
<slot />
<DialogClose
class="hover:bg-secondary absolute right-3 top-3 rounded-md p-0.5 transition-colors"
>
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { DialogTitle, type DialogTitleProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogTitle
v-bind="forwardedProps"
:class="
cn(
'text-lg font-semibold leading-none tracking-tight',
props.class,
)
"
>
<slot />
</DialogTitle>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogTrigger, type DialogTriggerProps } from 'radix-vue'
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger v-bind="props">
<slot />
</DialogTrigger>
</template>

View File

@ -0,0 +1,9 @@
export { default as Dialog } from './Dialog.vue'
export { default as DialogClose } from './DialogClose.vue'
export { default as DialogTrigger } from './DialogTrigger.vue'
export { default as DialogHeader } from './DialogHeader.vue'
export { default as DialogTitle } from './DialogTitle.vue'
export { default as DialogDescription } from './DialogDescription.vue'
export { default as DialogContent } from './DialogContent.vue'
export { default as DialogScrollContent } from './DialogScrollContent.vue'
export { default as DialogFooter } from './DialogFooter.vue'

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
MenubarRoot,
type MenubarRootEmits,
type MenubarRootProps,
useForwardPropsEmits,
} from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<MenubarRootProps & { class?: HTMLAttributes[`class`] }>()
const emits = defineEmits<MenubarRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarRoot
v-bind="forwarded"
:class="
cn(
'flex h-10 items-center gap-x-1 rounded-md border bg-background p-1',
props.class,
)
"
>
<slot />
</MenubarRoot>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
MenubarCheckboxItem,
type MenubarCheckboxItemEmits,
type MenubarCheckboxItemProps,
MenubarItemIndicator,
useForwardPropsEmits,
} from 'radix-vue'
import { Check } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<MenubarCheckboxItemProps & { class?: HTMLAttributes[`class`] }>()
const emits = defineEmits<MenubarCheckboxItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarCheckboxItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)"
>
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
<MenubarItemIndicator>
<Check class="h-4 w-4" />
</MenubarItemIndicator>
</span>
<slot />
</MenubarCheckboxItem>
</template>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
MenubarContent,
type MenubarContentProps,
MenubarPortal,
useForwardProps,
} from 'radix-vue'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<MenubarContentProps & { class?: HTMLAttributes[`class`] }>(),
{
align: `start`,
alignOffset: -4,
sideOffset: 8,
},
)
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarPortal>
<MenubarContent
v-bind="forwardedProps"
:class="
cn(
'z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot />
</MenubarContent>
</MenubarPortal>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { MenubarGroup, type MenubarGroupProps } from 'radix-vue'
const props = defineProps<MenubarGroupProps>()
</script>
<template>
<MenubarGroup v-bind="props">
<slot />
</MenubarGroup>
</template>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
MenubarItem,
type MenubarItemEmits,
type MenubarItemProps,
useForwardPropsEmits,
} from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<MenubarItemProps & { class?: HTMLAttributes[`class`], inset?: boolean }>()
const emits = defineEmits<MenubarItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
props.class,
)"
>
<slot />
</MenubarItem>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { MenubarLabel, type MenubarLabelProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<MenubarLabelProps & { class?: HTMLAttributes[`class`], inset?: boolean }>()
</script>
<template>
<MenubarLabel :class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)">
<slot />
</MenubarLabel>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { MenubarMenu, type MenubarMenuProps } from 'radix-vue'
const props = defineProps<MenubarMenuProps>()
</script>
<template>
<MenubarMenu v-bind="props">
<slot />
</MenubarMenu>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import {
MenubarRadioGroup,
type MenubarRadioGroupEmits,
type MenubarRadioGroupProps,
useForwardPropsEmits,
} from 'radix-vue'
const props = defineProps<MenubarRadioGroupProps>()
const emits = defineEmits<MenubarRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<MenubarRadioGroup v-bind="forwarded">
<slot />
</MenubarRadioGroup>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
MenubarItemIndicator,
MenubarRadioItem,
type MenubarRadioItemEmits,
type MenubarRadioItemProps,
useForwardPropsEmits,
} from 'radix-vue'
import { Circle } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<MenubarRadioItemProps & { class?: HTMLAttributes[`class`] }>()
const emits = defineEmits<MenubarRadioItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarRadioItem
v-bind="forwarded"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)"
>
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
<MenubarItemIndicator>
<Circle class="h-2 w-2 fill-current" />
</MenubarItemIndicator>
</span>
<slot />
</MenubarRadioItem>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { MenubarSeparator, type MenubarSeparatorProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<MenubarSeparatorProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarSeparator :class=" cn('-mx-1 my-1 h-px bg-muted', props.class)" v-bind="forwardedProps" />
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes[`class`]
}>()
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
<slot />
</span>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { MenubarSub, type MenubarSubEmits, useForwardPropsEmits } from 'radix-vue'
interface MenubarSubRootProps {
defaultOpen?: boolean
open?: boolean
}
const props = defineProps<MenubarSubRootProps>()
const emits = defineEmits<MenubarSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<MenubarSub v-bind="forwarded">
<slot />
</MenubarSub>
</template>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
MenubarPortal,
MenubarSubContent,
type MenubarSubContentEmits,
type MenubarSubContentProps,
useForwardPropsEmits,
} from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<MenubarSubContentProps & { class?: HTMLAttributes[`class`] }>()
const emits = defineEmits<MenubarSubContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarPortal>
<MenubarSubContent
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot />
</MenubarSubContent>
</MenubarPortal>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { MenubarSubTrigger, type MenubarSubTriggerProps, useForwardProps } from 'radix-vue'
import { ChevronRight } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<MenubarSubTriggerProps & { class?: HTMLAttributes[`class`], inset?: boolean }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarSubTrigger
v-bind="forwardedProps"
:class="cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
inset && 'pl-8',
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</MenubarSubTrigger>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { MenubarTrigger, type MenubarTriggerProps, useForwardProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<MenubarTriggerProps & { class?: HTMLAttributes[`class`] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarTrigger
v-bind="forwardedProps"
:class="
cn(
'flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none hover:bg-accent focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
props.class,
)
"
>
<slot />
</MenubarTrigger>
</template>

View File

@ -0,0 +1,15 @@
export { default as Menubar } from './Menubar.vue'
export { default as MenubarItem } from './MenubarItem.vue'
export { default as MenubarContent } from './MenubarContent.vue'
export { default as MenubarGroup } from './MenubarGroup.vue'
export { default as MenubarMenu } from './MenubarMenu.vue'
export { default as MenubarRadioGroup } from './MenubarRadioGroup.vue'
export { default as MenubarRadioItem } from './MenubarRadioItem.vue'
export { default as MenubarCheckboxItem } from './MenubarCheckboxItem.vue'
export { default as MenubarSeparator } from './MenubarSeparator.vue'
export { default as MenubarSub } from './MenubarSub.vue'
export { default as MenubarSubContent } from './MenubarSubContent.vue'
export { default as MenubarSubTrigger } from './MenubarSubTrigger.vue'
export { default as MenubarTrigger } from './MenubarTrigger.vue'
export { default as MenubarShortcut } from './MenubarShortcut.vue'
export { default as MenubarLabel } from './MenubarLabel.vue'

View File

@ -6,6 +6,7 @@ import App from './App.vue'
import 'virtual:uno.css' import 'virtual:uno.css'
import 'codemirror/lib/codemirror.css' import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/xq-light.css' import 'codemirror/theme/xq-light.css'
import 'codemirror/theme/darcula.css'
/* 每个页面公共css */ /* 每个页面公共css */
import '@/assets/index.css' import '@/assets/index.css'

View File

@ -1,4 +1,4 @@
import { computed, markRaw, onMounted, ref } from 'vue' import { computed, markRaw, onMounted, ref, toRaw, watch } from 'vue'
import { createPinia, defineStore } from 'pinia' import { createPinia, defineStore } from 'pinia'
import { marked } from 'marked' import { marked } from 'marked'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
@ -184,11 +184,11 @@ export const useStore = defineStore(`store`, () => {
onMounted(() => { onMounted(() => {
const cssEditorDom = document.querySelector(`#cssEditor`) const cssEditorDom = document.querySelector(`#cssEditor`)
cssEditorDom.value = getCurrentTab().content cssEditorDom.value = getCurrentTab().content
const theme = isDark.value ? `darcula` : `xq-light`
cssEditor.value = markRaw( cssEditor.value = markRaw(
CodeMirror.fromTextArea(cssEditorDom, { CodeMirror.fromTextArea(cssEditorDom, {
mode: `css`, mode: `css`,
theme: `xq-light`, theme,
lineNumbers: false, lineNumbers: false,
styleActiveLine: true, styleActiveLine: true,
lineWrapping: true, lineWrapping: true,
@ -219,6 +219,11 @@ export const useStore = defineStore(`store`, () => {
}) })
}) })
watch(isDark, () => {
const theme = isDark.value ? `darcula` : `xq-light`
toRaw(cssEditor.value)?.setOption?.(`theme`, theme)
})
// 重置样式 // 重置样式
const resetStyle = () => { const resetStyle = () => {
isCiteStatus.value = false isCiteStatus.value = false

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, ref, toRaw } from 'vue' import { onMounted, ref, toRaw, watch } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
@ -31,7 +31,7 @@ import {
} from '@/utils' } from '@/utils'
const store = useStore() const store = useStore()
const { output, editor, editorContent, isShowCssEditor } = storeToRefs(store) const { isDark, output, editor, editorContent, isShowCssEditor } = storeToRefs(store)
const { const {
editorRefresh, editorRefresh,
@ -178,6 +178,12 @@ function uploadImage(file, cb) {
const changeTimer = ref(0) const changeTimer = ref(0)
//
watch(isDark, () => {
const theme = isDark.value ? `darcula` : `xq-light`
toRaw(editor.value)?.setOption?.(`theme`, theme)
})
// //
function initEditor() { function initEditor() {
const editorDom = document.querySelector(`#editor`) const editorDom = document.querySelector(`#editor`)
@ -187,7 +193,7 @@ function initEditor() {
} }
editor.value = CodeMirror.fromTextArea(editorDom, { editor.value = CodeMirror.fromTextArea(editorDom, {
mode: `text/x-markdown`, mode: `text/x-markdown`,
theme: `xq-light`, theme: isDark.value ? `darcula` : `xq-light`,
lineNumbers: false, lineNumbers: false,
lineWrapping: true, lineWrapping: true,
styleActiveLine: true, styleActiveLine: true,
@ -370,88 +376,82 @@ onMounted(() => {
</script> </script>
<template> <template>
<div ref="container" class="container"> <div ref="container" class="container flex flex-col">
<el-container> <EditorHeader
<el-header class="editor__header"> @add-format="addFormat"
<EditorHeader @format-content="formatContent"
@add-format="addFormat" @start-copy="startCopy"
@format-content="formatContent" @end-copy="endCopy"
@start-copy="startCopy" />
@end-copy="endCopy" <main class="container-main flex-1">
/> <el-row class="container-main-section h-full border-1">
</el-header> <el-col
<el-main class="container-main"> ref="codeMirrorWrapper"
<el-row class="container-main-section"> :span="isShowCssEditor ? 8 : 12"
<el-col class="codeMirror-wrapper border-r-1"
ref="codeMirrorWrapper" :class="{
:span="isShowCssEditor ? 8 : 12" 'order-1': !store.isEditOnLeft,
class="codeMirror-wrapper" }"
:class="{ >
'order-1': !store.isEditOnLeft, <ContextMenu>
}" <ContextMenuTrigger>
> <textarea
<ContextMenu> id="editor"
<ContextMenuTrigger> type="textarea"
<textarea placeholder="Your markdown text here."
id="editor" />
type="textarea" </ContextMenuTrigger>
placeholder="Your markdown text here." <ContextMenuContent class="w-64">
/> <ContextMenuItem inset @click="toggleShowUploadImgDialog()">
</ContextMenuTrigger> 上传图片
<ContextMenuContent class="w-64"> </ContextMenuItem>
<ContextMenuItem inset @click="toggleShowUploadImgDialog()"> <ContextMenuItem inset @click="toggleShowInsertFormDialog()">
上传图片 插入表格
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem inset @click="toggleShowInsertFormDialog()"> <ContextMenuItem inset @click="resetStyleConfirm()">
插入表格 恢复默认样式
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem inset @click="resetStyleConfirm()"> <ContextMenuSeparator />
恢复默认样式 <ContextMenuItem inset @click="importMarkdownContent()">
</ContextMenuItem> 导入 .md 文档
<ContextMenuSeparator /> </ContextMenuItem>
<ContextMenuItem inset @click="importMarkdownContent()"> <ContextMenuItem inset @click="exportEditorContent2MD()">
导入 .md 文档 导出 .md 文档
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem inset @click="exportEditorContent2MD()"> <ContextMenuItem inset @click="exportEditorContent2HTML()">
导出 .md 文档 导出 .html
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem inset @click="exportEditorContent2HTML()"> <ContextMenuItem inset @click="formatContent()">
导出 .html 格式化
</ContextMenuItem> <ContextMenuShortcut>{{ altSign }} + {{ shiftSign }} + F</ContextMenuShortcut>
<ContextMenuItem inset @click="formatContent()"> </ContextMenuItem>
格式化 </ContextMenuContent>
<ContextMenuShortcut>{{ altSign }} + {{ shiftSign }} + F</ContextMenuShortcut> </ContextMenu>
</ContextMenuItem> </el-col>
</ContextMenuContent> <el-col
</ContextMenu> id="preview"
</el-col> ref="preview"
<el-col :span="isShowCssEditor ? 8 : 12"
id="preview" class="preview-wrapper p-5"
ref="preview" >
:span="isShowCssEditor ? 8 : 12" <div id="output-wrapper" :class="{ output_night: !backLight }">
class="preview-wrapper" <div class="preview border shadow-xl">
> <section id="output" v-html="output" />
<div id="output-wrapper" :class="{ output_night: !backLight }"> <div v-if="isCoping" class="loading-mask">
<div class="preview"> <div class="loading-mask-box">
<section id="output" v-html="output" /> <div class="loading__img" />
<div v-if="isCoping" class="loading-mask"> <span>正在生成</span>
<div class="loading-mask-box">
<div class="loading__img" />
<span>正在生成</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</el-col> </div>
<CssEditor /> </el-col>
</el-row> <CssEditor />
</el-main> </el-row>
</el-container> </main>
<UploadImgDialog <UploadImgDialog
@before-upload="beforeUpload"
@upload-image="uploadImage" @upload-image="uploadImage"
@uploaded="uploaded"
/> />
<InsertFormDialog /> <InsertFormDialog />
@ -466,20 +466,17 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.container { .container {
height: 100%; height: 100vh;
min-width: 100%; min-width: 100%;
padding: 0; padding: 0;
} }
.container-main { .container-main {
overflow: hidden;
padding: 20px; padding: 20px;
padding-top: 0; padding-top: 0;
} }
.container-main-section {
height: 100%;
}
#output-wrapper { #output-wrapper {
position: relative; position: relative;
user-select: text; user-select: text;