From b65ceefbee989f2b2cb5d5afe0ff4ebab46f6169 Mon Sep 17 00:00:00 2001
From: npmrun <1549469775@qq.com>
Date: Mon, 20 Apr 2026 09:50:34 +0800
Subject: [PATCH] feat(styles): integrate SCSS for improved styling and add
page title management
- Added SCSS support with new global and common stylesheets.
- Introduced markdown styling for enhanced content presentation.
- Implemented a `usePageTitle` composable for dynamic document title management across various pages.
- Updated multiple pages to utilize the new title management system, improving SEO and user experience.
This update enhances the overall styling and usability of the application.
---
app/app.vue | 6 +
app/assets/scss/common.scss | 2 +
app/assets/scss/global.scss | 0
app/assets/scss/markdown/green.scss | 306 ++++++++++++++++++
app/assets/scss/markdown/reset.scss | 454 +++++++++++++++++++++++++++
app/composables/usePageTitle.ts | 28 ++
app/pages/@[publicSlug]/about/index.vue | 6 +-
app/pages/@[publicSlug]/index.vue | 13 +
app/pages/@[publicSlug]/posts/[postSlug].vue | 11 +-
app/pages/@[publicSlug]/posts/index.vue | 10 +-
app/pages/@[publicSlug]/reading/index.vue | 10 +-
app/pages/@[publicSlug]/timeline/index.vue | 10 +-
app/pages/discover/index.vue | 4 +-
app/pages/index/index.vue | 2 +
app/pages/login/index.vue | 3 +-
app/pages/me/admin/config/index.vue | 2 +-
app/pages/me/admin/media-storage.vue | 2 +-
app/pages/me/admin/users/index.vue | 2 +-
app/pages/me/index.vue | 6 +-
app/pages/me/media.vue | 4 +-
app/pages/me/media/index.vue | 2 +-
app/pages/me/media/orphans.vue | 2 +-
app/pages/me/posts/[id].vue | 7 +-
app/pages/me/posts/index.vue | 2 +-
app/pages/me/posts/new.vue | 2 +-
app/pages/me/profile/index.vue | 2 +-
app/pages/me/rss/index.vue | 2 +-
app/pages/me/timeline/index.vue | 2 +-
app/pages/p/[publicSlug]/t/[shareToken].vue | 18 ++
app/pages/register/index.vue | 3 +-
bun.lock | 41 ++-
package.json | 1 +
packages/drizzle-pkg/db.sqlite | Bin 147456 -> 147456 bytes
33 files changed, 927 insertions(+), 38 deletions(-)
create mode 100644 app/assets/scss/common.scss
create mode 100644 app/assets/scss/global.scss
create mode 100644 app/assets/scss/markdown/green.scss
create mode 100644 app/assets/scss/markdown/reset.scss
create mode 100644 app/composables/usePageTitle.ts
diff --git a/app/app.vue b/app/app.vue
index 3439d66..1907ded 100644
--- a/app/app.vue
+++ b/app/app.vue
@@ -4,3 +4,9 @@
+
+
\ No newline at end of file
diff --git a/app/assets/scss/common.scss b/app/assets/scss/common.scss
new file mode 100644
index 0000000..d5e9838
--- /dev/null
+++ b/app/assets/scss/common.scss
@@ -0,0 +1,2 @@
+@use "./markdown/reset.scss";
+@use "./markdown/green.scss";
\ No newline at end of file
diff --git a/app/assets/scss/global.scss b/app/assets/scss/global.scss
new file mode 100644
index 0000000..e69de29
diff --git a/app/assets/scss/markdown/green.scss b/app/assets/scss/markdown/green.scss
new file mode 100644
index 0000000..55b310b
--- /dev/null
+++ b/app/assets/scss/markdown/green.scss
@@ -0,0 +1,306 @@
+
+@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;
+ }
+ }
+}
+.markdown-body {
+ --color-base: #ef4444;
+ --markdown-bg: white;
+ --color-bg: #ff47479c;
+ --color-light: #ef44441a;
+ --color-extra: rgba(239, 68, 68, 0.3);
+ --color-more: rgba(239, 68, 68, 0.4);
+}
+
+.markdown-body.green {
+ background-color: var(--markdown-bg);
+ @apply p-3 lg:p-6;
+
+ strong {
+ &::before{
+ content: "『";
+ }
+ &::after{
+ content: "』";
+ }
+ // color: #ff4d20;
+ }
+
+ /* 块/段落引用 */
+ // 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;
+ }
+ }
+ }
+
+ .tabs{
+ margin-top: 0;
+ margin-bottom: 1em;
+ }
+
+ /* 让链接在 hover 状态下显示下划线 */
+ // a {
+ // color: var(--color-base);
+ // text-decoration: none;
+ // &:hover {
+ // text-decoration: underline;
+ // }
+ // }
+ 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);
+ border-bottom: 1px solid currentColor;
+ padding: 0 4px;
+ &[data-footnote-backref],
+ &[data-footnote-ref] {
+ // text-decoration: none;
+ border: none;
+ &:hover {
+ background: none;
+ animation: none;
+ }
+ }
+ &: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;
+ }
+ @keyframes waveMove {
+ 0% {
+ background-position: 0 100%;
+ }
+ 100% {
+ background-position: -20px 100%;
+ }
+ }
+ }
+
+ // 由prism.scss处理了
+ pre {
+ background: var(--prism-background);
+ font-size: var(--prism-font-size);
+ // /* border: 1px solid #ddd; */
+ // padding: 1em 1.5em;
+ display: block;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ pre,
+ pre code {
+ font-family: var(--prism-font-family);
+ }
+
+ // /* 底部印刷体、版本等标记 */
+ 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;
+ }
+ code {
+ color: rgb(45, 55, 72);
+ background-color: rgba(160,174,192,0.25);
+ font-family: inherit;
+ font-size: 1em;
+ // color: #ffffff;
+ // background-color: #ff4c209c;
+ border-radius: 4px;
+ padding: 1px 6px;
+ // font-size: 0.875em;
+ // font-size: 1.0769em;
+ // position: relative;
+ // top: 0.1em;
+ margin: 0 2px;
+ vertical-align: bottom;
+ }
+ pre {
+ code {
+ padding: 0;
+ font-size: inherit;
+ font-weight: inherit;
+ color: inherit;
+ white-space: pre;
+ background-color: transparent;
+ vertical-align: baseline;
+ border-radius: 0;
+ margin: 0;
+ }
+ }
+ table {
+ width: 100%;
+ display: table;
+ border: 1px solid var(--color-light);
+ tr:hover {
+ background: var(--color-light);
+ }
+ &:hover{
+ border: 1px solid var(--color-extra);
+ th {
+ border-right: 1px solid var(--color-extra);
+ background: var(--color-extra);
+ border-bottom: 1px solid var(--color-extra);
+ }
+ td {
+ border-right: 1px solid var(--color-extra);
+ }
+ caption {
+ border-right: 1px solid var(--color-extra);
+ }
+ thead {
+ th {
+ background: var(--color-extra);
+ }
+ }
+ }
+ // table-layout: fixed;
+ th {
+ border-right: 1px solid var(--color-light);
+ padding: 0.5em 1em;
+ background: var(--color-light);
+ border-bottom: 1px solid var(--color-light);
+ }
+ td {
+ border-right: 1px solid var(--color-light);
+ padding: 0.5em 1em;
+ }
+ caption {
+ border-right: 1px solid var(--color-light);
+ padding: 0.5em 1em;
+ border-bottom: none;
+ }
+ thead {
+ th {
+ background: var(--color-extra);
+ }
+ }
+ }
+ 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;
+ }
+ h6 {
+ font-size: 1em;
+ }
+
+ ::-webkit-calendar-picker-indicator {
+ filter: invert(50%);
+ }
+ 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: '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・';
+ }
+ }
+ ul.toc {
+ li {
+ list-style-type: none;
+ a {
+ text-decoration: none;
+ border: 0;
+ list-style-type: none;
+ }
+ }
+ }
+ @include chinese;
+}
diff --git a/app/assets/scss/markdown/reset.scss b/app/assets/scss/markdown/reset.scss
new file mode 100644
index 0000000..85f7ac0
--- /dev/null
+++ b/app/assets/scss/markdown/reset.scss
@@ -0,0 +1,454 @@
+
+// https://verytoolz.com/blog/03bfb3598f/
+.markdown-body {
+ --color-fg-default: #24292f; // 文本色-默认
+ --color-fg-muted: #57606a; // 文本色-柔和
+ --color-fg-subtle: #6e7781; // 文本色-微妙
+ --color-canvas-default: #ffffff; // 底色-默认
+ --color-canvas-subtle: #f6f8fa; // 底色-微妙
+ --color-border-default: #d0d7de; // 边框色-默认
+ --color-border-muted: hsla(210, 18%, 87%, 1); // 边框色-柔和
+ --color-neutral-muted: rgba(175, 184, 193, 0.2); // 边框色-中性
+ --color-accent-fg: #0969da; // 文本强调色
+ --color-accent-emphasis: #0969da; // 背景强调色
+ --color-attention-subtle: #fff8c5; // 背景注意色
+ --color-danger-fg: #cf222e; // 文本危险色
+ --color-mark-default: rgb(255, 255, 0); // mark 默认色
+ --color-mark-fg: rgb(255, 187, 0); // mark 强调色
+}
+
+.markdown-body {
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ text-rendering: optimizelegibility;
+ margin: 0;
+ word-wrap: break-word;
+ color: var(--color-fg-muted);
+ &::before {
+ display: table;
+ content: '';
+ }
+ &::after {
+ display: table;
+ clear: both;
+ content: '';
+ }
+ > *:first-child {
+ margin-top: 0 !important;
+ }
+ > *:last-child {
+ margin-bottom: 0 !important;
+ }
+ p,
+ blockquote,
+ ul,
+ ol,
+ dl,
+ table,
+ hr,
+ form,
+ pre,
+ details {
+ margin-top: 0;
+ margin-bottom: 1em;
+ }
+ blockquote {
+ & > :first-child {
+ margin-top: 0;
+ }
+ & > :last-child {
+ margin-bottom: 0;
+ }
+ }
+ /* 统一显示成块状元素 */
+ details,
+ figcaption,
+ figure {
+ display: block;
+ }
+ /* HTML5 媒体文件跟 img 保持一致 */
+ audio,
+ canvas,
+ video {
+ display: inline-block;
+ }
+ /* 按钮内部间距统一 */
+ button::-moz-focus-inner,
+ input::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+ }
+ /* 定义元素显示为斜体 */
+ dfn {
+ font-style: italic;
+ }
+ /* 去掉各Table cell 的边距并让其边重合 */
+ table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ display: block;
+ width: max-content;
+ max-width: 100%;
+ overflow: auto;
+ }
+ /* 可拖动文件添加拖动手势 */
+ [draggable] {
+ cursor: move;
+ }
+ /* 加粗元素 */
+ b,
+ strong {
+ font-weight: var(--base-text-weight-semibold, 600);
+ }
+ /* 缩写元素样式统一 */
+ abbr,
+ acronym {
+ border-bottom: none;
+ font-variant: normal;
+ text-decoration: underline dotted;
+ }
+ /* 添加鼠标问号,进一步确保应用的语义是正确的(要知道,交互他们也有洁癖,如果你不去掉,那得多花点口舌) */
+ abbr {
+ cursor: help;
+ }
+ /* 一致的 del 样式 */
+ del {
+ text-decoration: line-through;
+ }
+ /* a标签去除下划线 */
+ a {
+ text-decoration: none;
+ &:not([href]) {
+ color: inherit;
+ text-decoration: none;
+ }
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ /* 默认不显示下划线,保持页面简洁 */
+ ins {
+ text-decoration: none;
+ }
+ /* 专名号:虽然 u 已经重回 html5 Draft,但在所有浏览器中都是可以使用的,
+ * 要做到更好,向后兼容的话,添加 class="typo-u" 来显示专名号
+ * 关于 标签:http://www.whatwg.org/specs/web-apps/current-work/multipage/text-level-semantics.html#the-u-element
+ * 被放弃的是 4,之前一直搞错 http://www.w3.org/TR/html401/appendix/changes.html#idx-deprecated
+ * 一篇关于 标签的很好文章:http://html5doctor.com/u-element/
+ */
+ u,
+ .typo-u {
+ text-decoration: underline;
+ }
+ /* 隐藏指定元素 */
+ [hidden] {
+ display: none !important;
+ }
+ /* 伸缩框显示为列表元素 */
+ summary {
+ display: list-item;
+ }
+ q:before,
+ q:after {
+ content: '';
+ }
+ caption,
+ th {
+ text-align: left;
+ }
+ caption[align='center'],
+ th[align='center'] {
+ text-align: center;
+ }
+ address,
+ caption,
+ cite,
+ em,
+ th,
+ var {
+ font-weight: 400;
+ }
+ /* 标记,类似于手写的荧光笔的作用 */
+ mark {
+ background: var(--color-mark-default);
+ // background: #fffdd1;
+ // border-bottom: 1px solid #ffedce;
+ padding: 2px;
+
+ &.active{
+ background: var(--color-mark-fg);
+ }
+ // margin: 0 5px;
+ }
+ /* 统一h1元素的间隔和字体大小 */
+ h1 {
+ margin: 0.67em 0;
+ font-weight: var(--base-text-weight-semibold, 600);
+ font-size: 2em;
+ }
+ /* small字体缩小 */
+ small {
+ font-size: 90%;
+ }
+ /* 上下标显示 */
+ sub,
+ sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+ }
+ sub a,
+ sup a {
+ padding: 0 0.1em;
+ }
+ sub {
+ bottom: -0.25em;
+ }
+ sup {
+ top: -0.5em;
+ }
+ /* 代码相关的字体大小统一 */
+ code,
+ kbd,
+ pre,
+ samp,
+ pre tt {
+ font-family: monospace;
+ font-size: 1em;
+ }
+ /* 去除默认边框 */
+ fieldset,
+ img {
+ border: 0;
+ }
+ /* 图片初始化样式 */
+ img {
+ border-style: none;
+ max-width: 100%;
+ box-sizing: content-box;
+ margin: 0 auto;
+ background-color: var(--color-canvas-default);
+ }
+ /* 可附标题内容元素的间距 */
+ figure {
+ margin: 1em 40px;
+ }
+ /* 间隔线 */
+ /* 一致化 horizontal rule */
+ hr {
+ box-sizing: content-box;
+ overflow: hidden;
+ background: transparent;
+ border-bottom: 1px solid var(--color-border-muted);
+ height: 0.25em;
+ padding: 0;
+ margin: 24px 0;
+ background-color: var(--color-border-default);
+ border: 0;
+ &::before {
+ display: table;
+ content: '';
+ }
+ &::after {
+ display: table;
+ clear: both;
+ content: '';
+ }
+ }
+
+ /* 表单元素并不继承父级 font 的问题 */
+ button,
+ input,
+ select,
+ textarea {
+ font: inherit;
+ margin: 0;
+ overflow: visible;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ }
+ /* 外观显示为按钮 */
+ [type='button'],
+ [type='reset'],
+ [type='submit'] {
+ -webkit-appearance: button;
+ }
+ /* 这两个表单样式规则覆盖 */
+ [type='checkbox'],
+ [type='radio'] {
+ box-sizing: border-box;
+ padding: 0;
+ }
+ /* 数字按钮内部高度自动 */
+ [type='number']::-webkit-inner-spin-button,
+ [type='number']::-webkit-outer-spin-button {
+ height: auto;
+ }
+ /* 搜索按钮内图标外观去除 */
+ [type='search']::-webkit-search-cancel-button,
+ [type='search']::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ /* 输入框的占位符样式 */
+ ::-webkit-input-placeholder {
+ color: inherit;
+ opacity: 0.54;
+ }
+ /* 文件选择按钮样式统一 */
+ ::-webkit-file-upload-button {
+ -webkit-appearance: button;
+ font: inherit;
+ }
+ /* 占位符显示统一 */
+ ::placeholder {
+ color: var(--color-fg-subtle);
+ opacity: 1;
+ }
+ /* table内的td,th去除留白 */
+ td,
+ th {
+ padding: 0;
+ }
+ /* 伸缩框鼠标显示 */
+ details summary {
+ cursor: pointer;
+ }
+
+ details:not([open]) > *:not(summary) {
+ display: none !important;
+ }
+ /* 按键显示 */
+ kbd {
+ display: inline-block;
+ padding: 3px 5px;
+ font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
+ Liberation Mono, monospace;
+ line-height: 10px;
+ color: var(--color-fg-default);
+ vertical-align: middle;
+ background-color: var(--color-canvas-subtle);
+ border: solid 1px var(--color-neutral-muted);
+ border-bottom-color: var(--color-neutral-muted);
+ border-radius: 6px;
+ box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
+ }
+ .clearfix:before,
+ .clearfix:after {
+ content: '';
+ display: table;
+ }
+ .clearfix:after {
+ clear: both;
+ }
+ .clearfix {
+ zoom: 1;
+ }
+ /* 强制文本换行 */
+ .textwrap,
+ .textwrap td,
+ .textwrap th {
+ word-wrap: break-word;
+ word-break: break-all;
+ }
+ .textwrap-table {
+ table-layout: fixed;
+ }
+ ul {
+ margin-left: 0;
+ padding-left: 0;
+ margin-left: 2em;
+ list-style: disc;
+ }
+ ol {
+ margin-left: 0;
+ padding-left: 0;
+ margin-left: 2em;
+ list-style: decimal;
+ li {
+ padding-left: 0.4em;
+ }
+ }
+ li + li {
+ margin-top: 0.25em;
+ }
+ li {
+ ul {
+ margin-bottom: 0.8em;
+ margin-left: 2em;
+ list-style: circle;
+ li {
+ ul {
+ list-style: square;
+ }
+ }
+ }
+ ol {
+ margin-bottom: 0.8em;
+ margin-left: 2em;
+ }
+ }
+ .task-list-item {
+ list-style-type: none;
+ position: relative;
+ > input {
+ &:nth-child(1) {
+ margin-right: 6px;
+ }
+ }
+ label {
+ font-weight: 400;
+ }
+ .handle {
+ display: none;
+ }
+ input[type='checkbox'] {
+ width: 0.9em;
+ height: 0.9em;
+ position: absolute;
+ left: -1.3em;
+ top: 0.35em;
+ }
+ }
+ .task-list-item.enabled {
+ label {
+ cursor: pointer;
+ }
+ }
+ .task-list-item + .task-list-item {
+ margin-top: 3px;
+ }
+ .contains-task-list {
+ // margin-left: 0.6em;
+ &:dir(rtl) {
+ .task-list-item {
+ input[type='checkbox'] {
+ margin: 0 -1.6em 0.25em 0.2em;
+ }
+ }
+ }
+ }
+
+ .toc{
+ margin-left: 0;
+ }
+
+ dl {
+ display: block;
+ margin-block-start: 1em;
+ margin-block-end: 1em;
+ margin-inline-start: 0px;
+ margin-inline-end: 0px;
+ unicode-bidi: isolate;
+ dt {
+ display: block;
+ unicode-bidi: isolate;
+ }
+ dd {
+ display: block;
+ margin-inline-start: 40px;
+ unicode-bidi: isolate;
+ }
+ }
+}
diff --git a/app/composables/usePageTitle.ts b/app/composables/usePageTitle.ts
new file mode 100644
index 0000000..9f27845
--- /dev/null
+++ b/app/composables/usePageTitle.ts
@@ -0,0 +1,28 @@
+import type { MaybeRefOrGetter } from 'vue'
+import { toValue } from 'vue'
+
+/**
+ * 统一设置 document title:`片段 · … · 站点名`(站点名来自后台 `siteName`)。
+ */
+export function usePageTitle(
+ parts: MaybeRefOrGetter,
+) {
+ const { siteName } = useGlobalConfig()
+
+ useHead(() => {
+ const raw = toValue(parts)
+ const list: (string | null | undefined)[] = Array.isArray(raw)
+ ? [...raw]
+ : [raw]
+
+ const cleaned = list
+ .map((s) => (typeof s === 'string' ? s.trim() : ''))
+ .filter((s) => s.length > 0)
+
+ const site = siteName.value.trim() || 'Person Panel'
+ const prefix = cleaned.join(' · ')
+ return {
+ title: prefix ? `${prefix} · ${site}` : site,
+ }
+ })
+}
diff --git a/app/pages/@[publicSlug]/about/index.vue b/app/pages/@[publicSlug]/about/index.vue
index 112a35b..37cb7d4 100644
--- a/app/pages/@[publicSlug]/about/index.vue
+++ b/app/pages/@[publicSlug]/about/index.vue
@@ -31,9 +31,9 @@ const displayName = computed(
() => data.value?.user.nickname || data.value?.user.publicSlug || slug.value,
)
-useHead(() => ({
- title: data.value ? `${displayName.value} · 关于` : '关于',
-}))
+usePageTitle(() =>
+ data.value ? [displayName.value, '关于'] : [slug.value, '关于'],
+)
diff --git a/app/pages/@[publicSlug]/index.vue b/app/pages/@[publicSlug]/index.vue
index 7e6764a..b3e6584 100644
--- a/app/pages/@[publicSlug]/index.vue
+++ b/app/pages/@[publicSlug]/index.vue
@@ -145,6 +145,19 @@ const showBioReadMore = computed(() => {
}
return stripFrontMatter(md).trim().length > 0
})
+
+usePageTitle(() => {
+ const s = slug.value
+ const d = data.value
+ if (!d) {
+ return [`@${s}`, '主页']
+ }
+ const name =
+ d.user.nickname
+ || (d.user.publicSlug ? `@${d.user.publicSlug}` : '')
+ || s
+ return [name, '主页']
+})
diff --git a/app/pages/@[publicSlug]/posts/[postSlug].vue b/app/pages/@[publicSlug]/posts/[postSlug].vue
index b4c6274..514fb47 100644
--- a/app/pages/@[publicSlug]/posts/[postSlug].vue
+++ b/app/pages/@[publicSlug]/posts/[postSlug].vue
@@ -54,10 +54,13 @@ const publishedAtIso = computed(() =>
data.value?.publishedAt != null ? occurredOnToIsoAttr(data.value.publishedAt) : '',
)
-watchEffect(() => {
- if (data.value?.title) {
- useHead({ title: data.value.title })
+usePageTitle(() => {
+ const ps = publicSlug.value
+ const t = data.value?.title?.trim()
+ if (!t) {
+ return ['文章', `@${ps}`]
}
+ return [t, `@${ps}`]
})
onMounted(() => {
@@ -117,7 +120,7 @@ const editPostHref = computed(() =>
{{ leadSummary }}
diff --git a/app/pages/@[publicSlug]/posts/index.vue b/app/pages/@[publicSlug]/posts/index.vue
index 3348049..4d13b10 100644
--- a/app/pages/@[publicSlug]/posts/index.vue
+++ b/app/pages/@[publicSlug]/posts/index.vue
@@ -7,7 +7,6 @@ import {
definePageMeta({
layout: 'public',
- title: '文章',
})
const route = useRoute()
@@ -73,6 +72,15 @@ const { data, pending, error } = await useAsyncData(
},
{ watch: [slug, page] },
)
+
+usePageTitle(() => {
+ const parts: string[] = []
+ if (page.value > 1) {
+ parts.push(`第 ${page.value} 页`)
+ }
+ parts.push('文章', `@${slug.value}`)
+ return parts
+})
diff --git a/app/pages/@[publicSlug]/reading/index.vue b/app/pages/@[publicSlug]/reading/index.vue
index 7daa3c8..7c8e59f 100644
--- a/app/pages/@[publicSlug]/reading/index.vue
+++ b/app/pages/@[publicSlug]/reading/index.vue
@@ -3,7 +3,6 @@ import { unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
definePageMeta({
layout: 'public',
- title: '阅读',
})
const route = useRoute()
@@ -85,6 +84,15 @@ const { data, pending, error } = await useAsyncData(
},
{ watch: [slug, page] },
)
+
+usePageTitle(() => {
+ const parts: string[] = []
+ if (page.value > 1) {
+ parts.push(`第 ${page.value} 页`)
+ }
+ parts.push('阅读', `@${slug.value}`)
+ return parts
+})
diff --git a/app/pages/@[publicSlug]/timeline/index.vue b/app/pages/@[publicSlug]/timeline/index.vue
index 0ff4929..9148d1c 100644
--- a/app/pages/@[publicSlug]/timeline/index.vue
+++ b/app/pages/@[publicSlug]/timeline/index.vue
@@ -7,7 +7,6 @@ import {
definePageMeta({
layout: 'public',
- title: '时光机',
})
const route = useRoute()
@@ -77,6 +76,15 @@ const { data, pending, error } = await useAsyncData(
},
{ watch: [slug, page] },
)
+
+usePageTitle(() => {
+ const parts: string[] = []
+ if (page.value > 1) {
+ parts.push(`第 ${page.value} 页`)
+ }
+ parts.push('时光机', `@${slug.value}`)
+ return parts
+})
diff --git a/app/pages/discover/index.vue b/app/pages/discover/index.vue
index 20d48dc..30c66a2 100644
--- a/app/pages/discover/index.vue
+++ b/app/pages/discover/index.vue
@@ -1,6 +1,4 @@
diff --git a/app/pages/index/index.vue b/app/pages/index/index.vue
index 35a6e36..712f88b 100644
--- a/app/pages/index/index.vue
+++ b/app/pages/index/index.vue
@@ -5,6 +5,8 @@ const { loggedIn, user, clear } = useAuthSession()
const { fetchData } = useClientApi()
const { allowRegister, siteName } = useGlobalConfig()
+usePageTitle('首页')
+
const logoutLoading = ref(false)
const features = [
diff --git a/app/pages/login/index.vue b/app/pages/login/index.vue
index 89a2705..efb875a 100644
--- a/app/pages/login/index.vue
+++ b/app/pages/login/index.vue
@@ -4,10 +4,11 @@ import { DEFAULT_AUTHENTICATED_LANDING_PATH, normalizeSafeRedirect } from '../..
import { useAuthSession } from '../../composables/useAuthSession'
definePageMeta({
- title: '登录',
layout: 'blank',
})
+usePageTitle('登录')
+
type LoginFormState = {
username: string
password: string
diff --git a/app/pages/me/admin/config/index.vue b/app/pages/me/admin/config/index.vue
index 4db7b30..e1e48ba 100644
--- a/app/pages/me/admin/config/index.vue
+++ b/app/pages/me/admin/config/index.vue
@@ -1,7 +1,7 @@