Commit 5e936ed

HPCesia <me@hpcesia.com>
2025-02-02 09:33:27
feat: comment support
only support twikoo yet. waline support are in plan.
1 parent 8a3e938
src/components/comment/Twikoo.astro
@@ -0,0 +1,32 @@
+---
+import '@/styles/twikoo.scss';
+---
+
+<div id="twikoo-wrap"></div>
+<script
+  src="https://cdn.jsdelivr.net/npm/twikoo@1.6.41/dist/twikoo.nocss.js"
+  crossorigin="anonymous"
+  is:inline></script>
+<script>
+  import { commentConfig } from '@/config';
+  const twikooConfig = commentConfig.twikoo;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  declare const twikoo: any;
+
+  function initTwikoo() {
+    if (typeof twikoo === 'undefined') {
+      setTimeout(initTwikoo, 100);
+    } else {
+      twikoo.init({
+        el: '#twikoo-wrap',
+        ...twikooConfig,
+      });
+    }
+  }
+  document.addEventListener('astro:page-load', initTwikoo);
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', initTwikoo);
+  } else {
+    initTwikoo();
+  }
+</script>
src/components/Comment.astro
@@ -0,0 +1,15 @@
+---
+import { commentConfig } from '@/config';
+import Twikoo from './comment/Twikoo.astro';
+---
+
+<div id="page-comment">
+  {
+    (() => {
+      switch (commentConfig.provider) {
+        case 'twikoo':
+          return <Twikoo />;
+      }
+    })()
+  }
+</div>
src/pages/posts/[article].astro
@@ -1,5 +1,6 @@
 ---
-import { articleConfig } from '@/config';
+import { articleConfig, commentConfig } from '@/config';
+import Comment from '@components/Comment.astro';
 import License from '@components/License.astro';
 import PostInfo from '@components/PostInfo.astro';
 import Markdown from '@components/utils/Markdown.astro';
@@ -39,6 +40,7 @@ const wordCount = countWords(article.body || '');
       <Content />
     </Markdown>
     <License time={article.data.published} />
+    {article.data.comment && commentConfig.enable && <Comment />}
   </div>
   <Fragment slot="aside-fixed">
     <ProfileCard />
src/styles/twikoo.scss
@@ -0,0 +1,633 @@
+/* stylelint-disable no-descending-specificity */
+/* stylelint-disable selector-class-pattern */
+
+@use './globals.scss' as global;
+
+.twikoo {
+  @apply relative flex h-fit w-full flex-col items-center;
+}
+
+.tk-comments {
+  @apply w-full;
+
+  .tk-row {
+    @apply flex w-full flex-row gap-2;
+  }
+
+  .tk-col {
+    @apply flex w-full flex-col gap-2;
+  }
+
+  // 回复框样式
+  .tk-submit {
+    @apply relative w-full;
+
+    .tk-avatar {
+      display: none;
+    }
+
+    // 输入框部分样式
+    .tk-col {
+      @apply flex-col-reverse;
+    }
+
+    // 文本框输入样式
+    .el-textarea {
+      @apply theme-border relative w-full overflow-hidden rounded-xl border-2 pb-9;
+
+      textarea {
+        @apply theme-card-bg w-full p-4;
+
+        resize: none;
+
+        &:focus {
+          outline: none;
+        }
+
+        &::placeholder {
+          @apply theme-text-second;
+        }
+      }
+
+      .el-input__count {
+        @apply theme-text-second absolute bottom-2 right-10 text-sm;
+      }
+    }
+
+    // 个人信息输入框样式
+    .tk-meta-input {
+      @apply flex w-[calc(100%-3.5rem)] flex-col items-center gap-2 md:w-[calc(100%-6.75rem)] md:flex-row;
+    }
+
+    .actions {
+      @apply relative w-full;
+
+      .tk-row-actions-start {
+        @apply absolute bottom-[8.15rem] ml-3 flex flex-row items-center gap-2 md:bottom-[3.15rem];
+
+        input {
+          display: none;
+        }
+      }
+
+      button {
+        @apply theme-border theme-card-bg-hl absolute py-[0.95rem] md:py-[0.3rem];
+
+        &.tk-preview {
+          @apply bottom-[3.7rem] right-0 md:bottom-0 md:right-[3.35rem];
+        }
+
+        &.tk-send {
+          @apply bottom-0 right-0;
+        }
+      }
+    }
+
+    .__markdown {
+      @apply absolute bottom-[7.75rem] right-3 md:bottom-[2.75rem];
+    }
+
+    &:has(.tk-cancel) {
+      .tk-meta-input {
+        @apply w-[calc(100%-3.5rem)] md:w-[calc(100%-10rem)];
+      }
+
+      .actions button {
+        @apply py-[0.3rem];
+
+        &.tk-preview {
+          @apply bottom-[4.9rem] md:bottom-0;
+        }
+
+        &.tk-cancel {
+          @apply theme-card-bg-contrary-hl theme-text-contrary bottom-[2.4rem] right-0 md:bottom-0 md:right-[6.7rem];
+        }
+      }
+    }
+
+    .tk-preview-container {
+      @apply theme-border theme-bg mt-2 w-full rounded-xl border-2 p-2;
+    }
+  }
+
+  .tk-comments-container {
+    // 评论整体样式
+    @apply mt-4 flex w-full flex-col gap-4;
+
+    .tk-comments-title {
+      @apply flex flex-row items-center justify-between px-2;
+
+      .tk-comments-count {
+        @apply text-lg font-bold;
+      }
+
+      .tk-icon {
+        @apply mx-2 cursor-pointer;
+      }
+    }
+
+    .tk-comment {
+      @apply relative scroll-m-20;
+    }
+
+    > .tk-comment {
+      @apply max-md:theme-card-bg max-md:theme-border p-4 max-md:rounded-xl max-md:border-2;
+
+      > .tk-avatar {
+        @apply top-3;
+      }
+    }
+
+    .tk-replies {
+      @apply ml-6 mt-4 flex flex-col gap-2;
+
+      &:not(.tk-replies-expand) {
+        @apply max-h-32 overflow-y-hidden;
+      }
+    }
+
+    // 详细样式
+    .tk-avatar {
+      @apply theme-border absolute top-0 h-8 min-h-8 w-8 min-w-8 overflow-hidden rounded-full border-2;
+    }
+
+    .tk-main > .tk-row {
+      @apply mb-2 ml-[2.5rem] w-[calc(100%-3rem)] justify-between;
+    }
+
+    .tk-meta {
+      a {
+        @apply hover:theme-text-hl duration-300;
+      }
+
+      small {
+        @apply theme-text-second;
+      }
+
+      .tk-actions {
+        @apply opacity-0 duration-300;
+      }
+    }
+
+    // 评论操作样式
+    .tk-main:not(:has(.tk-replies:hover)):hover > .tk-row .tk-meta .tk-actions {
+      @apply opacity-100;
+    }
+
+    .tk-action {
+      .tk-action-link {
+        @apply theme-border relative flex items-center justify-center rounded-xl border-2 px-2 py-1 text-center duration-300;
+
+        .tk-action-icon-solid {
+          @apply absolute left-2 opacity-0 duration-300;
+        }
+
+        .tk-action-count {
+          @apply text-sm;
+
+          &:not(:empty) {
+            @apply ml-2;
+          }
+        }
+
+        &:first-child {
+          display: none;
+        }
+
+        &:hover {
+          @apply theme-card-bg-hl;
+
+          .tk-action-icon-solid {
+            @apply opacity-100;
+          }
+        }
+      }
+    }
+
+    .tk-content {
+      @apply relative mx-2 mb-3;
+    }
+
+    .tk-extras {
+      @apply flex flex-row gap-3;
+
+      .tk-extra {
+        @apply theme-text-second theme-border flex flex-row items-center justify-center gap-2 rounded-md border-2 p-1 text-center text-xs;
+      }
+
+      .tk-icon {
+        @apply hidden;
+      }
+    }
+
+    .tk-replies .tk-content > span:first-child {
+      // 回复提示(回复:xxx)样式
+      @apply theme-text-second text-xs;
+    }
+
+    .tk-expand-wrap,
+    .tk-collapse-wrap {
+      @apply mt-1 flex items-center justify-center text-center;
+    }
+
+    .tk-expand {
+      @apply theme-card-bg hover:theme-card-bg-hl-trans w-full cursor-pointer rounded-md py-1 text-sm duration-300;
+    }
+  }
+
+  // 图标样式
+  .tk-submit-action-icon {
+    @apply inline-block max-h-6 min-h-6 min-w-6 max-w-6 fill-[var(--theme-text-color-light)] dark:fill-[var(--theme-text-color-dark)];
+  }
+
+  .tk-action-icon {
+    @apply inline-block max-h-5 min-h-5 min-w-5 max-w-5 overflow-clip fill-[var(--theme-text-color-light)] dark:fill-[var(--theme-text-color-dark)];
+  }
+
+  .tk-icon {
+    @apply inline-block max-h-4 min-h-4 min-w-4 max-w-4 overflow-clip fill-[var(--theme-text-color-light)] dark:fill-[var(--theme-text-color-dark)];
+  }
+
+  .tk-tag {
+    @apply theme-border rounded-lg border-2 p-1 text-xs;
+  }
+}
+
+.tk-footer {
+  @apply mt-4 w-full text-right text-sm;
+
+  a {
+    @apply text-[var(--theme-color-light)] dark:text-[var(--theme-color-dark)];
+  }
+}
+
+.tk-admin-container {
+  @apply absolute z-10 h-3/4 w-full overflow-hidden bg-black/30 backdrop-blur-sm;
+
+  &:not(:has(.tk-admin.__show)) {
+    display: none;
+  }
+
+  .tk-admin {
+    @apply relative flex h-full w-full items-center justify-center text-center;
+
+    .tk-admin-close {
+      @apply absolute right-2 top-[0.65rem] z-50 h-8 w-8 fill-[var(--theme-text-color-light)] p-2 dark:fill-[var(--theme-text-color-dark)];
+    }
+
+    > div {
+      @apply flex h-full w-full items-center justify-center;
+    }
+  }
+
+  .tk-login {
+    @apply flex flex-col items-center gap-4 p-4;
+
+    .tk-login-title {
+      @apply text-2xl font-bold;
+    }
+  }
+
+  .tk-panel {
+    @apply h-full w-full overflow-y-scroll;
+
+    .tk-panel-title {
+      @apply left-0 top-0 z-40 flex w-full flex-row items-center justify-between p-4;
+
+      // 管理面板标题
+      div {
+        @apply text-xl font-bold;
+      }
+
+      a {
+        @apply theme-border theme-card-bg hover:theme-card-bg-hl mx-6 rounded-xl border-2 px-3 py-1 text-xs duration-300;
+      }
+    }
+
+    .tk-tabs {
+      @apply theme-border flex flex-row items-center justify-between border-b-2 px-4 text-center text-lg;
+
+      .tk-tab {
+        @apply hover:theme-card-bg-hl w-full cursor-pointer py-1 duration-300;
+
+        &.__active {
+          @apply theme-border-hl border-b-2;
+        }
+      }
+    }
+
+    .tk-admin-warn {
+      @apply m-2 rounded-sm border-l-4 border-yellow-400 bg-yellow-100 p-4 text-start text-yellow-700;
+
+      a {
+        @apply font-semibold text-yellow-900;
+      }
+    }
+
+    // 评论管理样式
+    .tk-admin-comment {
+      @apply p-4;
+    }
+
+    .tk-admin-comment-filter {
+      @apply flex flex-row items-center justify-between gap-2 p-4;
+
+      div {
+        @apply w-full;
+
+        input {
+          @apply theme-border theme-card-bg w-full rounded-xl border-2 px-2 py-1;
+
+          &:focus {
+            outline: none;
+          }
+        }
+      }
+
+      select {
+        @apply theme-border theme-card-bg w-1/4 rounded-xl border-2 p-2;
+      }
+    }
+
+    .tk-admin-comment-list {
+      @apply px-3 text-start;
+    }
+
+    .tk-admin-comment-item {
+      @apply theme-border border-b-2 py-1;
+    }
+
+    .tk-admin-comment-meta {
+      @apply flex flex-row flex-wrap items-center gap-2 text-start;
+
+      .tk-avatar {
+        @apply theme-border h-8 w-8 overflow-hidden rounded-full border-2;
+      }
+
+      a {
+        @apply hover:theme-text-hl duration-300;
+      }
+
+      span:last-child,
+      .tk-time {
+        @apply theme-text-second text-sm;
+      }
+    }
+
+    .tk-pagination {
+      @apply flex flex-row flex-wrap items-center justify-between p-4;
+
+      > div {
+        @apply flex flex-row items-center gap-2;
+      }
+
+      input {
+        @apply theme-border theme-card-bg w-16 rounded-xl border-2 px-2 py-1;
+
+        &::-webkit-outer-spin-button,
+        &::-webkit-inner-spin-button {
+          appearance: none;
+        }
+
+        &[type='number'] {
+          appearance: textfield;
+        }
+
+        &:focus {
+          outline: none;
+        }
+      }
+    }
+
+    .tk-pagination-pager {
+      @apply hover:theme-card-bg-hl cursor-pointer rounded-md px-2 py-1;
+
+      &.__current {
+        @apply theme-card-bg-hl;
+      }
+    }
+
+    // 配置管理样式
+    .tk-admin-config {
+      @apply p-4;
+    }
+
+    .tk-admin-config-groups {
+      @apply flex flex-col items-center gap-3 px-4;
+    }
+
+    details {
+      @apply theme-border w-full overflow-hidden rounded-xl border-2 duration-300;
+
+      summary {
+        @apply hover:theme-card-bg-hl-trans w-full px-3 py-1 text-start text-2xl duration-300;
+
+        &::marker {
+          margin-right: 1.5rem;
+        }
+      }
+
+      &[open] {
+        summary {
+          @apply theme-card-bg-hl-trans;
+        }
+      }
+    }
+
+    .tk-admin-config-item {
+      @apply mt-4 grid w-full items-center gap-2 px-4;
+
+      grid-template-columns: 25% 75%;
+
+      .tk-admin-config-title {
+        @apply text-end text-lg;
+      }
+
+      input {
+        @apply theme-border theme-card-bg w-full rounded-xl border-2 px-2 py-1;
+
+        &:focus {
+          outline: none;
+        }
+      }
+
+      .tk-admin-config-desc {
+        @apply theme-text-second whitespace-pre-wrap text-start text-sm;
+      }
+    }
+
+    // 导入样式
+    .tk-admin-import {
+      @apply flex flex-col items-start gap-4 p-4;
+
+      .tk-admin-import-label {
+        @apply text-start text-xl font-bold;
+      }
+
+      select {
+        @apply theme-border theme-card-bg w-full rounded-xl border-2 p-2;
+      }
+
+      input {
+        @apply theme-border theme-card-bg w-full rounded-xl border-2 px-2 py-1;
+
+        &:focus {
+          outline: none;
+        }
+      }
+
+      .el-textarea {
+        @apply h-full w-full;
+
+        textarea {
+          @apply theme-border theme-card-bg h-full w-full rounded-xl border-2 px-2 py-1;
+
+          &:focus {
+            outline: none;
+          }
+        }
+      }
+    }
+
+    // 导出样式
+    .tk-admin-export {
+      @apply p-4;
+    }
+  }
+}
+
+.el-input-group {
+  @apply theme-card-bg theme-border flex w-full flex-row items-center overflow-hidden rounded-xl border-2 text-sm;
+
+  div {
+    @apply theme-card-bg-hl-trans w-fit whitespace-nowrap px-2 py-1;
+  }
+
+  input {
+    @apply w-full bg-transparent px-2 py-1;
+
+    &:focus {
+      outline: none;
+    }
+  }
+
+  .el-button {
+    @apply border-none bg-none p-0;
+  }
+}
+
+.el-button {
+  @apply theme-border theme-card-bg-hl text-nowrap rounded-xl border-2 px-2 py-1 text-center text-sm duration-300;
+
+  &:not(.is-disabled) {
+    @apply hover:scale-105 hover:brightness-110 active:scale-95 active:brightness-90;
+  }
+
+  &.is-disabled {
+    @apply cursor-not-allowed brightness-75;
+  }
+}
+
+// Markdown 样式
+.tk-content,
+.tk-preview-container {
+  // 标题通用样式
+  h1,
+  h2,
+  h3,
+  h4,
+  h5 {
+    display: inline;
+    width: 100%;
+    margin: 1rem 0 0.5rem;
+    scroll-margin-top: 4rem;
+    font-weight: bold;
+  }
+
+  // 基础文本元素
+  p {
+    margin: 0.5rem 0;
+  }
+
+  a {
+    @apply text-[var(--theme-color-light-darken)] underline decoration-dashed dark:text-[var(--theme-color-dark-lighten)];
+
+    &[data-footnote-ref],
+    &[data-footnote-backref] {
+      scroll-margin-top: 4rem;
+    }
+  }
+
+  // 代码样式
+  .code-toolbar {
+    @apply relative w-full;
+
+    .toolbar {
+      @apply absolute right-3 top-1 flex flex-row-reverse items-center justify-between gap-4 text-xs;
+    }
+
+    .copy-to-clipboard-button {
+      @apply theme-border theme-card-bg-hl rounded-md border-2 px-2 py-1 opacity-0 duration-300;
+    }
+
+    &:hover .copy-to-clipboard-button {
+      @apply opacity-100;
+    }
+
+    pre {
+      @apply rounded-md;
+    }
+  }
+
+  code:not(pre code) {
+    padding: 0 0.25rem;
+
+    @apply theme-text-hl-contrast rounded-md bg-[var(--theme-color-light-trans-1d8)] dark:bg-[var(--theme-color-dark-trans-1d8)];
+  }
+
+  // 媒体元素
+  img:not(.tk-owo-emotion) {
+    position: relative;
+    margin: 1rem auto;
+    max-width: 75%;
+    max-height: 40rem;
+
+    @apply rounded-md;
+  }
+
+  img.tk-owo-emotion {
+    @apply inline-block h-7 self-baseline;
+  }
+
+  // 分割线样式
+  hr {
+    margin: 1.5rem 0.25rem;
+    border: 1px dashed;
+  }
+
+  // 引用块样式
+  blockquote {
+    padding: 0.25rem 0.25rem 0.25rem 0.75rem;
+
+    @apply theme-border-hl my-2 rounded-sm border-l-4 bg-[var(--theme-color-light-trans-1d8)] dark:bg-[var(--theme-color-dark-trans-1d8)];
+  }
+
+  // 折叠块样式
+  details {
+    @apply theme-border w-full overflow-hidden rounded-xl border-2 duration-300;
+
+    summary {
+      @apply hover:theme-card-bg-hl-trans w-full px-3 py-1 text-start text-2xl duration-300;
+
+      &::marker {
+        margin-right: 1.5rem;
+      }
+    }
+
+    &[open] {
+      summary {
+        @apply theme-card-bg-hl-trans;
+      }
+    }
+  }
+}
src/types/config.ts
@@ -94,7 +94,7 @@ export type SiteConfig = {
    *
    * 站点的 favicon。
    */
-  favicon: (string | { src: string; theme?: 'light' | 'dark' })[];
+  favicon: string[];
   /**
    * The number of posts displayed per page.
    *
@@ -291,3 +291,41 @@ export type SearchConfig = {
    */
   provider: 'pagefind' | 'algolia';
 };
+
+export type CommentConfig = {
+  /**
+   * Whether to enable comments.
+   *
+   * 是否启用评论。
+   */
+  enable: boolean;
+  /**
+   * `'twikoo'`.
+   *
+   * Twikoo is the only comment provider supported yet.
+   *
+   * Twikoo 是目前唯一支持的评论系统。
+   */
+  provider: 'twikoo';
+  /**
+   * The configuration of Twikoo.
+   *
+   * Twikoo 的配置。
+   *
+   * @see https://twikoo.js.org/
+   */
+  twikoo?: {
+    /**
+     * The envID of Twikoo.
+     *
+     * Twikoo 的 envID。
+     */
+    envId: string;
+    /**
+     * The region of Twikoo backend.
+     *
+     * Twikoo 后端的地区设置。
+     */
+    region?: string;
+  };
+};
src/types/data.ts
@@ -8,4 +8,5 @@ export type BlogPostData = {
   draft?: boolean;
   cover?: string;
   category?: string;
+  comment?: boolean;
 };
src/config.ts
@@ -1,5 +1,6 @@
 import type {
   ArticleConfig,
+  CommentConfig,
   FooterConfig,
   LicenseConfig,
   NavbarConfig,
@@ -120,3 +121,11 @@ export const searchConfig: SearchConfig = {
   enable: true,
   provider: 'pagefind',
 };
+
+export const commentConfig: CommentConfig = {
+  enable: true,
+  provider: 'twikoo',
+  twikoo: {
+    envId: 'https://comment.hpcesia.com/.netlify/functions/twikoo',
+  },
+};
src/content.config.ts
@@ -17,6 +17,7 @@ const postsCollection = defineCollection({
     tags: z.array(z.string()).optional().default([]),
     category: z.string().optional().default(''),
     lang: z.string().optional().default(''),
+    comment: z.boolean().optional().default(true),
   }),
 });