Browse Source

style(markdown): refine SCSS styles for markdown components and enhance code block presentation

- Updated SCSS styles for markdown elements, improving visual consistency and readability.
- Enhanced the appearance of code blocks with new background colors, borders, and shadows.
- Introduced responsive design adjustments for better mobile compatibility.
- Added new styles for headings, links, and blockquotes to improve overall aesthetics.

These changes significantly enhance the user experience when interacting with markdown content, making it more visually appealing and easier to read.
main
npmrun 2 months ago
parent
commit
adaf5b2195
  1. 446
      app/assets/scss/markdown/green.scss
  2. 54
      app/assets/scss/markdown/highlight.scss
  3. 2
      app/pages/@[publicSlug]/posts/[postSlug].vue
  4. 43
      app/pages/login/index.vue
  5. 18
      app/pages/register/index.vue
  6. BIN
      packages/drizzle-pkg/db.sqlite

446
app/assets/scss/markdown/green.scss

@ -3,6 +3,7 @@
&.chinese { &.chinese {
text-indent: 1.5em; text-indent: 1.5em;
font-weight: 300; font-weight: 300;
h1, h1,
h2, h2,
h3, h3,
@ -20,131 +21,145 @@
} }
} }
} }
.markdown-body { .markdown-body {
--color-base: #ef4444; --color-base: var(--color-accent-fg);
--markdown-bg: transparent; --color-soft-bg: color-mix(in srgb, var(--color-base) 8%, transparent);
--color-bg: #ff47479c; --color-softer-bg: color-mix(in srgb, var(--color-base) 4%, transparent);
--color-light: #ef44441a; --markdown-bg: color-mix(in srgb, var(--color-canvas-default) 92%, transparent);
--color-extra: rgba(239, 68, 68, 0.3); --markdown-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
--color-more: rgba(239, 68, 68, 0.4); --markdown-radius: 10px;
--markdown-padding: 1.6em;
}
.dark .markdown-body {
--color-fg-default: #e5e7eb;
--color-fg-muted: #c0c7d1;
--color-fg-subtle: #9ca3af;
--color-canvas-default: #111827;
--color-canvas-subtle: #1f2937;
--color-border-default: #374151;
--color-border-muted: #2c3646;
--color-neutral-muted: rgba(148, 163, 184, 0.25);
--color-accent-fg: #60a5fa;
--markdown-bg: color-mix(in srgb, var(--color-canvas-default) 86%, transparent);
--markdown-shadow: 0 10px 28px rgba(2, 6, 23, 0.28);
--code-bg: #0f172a;
--code-head-bg: #172133;
--code-border: #334155;
--code-shadow: rgba(2, 6, 23, 0.45);
} }
.markdown-body.green { .markdown-body.green {
background-color: var(--markdown-bg); background-color: var(--markdown-bg);
box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-radius: var(--markdown-radius);
border-radius: 10px; padding: var(--markdown-padding);
padding: 1.6em; box-shadow: var(--markdown-shadow);
border: 1px solid color-mix(in srgb, var(--color-border-default) 72%, transparent);
color: var(--color-fg-default);
font-size: 1rem;
line-height: 1.9;
letter-spacing: 0.01em;
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
padding: 0; background: transparent;
border: 0;
box-shadow: none; box-shadow: none;
border-radius: 0;
padding: 0;
font-size: 0.98rem;
line-height: 1.85;
} }
strong { p {
&::before{ color: var(--color-fg-default);
content: "";
} }
&::after{
content: ""; h1,
} h2,
// color: #ff4d20; h3,
h4,
h5,
h6 {
margin-top: 1.35em;
margin-bottom: 0.65em;
line-height: 1.35;
color: var(--color-fg-default);
scroll-margin-top: 5rem;
} }
/* 块/段落引用 */ h1 {
// blockquote { font-size: 2em;
// position: relative; }
// // color: #555;
// font-weight: 400;
// border-left: 6px solid var(--color-base);
// padding-left: 1em;
// margin-left: 0;
// padding: 1em;
// background-color: var(--color-light);
// }
blockquote { h2 {
position: relative; font-size: 1.65em;
z-index: 600; padding-bottom: 0.25em;
padding: 20px 20px 15px 20px; border-bottom: 1px solid var(--color-border-muted);
line-height: 1.4 !important;
background-color: rgba(239, 68, 68, 0.06);
border-radius: .4em;
> * {
position: relative;
&:first-child:before {
content: '\201C';
color: var(--color-light);
font-size: 6.5em;
font-weight: 700;
transform: rotate(15deg) translateX(-10px);
opacity: 1;
position: absolute;
top: -.4em;
left: -.2em;
text-shadow: none;
z-index: -10;
} }
h3 {
font-size: 1.35em;
} }
h4 {
font-size: 1.15em;
} }
.tabs{
margin-top: 0;
margin-bottom: 1em;
}
/* 让链接在 hover 状态下显示下划线 */
// a {
// color: var(--color-base);
// text-decoration: none;
// &:hover {
// text-decoration: underline;
// }
// }
a { a {
position: relative;
z-index: 10;
transition: color 0.3s linear;
cursor: pointer;
font-weight: bolder;
// text-decoration: underline #c7254e;
text-decoration: none;
color: var(--color-base); color: var(--color-base);
border-bottom: 1px solid currentColor; text-decoration: underline;
padding: 0 4px; text-decoration-thickness: 1px;
&[data-footnote-backref], text-decoration-color: color-mix(in srgb, var(--color-base) 45%, transparent);
&[data-footnote-ref] { text-underline-offset: 0.18em;
// text-decoration: none; transition: color 0.2s ease, text-decoration-color 0.2s ease;
border: none;
&:hover { &:hover {
background: none; color: color-mix(in srgb, var(--color-base) 78%, var(--color-fg-default));
animation: none; text-decoration-color: currentColor;
} }
} }
&:hover {
content: ''; blockquote {
// text-decoration: none; margin-left: 0;
border: none; padding: 0.95em 1.05em;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 4'%3E%3Cpath fill='none' stroke='%23ff4d20' d='M0 3.5c5 0 5-3 10-3s5 3 10 3 5-3 10-3 5 3 10 3'/%3E%3C/svg%3E") border-left: 4px solid var(--color-base);
repeat-x 0 100%; border-radius: 0.55rem;
background-size: 20px auto; background: var(--color-softer-bg);
animation: waveMove 1s infinite linear; color: var(--color-fg-muted);
} }
@keyframes waveMove {
0% { hr {
background-position: 0 100%; height: 1px;
margin: 1.6rem 0;
background: var(--color-border-muted);
} }
100% {
background-position: -20px 100%; img {
border-radius: 0.8rem;
border: 1px solid var(--color-border-default);
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.08);
} }
figure {
margin: 1.4em auto;
} }
figcaption {
margin-top: 0.55rem;
font-size: 0.82em;
color: var(--color-fg-subtle);
text-align: center;
} }
pre { :not(pre) > code {
background: #f7f7f7; padding: 0.12em 0.48em;
font-size: 0.95em; margin: 0 0.12em;
// /* border: 1px solid #ddd; */ border-radius: 0.4rem;
// padding: 1em 1.5em; background: var(--color-soft-bg);
display: block; border: 1px solid color-mix(in srgb, var(--color-base) 26%, var(--color-border-default));
-webkit-overflow-scrolling: touch; color: color-mix(in srgb, var(--color-base) 72%, var(--color-fg-default));
font-size: 0.92em;
} }
pre, pre,
@ -152,140 +167,100 @@
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
} }
// /* 底部印刷体版本等标记 */
small,
/* 图片说明 */
figcaption {
font-size: 0.75em;
color: #888;
}
// .markdown-body {
legend {
color: #000;
font-weight: inherit;
}
caption {
color: #000;
font-weight: inherit;
}
del {
text-decoration: line-through var(--color-base) 2px;
}
/* 行内代码:仅匹配非代码块场景,避免和 pre/code 嵌套互相覆盖 */
:not(pre) > code {
color: #d73a49;
background-color: rgba(160, 174, 192, 0.25);
font-family: inherit;
font-size: 1em;
border-radius: 4px;
padding: 1px 6px;
margin: 0 2px;
vertical-align: bottom;
}
/* MarkdownIt highlight 输出的代码块容器 */
.code-block-wrapper { .code-block-wrapper {
margin: 1rem 0; margin: 1rem 0 1.25rem;
border-radius: 10px; border: 1px solid var(--color-border-default);
border-radius: 0.8rem;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); background: var(--color-canvas-subtle);
background: #f7f7f7; color: var(--color-fg-default);
color: #333; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.07);
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
font-size: 0.95em;
line-height: 1.5;
.code-header { .code-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0.6rem 1rem; gap: 0.75rem;
background: #efefef; padding: 0.55rem 0.85rem;
border-bottom: 1px solid var(--color-border-default);
background: color-mix(in srgb, var(--color-base) 7%, var(--color-canvas-subtle));
.code-lang { .code-lang {
font-size: 0.78rem; font-size: 0.76rem;
font-weight: 600; font-weight: 600;
letter-spacing: 0.02em; letter-spacing: 0.05em;
color: #64748b; color: var(--color-fg-subtle);
text-transform: uppercase; text-transform: uppercase;
} }
.copy-btn { .copy-btn {
border: 1px solid #d0d7de; border: 1px solid var(--color-border-default);
background: #fff; background: var(--color-canvas-default);
color: #475569; color: var(--color-fg-muted);
border-radius: 6px; border-radius: 0.45rem;
padding: 0.2rem 0.6rem; padding: 0.2rem 0.58rem;
font-size: 0.75rem; font-size: 0.74rem;
line-height: 1.3; line-height: 1.35;
cursor: pointer; cursor: pointer;
transition: all 0.18s ease; transition: all 0.18s ease;
&:hover { &:hover {
background: #f8fafc; border-color: var(--color-base);
border-color: #b8c2cc; color: var(--color-base);
color: #334155;
} }
&.is-copied { &.is-copied {
background: #dcfce7; border-color: #22c55e;
border-color: #86efac; color: #16a34a;
color: #166534; background: color-mix(in srgb, #22c55e 12%, var(--color-canvas-default));
} }
} }
} }
pre { pre {
margin: 0; margin: 0;
padding: 1.2rem; padding: 1.05rem 1.1rem;
overflow-x: auto; overflow-x: auto;
background: transparent; background: transparent;
box-shadow: none; box-shadow: none;
code {
display: block;
padding: 0;
margin: 0;
color: inherit;
background: transparent;
border-radius: 0;
white-space: pre;
}
} }
} }
table { table {
width: fit-content; width: max-content;
min-width: 0;
display: block; display: block;
max-width: 100%; max-width: 100%;
overflow-x: auto; overflow-x: auto;
border: 1px solid var(--color-light); margin: 1.2rem 0;
border-radius: 8px; border: 1px solid color-mix(in srgb, var(--color-border-default) 88%, transparent);
border-radius: 0.8rem;
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
margin: 1rem 0; background: color-mix(in srgb, var(--color-canvas-default) 94%, transparent);
background: #fff; box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
caption { caption {
padding: 0.6em 1em; padding: 0.62em 0.95em;
text-align: left; text-align: left;
color: var(--color-fg-subtle); color: var(--color-fg-subtle);
border-bottom: 1px solid var(--color-light); border-bottom: 1px solid color-mix(in srgb, var(--color-border-default) 84%, transparent);
background: #fff; background: color-mix(in srgb, var(--color-canvas-subtle) 80%, transparent);
} }
thead th { thead th {
background: var(--color-extra);
border-bottom: 1px solid var(--color-light);
font-weight: 600; font-weight: 600;
color: var(--color-fg-default);
background: color-mix(in srgb, var(--color-base) 12%, var(--color-canvas-default));
border-bottom: 1px solid color-mix(in srgb, var(--color-border-default) 86%, transparent);
} }
th, th,
td { td {
padding: 0.55em 1em; padding: 0.62em 0.95em;
white-space: nowrap; white-space: nowrap;
border-right: 1px solid var(--color-light); border-right: 1px solid color-mix(in srgb, var(--color-border-default) 82%, transparent);
border-bottom: 1px solid var(--color-light); border-bottom: 1px solid color-mix(in srgb, var(--color-border-default) 82%, transparent);
} }
th:last-child, th:last-child,
@ -297,68 +272,85 @@
border-bottom: 0; border-bottom: 0;
} }
tbody tr:nth-child(even) td {
background: color-mix(in srgb, var(--color-canvas-subtle) 52%, transparent);
}
tbody tr:hover td { tbody tr:hover td {
background: var(--color-light); background: color-mix(in srgb, var(--color-base) 10%, var(--color-canvas-subtle));
} }
} }
h1,
h2, ul.toc li {
h3, list-style-type: none;
h4,
h5, a {
h6 { border: 0;
margin-top: 1.2em; text-decoration: none;
margin-bottom: 0.6em;
line-height: 1.35;
position: relative;
} }
h1 {
font-size: 1.8em;
} }
h2 {
font-size: 1.6em; @include chinese;
} }
h3 {
font-size: 1.4em; .dark .markdown-body.green {
.code-block-wrapper {
border-color: var(--code-border);
background: var(--code-bg);
box-shadow: 0 10px 26px var(--code-shadow);
.code-header {
border-bottom-color: var(--code-border);
background: var(--code-head-bg);
.code-lang {
color: #bfdbfe;
} }
h4 {
font-size: 1.2em; .copy-btn {
border-color: #475569;
background: #1e293b;
color: #cbd5e1;
&:hover {
border-color: #60a5fa;
background: #0f172a;
color: #93c5fd;
}
&.is-copied {
border-color: #22c55e;
color: #86efac;
background: #052e16;
} }
h5 {
font-size: 1em;
} }
h6 {
font-size: 1em;
} }
::-webkit-calendar-picker-indicator { pre {
filter: invert(50%); background: transparent;
} }
em {
// color: #000;
font-weight: inherit;
position: relative;
&:after {
position: absolute;
top: 0.65em;
left: 0;
width: 100%;
overflow: hidden;
white-space: nowrap;
pointer-events: none;
color: var(--color-base);
content: '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・';
} }
table {
border-color: color-mix(in srgb, var(--color-border-default) 92%, transparent);
background: color-mix(in srgb, var(--color-canvas-default) 90%, transparent);
box-shadow: 0 10px 24px rgba(2, 6, 23, 0.26);
caption {
background: color-mix(in srgb, var(--color-canvas-subtle) 90%, transparent);
border-bottom-color: color-mix(in srgb, var(--color-border-default) 90%, transparent);
}
thead th {
background: color-mix(in srgb, var(--color-base) 18%, var(--color-canvas-subtle));
} }
ul.toc {
li { tbody tr:nth-child(even) td {
list-style-type: none; background: color-mix(in srgb, var(--color-canvas-subtle) 70%, transparent);
a {
text-decoration: none;
border: 0;
list-style-type: none;
} }
tbody tr:hover td {
background: color-mix(in srgb, var(--color-base) 16%, var(--color-canvas-subtle));
} }
} }
@include chinese;
} }

54
app/assets/scss/markdown/highlight.scss

@ -51,3 +51,57 @@
} }
} }
} }
.dark .markdown-body {
.code-block-wrapper {
pre {
code.hljs {
color: #e5e7eb;
}
.hljs-comment,
.hljs-quote {
color: #94a3b8;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-section,
.hljs-link {
color: #c084fc;
}
.hljs-string,
.hljs-title,
.hljs-name,
.hljs-type,
.hljs-attribute,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #5eead4;
}
.hljs-number,
.hljs-variable,
.hljs-template-variable,
.hljs-regexp,
.hljs-meta .hljs-string {
color: #fbbf24;
}
.hljs-built_in,
.hljs-function,
.hljs-class .hljs-title {
color: #60a5fa;
}
.hljs-deletion,
.hljs-subst {
color: #f87171;
}
}
}
}

2
app/pages/@[publicSlug]/posts/[postSlug].vue

@ -116,7 +116,7 @@ const editPostHref = computed(() =>
{{ leadSummary }} {{ leadSummary }}
</p> </p>
<article <article
class="prose dark:prose-invert max-w-none prose-a:text-primary prose-img:rounded-lg markdown-body green" class="prose prose-neutral dark:prose-invert max-w-none prose-a:text-primary prose-img:rounded-lg prose-headings:text-highlighted prose-p:text-default prose-strong:text-highlighted markdown-body green"
v-html="renderedBody" v-html="renderedBody"
/> />
<PostComments mode="public-post" :public-slug="publicSlug" :post-slug="postSlug" /> <PostComments mode="public-post" :public-slug="publicSlug" :post-slug="postSlug" />

43
app/pages/login/index.vue

@ -49,9 +49,8 @@ onMounted(() => {
}) })
const loading = ref(false) const loading = ref(false)
const resultType = ref<'success' | 'error' | ''>('')
const resultMessage = ref('')
const route = useRoute() const route = useRoute()
const toast = useToast()
const { refresh } = useAuthSession() const { refresh } = useAuthSession()
const { allowRegister } = useGlobalConfig() const { allowRegister } = useGlobalConfig()
const { fetchData, getApiErrorMessage } = useClientApi() const { fetchData, getApiErrorMessage } = useClientApi()
@ -79,8 +78,6 @@ const validate = (formState: LoginFormState): FormError[] => {
} }
const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => { const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => {
resultType.value = ''
resultMessage.value = ''
loading.value = true loading.value = true
try { try {
@ -97,16 +94,20 @@ const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => {
await refresh(true) await refresh(true)
resultType.value = 'success' toast.add({
resultMessage.value = `登录成功,欢迎 ${res.user.username}` title: `登录成功,欢迎 ${res.user.username}`,
color: 'success',
})
const redirectCandidate = Array.isArray(route.query.redirect) const redirectCandidate = Array.isArray(route.query.redirect)
? route.query.redirect[0] ? route.query.redirect[0]
: route.query.redirect : route.query.redirect
const redirectTarget = normalizeSafeRedirect(redirectCandidate, DEFAULT_AUTHENTICATED_LANDING_PATH) const redirectTarget = normalizeSafeRedirect(redirectCandidate, DEFAULT_AUTHENTICATED_LANDING_PATH)
await navigateTo(redirectTarget) await navigateTo(redirectTarget)
} catch (error: unknown) { } catch (error: unknown) {
resultType.value = 'error' toast.add({
resultMessage.value = getApiErrorMessage(error) title: getApiErrorMessage(error),
color: 'error',
})
try { try {
await refreshCaptcha() await refreshCaptcha()
} catch { } catch {
@ -160,22 +161,28 @@ const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => {
</UFormField> </UFormField>
<UFormField label="验证码" name="captchaAnswer" required> <UFormField label="验证码" name="captchaAnswer" required>
<div class="flex flex-wrap gap-2 items-center"> <div class="space-y-2">
<div class="flex items-center gap-2">
<img <img
v-if="captchaImageSrc" v-if="captchaImageSrc"
:src="captchaImageSrc" :src="captchaImageSrc"
alt="验证码" alt="验证码"
class="h-10 rounded border border-default bg-elevated shrink-0" class="h-10 w-32 rounded border border-default bg-elevated object-cover shrink-0"
/> />
<div v-else class="h-10 w-32 rounded border border-default bg-elevated shrink-0" />
<UButton type="button" color="neutral" variant="outline" class="shrink-0" @click="refreshCaptcha">
看不清换一张
</UButton>
</div>
<UInput <UInput
v-model="state.captchaAnswer" v-model="state.captchaAnswer"
placeholder="请输入图中字符" placeholder="请输入图中字符"
class="flex-1 min-w-[8rem]" class="w-full"
autocomplete="off" autocomplete="off"
/> />
<UButton type="button" color="neutral" variant="outline" class="shrink-0" @click="refreshCaptcha"> <p class="text-xs text-muted">
换一张 请先识别图片字符再输入不区分大小写
</UButton> </p>
</div> </div>
</UFormField> </UFormField>
@ -184,14 +191,6 @@ const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => {
</UButton> </UButton>
</UForm> </UForm>
<UAlert
v-if="resultType"
:color="resultType === 'success' ? 'success' : 'error'"
:title="resultType === 'success' ? '操作成功' : '操作失败'"
:description="resultMessage"
class="mt-4"
/>
<div v-if="allowRegister" class="mt-4 flex items-center justify-center text-sm"> <div v-if="allowRegister" class="mt-4 flex items-center justify-center text-sm">
<NuxtLink to="/register" class="text-primary hover:underline"> <NuxtLink to="/register" class="text-primary hover:underline">
没有账号去注册 没有账号去注册

18
app/pages/register/index.vue

@ -155,22 +155,28 @@ const onSubmit = async (_event: FormSubmitEvent<RegisterFormState>) => {
</UFormField> </UFormField>
<UFormField label="验证码" name="captchaAnswer" required> <UFormField label="验证码" name="captchaAnswer" required>
<div class="flex flex-wrap gap-2 items-center"> <div class="space-y-2">
<div class="flex items-center gap-2">
<img <img
v-if="captchaImageSrc" v-if="captchaImageSrc"
:src="captchaImageSrc" :src="captchaImageSrc"
alt="验证码" alt="验证码"
class="h-10 rounded border border-default bg-elevated shrink-0" class="h-10 w-32 rounded border border-default bg-elevated object-cover shrink-0"
/> />
<div v-else class="h-10 w-32 rounded border border-default bg-elevated shrink-0" />
<UButton type="button" color="neutral" variant="outline" class="shrink-0" @click="refreshCaptcha">
看不清换一张
</UButton>
</div>
<UInput <UInput
v-model="state.captchaAnswer" v-model="state.captchaAnswer"
placeholder="请输入图中字符" placeholder="请输入图中字符"
class="flex-1 min-w-[8rem]" class="w-full"
autocomplete="off" autocomplete="off"
/> />
<UButton type="button" color="neutral" variant="outline" class="shrink-0" @click="refreshCaptcha"> <p class="text-xs text-muted">
换一张 请先识别图片字符再输入不区分大小写
</UButton> </p>
</div> </div>
</UFormField> </UFormField>

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.
Loading…
Cancel
Save