mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-07-27 04:20:43 +08:00
feat: 新增博客token配置,完善文章详情页与markdown样式
1. 新增IUniHaloConfig的token配置项并关联环境变量 2. 调整文章跳转路径到subpkg分包目录 3. 重构markdown配置使用主题色替换硬编码值 4. 升级代码块样式为深色主题并添加圆角 5. 新增完整的文章详情页面实现 6. 删除冗余的markdown样式scss文件 7. 添加项目claude开发指南文档
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
uni-halo v3.0 is a multi-platform blog client built on the **Halo 2.x** headless CMS API. It targets WeChat mini-program (primary), H5, Alipay, and native apps. Built on the **unibest** scaffold with Vue 3 + TypeScript + Vite + UnoCSS.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Install dependencies | `pnpm install` |
|
||||||
|
| Dev (H5) | `pnpm dev` or `pnpm dev:h5` |
|
||||||
|
| Dev (WeChat mini-program) | `pnpm dev:mp` or `pnpm dev:mp-weixin` |
|
||||||
|
| Dev (Alipay mini-program) | `pnpm dev:mp-alipay` |
|
||||||
|
| Dev (native app) | `pnpm dev:app` |
|
||||||
|
| Build (H5) | `pnpm build` or `pnpm build:h5` |
|
||||||
|
| Build (WeChat) | `pnpm build:mp` |
|
||||||
|
| Build (app) | `pnpm build:app` |
|
||||||
|
| Lint | `pnpm lint` |
|
||||||
|
| Lint + auto-fix | `pnpm lint:fix` |
|
||||||
|
| Type check | `pnpm type-check` |
|
||||||
|
| Run tests (watch) | `pnpm test` |
|
||||||
|
| Run tests (single) | `pnpm test:run` |
|
||||||
|
| Generate API client from OpenAPI | `pnpm openapi` |
|
||||||
|
| Upload to WeChat platform | `pnpm upload:mp` |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Entry Flow
|
||||||
|
`src/main.ts` → creates Vue app → installs Pinia → route interceptor → request interceptor → i18n. On launch, `src/App.vue` fetches the uni-halo plugin configuration from the Halo server and stores it in the `uniHaloConfig` Pinia store.
|
||||||
|
|
||||||
|
### Routing (Convention-Based)
|
||||||
|
Pages in `src/pages/` are auto-registered by `@uni-helper/vite-plugin-uni-pages`. Use the `definePage()` macro at the top of page files to set metadata (navigation style, title, etc.). Sub-packages live in `src/subpkg-blog/` and `src/subpkg-demo/`.
|
||||||
|
|
||||||
|
Route guards in `src/router/interceptor.ts` intercept `navigateTo`, `reLaunch`, `redirectTo`, `switchTab`. Supports whitelist (default: login required) and blacklist strategies, configured in `src/router/config.ts`.
|
||||||
|
|
||||||
|
### HTTP Layer — Three Coexisting Strategies
|
||||||
|
1. **Custom `uni.request` wrapper** (`src/http/http.ts`) — GET/POST/PUT/DELETE helpers with transparent 401 token refresh (queues pending requests during refresh). Used by `src/api/login.ts`.
|
||||||
|
2. **Alova** (`src/http/alova.ts`) — Alova 3.3+ with `@alova/adapter-uniapp`, Halo-specific response handling. Used by `src/api/halo-base/*` and `src/api/halo-plugin/*`.
|
||||||
|
3. **vue-query** (`src/http/vue-query.ts`) — skeleton/option, not actively used.
|
||||||
|
|
||||||
|
Request interceptor (`src/http/interceptor.ts`) attaches `Authorization: Bearer <token>` to all requests.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
Dual token mode support (single access token OR access+refresh pair), controlled by `VITE_AUTH_MODE` env var. Token store (`src/store/token.ts`) handles login (standard + WeChat mini-program), logout, and automatic refresh with computed expiry checks.
|
||||||
|
|
||||||
|
### API Layer
|
||||||
|
- `src/api/halo-base/` — Core Halo APIs: posts, categories, tags, comments, stats, trackers
|
||||||
|
- `src/api/halo-plugin/` — uni-halo plugin APIs: plugin config, moments, links, gallery, votes, douban, muyin, data stats
|
||||||
|
- `src/api/login.ts` — Auth APIs (login, register, WeChat login, token refresh)
|
||||||
|
- `src/api/types/` — TypeScript interfaces for API responses
|
||||||
|
- Types from `@halo-dev/api-client` (e.g., `ListedPostVo`, `PostVo`)
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
Pinia with `pinia-plugin-persistedstate` using `uni.getStorageSync`/`uni.setStorageSync`. All stores use Composition API style (`defineStore` with setup function):
|
||||||
|
- `src/store/token.ts` — Authentication
|
||||||
|
- `src/store/user.ts` — User profile
|
||||||
|
- `src/store/config.ts` — uni-halo plugin configuration
|
||||||
|
|
||||||
|
### Custom Tabbar
|
||||||
|
Fully custom tabbar in `src/tabbar/` with its own Pinia store. Configurable strategy: no tabbar, native tabbar, or custom tabbar. Currently set to `CUSTOM_TABBAR` with 5 tabs.
|
||||||
|
|
||||||
|
## Coding Conventions
|
||||||
|
|
||||||
|
### Vue Components
|
||||||
|
- Use `<script setup lang="ts">` exclusively — script must be first, template second, style last
|
||||||
|
- Name components with `defineOptions({ name: '...' })`
|
||||||
|
- Define props with `interface` + `withDefaults(defineProps<IProps>(), {...})`
|
||||||
|
- Project-specific components prefixed with `uh-` (e.g., `uh-post-card`, `uh-image`)
|
||||||
|
- Global components in `src/components/`, page-local in `src/pages/xx/components/`
|
||||||
|
- UI library: wot-ui (`wd-*` components), auto-imported via easycom
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
- Use `interface` for object types, `type` for union types
|
||||||
|
- Use `import type` for type-only imports
|
||||||
|
- Avoid `any`
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
- Prefer UnoCSS atomic classes over custom CSS
|
||||||
|
- SCSS with `scoped` when custom styles are needed; follow BEM naming
|
||||||
|
- Use `rpx` units for cross-platform screen adaptation
|
||||||
|
- Safe-area utilities: `p-safe`, `pt-safe`, `pb-safe`
|
||||||
|
- UnoCSS shortcut `center` = `flex items-center justify-center`
|
||||||
|
- Theme color `primary` from wot-ui CSS variable `--wd-primary-color`
|
||||||
|
|
||||||
|
### Platform Conditional Compilation
|
||||||
|
Use uni-app directives for platform-specific code:
|
||||||
|
```vue
|
||||||
|
<!-- #ifdef H5 -->
|
||||||
|
<view>H5 only</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<view>WeChat only</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
```
|
||||||
|
Import boundaries must not be reordered by ESLint — `perfectionist/sort-imports` is disabled for this reason.
|
||||||
|
|
||||||
|
### Git
|
||||||
|
- Conventional commits enforced via commitlint (`@commitlint/config-conventional`)
|
||||||
|
- Write commit messages in Chinese
|
||||||
|
- Pre-commit runs lint-staged via Husky
|
||||||
|
|
||||||
|
### i18n
|
||||||
|
English and Simplified Chinese via vue-i18n. Locale files in `src/locale/` (en.json, zh-Hans.json).
|
||||||
|
|
||||||
|
## Path Aliases
|
||||||
|
- `@` → `./src`
|
||||||
|
- `@img` → `./src/static/images`
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
Environment files in `env/` directory:
|
||||||
|
- `.env` — Base config (app title, port 9000, app IDs, auth mode)
|
||||||
|
- `.env.development` — Dev backend URL, console enabled
|
||||||
|
- `.env.test` — Test environment
|
||||||
|
- `.env.production` — Production (console stripped)
|
||||||
@@ -4,6 +4,8 @@ export interface IUniHaloConfig {
|
|||||||
base: {
|
base: {
|
||||||
// 博客域名
|
// 博客域名
|
||||||
domain: string
|
domain: string
|
||||||
|
// 博客token
|
||||||
|
token: string
|
||||||
// 是否为个人资质的小程序
|
// 是否为个人资质的小程序
|
||||||
isIndividual: boolean
|
isIndividual: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -821,7 +821,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>/deep/ .hl-code,/deep/ .hl-pre{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}/deep/ .hl-pre{padding:1em;margin:.5em 0;overflow:auto}/deep/ .hl-pre{background:#2d2d2d}/deep/ .hl-block-comment,/deep/ .hl-cdata,/deep/ .hl-comment,/deep/ .hl-doctype,/deep/ .hl-prolog{color:#999}/deep/ .hl-punctuation{color:#ccc}/deep/ .hl-attr-name,/deep/ .hl-deleted,/deep/ .hl-namespace,/deep/ .hl-tag{color:#e2777a}/deep/ .hl-function-name{color:#6196cc}/deep/ .hl-boolean,/deep/ .hl-function,/deep/ .hl-number{color:#f08d49}/deep/ .hl-class-name,/deep/ .hl-constant,/deep/ .hl-property,/deep/ .hl-symbol{color:#f8c555}/deep/ .hl-atrule,/deep/ .hl-builtin,/deep/ .hl-important,/deep/ .hl-keyword,/deep/ .hl-selector{color:#cc99cd}/deep/ .hl-attr-value,/deep/ .hl-char,/deep/ .hl-regex,/deep/ .hl-string,/deep/ .hl-variable{color:#7ec699}/deep/ .hl-entity,/deep/ .hl-operator,/deep/ .hl-url{color:#67cdcc}/deep/ .hl-bold,/deep/ .hl-important{font-weight:700}/deep/ .hl-italic{font-style:italic}/deep/ .hl-entity{cursor:help}/deep/ .hl-inserted{color:green}/deep/ .md-p {
|
<style>/deep/ .hl-code,/deep/ .hl-pre{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}/deep/ .hl-pre{padding:1em;margin:.5em 0;overflow:auto}/deep/ .hl-pre{background:#111827;border-radius:12px}/deep/ .hl-block-comment,/deep/ .hl-cdata,/deep/ .hl-comment,/deep/ .hl-doctype,/deep/ .hl-prolog{color:#999}/deep/ .hl-punctuation{color:#ccc}/deep/ .hl-attr-name,/deep/ .hl-deleted,/deep/ .hl-namespace,/deep/ .hl-tag{color:#e2777a}/deep/ .hl-function-name{color:#6196cc}/deep/ .hl-boolean,/deep/ .hl-function,/deep/ .hl-number{color:#f08d49}/deep/ .hl-class-name,/deep/ .hl-constant,/deep/ .hl-property,/deep/ .hl-symbol{color:#f8c555}/deep/ .hl-atrule,/deep/ .hl-builtin,/deep/ .hl-important,/deep/ .hl-keyword,/deep/ .hl-selector{color:#cc99cd}/deep/ .hl-attr-value,/deep/ .hl-char,/deep/ .hl-regex,/deep/ .hl-string,/deep/ .hl-variable{color:#7ec699}/deep/ .hl-entity,/deep/ .hl-operator,/deep/ .hl-url{color:#67cdcc}/deep/ .hl-bold,/deep/ .hl-important{font-weight:700}/deep/ .hl-italic{font-style:italic}/deep/ .hl-entity{cursor:help}/deep/ .hl-inserted{color:green}/deep/ .md-p {
|
||||||
margin-block-start: 1em;
|
margin-block-start: 1em;
|
||||||
margin-block-end: 1em;
|
margin-block-end: 1em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<style>.hl-code,.hl-pre{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.hl-pre{padding:1em;margin:.5em 0;overflow:auto}.hl-pre{background:#2d2d2d}.hl-block-comment,.hl-cdata,.hl-comment,.hl-doctype,.hl-prolog{color:#999}.hl-punctuation{color:#ccc}.hl-attr-name,.hl-deleted,.hl-namespace,.hl-tag{color:#e2777a}.hl-function-name{color:#6196cc}.hl-boolean,.hl-function,.hl-number{color:#f08d49}.hl-class-name,.hl-constant,.hl-property,.hl-symbol{color:#f8c555}.hl-atrule,.hl-builtin,.hl-important,.hl-keyword,.hl-selector{color:#cc99cd}.hl-attr-value,.hl-char,.hl-regex,.hl-string,.hl-variable{color:#7ec699}.hl-entity,.hl-operator,.hl-url{color:#67cdcc}.hl-bold,.hl-important{font-weight:700}.hl-italic{font-style:italic}.hl-entity{cursor:help}.hl-inserted{color:green}</style><style>.md-p{margin-block-start:1em;margin-block-end:1em}.md-blockquote,.md-table{margin-bottom:16px}.md-table{box-sizing:border-box;width:100%;overflow:auto;border-spacing:0;border-collapse:collapse}.md-tr{background-color:#fff;border-top:1px solid #c6cbd1}.md-table .md-tr:nth-child(2n){background-color:#f6f8fa}.md-td,.md-th{padding:6px 13px!important;border:1px solid #dfe2e5}.md-th{font-weight:600}.md-blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}.md-code{padding:.2em .4em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:85%;background-color:rgba(27,31,35,.05);border-radius:3px}.md-pre .md-code{padding:0;font-size:100%;background:0 0;border:0}.hl-pre{position:relative}.hl-code{overflow:auto;display:block}.hl-language{font-size:12px;font-weight:600;position:absolute;right:8px;text-align:right;top:3px}.hl-pre{padding-top:1.5em}.hl-pre{font-size:14px;padding-left:3.8em;counter-reset:linenumber}.line-numbers-rows{position:absolute;pointer-events:none;top:1.5em;font-size:100%;left:0;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows .span{display:block;counter-increment:linenumber}.line-numbers-rows .span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}._address,._article,._aside,._body,._caption,._center,._cite,._footer,._header,._html,._nav,._pre,._section{display:block}._video{width:300px;height:225px;display:inline-block;background-color:#000}</style><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>body,html{width:100%;height:100%;overflow-x:scroll;overflow-y:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}</style></head><body><div id="content" style="overflow:hidden"></div><script type="text/javascript" src="./js/uni.webview.min.js"></script><script type="text/javascript" src="./js/handler.js"></script></body>
|
<style>.hl-code,.hl-pre{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.hl-pre{padding:1em;margin:.5em 0;overflow:auto}.hl-pre{background:#111827;border-radius:12px}.hl-block-comment,.hl-cdata,.hl-comment,.hl-doctype,.hl-prolog{color:#999}.hl-punctuation{color:#ccc}.hl-attr-name,.hl-deleted,.hl-namespace,.hl-tag{color:#e2777a}.hl-function-name{color:#6196cc}.hl-boolean,.hl-function,.hl-number{color:#f08d49}.hl-class-name,.hl-constant,.hl-property,.hl-symbol{color:#f8c555}.hl-atrule,.hl-builtin,.hl-important,.hl-keyword,.hl-selector{color:#cc99cd}.hl-attr-value,.hl-char,.hl-regex,.hl-string,.hl-variable{color:#7ec699}.hl-entity,.hl-operator,.hl-url{color:#67cdcc}.hl-bold,.hl-important{font-weight:700}.hl-italic{font-style:italic}.hl-entity{cursor:help}.hl-inserted{color:green}</style><style>.md-p{margin-block-start:1em;margin-block-end:1em}.md-blockquote,.md-table{margin-bottom:16px}.md-table{box-sizing:border-box;width:100%;overflow:auto;border-spacing:0;border-collapse:collapse}.md-tr{background-color:#fff;border-top:1px solid #c6cbd1}.md-table .md-tr:nth-child(2n){background-color:#f6f8fa}.md-td,.md-th{padding:6px 13px!important;border:1px solid #dfe2e5}.md-th{font-weight:600}.md-blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}.md-code{padding:.2em .4em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:85%;background-color:rgba(27,31,35,.05);border-radius:3px}.md-pre .md-code{padding:0;font-size:100%;background:0 0;border:0}.hl-pre{position:relative}.hl-code{overflow:auto;display:block}.hl-language{font-size:12px;font-weight:600;position:absolute;right:8px;text-align:right;top:3px}.hl-pre{padding-top:1.5em}.hl-pre{font-size:14px;padding-left:3.8em;counter-reset:linenumber}.line-numbers-rows{position:absolute;pointer-events:none;top:1.5em;font-size:100%;left:0;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows .span{display:block;counter-increment:linenumber}.line-numbers-rows .span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}._address,._article,._aside,._body,._caption,._center,._cite,._footer,._header,._html,._nav,._pre,._section{display:block}._video{width:300px;height:225px;display:inline-block;background-color:#000}</style><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>body,html{width:100%;height:100%;overflow-x:scroll;overflow-y:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}</style></head><body><div id="content" style="overflow:hidden"></div><script type="text/javascript" src="./js/uni.webview.min.js"></script><script type="text/javascript" src="./js/handler.js"></script></body>
|
||||||
@@ -96,7 +96,7 @@ const textFooterClass = computed(() => {
|
|||||||
|
|
||||||
function handleToPostDetail() {
|
function handleToPostDetail() {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages-blog/post-detail/post-detail?metadataName=${props.post.metadata.name}`,
|
url: `/subpkg-blog/post-detail/post-detail?metadataName=${props.post.metadata.name}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useAppConfig } from '../useAppConfig'
|
import { useAppConfig } from '../useAppConfig'
|
||||||
|
|
||||||
|
const primaryColor = '#111827'
|
||||||
|
|
||||||
export const markdownConfig = {
|
export const markdownConfig = {
|
||||||
domain: useAppConfig().appBaseUrl.value,
|
domain: useAppConfig().appBaseUrl.value,
|
||||||
tagStyle: {
|
tagStyle: {
|
||||||
@@ -28,7 +30,7 @@ border-bottom: 1px solid var(--classE);
|
|||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
background: #f2f6fc;
|
background: #f2f6fc;
|
||||||
border-left: 5px solid #50bfff;
|
border-left: 5px solid ${primaryColor};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
line-height: 26px;
|
line-height: 26px;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
@@ -87,7 +89,7 @@ font-size: 14px;
|
|||||||
strong: 'font-weight: 700;color: rgb(248, 57, 41);',
|
strong: 'font-weight: 700;color: rgb(248, 57, 41);',
|
||||||
video: 'width: 100%',
|
video: 'width: 100%',
|
||||||
},
|
},
|
||||||
containStyle: 'font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;padding:12px;font-size: 14px;color: #606266;word-spacing: 0.8px;letter-spacing: 0.8px;border-radius: 6px;background-color:#FFFFFF;',
|
containStyle: 'font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 14px;color: #606266;word-spacing: 0.8px;letter-spacing: 0.8px;border-radius: 6px;background-color:#FFFFFF;',
|
||||||
|
|
||||||
loadingGif: '',
|
loadingGif: '',
|
||||||
emptyGif: '',
|
emptyGif: '',
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
:root {
|
|
||||||
--main: #303133;
|
|
||||||
--theme: #fb6c28;
|
|
||||||
--code-background: #e8f3ff;
|
|
||||||
--radius-inner: 4px;
|
|
||||||
--classA: #dcdfe6;
|
|
||||||
--classB: #e4e7ed;
|
|
||||||
--classC: #ebeef5;
|
|
||||||
--classD: #f2f6fc;
|
|
||||||
--classE: #dcdcdc;
|
|
||||||
--classF: #333;
|
|
||||||
--classG: #dcdcdc;
|
|
||||||
--classH: #e9f2ff;
|
|
||||||
--classI: #5a3713;
|
|
||||||
--classJ: #f9e5fb;
|
|
||||||
--classK: #e4e7ed;
|
|
||||||
--classL: #666;
|
|
||||||
--classM: #2d2e37;
|
|
||||||
--quote: #50bfff;
|
|
||||||
--code: #409eff;
|
|
||||||
}
|
|
||||||
.uh-markdown {
|
|
||||||
::v-deep {
|
|
||||||
h1::before,
|
|
||||||
h2::before,
|
|
||||||
h3::before,
|
|
||||||
h4::before,
|
|
||||||
h5::before,
|
|
||||||
h6::before {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
content: '';
|
|
||||||
margin-right: 6px;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
h1::before {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
content: '¶';
|
|
||||||
top: -4px;
|
|
||||||
margin-right: 12px;
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--theme);
|
|
||||||
}
|
|
||||||
h2::before {
|
|
||||||
top: -2px;
|
|
||||||
left: 0;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
background-size: auto 100%;
|
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAVJJREFUWEftl7FKw1AUhv9rbmf1PRw6utkHEOoi6WPoE1ifQJ8gySKCOtRFRBKsDuIiDdZAURBFF50M2sFIzpGogVK03JA2cbh3S/i55zvfvYETgRyLj5erIKOO949FyKm5eHvvHEBV2jyruq1QDQ7n2DNXALEx+D7e2vl6lBYp76scHCzErtmCEPVhqEIAfus8BZk4wM+Zd/46tskDeI0mgLUyAdoAFsoDcM0XCDFdHoDX4FGfbRF3QANoA9qANqANaAPawD82EEWId1sAOJQWz6iO+5nGch41kDw9I3bbYOaTis21wgG4G4AuAzCwXrEoGV6V1ngMJPoPjoB+PzSIa8KBr1QdwFgA6KID7t1AgFYNC5uqxZNcbgB+eASdngHM+9LmpSzF8wFEEagbgHvXoQA3s3aegmYz8P1f4NNVME+39z5e3w4lkSMc3GXtPM1/AjYDFjDGddN5AAAAAElFTkSuQmCC);
|
|
||||||
}
|
|
||||||
h3::before {
|
|
||||||
top: -3px;
|
|
||||||
left: 0;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
background-size: auto 100%;
|
|
||||||
background-repeat: none;
|
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAH1JREFUWEft1qENgDAQheH/JANg60DAJIgmrMFCzEEYgTFgDyRFIClp0yBf9evl8pl3RsYbp7BjNLHoMptljPiMZH3WAhKQgAQkIIGXQAXUT79cniHaNMa5draliqqojIID86nRHEtvbSqlBSQgAQlIQAISkECRAA746SC5Ad6XpiGnnOGPAAAAAElFTkSuQmCC);
|
|
||||||
}
|
|
||||||
h4::before {
|
|
||||||
top: -2px;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
color: var(--theme);
|
|
||||||
background-size: auto 100%;
|
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAndJREFUWEftVkty2kAQfT1SBLtwg6AqwzbxCQIniG8QvAmwCp8kRqvgFa5KwHjlkI3NCeIbhJwgXptUgW9AdiDQdGoITqUsjcZyuYqNZ6WSXs88vel+3YQtL9ry+Xgk8KhApALNAe8x5PPbCUrgabdsD2/e+1edEpN4FsIRps7OwT9cXKKHCLw75QIL+V0XRFIUP1dptBx3ChKkxQlw8UnOG5mqLESgdsYZy5dTAE8jg5lH3Ypd5Mlxxl8ttDgGRulcq5iYgApofgn6ILzVBQsSu5/e0OXiqnMOotdatVjsOvkPl4muQIFrp5y1hJzEBA67ZavEk07WX5EexzxM5b1SYgJrFQbBOQDt3wVSuP0qTRfjowsAr3SHODa75HrqqiKX1gdMycjAYa9stU3JCPBhKue1ExNQAY3BakSgl1HBDMykI9z+Ps0WV0eXIITKdhM3c+yUS259FrVPrBM2TlclEnSmY8+S93tV+/yvH+hxxLzv5D11paFltOLmIFD3FzIbtRMzpr2K5arn+bgzJZAWl8631rjby0zAUJI3xnTfkjQTiFEAwHW3bGWNCoCv0zlvjUukgOoJgPymzQGg3itbfVMOMKiezh30ExOIqwIAvwNHZFUVzMcdbbUonGOnsomr4P1XfiFZ/tS6GOOkW7FqJh8gwomz06ol9oE7O6GhH9zLCbfeCxqDoE3AR61sm5nA/3XUZ47pmneYCSLnAeHLCQGZaAvmH72yXdjMA6oTanHpnFfQ5tDmQ+KJ6MZ+jckXY7//k4pWYB7sQVCUccxU3a9teHKcWS7ne0wI4Rhipqv7REZkku8hvhut+CEOidvjkcDWFfgD9RMzMKE7f80AAAAASUVORK5CYII=);
|
|
||||||
}
|
|
||||||
h5::before {
|
|
||||||
top: -1px;
|
|
||||||
left: 0;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAC8klEQVRYR+3WP2gTURwH8O/vKnVRRHKXP52cBO3g4p/BqYNIhy4muajUQRBFKjQV1En6ZxJBcmlRCoJDRe3FDiJVwamLS3FwqbgIgss1l2ZQF5XeT3I1Z3NJ7u5dLlAwN9699/t97vfe7/EIO/yhHe5DD9jpCv3fFVTu856+Xzi62Y/35hj9CFPNrlUwWeRJAJfBGADwBcBNI0/PRZFdAdo4xlQLjCqKjBzogat7hZCRAgPghJGBgbFnxglAOvS7b/fLb+q+qnv5BHBCyEBAWTdnANy2IxOtWSC1qsY+1jOFwAVG+gIVvXyHQbcaKuZCxmf5iMQogXFQtEsBeO5JT6BcMu+Bcb1lUhcyqfExACUAB6JEtgUqi+U5JrrmmcyFHCjwSYtQO+tSUSFbAmXdnAdwJVASdyWLPAS2kbFA8xsHNS13EzCmlx8R6KJQcBcypfFpho3cKxRna3ADsgEo6+ZjAKMhgjZ1d2KWR2gTSyD0h4jnIB2gXDIXwciFCPZvSnMl0wwshYrJGDImaMUGKovlLBPVOrDjh8APzFx8zDkjNa7FzYoGZsKT9XEarQOHmei1aJA246cqOWW6/i2l8VMGzoWIPW/k6eq2Ja6UwCz8pw2JGZ8sS8pUz8fWau/jGp+SgLchcGBgZD1Py41NUuoI+ZloV8ZU93+ogZKzfBiWfXAPCgMJU8Y42avQdMzI4ZBfmSizocqrUeJaAmsvBZGGBCtTziXeRY1rCxRAViEhXckqK93AeQJ9kYTvlmVlqmcTdhNEtefc+9X3utVmuX+CkK6oyqtu4nwrWP8bF5IZdGYjJ79wDuMCz4D+XmhFWnZbt7ab5ltBB6mbkyAaBPFCJassuwMmNb4L4EZgXwBc4AoGTZrUeA6A9x1yK6tzzvnFDlxBv0D176kCP2TCpbbjBXCRV9DZk0VeAONCE1IQ1zWg3dlF1sFQHWQIXFeBNrLAw5BwHBZWjQl6E3SbbB8X+R4Mg/Ca0wN2WtFeBTut4B84mFI4VpekyAAAAABJRU5ErkJggg==);
|
|
||||||
}
|
|
||||||
h6::before {
|
|
||||||
top: -1px;
|
|
||||||
left: 0;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background-size: auto 100%;
|
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAEI0lEQVRYR+3Xb2wTZRwH8G/vL22RPy5GW8fK6rJpGeFFY9RO3TRRE1HfmEAammEyjZmOSEg1RrPZaTD6xjhU/Ndlf0CZgwmD+qcgZBRIETeqY0Vcnc7pGonhRY2l3m2t5upqjq693l2vcy+8N81zz/NcPvf75fd7rjos8ku3yH34HyjK0PqlDLPzD56vMNL0VHxmpgXAoUIZXKgI3sMSpK+cNTAbylaj/9IkprnLM3+mkg8XQi4EsJ4lSL+ZNbAfVd+JG/XLcSERg3M8IAtZauBtDEkeNdN6fX9NfRqXueQiSwm00wQRMDMGw94snBJkqYBrSYIIXs/ojftqGq6IXHZRFIpkKYC2JSw9SJE663W0nthlqcMawwrJYpVCag1M48pNy1YNvLWRffGNIYSCv6Cnog7rjCtVIbUEXoG7qeqaNMj51F58dWoKPZY62JeWyUJG+ctcIplcIizWCpgTl9EIyODJn9BtceDWq/6B57tO/H4R688fFaZ7AWzWAiiJEyMDx39Ed6UDdyy7Ni/w1ekxbP95VJi/F8CRYoGycGLksaEJdFU6cPdy0zxkR/RbtE6FhPvHATQUm2JFODHSfyyCLmsd7lth/hf53q/jcE8OC+NTAG7PTKiNoCqcGPnpF+PotDrwwNXl+OC3H9A8cVqYPgPgFnFo1QCLwomRg4cvoPMGBzZHTgq3zwKwZ+ddKVATXAZhf+gdnPvuojAUqmJdrspRAtQU93pXEM+8clgwhQHU5itruUBNcTt6TsP9sl8weQC0S/VFOUBNcW/2folt2z+XhZPTZh6kSKJ79aqVRuFszRxfUm8sNbdz9xlsfekz2bhCQBvDMMM8z+ubN92Mjrb71brS+97dM4wtnk8U4SSBBEGErVarzeVywePxoHVLA1pb6lUh3+8bwZMv+BTjpIAVACa9Xq+uqakJ7e3tqpGd/WfR3Jr+81awIJS0mY0A+sLhMGw2W3qfGmTXvhAef/6gapxUBDuqqqoejUQiBvFbKUH2DHyNx54bLAqXF0jT9HBjY6Pd6/XOi7oc5K7936Dp2QNF4/IBrTqdLuLxeIi2tracRSGF/PDgKB55er8muHzATQB2m81m+P1+1NbmPoVyIfsOnUOj+2PNcPmAO1iWbeE4TmexWODz+WQhayrL4No2oCkuJ5Bl2VGO49ZmcisXObdeVSuRaq7ZZ3G10KBTqRQl3pQPGY1GEQgE4HQ6heVDAO5S1cklNmUDXQRB9KZSqXkfESaTCW63G7FYDKFQKDEyMoJoNKqfe/bbAJ7QGpcrxa8xDLOV5/k0kGXZv2ZnZ5FMJtNjmqYvURR1IpFInAcwAeD7ud/pUuDmAY1G41g8Hl9DkmScoqggx3FHAIyJMMlSQfI9V5zKagDC93dsoRFKimQx2dIWOV/U/yn6bx0WyDj8vgLOAAAAAElFTkSuQmCC);
|
|
||||||
}
|
|
||||||
blockquote > p {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
font-size: 0.9em !important;
|
|
||||||
}
|
|
||||||
code[class='md-code'],
|
|
||||||
code:not([class]) {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #409eff;
|
|
||||||
margin: 2px 5px;
|
|
||||||
padding: 0 8px;
|
|
||||||
white-space: normal;
|
|
||||||
text-indent: 0;
|
|
||||||
-webkit-user-select: auto;
|
|
||||||
-moz-user-select: auto;
|
|
||||||
-ms-user-select: auto;
|
|
||||||
user-select: auto;
|
|
||||||
vertical-align: baseline;
|
|
||||||
word-break: break-word;
|
|
||||||
background: #e8f3ff;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
code[class*='language-'] {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
// border-radius: 0 0 8px 8px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
user-select: auto;
|
|
||||||
padding: 12px 12px 14px 18px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
background: #282c34;
|
|
||||||
color: #abb2bf;
|
|
||||||
border-radius: 4px;
|
|
||||||
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
|
||||||
font-family: 'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
||||||
direction: ltr;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre;
|
|
||||||
word-spacing: normal;
|
|
||||||
word-break: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
-moz-tab-size: 2;
|
|
||||||
-o-tab-size: 2;
|
|
||||||
tab-size: 2;
|
|
||||||
-webkit-hyphens: none;
|
|
||||||
-moz-hyphens: none;
|
|
||||||
-ms-hyphens: none;
|
|
||||||
hyphens: none;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
td {
|
|
||||||
padding: 8px;
|
|
||||||
border-right: 1px solid var(--classE);
|
|
||||||
border-bottom: 1px solid var(--classE);
|
|
||||||
}
|
|
||||||
thead th {
|
|
||||||
font-weight: 500;
|
|
||||||
background: var(--classC);
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr {
|
|
||||||
transition: background 0.35s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import { deepMerge } from '@/utils/base/deepMerge'
|
|||||||
const defaultConfig: IUniHaloConfig = {
|
const defaultConfig: IUniHaloConfig = {
|
||||||
base: {
|
base: {
|
||||||
domain: import.meta.env.VITE_UNI_HALO_DOMAIN,
|
domain: import.meta.env.VITE_UNI_HALO_DOMAIN,
|
||||||
|
token: import.meta.env.VITE_UNI_HALO_TOKEN,
|
||||||
isIndividual: false,
|
isIndividual: false,
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useRequest } from '@/hooks/useRequest'
|
||||||
import { markdownConfig } from '@/config/markdown/config'
|
import { markdownConfig } from '@/config/markdown/config'
|
||||||
|
import { useUniHaloConfigStore } from '@/store/config'
|
||||||
|
import { queryPostByName, queryPostNavigationByName } from '@/api/halo-base/post'
|
||||||
|
import { upvote } from '@/api/halo-base/trackers'
|
||||||
|
import { completeUrl } from '@/utils/url'
|
||||||
|
import { timeFrom } from '@/utils/base/timeFrom'
|
||||||
|
import { formatNumberUnit } from '@/utils/base/digital'
|
||||||
|
import type { NavigationPostVo, PostV1alpha1PublicApiQueryPostByNameRequest, PostVo } from '@halo-dev/api-client'
|
||||||
|
import type { VoteRequest } from '@/api/halo-base/trackers'
|
||||||
|
import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue'
|
||||||
|
|
||||||
|
interface IPostDetailLoadOptions {
|
||||||
|
metadataName: string
|
||||||
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'PostDetail',
|
name: 'PostDetail',
|
||||||
@@ -8,14 +22,281 @@ defineOptions({
|
|||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '文章详情',
|
navigationBarTitleText: '文章详情',
|
||||||
navigationBarBackgroundColor: '#ffffff',
|
navigationStyle: 'custom',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const uniHaloConfigStore = useUniHaloConfigStore()
|
||||||
|
|
||||||
|
// --- 数据请求 ---
|
||||||
|
const { loading, data: post, run: fetchPost } = useRequest<PostVo, PostV1alpha1PublicApiQueryPostByNameRequest>(queryPostByName, { loadingToast: true, loadingToastContent: '加载中...' })
|
||||||
|
const { data: navigation, run: fetchNavigation } = useRequest<NavigationPostVo, PostV1alpha1PublicApiQueryPostByNameRequest>(queryPostNavigationByName)
|
||||||
|
|
||||||
|
let postName = ''
|
||||||
|
|
||||||
|
// --- 点赞状态 ---
|
||||||
|
const isUpvoted = ref(false)
|
||||||
|
const upvoteCount = ref(0)
|
||||||
|
|
||||||
|
// --- 计算属性 ---
|
||||||
|
const coverUrl = computed(() => {
|
||||||
|
const raw = post.value?.spec?.cover
|
||||||
|
return raw ? completeUrl(raw) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const categories = computed(() => {
|
||||||
|
return post.value?.categories ?? []
|
||||||
|
})
|
||||||
|
|
||||||
|
const tags = computed(() => {
|
||||||
|
return post.value?.tags ?? []
|
||||||
|
})
|
||||||
|
|
||||||
|
const publishTime = computed(() => {
|
||||||
|
return timeFrom(post.value?.spec?.publishTime ?? null)
|
||||||
|
})
|
||||||
|
|
||||||
|
const stats = computed(() => {
|
||||||
|
return {
|
||||||
|
visit: formatNumberUnit(post.value?.stats?.visit ?? 0),
|
||||||
|
comment: formatNumberUnit(post.value?.stats?.comment ?? 0),
|
||||||
|
upvote: formatNumberUnit(upvoteCount.value || (post.value?.stats?.upvote ?? 0)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const htmlContent = computed(() => {
|
||||||
|
return post.value?.content?.content ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const prevPost = computed(() => navigation.value?.previous)
|
||||||
|
const nextPost = computed(() => navigation.value?.next)
|
||||||
|
|
||||||
|
// --- 方法 ---
|
||||||
|
function handleBack() {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpvote() {
|
||||||
|
if (isUpvoted.value)
|
||||||
|
return
|
||||||
|
try {
|
||||||
|
await upvote({
|
||||||
|
group: 'content.halo.run',
|
||||||
|
plural: 'posts',
|
||||||
|
name: postName,
|
||||||
|
} as VoteRequest)
|
||||||
|
isUpvoted.value = true
|
||||||
|
upvoteCount.value = (post.value?.stats?.upvote ?? 0) + 1
|
||||||
|
uni.showToast({ title: '点赞成功', icon: 'none' })
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
uni.showToast({ title: '点赞失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShare() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 微信小程序中由页面右上角菜单处理分享
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: post.value?.status?.permalink ?? '',
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({ title: '链接已复制', icon: 'none' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNavigatePost(name: string) {
|
||||||
|
postName = name
|
||||||
|
fetchPost({ name })
|
||||||
|
fetchNavigation({ name })
|
||||||
|
uni.pageScrollTo({ scrollTop: 0, duration: 300 })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
await fetchPost({ name: postName })
|
||||||
|
fetchNavigation({ name: postName })
|
||||||
|
upvoteCount.value = post.value?.stats?.upvote ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 生命周期 ---
|
||||||
|
onLoad((options: IPostDetailLoadOptions) => {
|
||||||
|
postName = options.metadataName
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
onPullDownRefresh(async () => {
|
||||||
|
await loadData()
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 微信小程序分享
|
||||||
|
onShareAppMessage(() => {
|
||||||
|
return {
|
||||||
|
title: post.value?.spec?.title ?? '文章详情',
|
||||||
|
path: `/subpkg-blog/post-detail/post-detail?metadataName=${postName}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="min-h-screen w-full">
|
<view class="min-h-screen w-full flex flex-col bg-page3">
|
||||||
文章详情页面
|
<!-- 顶部导航栏 -->
|
||||||
|
<view class="fixed left-0 z-100 box-border w-full flex items-center justify-between"
|
||||||
|
:style="{ paddingTop: 'calc(var(--status-bar-height, 44px) ' }">
|
||||||
|
<view class="box-border h-44px w-full flex items-center justify-between px-2">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<view class="h-8 w-9 center rounded-lg bg-gray-900 text-white" @click="handleBack">
|
||||||
|
<wd-icon name="arrow-left" color="#fff" :size="16" />
|
||||||
|
</view>
|
||||||
|
<view v-if="false" class="line-clamp-1 box-border pl-4 pr-6 text-white font-bold">
|
||||||
|
{{ post?.spec?.title ?? '笔记详情' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 顶部封面区域 -->
|
||||||
|
<view class="fixed left-0 right-0 top-0 z-10 h-52 w-full overflow-hidden">
|
||||||
|
<image v-if="coverUrl" class="h-full w-full" mode="aspectFill" :src="coverUrl" />
|
||||||
|
<view v-else class="h-full w-full bg-gray-200" />
|
||||||
|
|
||||||
|
<!-- 渐变遮罩 -->
|
||||||
|
<view class="absolute bottom-0 left-0 right-0 z-10 h-3/5 from-black/0 via-page/50 to-page3 bg-gradient-to-b" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 文章信息卡片(覆盖封面底部) -->
|
||||||
|
<view class="uh-shadow-card relative z-20 box-border w-full flex flex-col gap-y-4 pt-52 -mt-8">
|
||||||
|
<!-- 主信息卡片 -->
|
||||||
|
<view v-if="htmlContent" class="box-border w-full flex flex-col gap-y-3 overflow-hidden rounded-3xl bg-white p-4">
|
||||||
|
<!-- 标题 -->
|
||||||
|
<text class="text-lg text-gray-900 font-bold leading-6">{{ post?.spec?.title ?? '加载中...' }}</text>
|
||||||
|
|
||||||
|
<!-- 分类标签 -->
|
||||||
|
<view v-if="categories.length > 0" class="flex flex-wrap items-center gap-2">
|
||||||
|
<view v-for="cat in categories" :key="cat.metadata.name"
|
||||||
|
class="rounded-full bg-gray-50 px-2.5 py-0.5 text-xs text-gray-600">
|
||||||
|
{{ cat.spec?.displayName }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 元信息 -->
|
||||||
|
<view class="flex items-center gap-x-1">
|
||||||
|
<text class="text-[11px] text-gray-500">{{ publishTime }}</text>
|
||||||
|
<text class="text-[11px] text-gray-400">·</text>
|
||||||
|
<text class="text-[11px] text-gray-500">浏览 {{ stats.visit }}</text>
|
||||||
|
<text class="text-[11px] text-gray-400">·</text>
|
||||||
|
<text class="text-[11px] text-gray-500">评论 {{ stats.comment }}</text>
|
||||||
|
<text class="text-[11px] text-gray-400">·</text>
|
||||||
|
<text class="text-[11px] text-gray-500">点赞 {{ stats.upvote }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分割线 -->
|
||||||
|
<view class="h-1px w-full bg-gray-50" />
|
||||||
|
|
||||||
|
<!-- 作者信息 -->
|
||||||
|
<view v-if="post?.owner" class="flex items-center gap-x-2 hidden!">
|
||||||
|
<image v-if="post.owner.avatar" class="h-8 w-8 border-2 border-white rounded-full border-solid"
|
||||||
|
:src="completeUrl(post.owner.avatar)" mode="aspectFill" />
|
||||||
|
<view class="flex flex-col">
|
||||||
|
<text class="text-xs text-gray-900 font-medium">{{ post.owner.displayName }}</text>
|
||||||
|
<text v-if="post.owner.bio" class="line-clamp-1 text-[11px] text-gray-400">{{ post.owner.bio }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 文章正文 -->
|
||||||
|
<mp-html :content="htmlContent" :tag-style="markdownConfig.tagStyle"
|
||||||
|
:container-style="markdownConfig.containStyle" :domain="markdownConfig.domain" lazy-load selectable />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载中占位 -->
|
||||||
|
<view v-else-if="loading" class="uh-shadow-xs box-border w-full center rounded-2xl bg-white py-16">
|
||||||
|
<wd-loading />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 标签区域 -->
|
||||||
|
<view v-if="tags.length > 0" class="uh-shadow-xs box-border w-full rounded-2xl bg-white p-4">
|
||||||
|
<view class="mb-3 text-sm text-gray-900 font-bold">
|
||||||
|
标签
|
||||||
|
</view>
|
||||||
|
<view class="flex flex-wrap gap-2">
|
||||||
|
<view v-for="tag in tags" :key="tag.metadata.name"
|
||||||
|
class="border border-gray-200 rounded-full px-3 py-1 text-xs text-gray-600">
|
||||||
|
# {{ tag.spec?.displayName }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 互动操作栏 -->
|
||||||
|
<view class="uh-shadow-xs box-border w-full flex items-center justify-around rounded-2xl bg-white p-4">
|
||||||
|
<view class="flex flex-col items-center gap-y-1" @click="handleUpvote">
|
||||||
|
<view class="h-10 w-10 center rounded-full" :class="isUpvoted ? 'bg-red-50' : 'bg-gray-50'">
|
||||||
|
<wd-icon :name="isUpvoted ? 'thumb-up' : 'thumb-up'" :color="isUpvoted ? '#EF4444' : '#6B7280'"
|
||||||
|
:size="20" />
|
||||||
|
</view>
|
||||||
|
<text class="text-[11px]" :class="isUpvoted ? 'text-red-500' : 'text-gray-500'">
|
||||||
|
{{ stats.upvote }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="flex flex-col items-center gap-y-1" @click="handleShare">
|
||||||
|
<view class="h-10 w-10 center rounded-full bg-gray-50">
|
||||||
|
<wd-icon name="share" color="#6B7280" :size="20" />
|
||||||
|
</view>
|
||||||
|
<text class="text-[11px] text-gray-500">分享</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 上一篇/下一篇导航 -->
|
||||||
|
<view v-if="prevPost || nextPost"
|
||||||
|
class="uh-shadow-xs box-border w-full flex flex-col gap-y-2 rounded-2xl bg-white p-4">
|
||||||
|
<view class="mb-1 text-sm text-gray-900 font-bold">
|
||||||
|
文章导航
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 上一篇 -->
|
||||||
|
<view v-if="prevPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3"
|
||||||
|
@click="handleNavigatePost(prevPost.metadata.name)">
|
||||||
|
<view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg">
|
||||||
|
<image class="h-full w-full" mode="aspectFill" :src="completeUrl(prevPost.spec?.cover ?? '')" />
|
||||||
|
</view>
|
||||||
|
<view class="flex flex-1 flex-col gap-y-0.5 overflow-hidden">
|
||||||
|
<text class="text-[11px] text-gray-400">上一篇</text>
|
||||||
|
<text class="line-clamp-1 text-xs text-gray-900 font-medium">{{ prevPost.spec?.title }}</text>
|
||||||
|
</view>
|
||||||
|
<wd-icon name="arrow-right" color="#9CA3AF" :size="16" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 下一篇 -->
|
||||||
|
<view v-if="nextPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3"
|
||||||
|
@click="handleNavigatePost(nextPost.metadata.name)">
|
||||||
|
<view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg">
|
||||||
|
<image class="h-full w-full" mode="aspectFill" :src="completeUrl(nextPost.spec?.cover ?? '')" />
|
||||||
|
</view>
|
||||||
|
<view class="flex flex-1 flex-col gap-y-0.5 overflow-hidden">
|
||||||
|
<text class="text-[11px] text-gray-400">下一篇</text>
|
||||||
|
<text class="line-clamp-1 text-xs text-gray-900 font-medium">{{ nextPost.spec?.title }}</text>
|
||||||
|
</view>
|
||||||
|
<wd-icon name="arrow-right" color="#9CA3AF" :size="16" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部安全区占位 -->
|
||||||
|
<view class="pb-safe">
|
||||||
|
<view class="h-4 w-full" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.uh-shadow-xs {
|
||||||
|
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uh-shadow-card {
|
||||||
|
box-shadow: 0 16rpx 60rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user