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. 626
      app/assets/scss/markdown/green.scss
  2. 54
      app/assets/scss/markdown/highlight.scss
  3. 2
      app/pages/@[publicSlug]/posts/[postSlug].vue
  4. 53
      app/pages/login/index.vue
  5. 28
      app/pages/register/index.vue
  6. BIN
      packages/drizzle-pkg/db.sqlite

626
app/assets/scss/markdown/green.scss

@ -1,364 +1,356 @@
@mixin chinese() {
&.chinese {
text-indent: 1.5em;
font-weight: 300;
h1,
h2,
h3,
h4,
h5,
h6,
ol,
ul,
blockquote,
details,
summary,
pre,
.tabs {
text-indent: 0;
}
&.chinese {
text-indent: 1.5em;
font-weight: 300;
h1,
h2,
h3,
h4,
h5,
h6,
ol,
ul,
blockquote,
details,
summary,
pre,
.tabs {
text-indent: 0;
}
}
}
.markdown-body {
--color-base: #ef4444;
--markdown-bg: transparent;
--color-bg: #ff47479c;
--color-light: #ef44441a;
--color-extra: rgba(239, 68, 68, 0.3);
--color-more: rgba(239, 68, 68, 0.4);
--color-base: var(--color-accent-fg);
--color-soft-bg: color-mix(in srgb, var(--color-base) 8%, transparent);
--color-softer-bg: color-mix(in srgb, var(--color-base) 4%, transparent);
--markdown-bg: color-mix(in srgb, var(--color-canvas-default) 92%, transparent);
--markdown-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
--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 {
background-color: var(--markdown-bg);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-radius: 10px;
padding: 1.6em;
@media screen and (max-width: 768px) {
padding: 0;
box-shadow: none;
}
background-color: var(--markdown-bg);
border-radius: var(--markdown-radius);
padding: var(--markdown-padding);
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;
strong {
&::before{
content: "";
}
&::after{
content: "";
}
// color: #ff4d20;
}
@media screen and (max-width: 768px) {
background: transparent;
border: 0;
box-shadow: none;
border-radius: 0;
padding: 0;
font-size: 0.98rem;
line-height: 1.85;
}
/* 块/段落引用 */
// blockquote {
// 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 {
position: relative;
z-index: 600;
padding: 20px 20px 15px 20px;
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;
}
}
}
p {
color: var(--color-fg-default);
}
h1,
h2,
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 {
font-size: 2em;
}
h2 {
font-size: 1.65em;
padding-bottom: 0.25em;
border-bottom: 1px solid var(--color-border-muted);
}
h3 {
font-size: 1.35em;
}
.tabs{
margin-top: 0;
margin-bottom: 1em;
h4 {
font-size: 1.15em;
}
a {
color: var(--color-base);
text-decoration: underline;
text-decoration-thickness: 1px;
text-decoration-color: color-mix(in srgb, var(--color-base) 45%, transparent);
text-underline-offset: 0.18em;
transition: color 0.2s ease, text-decoration-color 0.2s ease;
&:hover {
color: color-mix(in srgb, var(--color-base) 78%, var(--color-fg-default));
text-decoration-color: currentColor;
}
}
/* 让链接在 hover 状态下显示下划线 */
// a {
// color: var(--color-base);
// text-decoration: none;
// &:hover {
// text-decoration: underline;
// }
// }
a {
position: relative;
z-index: 10;
transition: color 0.3s linear;
blockquote {
margin-left: 0;
padding: 0.95em 1.05em;
border-left: 4px solid var(--color-base);
border-radius: 0.55rem;
background: var(--color-softer-bg);
color: var(--color-fg-muted);
}
hr {
height: 1px;
margin: 1.6rem 0;
background: var(--color-border-muted);
}
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;
}
:not(pre) > code {
padding: 0.12em 0.48em;
margin: 0 0.12em;
border-radius: 0.4rem;
background: var(--color-soft-bg);
border: 1px solid color-mix(in srgb, var(--color-base) 26%, var(--color-border-default));
color: color-mix(in srgb, var(--color-base) 72%, var(--color-fg-default));
font-size: 0.92em;
}
pre,
pre code {
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
}
.code-block-wrapper {
margin: 1rem 0 1.25rem;
border: 1px solid var(--color-border-default);
border-radius: 0.8rem;
overflow: hidden;
background: var(--color-canvas-subtle);
color: var(--color-fg-default);
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.07);
.code-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
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 {
font-size: 0.76rem;
font-weight: 600;
letter-spacing: 0.05em;
color: var(--color-fg-subtle);
text-transform: uppercase;
}
.copy-btn {
border: 1px solid var(--color-border-default);
background: var(--color-canvas-default);
color: var(--color-fg-muted);
border-radius: 0.45rem;
padding: 0.2rem 0.58rem;
font-size: 0.74rem;
line-height: 1.35;
cursor: pointer;
font-weight: bolder;
// text-decoration: underline #c7254e;
text-decoration: none;
color: var(--color-base);
border-bottom: 1px solid currentColor;
padding: 0 4px;
&[data-footnote-backref],
&[data-footnote-ref] {
// text-decoration: none;
border: none;
&:hover {
background: none;
animation: none;
}
}
transition: all 0.18s ease;
&:hover {
content: '';
// text-decoration: none;
border: none;
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")
repeat-x 0 100%;
background-size: 20px auto;
animation: waveMove 1s infinite linear;
border-color: var(--color-base);
color: var(--color-base);
}
@keyframes waveMove {
0% {
background-position: 0 100%;
}
100% {
background-position: -20px 100%;
}
&.is-copied {
border-color: #22c55e;
color: #16a34a;
background: color-mix(in srgb, #22c55e 12%, var(--color-canvas-default));
}
}
}
pre {
background: #f7f7f7;
font-size: 0.95em;
// /* border: 1px solid #ddd; */
// padding: 1em 1.5em;
display: block;
-webkit-overflow-scrolling: touch;
margin: 0;
padding: 1.05rem 1.1rem;
overflow-x: auto;
background: transparent;
box-shadow: none;
}
}
pre,
pre code {
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
table {
width: max-content;
min-width: 0;
display: block;
max-width: 100%;
overflow-x: auto;
margin: 1.2rem 0;
border: 1px solid color-mix(in srgb, var(--color-border-default) 88%, transparent);
border-radius: 0.8rem;
border-collapse: separate;
border-spacing: 0;
background: color-mix(in srgb, var(--color-canvas-default) 94%, transparent);
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
caption {
padding: 0.62em 0.95em;
text-align: left;
color: var(--color-fg-subtle);
border-bottom: 1px solid color-mix(in srgb, var(--color-border-default) 84%, transparent);
background: color-mix(in srgb, var(--color-canvas-subtle) 80%, transparent);
}
// /* 底部印刷体版本等标记 */
small,
/* 图片说明 */
figcaption {
font-size: 0.75em;
color: #888;
thead th {
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);
}
// .markdown-body {
legend {
color: #000;
font-weight: inherit;
th,
td {
padding: 0.62em 0.95em;
white-space: nowrap;
border-right: 1px solid color-mix(in srgb, var(--color-border-default) 82%, transparent);
border-bottom: 1px solid color-mix(in srgb, var(--color-border-default) 82%, transparent);
}
caption {
color: #000;
font-weight: inherit;
th:last-child,
td:last-child {
border-right: 0;
}
del {
text-decoration: line-through var(--color-base) 2px;
tbody tr:last-child td {
border-bottom: 0;
}
/* 行内代码:仅匹配非代码块场景,避免和 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;
tbody tr:nth-child(even) td {
background: color-mix(in srgb, var(--color-canvas-subtle) 52%, transparent);
}
/* MarkdownIt highlight 输出的代码块容器 */
.code-block-wrapper {
margin: 1rem 0;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
background: #f7f7f7;
color: #333;
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
font-size: 0.95em;
line-height: 1.5;
.code-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 1rem;
background: #efefef;
.code-lang {
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
color: #64748b;
text-transform: uppercase;
}
.copy-btn {
border: 1px solid #d0d7de;
background: #fff;
color: #475569;
border-radius: 6px;
padding: 0.2rem 0.6rem;
font-size: 0.75rem;
line-height: 1.3;
cursor: pointer;
transition: all 0.18s ease;
&:hover {
background: #f8fafc;
border-color: #b8c2cc;
color: #334155;
}
&.is-copied {
background: #dcfce7;
border-color: #86efac;
color: #166534;
}
}
}
tbody tr:hover td {
background: color-mix(in srgb, var(--color-base) 10%, var(--color-canvas-subtle));
}
}
pre {
margin: 0;
padding: 1.2rem;
overflow-x: auto;
background: transparent;
box-shadow: none;
code {
display: block;
padding: 0;
margin: 0;
color: inherit;
background: transparent;
border-radius: 0;
white-space: pre;
}
ul.toc li {
list-style-type: none;
}
a {
border: 0;
text-decoration: none;
}
table {
width: fit-content;
display: block;
max-width: 100%;
overflow-x: auto;
border: 1px solid var(--color-light);
border-radius: 8px;
border-collapse: separate;
border-spacing: 0;
margin: 1rem 0;
background: #fff;
caption {
padding: 0.6em 1em;
text-align: left;
color: var(--color-fg-subtle);
border-bottom: 1px solid var(--color-light);
background: #fff;
}
}
thead th {
background: var(--color-extra);
border-bottom: 1px solid var(--color-light);
font-weight: 600;
}
@include chinese;
}
th,
td {
padding: 0.55em 1em;
white-space: nowrap;
border-right: 1px solid var(--color-light);
border-bottom: 1px solid var(--color-light);
}
.dark .markdown-body.green {
.code-block-wrapper {
border-color: var(--code-border);
background: var(--code-bg);
box-shadow: 0 10px 26px var(--code-shadow);
th:last-child,
td:last-child {
border-right: 0;
}
.code-header {
border-bottom-color: var(--code-border);
background: var(--code-head-bg);
tbody tr:last-child td {
border-bottom: 0;
.code-lang {
color: #bfdbfe;
}
.copy-btn {
border-color: #475569;
background: #1e293b;
color: #cbd5e1;
&:hover {
border-color: #60a5fa;
background: #0f172a;
color: #93c5fd;
}
tbody tr:hover td {
background: var(--color-light);
&.is-copied {
border-color: #22c55e;
color: #86efac;
background: #052e16;
}
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 1.2em;
margin-bottom: 0.6em;
line-height: 1.35;
position: relative;
}
h1 {
font-size: 1.8em;
}
h2 {
font-size: 1.6em;
}
h3 {
font-size: 1.4em;
}
h4 {
font-size: 1.2em;
}
h5 {
font-size: 1em;
pre {
background: transparent;
}
h6 {
font-size: 1em;
}
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);
}
::-webkit-calendar-picker-indicator {
filter: invert(50%);
thead th {
background: color-mix(in srgb, var(--color-base) 18%, var(--color-canvas-subtle));
}
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: '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・';
}
tbody tr:nth-child(even) td {
background: color-mix(in srgb, var(--color-canvas-subtle) 70%, transparent);
}
ul.toc {
li {
list-style-type: none;
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 }}
</p>
<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"
/>
<PostComments mode="public-post" :public-slug="publicSlug" :post-slug="postSlug" />

53
app/pages/login/index.vue

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

28
app/pages/register/index.vue

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

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.
Loading…
Cancel
Save