From 067f3ed98ae3bb758f7c532a2ad33a2191939bae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=8F=E8=8E=AB=E5=94=90=E5=B0=BC?=
Date: Mon, 31 Aug 2026 19:56:16 +0800
Subject: [PATCH] =?UTF-8?q?chore:=20=E6=89=B9=E9=87=8F=E6=96=B0=E5=A2=9E?=
=?UTF-8?q?=E9=A1=B9=E7=9B=AE=E4=BE=9D=E8=B5=96=E3=80=81=E5=B7=A5=E5=85=B7?=
=?UTF-8?q?=E5=87=BD=E6=95=B0=E3=80=81=E9=A1=B5=E9=9D=A2=E4=B8=8E=E7=BB=84?=
=?UTF-8?q?=E4=BB=B6=E8=B5=84=E6=BA=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 新增mp-html、qs等生产依赖,补全项目基础库
2. 新增平台判断、缓存、工具函数等通用工具集
3. 新增标签页、网站浏览页、关于页等业务页面
4. 新增分类卡片、通知弹窗等业务组件
5. 新增uts-progressNotification、liu-poster、uhalo-upgrade等uni模块
6. 补充audio/video组件样式补件,修复uni-components路径缺失问题
7. 新增环境变量Halo个人令牌配置项
8. 重构store导出结构,新增appConfig/halo/setting三个状态模块
9. 新增tsconfig编译目标配置,适配更高版本ES语法
---
env/.env | 13 +-
env/.env.development | 3 +
env/.env.production | 3 +
env/.env.test | 3 +
eslint.config.mjs | 13 +
package.json | 2 +
pages.config.ts | 6 +-
pnpm-lock.yaml | 19 +
src/api/halo.ts | 299 ++++
src/api/types/halo.ts | 432 ++++++
src/api/types/uni-halo.ts | 313 ++++
src/api/uni-halo.ts | 360 +++++
.../components/mp-html/editable/config.js | 11 +
.../components/mp-html/editable/index.js | 532 +++++++
.../mp-html/components/mp-html/emoji/index.js | 203 +++
.../components/mp-html/highlight/config.js | 5 +
.../components/mp-html/highlight/index.js | 96 ++
.../mp-html/highlight/prism.min.d.ts | 7 +
.../components/mp-html/highlight/prism.min.js | 7 +
.../components/mp-html/img-cache/index.js | 138 ++
.../components/mp-html/markdown/index.js | 34 +
.../mp-html/markdown/marked.min.d.ts | 7 +
.../components/mp-html/markdown/marked.min.js | 6 +
.../mp-html/components/mp-html/mp-html.vue | 579 +++++++
.../mp-html/components/mp-html/node/node.vue | 1119 ++++++++++++++
.../mp-html/components/mp-html/parser.js | 1342 +++++++++++++++++
.../mp-html/components/mp-html/style/index.js | 129 ++
.../components/mp-html/style/parser.js | 175 +++
.../static/app-plus/mp-html/js/handler.js | 1 +
.../app-plus/mp-html/js/uni.webview.min.js | 1 +
.../static/app-plus/mp-html/local.html | 1 +
.../uh-album-photo-viewer.vue | 166 ++
.../uh-album-unlock-modal.vue | 107 ++
.../uh-article-card/uh-article-card.vue | 339 +++++
.../uh-article-min-card.vue | 118 ++
.../uh-audio-player/uh-audio-player.vue | 116 ++
.../uh-category-mini-card.vue | 54 +
.../uh-comment-item/uh-comment-item.vue | 157 ++
.../uh-comment-list/uh-comment-list.vue | 206 +++
.../uh-comment-modal/uh-comment-modal.vue | 321 ++++
src/components/uh-heatmap/uh-heatmap.vue | 189 +++
.../uh-notify-dialog/uh-notify-dialog.vue | 85 ++
.../uh-plugin-unavailable.vue | 135 ++
.../uh-restrict-read-skeleton.vue | 157 ++
src/components/uh-swiper/uh-swiper.vue | 359 +++++
src/components/uh-vote-card/uh-vote-card.vue | 145 ++
src/config/appConfig.ts | 55 +
src/config/appSettings.ts | 91 ++
src/config/haloGlobal.ts | 14 +
src/config/markdown.ts | 102 ++
src/http/alova.ts | 207 +--
src/http/interceptor.ts | 126 +-
src/http/tools/categoryPassword.ts | 97 ++
src/http/tools/commentCookies.ts | 32 +
src/http/tools/enum.ts | 10 +
src/locale/en.json | 16 +
src/locale/zh-Hans.json | 16 +
src/pages-blog/about/about.vue | 65 +
src/pages-blog/archives/archives.vue | 380 +++++
.../article-detail/article-detail.vue | 716 +++++++++
src/pages-blog/articles/articles.vue | 211 +++
.../category-detail/category-detail.vue | 142 ++
src/pages-blog/contact/contact.vue | 122 ++
src/pages-blog/data-visual/data-visual.vue | 283 ++++
src/pages-blog/disclaimers/disclaimers.vue | 82 +
src/pages-blog/friend-links/friend-links.vue | 274 ++++
src/pages-blog/love/album.vue | 273 ++++
src/pages-blog/love/journey.vue | 281 ++++
src/pages-blog/love/list.vue | 212 +++
src/pages-blog/love/love.vue | 355 +++++
.../moment-detail/moment-detail.vue | 299 ++++
src/pages-blog/setting/setting.vue | 263 ++++
src/pages-blog/submit-link/submit-link.vue | 232 +++
src/pages-blog/tag-detail/tag-detail.vue | 132 ++
src/pages-blog/tags/tags.vue | 22 +
src/pages-blog/vote-detail/vote-detail.vue | 416 +++++
src/pages-blog/votes/votes.vue | 159 ++
src/pages-blog/website/website.vue | 45 +
src/pages/index/index.vue | 161 +-
src/pages/start/start.vue | 299 ++++
src/pages/tabbar/about/about.vue | 325 +++-
src/pages/tabbar/category/category.vue | 367 ++++-
src/pages/tabbar/gallery/gallery.vue | 268 +++-
src/pages/tabbar/home/home.vue | 552 ++++++-
src/pages/tabbar/moments/moments.vue | 370 ++++-
src/static/wave/wave-1.png | Bin 0 -> 4147 bytes
src/static/wave/wave-2.png | Bin 0 -> 5203 bytes
src/store/appConfig.ts | 86 ++
src/store/halo.ts | 48 +
src/store/index.ts | 3 +
src/store/setting.ts | 35 +
src/store/token.ts | 13 +
src/style/uni-components/audio.css | 12 +
src/style/uni-components/video.css | 6 +
src/tabbar/TabbarItem.test.ts | 2 +-
src/tabbar/config.ts | 256 ++--
src/uni_modules/liu-poster/changelog.md | 22 +
.../components/liu-poster/liu-poster.vue | 377 +++++
src/uni_modules/liu-poster/license.md | 6 +
src/uni_modules/liu-poster/package.json | 100 ++
src/uni_modules/liu-poster/readme.md | 260 ++++
src/uni_modules/uhalo-upgrade/changelog.md | 141 ++
src/uni_modules/uhalo-upgrade/package.json | 110 ++
.../uhalo-upgrade/pages/upgrade-popup.vue | 679 +++++++++
src/uni_modules/uhalo-upgrade/readme.md | 98 ++
.../static/app/app_update_close.png | Bin 0 -> 7644 bytes
.../uhalo-upgrade/static/app/bg_top.png | Bin 0 -> 30486 bytes
.../uhalo-upgrade/utils/call-check-version.ts | 121 ++
.../uhalo-upgrade/utils/check-update-nvue.js | 180 +++
.../uhalo-upgrade/utils/check-update.ts | 185 +++
src/uni_modules/uhalo-upgrade/utils/utils.ts | 46 +
.../uts-progressNotification/changelog.md | 28 +
.../uts-progressNotification/package.json | 85 ++
.../uts-progressNotification/readme.md | 71 +
.../utssdk/app-android/AndroidManifest.xml | 11 +
.../app-android/TransparentActivity.uts | 62 +
.../utssdk/app-android/callbacks.uts | 19 +
.../utssdk/app-android/config.json | 3 +
.../utssdk/app-android/constant.uts | 2 +
.../utssdk/app-android/index.uts | 156 ++
.../values/notification_progress_styles.xml | 11 +
.../utssdk/interface.uts | 46 +
.../utssdk/unierror.uts | 0
src/utils/auth.ts | 37 +
src/utils/checkUpdate.ts | 148 ++
src/utils/cookies.ts | 34 +
src/utils/formatTime.ts | 67 +
src/utils/imageCache.ts | 53 +
src/utils/index.ts | 8 +-
src/utils/json.ts | 28 +
src/utils/merge.ts | 55 +
src/utils/network.ts | 40 +
src/utils/page.ts | 55 +
src/utils/platform.ts | 13 +
src/utils/plugin.ts | 133 ++
src/utils/random.ts | 20 +
src/utils/restrictRead.ts | 135 ++
src/utils/showModal.ts | 101 ++
src/utils/storage.ts | 55 +
src/utils/throttle.ts | 26 +
src/utils/upvote.ts | 45 +
src/utils/url.ts | 89 ++
src/utils/urlParams.ts | 55 +
src/utils/uuid.ts | 20 +
src/utils/vote.ts | 102 ++
tsconfig.json | 1 +
vite.config.ts | 6 +-
147 files changed, 20866 insertions(+), 325 deletions(-)
create mode 100644 src/api/halo.ts
create mode 100644 src/api/types/halo.ts
create mode 100644 src/api/types/uni-halo.ts
create mode 100644 src/api/uni-halo.ts
create mode 100644 src/components/mp-html/components/mp-html/editable/config.js
create mode 100644 src/components/mp-html/components/mp-html/editable/index.js
create mode 100644 src/components/mp-html/components/mp-html/emoji/index.js
create mode 100644 src/components/mp-html/components/mp-html/highlight/config.js
create mode 100644 src/components/mp-html/components/mp-html/highlight/index.js
create mode 100644 src/components/mp-html/components/mp-html/highlight/prism.min.d.ts
create mode 100644 src/components/mp-html/components/mp-html/highlight/prism.min.js
create mode 100644 src/components/mp-html/components/mp-html/img-cache/index.js
create mode 100644 src/components/mp-html/components/mp-html/markdown/index.js
create mode 100644 src/components/mp-html/components/mp-html/markdown/marked.min.d.ts
create mode 100644 src/components/mp-html/components/mp-html/markdown/marked.min.js
create mode 100644 src/components/mp-html/components/mp-html/mp-html.vue
create mode 100644 src/components/mp-html/components/mp-html/node/node.vue
create mode 100644 src/components/mp-html/components/mp-html/parser.js
create mode 100644 src/components/mp-html/components/mp-html/style/index.js
create mode 100644 src/components/mp-html/components/mp-html/style/parser.js
create mode 100644 src/components/mp-html/static/app-plus/mp-html/js/handler.js
create mode 100644 src/components/mp-html/static/app-plus/mp-html/js/uni.webview.min.js
create mode 100644 src/components/mp-html/static/app-plus/mp-html/local.html
create mode 100644 src/components/uh-album-photo-viewer/uh-album-photo-viewer.vue
create mode 100644 src/components/uh-album-unlock-modal/uh-album-unlock-modal.vue
create mode 100644 src/components/uh-article-card/uh-article-card.vue
create mode 100644 src/components/uh-article-min-card/uh-article-min-card.vue
create mode 100644 src/components/uh-audio-player/uh-audio-player.vue
create mode 100644 src/components/uh-category-mini-card/uh-category-mini-card.vue
create mode 100644 src/components/uh-comment-item/uh-comment-item.vue
create mode 100644 src/components/uh-comment-list/uh-comment-list.vue
create mode 100644 src/components/uh-comment-modal/uh-comment-modal.vue
create mode 100644 src/components/uh-heatmap/uh-heatmap.vue
create mode 100644 src/components/uh-notify-dialog/uh-notify-dialog.vue
create mode 100644 src/components/uh-plugin-unavailable/uh-plugin-unavailable.vue
create mode 100644 src/components/uh-restrict-read-skeleton/uh-restrict-read-skeleton.vue
create mode 100644 src/components/uh-swiper/uh-swiper.vue
create mode 100644 src/components/uh-vote-card/uh-vote-card.vue
create mode 100644 src/config/appConfig.ts
create mode 100644 src/config/appSettings.ts
create mode 100644 src/config/haloGlobal.ts
create mode 100644 src/config/markdown.ts
create mode 100644 src/http/tools/categoryPassword.ts
create mode 100644 src/http/tools/commentCookies.ts
create mode 100644 src/pages-blog/about/about.vue
create mode 100644 src/pages-blog/archives/archives.vue
create mode 100644 src/pages-blog/article-detail/article-detail.vue
create mode 100644 src/pages-blog/articles/articles.vue
create mode 100644 src/pages-blog/category-detail/category-detail.vue
create mode 100644 src/pages-blog/contact/contact.vue
create mode 100644 src/pages-blog/data-visual/data-visual.vue
create mode 100644 src/pages-blog/disclaimers/disclaimers.vue
create mode 100644 src/pages-blog/friend-links/friend-links.vue
create mode 100644 src/pages-blog/love/album.vue
create mode 100644 src/pages-blog/love/journey.vue
create mode 100644 src/pages-blog/love/list.vue
create mode 100644 src/pages-blog/love/love.vue
create mode 100644 src/pages-blog/moment-detail/moment-detail.vue
create mode 100644 src/pages-blog/setting/setting.vue
create mode 100644 src/pages-blog/submit-link/submit-link.vue
create mode 100644 src/pages-blog/tag-detail/tag-detail.vue
create mode 100644 src/pages-blog/tags/tags.vue
create mode 100644 src/pages-blog/vote-detail/vote-detail.vue
create mode 100644 src/pages-blog/votes/votes.vue
create mode 100644 src/pages-blog/website/website.vue
create mode 100644 src/pages/start/start.vue
create mode 100644 src/static/wave/wave-1.png
create mode 100644 src/static/wave/wave-2.png
create mode 100644 src/store/appConfig.ts
create mode 100644 src/store/halo.ts
create mode 100644 src/store/setting.ts
create mode 100644 src/style/uni-components/audio.css
create mode 100644 src/style/uni-components/video.css
create mode 100644 src/uni_modules/liu-poster/changelog.md
create mode 100644 src/uni_modules/liu-poster/components/liu-poster/liu-poster.vue
create mode 100644 src/uni_modules/liu-poster/license.md
create mode 100644 src/uni_modules/liu-poster/package.json
create mode 100644 src/uni_modules/liu-poster/readme.md
create mode 100644 src/uni_modules/uhalo-upgrade/changelog.md
create mode 100644 src/uni_modules/uhalo-upgrade/package.json
create mode 100644 src/uni_modules/uhalo-upgrade/pages/upgrade-popup.vue
create mode 100644 src/uni_modules/uhalo-upgrade/readme.md
create mode 100644 src/uni_modules/uhalo-upgrade/static/app/app_update_close.png
create mode 100644 src/uni_modules/uhalo-upgrade/static/app/bg_top.png
create mode 100644 src/uni_modules/uhalo-upgrade/utils/call-check-version.ts
create mode 100644 src/uni_modules/uhalo-upgrade/utils/check-update-nvue.js
create mode 100644 src/uni_modules/uhalo-upgrade/utils/check-update.ts
create mode 100644 src/uni_modules/uhalo-upgrade/utils/utils.ts
create mode 100644 src/uni_modules/uts-progressNotification/changelog.md
create mode 100644 src/uni_modules/uts-progressNotification/package.json
create mode 100644 src/uni_modules/uts-progressNotification/readme.md
create mode 100644 src/uni_modules/uts-progressNotification/utssdk/app-android/AndroidManifest.xml
create mode 100644 src/uni_modules/uts-progressNotification/utssdk/app-android/TransparentActivity.uts
create mode 100644 src/uni_modules/uts-progressNotification/utssdk/app-android/callbacks.uts
create mode 100644 src/uni_modules/uts-progressNotification/utssdk/app-android/config.json
create mode 100644 src/uni_modules/uts-progressNotification/utssdk/app-android/constant.uts
create mode 100644 src/uni_modules/uts-progressNotification/utssdk/app-android/index.uts
create mode 100644 src/uni_modules/uts-progressNotification/utssdk/app-android/res/values/notification_progress_styles.xml
create mode 100644 src/uni_modules/uts-progressNotification/utssdk/interface.uts
create mode 100644 src/uni_modules/uts-progressNotification/utssdk/unierror.uts
create mode 100644 src/utils/auth.ts
create mode 100644 src/utils/checkUpdate.ts
create mode 100644 src/utils/cookies.ts
create mode 100644 src/utils/formatTime.ts
create mode 100644 src/utils/imageCache.ts
create mode 100644 src/utils/json.ts
create mode 100644 src/utils/merge.ts
create mode 100644 src/utils/network.ts
create mode 100644 src/utils/page.ts
create mode 100644 src/utils/platform.ts
create mode 100644 src/utils/plugin.ts
create mode 100644 src/utils/random.ts
create mode 100644 src/utils/restrictRead.ts
create mode 100644 src/utils/showModal.ts
create mode 100644 src/utils/storage.ts
create mode 100644 src/utils/throttle.ts
create mode 100644 src/utils/upvote.ts
create mode 100644 src/utils/url.ts
create mode 100644 src/utils/urlParams.ts
create mode 100644 src/utils/uuid.ts
create mode 100644 src/utils/vote.ts
diff --git a/env/.env b/env/.env
index 0f070f9..5637574 100644
--- a/env/.env
+++ b/env/.env
@@ -1,8 +1,8 @@
-VITE_APP_TITLE = 'unibest'
-VITE_APP_PORT = 9000
+VITE_APP_TITLE = 'uni-halo'
+VITE_APP_PORT = 9520
-VITE_UNI_APPID = '__UNI__D1E5001'
-VITE_WX_APPID = 'wxa2abb91f64032a2b'
+VITE_UNI_APPID = '__UNI__5748B6E'
+VITE_WX_APPID = 'wx1bb161867082c72f'
# 微信开发者工具 CLI 路径,仅当默认安装路径不正确时配置(就是当 pnpm dev:mp 无法自动打开微信开发者工具时,才需要配置,通常是你更改了默认的安装位置导致的,一般出现在windows系统)
# macOS 示例:
@@ -12,9 +12,12 @@ VITE_WX_APPID = 'wxa2abb91f64032a2b'
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
-# 比如你要部署到 https://unibest.tech/doc/ ,则配置为 /doc/
+# 比如你要部署到 https://uni-halo.925i.cn/uni-halo/ ,则配置为 /uni-halo/
VITE_APP_PUBLIC_BASE=/
+# 在halo个人中心生成的 token
+VITE_APP_HALO_TOKEN = ''
+
# 默认后台请求地址
# 不同命令会按 mode 叠加读取 .env.development / .env.test / .env.production。
# 微信小程序如果没有配置下面的专用地址,也会回退使用这个值。
diff --git a/env/.env.development b/env/.env.development
index 1c4f073..866b6e6 100644
--- a/env/.env.development
+++ b/env/.env.development
@@ -5,5 +5,8 @@ VITE_DELETE_CONSOLE = false
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = false
+# 在halo个人中心生成的 token
+VITE_APP_HALO_TOKEN = ''
+
# development mode 后台请求地址
VITE_SERVER_BASEURL = 'http://localhost:8090'
diff --git a/env/.env.production b/env/.env.production
index 1bfe46f..2a16aea 100644
--- a/env/.env.production
+++ b/env/.env.production
@@ -5,5 +5,8 @@ VITE_DELETE_CONSOLE = true
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = false
+# 在halo个人中心生成的 token
+VITE_APP_HALO_TOKEN = ''
+
# production mode 后台请求地址
# VITE_SERVER_BASEURL = 'http://localhost:8090
diff --git a/env/.env.test b/env/.env.test
index 42ac437..f249a6b 100644
--- a/env/.env.test
+++ b/env/.env.test
@@ -5,5 +5,8 @@ VITE_DELETE_CONSOLE = false
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = false
+# 在halo个人中心生成的 token
+VITE_APP_HALO_TOKEN = ''
+
# test mode 后台请求地址
# VITE_SERVER_BASEURL = 'http://localhost:8090'
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 426985a..fedfbfb 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -10,6 +10,17 @@ export default uniHelper({
// 忽略原生插件目录
'**/nativeplugins/',
'dist',
+ // AI skills 示例文件(非项目代码,避免 lint --fix 误改)
+ '.agents/',
+ // 环境变量与脚本目录(非 lint 目标)
+ 'env/',
+ 'scripts/',
+ // 第三方图表组件(从旧项目带入,保持原样)
+ 'src/components/qiun-*/',
+ // mp-html 富文本组件(第三方,从旧项目带入,保持原样)
+ 'src/components/mp-html/',
+ // unibest 演示页面与数据
+ 'src/pages-demo/',
// unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
'auto-import.d.ts',
// vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
@@ -32,6 +43,8 @@ export default uniHelper({
'jsdoc/require-returns-description': 'off',
'ts/no-empty-object-type': 'off',
'no-extend-native': 'off',
+ // uni-app 中 view 组件用 v-html 渲染服务端富文本(评论/免责声明/故事)是常规需求,关闭该规则
+ 'vue/no-v-text-v-html-on-component': 'off',
// uni 条件编译注释可能包裹 import,自动排序会破坏平台条件边界
'perfectionist/sort-imports': 'off',
'vue/singleline-html-element-content-newline': [
diff --git a/package.json b/package.json
index 924c5da..4d161cd 100644
--- a/package.json
+++ b/package.json
@@ -141,8 +141,10 @@
"abortcontroller-polyfill": "^1.7.8",
"alova": "^3.3.3",
"dayjs": "1.11.10",
+ "mp-html": "^2.5.2",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
+ "qs": "6.12.1",
"vue": "^3.4.21",
"vue-i18n": "9.1.9",
"vue-router": "4.5.1",
diff --git a/pages.config.ts b/pages.config.ts
index 620d486..65a290e 100644
--- a/pages.config.ts
+++ b/pages.config.ts
@@ -13,17 +13,19 @@ export default defineUniPages({
autoscan: true,
custom: {
'^fg-(.*)': '@/components/fg-$1/fg-$1.vue',
+ '^uh-(.*)': '@/components/uh-$1/uh-$1.vue',
'^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
'z-paging/components/z-paging$1/z-paging$1.vue',
'^wd-(.*)': '@wot-ui/ui/components/wd-$1/wd-$1.vue',
-},
+ '^mp-html(.*)': 'mp-html/dist/uni-app/components/mp-html/mp-html$1.vue',
+ },
},
// tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
// pages-blog 是博客页面,对应旧版项目的 pagesA 子包
subPackages: [{
root: 'pages-demo',
pages: [],
- },{
+ }, {
root: 'pages-blog',
pages: [],
}],
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 693964f..3fd664d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -84,12 +84,18 @@ importers:
dayjs:
specifier: 1.11.10
version: 1.11.10
+ mp-html:
+ specifier: ^2.5.2
+ version: 2.5.2
pinia:
specifier: 2.0.36
version: 2.0.36(typescript@5.8.3)(vue@3.4.21(typescript@5.8.3))
pinia-plugin-persistedstate:
specifier: 3.2.1
version: 3.2.1(pinia@2.0.36(typescript@5.8.3)(vue@3.4.21(typescript@5.8.3)))
+ qs:
+ specifier: 6.12.1
+ version: 6.12.1
vue:
specifier: ^3.4.21
version: 3.4.21(typescript@5.8.3)
@@ -7321,6 +7327,9 @@ packages:
moment@2.30.1:
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
+ mp-html@2.5.2:
+ resolution: {integrity: sha512-45e8c32Qgux4YU4iC3qCSFsOh3y+RwPwZ+iz/vvLkDgSGWk+1zsL4WUzWWQc9w3AsAfkaD/QR0oIufIDngBmXA==}
+
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@@ -8189,6 +8198,10 @@ packages:
resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==}
hasBin: true
+ qs@6.12.1:
+ resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==}
+ engines: {node: '>=0.6'}
+
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
@@ -19774,6 +19787,8 @@ snapshots:
moment@2.30.1: {}
+ mp-html@2.5.2: {}
+
mri@1.2.0: {}
mrmime@2.0.1: {}
@@ -20602,6 +20617,10 @@ snapshots:
qrcode-terminal@0.12.0: {}
+ qs@6.12.1:
+ dependencies:
+ side-channel: 1.1.0
+
qs@6.13.0:
dependencies:
side-channel: 1.1.0
diff --git a/src/api/halo.ts b/src/api/halo.ts
new file mode 100644
index 0000000..d2f9d65
--- /dev/null
+++ b/src/api/halo.ts
@@ -0,0 +1,299 @@
+/**
+ * Halo 官方 API 接口定义
+ * 覆盖官方扩展点:api.content.halo.run / api.halo.run / api.moment.halo.run /
+ * api.photo.halo.run / api.link.halo.run / api.plugin.halo.run
+ * 风格参考 src/api/foo-alova.ts:http.Get>(url, { params, header, meta })
+ * 源自旧项目 api/v2/all.api.js(官方部分),按需命名导出
+ */
+import { http } from '@/http/alova';
+import { RequestFrom } from '@/http/tools/enum';
+import type { IResponse } from '@/http/types';
+import { getCache } from '@/utils/storage';
+import { getNologinEmail, getOpenid } from '@/utils/auth';
+import type {
+ IBlogStats,
+ ICategory,
+ ICategoryListReq,
+ ICategoryListRes,
+ IComment,
+ ICommentListReq,
+ ICommentListRes,
+ ILink,
+ ILinkGroupListRes,
+ ILinkListRes,
+ IMoment,
+ IMomentListReq,
+ IMomentListRes,
+ IPhotoGroupListReq,
+ IPhotoGroupListRes,
+ IPhotoListReq,
+ IPhotoListRes,
+ IPluginAvailable,
+ IPost,
+ IPostListReq,
+ IPostListRes,
+ ISearchReq,
+ ISearchRes,
+ ITagListRes,
+ ITrackerCounterReq,
+ IUpvoteReq
+} from './types/halo';
+
+/** 评论验证码 cookie key */
+const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha';
+
+/* ==================== 文章 ==================== */
+
+/**
+ * 文章列表
+ */
+export function getPostList(params: IPostListReq) {
+ return http.Get>('/apis/api.content.halo.run/v1alpha1/posts', {
+ query: params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 文章详情(带访客标识头)
+ */
+export function getPostByName(name: string) {
+ return http.Get>(`/apis/api.content.halo.run/v1alpha1/posts/${name}`, {
+ headers: {
+ 'Wechat-Session-Id': getOpenid(),
+ 'nologin-email': getNologinEmail()
+ },
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 关键词搜索文章
+ */
+export function getPostListByKeyword(params: ISearchReq) {
+ return http.Post>('/apis/api.halo.run/v1alpha1/indices/-/search', params, {
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/* ==================== 分类 / 标签 ==================== */
+
+/**
+ * 分类列表
+ * 注:旧代码用 qs.stringify 特殊序列化(arrayFormat:'repeat'、allowDots),
+ * alova 的 params 对数组默认即 repeat 形式(a=1&a=2),已等价;如遇嵌套对象场景再单独处理
+ */
+export function getCategoryList(params: ICategoryListReq) {
+ return http.Get>('/apis/api.content.halo.run/v1alpha1/categories', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 分类下文章列表
+ */
+export function getCategoryPostList(name: string, params: IPostListReq) {
+ return http.Get>(`/apis/api.content.halo.run/v1alpha1/categories/${name}/posts`, {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 标签列表
+ */
+export function getTagList(params: ICategoryListReq) {
+ return http.Get>('/apis/api.content.halo.run/v1alpha1/tags', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 标签下文章列表
+ */
+export function getPostByTagName(tagName: string, params: IPostListReq) {
+ return http.Get>(`/apis/api.content.halo.run/v1alpha1/tags/${tagName}/posts`, {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/* ==================== 评论(含验证码 cookie 链路) ==================== */
+
+/**
+ * 评论列表
+ */
+export function getPostCommentList(params: ICommentListReq) {
+ return http.Get>('/apis/api.halo.run/v1alpha1/comments', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 评论回复列表
+ */
+export function getPostCommentReplyList(commentName: string, params: ICommentListReq) {
+ return http.Get>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/** 新增评论(带验证码,captchaCode 转入请求头) */
+export interface IAddCommentReq {
+ raw: string;
+ content?: string;
+ owner?: Record;
+ /** 评论目标引用(subjectRef: group/kind/name/version) */
+ subjectRef?: {
+ group: string;
+ kind: string;
+ name: string;
+ version?: string;
+ };
+ /** 验证码,提交时转入 X-Captcha-Code 头 */
+ captchaCode?: string;
+}
+
+/**
+ * 新增评论(captchaCode 拆出转 X-Captcha-Code 头 + Cookie)
+ */
+export function addPostComment(data: IAddCommentReq) {
+ const { captchaCode, ...rest } = data;
+ const headers: Record = {
+ Accept: 'application/json'
+ };
+ if (captchaCode) headers['X-Captcha-Code'] = captchaCode;
+ const cookie = getCache(COMMENT_WIDGET_CAPTCHA_COOKIES);
+ if (cookie) headers.Cookie = cookie;
+ return http.Post>('/apis/api.halo.run/v1alpha1/comments', rest, {
+ headers,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 新增评论回复(同上,验证码逻辑)
+ */
+export function addPostCommentReply(commentName: string, data: IAddCommentReq) {
+ const { captchaCode, ...rest } = data;
+ const headers: Record = {
+ Accept: 'application/json'
+ };
+ if (captchaCode) headers['X-Captcha-Code'] = captchaCode;
+ const cookie = getCache(COMMENT_WIDGET_CAPTCHA_COOKIES);
+ if (cookie) headers.Cookie = cookie;
+ return http.Post>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, rest, {
+ headers,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/* ==================== 瞬间 ==================== */
+
+/**
+ * 瞬间列表
+ */
+export function getMomentList(params: IMomentListReq) {
+ return http.Get>('/apis/api.moment.halo.run/v1alpha1/moments', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 瞬间详情
+ */
+export function getMomentByName(name: string) {
+ return http.Get>(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/* ==================== 图库 ==================== */
+
+/**
+ * 相册分组列表
+ */
+export function getPhotoGroupList(params: IPhotoGroupListReq) {
+ return http.Get>('/apis/api.photo.halo.run/v1alpha1/photogroups', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 照片列表(按相册)
+ */
+export function getPhotoListByGroupName(params: IPhotoListReq) {
+ return http.Get>('/apis/api.photo.halo.run/v1alpha1/photos', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/* ==================== 友链 ==================== */
+
+/**
+ * 友链分组列表
+ */
+export function getFriendLinkGroupList(params: ICategoryListReq) {
+ return http.Get>('/apis/api.link.halo.run/v1alpha1/linkgroups', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 友链列表
+ */
+export function getFriendLinkList(params: ICategoryListReq) {
+ return http.Get>('/apis/api.link.halo.run/v1alpha1/links', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/* ==================== 统计 / 埋点 / 插件 ==================== */
+
+/**
+ * 博客统计信息
+ */
+export function getBlogStatistics() {
+ return http.Get>('/apis/api.halo.run/v1alpha1/stats/-', {
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 提交点赞
+ */
+export function submitUpvote(data: IUpvoteReq) {
+ return http.Post>('/apis/api.halo.run/v1alpha1/trackers/upvote', data, {
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 提交计数埋点
+ */
+export function postTrackersCounter(data: ITrackerCounterReq) {
+ return http.Post>('/apis/api.halo.run/v1alpha1/trackers/counter', data, {
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/**
+ * 检查插件是否可用
+ */
+export function checkPluginAvailable(name: string) {
+ return http.Get>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${name}/available`, {
+ meta: { requestFrom: RequestFrom.Halo }
+ });
+}
+
+/** 分类资源(供加密分类判断等场景) */
+export type { ICategory, ILink, IMoment, IPost };
diff --git a/src/api/types/halo.ts b/src/api/types/halo.ts
new file mode 100644
index 0000000..4fbab4f
--- /dev/null
+++ b/src/api/types/halo.ts
@@ -0,0 +1,432 @@
+/**
+ * Halo 官方 API 类型定义
+ * 对应 src/api/halo.ts(Halo 官方 api.*.halo.run 扩展点)
+ * 结构基于 Halo 2.x API 规范
+ */
+
+/** Halo 通用元数据 */
+export interface IMetadata {
+ name: string
+ labels?: Record
+ annotations?: Record
+ version?: number
+ creationTimestamp?: string
+ updateTimestamp?: string
+}
+
+/** Halo 通用分页结构(ListResult) */
+export interface IListResult {
+ page: number
+ size: number
+ total: number
+ items: T[]
+ first: boolean
+ last: boolean
+ hasNext: boolean
+ hasPrevious: boolean
+ totalPages: number
+}
+
+/* ---------- 文章 Post ---------- */
+
+export interface IPostSpec {
+ title: string
+ slug: string
+ excerpt?: string
+ cover?: string
+ /** 发布时间(Halo 2.x 结构,旧项目直接使用) */
+ publishTime?: string
+ deleted: boolean
+ publish: boolean
+ pinned: boolean
+ allowComment: boolean
+ visible: 'PUBLIC' | 'PRIVATE'
+ priority: number
+ categories: string[]
+ tags: string[]
+ template?: string
+ htmlMetas?: Record
+ baseSnapshot?: string
+ headSnapshot?: string
+ snapshotVersion?: number
+ excerptAutoGenerate?: boolean
+}
+
+export interface IPostStatus {
+ permalink: string
+ inProgress: boolean
+ commentsCount?: number
+ contributors?: IContributor[]
+ stats?: IPostStats
+ excerpt?: string
+}
+
+export interface IContributor {
+ name: string
+ displayName: string
+ avatar: string
+}
+
+export interface IPostStats {
+ visits?: number
+ upvotes?: number
+ comments?: number
+}
+
+/** 文章内容(含受限阅读的 raw 源文本) */
+export interface IContent {
+ raw: string
+ content: string
+ displayVersion?: string
+}
+
+export interface IPost {
+ metadata: IMetadata
+ spec: IPostSpec
+ status?: IPostStatus
+ content?: IContent
+ categories?: ICategory[]
+ tags?: ITag[]
+ contributors?: IContributor[]
+ /** 旧项目使用的顶层统计(兼容 Halo 旧版结构) */
+ stats?: {
+ visit?: number
+ upvote?: number
+ comment?: number
+ [key: string]: unknown
+ }
+}
+
+/** 文章列表请求参数(分页 + 过滤) */
+export interface IPostListReq {
+ page?: number
+ size?: number
+ sort?: string[]
+ keyword?: string
+ category?: string
+ tag?: string
+ visible?: 'PUBLIC' | 'PRIVATE'
+ [key: string]: unknown
+}
+
+export type IPostListRes = IListResult
+
+/** 文章搜索请求参数(关键字) */
+export interface ISearchReq {
+ keyword?: string
+ page?: number
+ size?: number
+ highlightPreTag?: string
+ highlightPostTag?: string
+}
+
+export type ISearchRes = IListResult
+
+/* ---------- 分类 Category ---------- */
+
+export interface ICategory {
+ metadata: IMetadata
+ spec: {
+ displayName: string
+ slug: string
+ description?: string
+ cover?: string
+ template?: string
+ priority: number
+ children?: string[]
+ }
+ status?: {
+ permalink: string
+ postCount?: number
+ visiblePostCount?: number
+ }
+ children?: ICategory[]
+ /** 文章数(旧项目首页直接读取/排序用) */
+ postCount?: number
+}
+
+export interface ICategoryListReq {
+ page?: number
+ size?: number
+ sort?: string[]
+ tree?: boolean
+ [key: string]: unknown
+}
+
+export type ICategoryListRes = IListResult
+
+/** 分类下文章列表请求 */
+export interface ICategoryPostListReq extends IPostListReq {
+ /** 私密分类密码(加密分类访问) */
+ password?: string
+}
+
+export type ICategoryPostListRes = IListResult
+
+/* ---------- 标签 Tag ---------- */
+
+export interface ITag {
+ metadata: IMetadata
+ spec: {
+ displayName: string
+ slug: string
+ color?: string
+ cover?: string
+ }
+ status?: {
+ permalink: string
+ postCount?: number
+ visiblePostCount?: number
+ }
+}
+
+export interface ITagListReq {
+ page?: number
+ size?: number
+ sort?: string[]
+ [key: string]: unknown
+}
+
+export type ITagListRes = IListResult
+
+/* ---------- 评论 Comment ---------- */
+
+export interface ICommentOwner {
+ kind: 'EMAIL' | 'PHONE' | 'GRAVATAR' | 'WEIXIN' | 'QQ'
+ displayName: string
+ avatar?: string
+ email?: string
+ website?: string
+}
+
+export interface IComment {
+ metadata: IMetadata
+ spec: {
+ raw: string
+ content: string
+ owner: ICommentOwner
+ userAgent?: string
+ ipAddress?: string
+ priority?: number
+ top?: boolean
+ allowNotification?: boolean
+ approved: boolean
+ hidden: boolean
+ subjectRef: {
+ group: string
+ version: string
+ kind: string
+ name: string
+ }
+ lastReadTime?: string
+ creationTime?: string
+ }
+ status?: {
+ hasReply?: boolean
+ replyCount?: number
+ visibleTime?: string
+ }
+ replies?: ICommentReply[]
+}
+
+export interface ICommentReply {
+ metadata: IMetadata
+ spec: {
+ raw: string
+ content: string
+ owner: ICommentOwner
+ quoteReply?: string
+ userAgent?: string
+ ipAddress?: string
+ priority?: number
+ top?: boolean
+ approved: boolean
+ hidden: boolean
+ subjectRef: {
+ group: string
+ version: string
+ kind: string
+ name: string
+ }
+ lastReadTime?: string
+ creationTime?: string
+ }
+}
+
+export interface ICommentListReq {
+ page?: number
+ size?: number
+ sort?: string[]
+ /** 主题名称(文章 name) */
+ name?: string
+ [key: string]: unknown
+}
+
+export type ICommentListRes = IListResult
+
+/* ---------- 瞬间 Moment ---------- */
+
+export interface IMoment {
+ metadata: IMetadata
+ spec: {
+ content: string
+ owner: {
+ displayName: string
+ avatar?: string
+ website?: string
+ }
+ visible: 'PUBLIC' | 'PRIVATE'
+ allowComment: boolean
+ approved: boolean
+ pinned?: boolean
+ priority?: number
+ tags?: string[]
+ releaseTime?: string
+ }
+ status?: {
+ permalink: string
+ approved?: boolean
+ inProgress?: boolean
+ commentsCount?: number
+ }
+}
+
+export interface IMomentListReq {
+ page?: number
+ size?: number
+ sort?: string[]
+ [key: string]: unknown
+}
+
+export type IMomentListRes = IListResult
+
+/* ---------- 图库 Photo ---------- */
+
+export interface IPhotoGroup {
+ metadata: IMetadata
+ spec: {
+ displayName: string
+ description?: string
+ priority?: number
+ cover?: string
+ }
+ status?: {
+ permalink?: string
+ photoCount?: number
+ }
+}
+
+export interface IPhoto {
+ metadata: IMetadata
+ spec: {
+ displayName: string
+ description?: string
+ url: string
+ /** 封面图(部分接口返回,兼容旧代码) */
+ cover?: string
+ priority?: number
+ takeTime?: string
+ location?: string
+ latitude?: number
+ longitude?: number
+ }
+}
+
+export interface IPhotoGroupListReq {
+ page?: number
+ size?: number
+ sort?: string[]
+ [key: string]: unknown
+}
+
+export type IPhotoGroupListRes = IListResult
+
+export interface IPhotoListReq {
+ page?: number
+ size?: number
+ sort?: string[]
+ groupName?: string
+ [key: string]: unknown
+}
+
+export type IPhotoListRes = IListResult
+
+/* ---------- 友链 FriendLink ---------- */
+
+export interface ILinkGroup {
+ metadata: IMetadata
+ spec: {
+ displayName: string
+ priority?: number
+ description?: string
+ }
+}
+
+export interface ILink {
+ metadata: IMetadata
+ spec: {
+ displayName: string
+ url: string
+ logo: string
+ description?: string
+ priority?: number
+ groupName?: string
+ }
+}
+
+export interface ILinkGroupListReq {
+ page?: number
+ size?: number
+ sort?: string[]
+ [key: string]: unknown
+}
+
+export type ILinkGroupListRes = IListResult
+
+export interface ILinkListReq {
+ page?: number
+ size?: number
+ sort?: string[]
+ [key: string]: unknown
+}
+
+export type ILinkListRes = IListResult
+
+/* ---------- 统计 / 埋点 ---------- */
+
+export interface IBlogStats {
+ visit?: number
+ upvote?: number
+ comment?: number
+ post?: number
+ category?: number
+ tag?: number
+ [key: string]: unknown
+}
+
+export interface ITrackerCounterReq {
+ group: string
+ name: string
+ plural?: string
+ endpoint?: string
+ hostname?: string
+ screen?: string
+ title?: string
+ referrer?: string
+ language?: string
+ /** 访问页面路径(旧项目埋点 url 字段) */
+ url?: string
+}
+
+export interface IUpvoteReq {
+ group: string
+ name: string
+ plural?: string
+}
+
+/* ---------- 插件可用性 ---------- */
+
+export interface IPluginAvailable {
+ available: boolean
+ reason?: string
+}
diff --git a/src/api/types/uni-halo.ts b/src/api/types/uni-halo.ts
new file mode 100644
index 0000000..20d25d0
--- /dev/null
+++ b/src/api/types/uni-halo.ts
@@ -0,0 +1,313 @@
+/**
+ * plugin-uni-halo 自定义接口 / 三方插件 API 类型定义
+ * 对应 src/api/uni-halo.ts(plugin-uni-halo 自身接口 + 三方插件)
+ */
+
+/* ---------- plugin-uni-halo 应用配置 ---------- */
+
+/** 图片相关配置 */
+export interface IImagesConfig {
+ defaultThumbnailUrl?: string
+ defaultStaticThumbnailUrl?: string
+ defaultImageUrl?: string
+ defaultAvatarUrl?: string
+ [key: string]: unknown
+}
+
+/** 插件配置(toolsPlugin/linksSubmitPlugin 等带 Authorization) */
+export interface IPluginConfig {
+ votePlugin?: Record
+ toolsPlugin?: { Authorization?: string } & Record
+ linksPlugin?: Record
+ linksSubmitPlugin?: { Authorization?: string } & Record
+ doubanPlugin?: { position?: string } & Record
+ [key: string]: unknown
+}
+
+/** 首页配置(banner 等) */
+export interface IBannerConfig {
+ enabled?: boolean
+ showTitle?: boolean
+ showIndicator?: boolean
+ height?: string
+ dotPosition?: string
+ type?: string
+ list?: unknown[]
+}
+
+export interface IPageConfig {
+ homeConfig?: {
+ pageTitle?: string
+ useCategory?: boolean
+ /** 是否显示快捷导航(首页) */
+ useQuickNavigation?: boolean
+ bannerConfig?: IBannerConfig
+ }
+ categoryConfig?: { type?: string }
+ momentConfig?: { useTagRandomColor?: boolean }
+ galleryConfig?: { pageTitle?: string }
+ aboutConfig?: {
+ bgImageUrl?: string
+ waveImageUrl?: string
+ }
+ [key: string]: unknown
+}
+
+/** 审计模式配置 */
+export interface IAuditConfig {
+ auditModeEnabled?: boolean
+ auditModeData?: {
+ jsonUrl?: string
+ jsonData?: string
+ }
+}
+
+/** 应用基础配置(对应旧 DefaultAppConfigs) */
+export interface IAppConfig {
+ basicConfig?: {
+ tokenConfig?: {
+ personalToken?: string
+ }
+ }
+ loveConfig?: Record
+ imagesConfig?: IImagesConfig
+ authorConfig?: Record
+ appConfig?: Record
+ pluginConfig?: IPluginConfig
+ pageConfig?: IPageConfig
+ auditConfig?: IAuditConfig
+ [key: string]: unknown
+}
+
+/** Halo 全局配置(对应旧 DefaultHaloGlobalConfigs,/actuator/globalinfo) */
+export interface IHaloGlobalConfig {
+ allowAnonymousComments?: boolean
+ allowRegistration?: boolean
+ mustVerifyEmailOnRegistration?: boolean
+ mustVerifyPhoneOnRegistration?: boolean
+ restrictRegistrationDomain?: boolean
+ shopDisabled?: boolean
+ [key: string]: unknown
+}
+
+/** 审计模式模拟数据 */
+export type IMockJson = Record
+
+/* ---------- plugin-uni-halo 二维码 / 检查更新 ---------- */
+
+export interface IQRCodeInfo {
+ [key: string]: unknown
+}
+
+export interface IQRCodeImg {
+ [key: string]: unknown
+}
+
+/** 检查更新结果(uhalo-upgrade 对接) */
+export interface IUpdateCheckRes {
+ code: number
+ message: string
+ url?: string
+ title?: string
+ contents?: string
+ is_mandatory?: boolean
+ is_silently?: boolean
+ type?: 'wgt' | 'apk' | 'ipa'
+ platform?: string[]
+ version?: string
+ [key: string]: unknown
+}
+
+/* ---------- 受限阅读(tools.muyin.site) ---------- */
+
+export type RestrictReadType = 'password' | 'code' | 'login' | 'pay' | 'comment'
+
+/** 受限阅读校验请求 */
+export interface IRestrictReadCheckReq {
+ code: string
+ templateType: 'post'
+ restrictType: RestrictReadType
+ keyId: string
+}
+
+export interface IRestrictReadCheckRes {
+ success?: boolean
+ [key: string]: unknown
+}
+
+/** 验证码创建结果 */
+export interface IVerificationCodeRes {
+ code?: string
+ [key: string]: unknown
+}
+
+/* ---------- 友链提交(linkssubmit.muyin.site) ---------- */
+
+export interface ISubmitLinkForm {
+ name?: string
+ url?: string
+ logo?: string
+ description?: string
+ email?: string
+ [key: string]: unknown
+}
+
+export interface ILinkSubmitRes {
+ success?: boolean
+ message?: string
+ [key: string]: unknown
+}
+
+/* ---------- 投票(api.vote.kunkunyu.com) ---------- */
+
+export interface IVoteListReq {
+ page?: number
+ size?: number
+ [key: string]: unknown
+}
+
+export interface IVoteItem {
+ name: string
+ title: string
+ description?: string
+ [key: string]: unknown
+}
+
+export type IVoteListRes = IVoteItem[]
+
+export interface IVote {
+ name: string
+ title: string
+ description?: string
+ options?: IVoteOption[]
+ [key: string]: unknown
+}
+
+export interface IVoteOption {
+ name?: string
+ label?: string
+ count?: number
+ [key: string]: unknown
+}
+
+export interface IVoteUser {
+ [key: string]: unknown
+}
+
+export type IVoteUserListRes = IVoteUser[]
+
+/** 提交投票 */
+export interface IVoteSubmitReq {
+ optionName?: string
+ [key: string]: unknown
+}
+
+export interface IVoteSubmitRes {
+ success?: boolean
+ [key: string]: unknown
+}
+
+/* ---------- 豆瓣(api.douban.moony.la) ---------- */
+
+export interface IDoubanDetail {
+ title?: string
+ poster?: string
+ rating?: string
+ year?: string
+ [key: string]: unknown
+}
+
+/* ---------- 评论组件(api.commentwidget.halo.run) ---------- */
+
+export interface ICommentWidgetConfig {
+ [key: string]: unknown
+}
+
+/** 评论验证码 */
+export interface ICommentCaptcha {
+ [key: string]: unknown
+}
+
+/* ---------- 恋爱模块(plugin-uni-halo love) ---------- */
+
+export interface ILoveConfig {
+ /** 纪念日 */
+ anniversary?: string
+ /** 恋人信息 */
+ lovers?: {
+ name?: string
+ avatar?: string
+ [key: string]: unknown
+ }
+ [key: string]: unknown
+}
+
+export interface ILoveAlbum {
+ name?: string
+ title?: string
+ /** 相册展示名(接口返回 displayName) */
+ displayName?: string
+ description?: string
+ locked?: boolean
+ cover?: string
+ photos?: ILovePhoto[]
+ [key: string]: unknown
+}
+
+export interface ILovePhoto {
+ url?: string
+ description?: string
+ [key: string]: unknown
+}
+
+export interface ILoveAlbumListReq {
+ page?: number
+ size?: number
+ [key: string]: unknown
+}
+
+export type ILoveAlbumListRes = ILoveAlbum[]
+
+export interface ILoveAlbumDetailReq {
+ [key: string]: unknown
+}
+
+export interface IUnlockAlbumReq {
+ password: string
+}
+
+export interface ILoveUnlockRes {
+ success?: boolean
+ [key: string]: unknown
+}
+
+export interface ILoveDailyItem {
+ id?: string
+ content?: string
+ date?: string
+ [key: string]: unknown
+}
+
+export interface ILoveDailyItemListReq {
+ page?: number
+ size?: number
+ [key: string]: unknown
+}
+
+export type ILoveDailyItemListRes = ILoveDailyItem[]
+
+export interface ILoveStory {
+ id?: string
+ title?: string
+ content?: string
+ date?: string
+ [key: string]: unknown
+}
+
+export interface ILoveStoryListReq {
+ page?: number
+ size?: number
+ [key: string]: unknown
+}
+
+export type ILoveStoryListRes = ILoveStory[]
diff --git a/src/api/uni-halo.ts b/src/api/uni-halo.ts
new file mode 100644
index 0000000..3e7606c
--- /dev/null
+++ b/src/api/uni-halo.ts
@@ -0,0 +1,360 @@
+/**
+ * uni-halo 自定义 API 接口定义
+ * 覆盖 plugin-uni-halo(/apis/api.unihalo.ialley.cn)与三方插件接口:
+ * 受限阅读(tools.muyin.site)、友链提交(linkssubmit.muyin.site)、投票(api.vote.kunkunyu.com)、
+ * 豆瓣(api.douban.moony.la)、评论组件(api.commentwidget.halo.run)
+ * 风格参考 src/api/foo-alova.ts:http.Get>(url, { params, header, meta })
+ * 源自旧项目 api/v2/all.config.js、all.api.js(三方部分)、love.*.js,按需命名导出
+ */
+import { http } from '@/http/alova'
+import { RequestFrom } from '@/http/tools/enum'
+import type { IResponse } from '@/http/types'
+import { getCache } from '@/utils/storage'
+import { getNologinEmail, getOpenid } from '@/utils/auth'
+import { getPersonalToken } from '@/store/token'
+import type {
+ IAppConfig,
+ ICommentWidgetConfig,
+ IDoubanDetail,
+ IHaloGlobalConfig,
+ ILoveAlbum,
+ ILoveAlbumListReq,
+ ILoveAlbumListRes,
+ ILoveConfig,
+ ILoveDailyItem,
+ ILoveDailyItemListReq,
+ ILoveDailyItemListRes,
+ ILoveStory,
+ ILoveStoryListReq,
+ ILoveStoryListRes,
+ IQRCodeInfo,
+ IRestrictReadCheckReq,
+ IRestrictReadCheckRes,
+ ISubmitLinkForm,
+ IUpdateCheckRes,
+ IVote,
+ IVoteListReq,
+ IVoteListRes,
+ IVoteSubmitReq,
+ RestrictReadType,
+} from './types/uni-halo'
+
+/** 评论验证码 cookie key */
+const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha'
+
+/* ==================== plugin-uni-halo 配置 ==================== */
+
+/**
+ * 获取应用配置
+ */
+export function getAppConfigs() {
+ return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/getConfigs', {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 获取 Halo 全局配置信息
+ */
+export function getHaloGlobalInfo() {
+ return http.Get>('/actuator/globalinfo', {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 获取评论组件配置
+ */
+export function getCommentWidgetConfig() {
+ return http.Get>('/apis/api.commentwidget.halo.run/v1alpha1/config', {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 检查更新(uhalo-upgrade 插件对接,插件本体引入后业务侧入口)
+ */
+export function checkVersion(baseUrl: string) {
+ return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/checkVersion', {
+ params: { baseUrl },
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 获取二维码信息
+ */
+export function getQRCodeInfo(key: string) {
+ return http.Get>(`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/getQRCodeInfo/${key}`, {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 获取文章二维码图片
+ */
+export function getQRCodeImg(postId: string) {
+ return http.Get>(`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/getQRCodeImg/${postId}`, {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/* ==================== 恋爱模块 ==================== */
+
+/**
+ * 获取恋爱配置(纪念日 + 恋人信息)
+ */
+export function getLoveConfig() {
+ return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-config', {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 获取恋爱相册列表
+ */
+export function getLoveAlbums(params: ILoveAlbumListReq) {
+ return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 获取恋爱相册详情
+ */
+export function getLoveAlbumByName(name: string, params: ILoveAlbumListReq) {
+ return http.Get>(`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums/${name}`, {
+ params,
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 密码解锁相册
+ */
+export function unlockAlbum(name: string, password: string) {
+ return http.Post>(
+ `/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums/${name}/unlock`,
+ {
+ password,
+ },
+ {
+ meta: { requestFrom: RequestFrom.Halo },
+ },
+ )
+}
+
+/**
+ * 获取恋爱清单列表(分页)
+ */
+export function getLoveDailyItems(params: ILoveDailyItemListReq) {
+ return http.Get>(
+ '/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-daily-items',
+ {
+ params,
+ meta: { requestFrom: RequestFrom.Halo },
+ },
+ )
+}
+
+/**
+ * 获取恋爱故事列表
+ */
+export function getLoveStories(params: ILoveStoryListReq) {
+ return http.Get>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-stories', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/* ==================== 受限阅读(tools.muyin.site) ==================== */
+
+/**
+ * 受限阅读检查(密码/验证码/登录/付费/评论)
+ */
+export function requestRestrictReadCheck(restrictType: RestrictReadType, code: string, keyId: string) {
+ const data: IRestrictReadCheckReq = {
+ code,
+ templateType: 'post',
+ restrictType,
+ keyId,
+ }
+ return http.Post>('/apis/tools.muyin.site/v1alpha1/restrict-read/check', data, {
+ headers: {
+ 'Authorization': getToolsAuthorization(),
+ 'Wechat-Session-Id': getOpenid(),
+ 'nologin-email': getNologinEmail(),
+ },
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 创建验证码(受限阅读)
+ */
+export function createVerificationCode() {
+ return http.Get>('/apis/tools.muyin.site/v1alpha1/restrict-read/create', {
+ headers: {
+ 'Authorization': getToolsAuthorization(),
+ 'Wechat-Session-Id': getOpenid(),
+ },
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/* ==================== 友链提交(linkssubmit.muyin.site) ==================== */
+
+/**
+ * 提交友链
+ */
+export function submitLink(form: ISubmitLinkForm) {
+ return http.Post>('/apis/linkssubmit.muyin.site/v1alpha1/submit', form, {
+ headers: {
+ 'Authorization': getLinksSubmitAuthorization(),
+ 'Wechat-Session-Id': getOpenid(),
+ },
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/* ==================== 投票(api.vote.kunkunyu.com) ==================== */
+
+/**
+ * 投票列表
+ */
+export function getVoteList(params: IVoteListReq) {
+ return http.Get>('/apis/api.vote.kunkunyu.com/v1alpha1/votes', {
+ params,
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 投票详情
+ */
+export function getVoteDetail(name: string) {
+ return http.Get>(`/apis/api.vote.kunkunyu.com/v1alpha1/votes/${name}/detail`, {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 投票用户列表
+ */
+export function getVoteUserList(name: string) {
+ return http.Get>(`/apis/api.vote.kunkunyu.com/v1alpha1/votes/${name}/user-list`, {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 提交投票
+ * @param canAnonymously 是否允许匿名;非匿名时带个人 token
+ */
+export function submitVote(name: string, data: IVoteSubmitReq, canAnonymously = true) {
+ const headers: Record = {}
+ if (!canAnonymously) {
+ const token = getPersonalToken()
+ if (token)
+ headers.Authorization = `Bearer ${token}`
+ }
+ return http.Post>(`/apis/api.vote.kunkunyu.com/v1alpha1/votes/${name}/submit`, data, {
+ headers,
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/* ==================== 豆瓣(api.douban.moony.la) ==================== */
+
+/**
+ * 获取豆瓣条目详情
+ */
+export function getDoubanDetail(url: string) {
+ return http.Get>('/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/getDoubanDetail', {
+ params: { url },
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/* ==================== 评论验证码(api.commentwidget.halo.run) ==================== */
+
+/**
+ * 获取评论验证码
+ */
+export function getCommentWidgetCaptcha() {
+ return http.Get>('/apis/api.commentwidget.halo.run/v1alpha1/captcha/-/generate', {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/* ==================== 数据看板(api.data.statistics.xhhao.com) ==================== */
+
+/** 图表统计数据 */
+export interface IDataStatistics {
+ tags: { name: string, count: number }[]
+ categories: { name: string, total: number }[]
+ articles: { date: string, count: number }[]
+ comments: { username: string, count: number }[]
+ top10Articles: { name: string, views: number }[]
+ [key: string]: unknown
+}
+
+/**
+ * 获取图表统计数据
+ * @description 标签、分类、文章发布趋势、评论活跃用户、热门文章 top10
+ */
+export function getChartData() {
+ return http.Get>('/apis/api.data.statistics.xhhao.com/v1alpha1/chart/data', {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 获取 Github 配置信息
+ */
+export function getGithubConfig() {
+ return http.Get>('/apis/api.data.statistics.xhhao.com/v1alpha1/github/config', {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/**
+ * 获取 Uptime Kuma 状态页面数据
+ */
+export function getUptimeKumaStatus() {
+ return http.Get>('/apis/api.data.statistics.xhhao.com/v1alpha1/uptime/status', {
+ meta: { requestFrom: RequestFrom.Halo },
+ })
+}
+
+/* ==================== 内部辅助 ==================== */
+
+/**
+ * 工具类插件授权头(源自应用配置 pluginConfig.toolsPlugin.Authorization)
+ */
+function getToolsAuthorization(): string {
+ return getAppConfigFromStore().pluginConfig?.toolsPlugin?.Authorization || ''
+}
+
+/**
+ * 友链提交插件授权头(源自应用配置 pluginConfig.linksSubmitPlugin.Authorization)
+ */
+function getLinksSubmitAuthorization(): string {
+ return getAppConfigFromStore().pluginConfig?.linksSubmitPlugin?.Authorization || ''
+}
+
+/**
+ * 读取应用配置(store 未就绪时兜底为空)
+ * 注:待 src/store/appConfig.ts 建立后改为 useAppConfigStore 读取
+ */
+function getAppConfigFromStore(): IAppConfig {
+ // TODO: 迁移 store 后改为从 Pinia 读取,避免每次从 storage 解析
+ const cached = getCache('APP_GLOBAL_CONFIGS')
+ return cached || {}
+}
+
+/** 评论验证码 cookie key(供拦截器/页面使用) */
+export { COMMENT_WIDGET_CAPTCHA_COOKIES }
+
+export type { ILoveDailyItem, ILoveStory }
diff --git a/src/components/mp-html/components/mp-html/editable/config.js b/src/components/mp-html/components/mp-html/editable/config.js
new file mode 100644
index 0000000..8a21a3a
--- /dev/null
+++ b/src/components/mp-html/components/mp-html/editable/config.js
@@ -0,0 +1,11 @@
+// 以下项目可以删减或更换顺序,但不能添加或更改名字
+export default {
+ // 普通标签的菜单项
+ node: ['大小', '斜体', '粗体', '下划线', '居中', '缩进', '上移', '下移', '删除'],
+ // 图片的菜单项
+ img: ['换图', '宽度', '超链接', '预览图', '禁用预览', '上移', '下移', '删除'],
+ // 链接的菜单项
+ link: ['更换链接', '上移', '下移', '删除'],
+ // 音视频的菜单项
+ media: ['封面', '循环', '自动播放', '上移', '下移', '删除']
+}
diff --git a/src/components/mp-html/components/mp-html/editable/index.js b/src/components/mp-html/components/mp-html/editable/index.js
new file mode 100644
index 0000000..a6daf8a
--- /dev/null
+++ b/src/components/mp-html/components/mp-html/editable/index.js
@@ -0,0 +1,532 @@
+/**
+ * @fileoverview editable 插件
+ */
+import config from './config'
+import Parser from '../parser'
+
+function Editable (vm) {
+ this.vm = vm
+ this.editHistory = [] // 历史记录
+ this.editI = -1 // 历史记录指针
+ vm._mask = [] // 蒙版被点击时进行的操作
+
+ vm._setData = function (path, val) {
+ const paths = path.split('.')
+ let target = vm
+ for (let i = 0; i < paths.length - 1; i++) {
+ target = target[paths[i]]
+ }
+ vm.$set(target, paths.pop(), val)
+ }
+
+ /**
+ * @description 移动历史记录指针
+ * @param {Number} num 移动距离
+ */
+ const move = num => {
+ setTimeout(() => {
+ const item = this.editHistory[this.editI + num]
+ if (item) {
+ this.editI += num
+ vm._setData(item.key, item.value)
+ }
+ }, 200)
+ }
+ vm.undo = () => move(-1) // 撤销
+ vm.redo = () => move(1) // 重做
+
+ /**
+ * @description 更新记录
+ * @param {String} path 更新内容路径
+ * @param {*} oldVal 旧值
+ * @param {*} newVal 新值
+ * @param {Boolean} set 是否更新到视图
+ * @private
+ */
+ vm._editVal = (path, oldVal, newVal, set) => {
+ // 当前指针后的内容去除
+ while (this.editI < this.editHistory.length - 1) {
+ this.editHistory.pop()
+ }
+
+ // 最多存储 30 条操作记录
+ while (this.editHistory.length > 30) {
+ this.editHistory.pop()
+ this.editI--
+ }
+
+ const last = this.editHistory[this.editHistory.length - 1]
+ if (!last || last.key !== path) {
+ if (last) {
+ // 去掉上一次的新值
+ this.editHistory.pop()
+ this.editI--
+ }
+ // 存入这一次的旧值
+ this.editHistory.push({
+ key: path,
+ value: oldVal
+ })
+ this.editI++
+ }
+
+ // 存入本次的新值
+ this.editHistory.push({
+ key: path,
+ value: newVal
+ })
+ this.editI++
+
+ // 更新到视图
+ if (set) {
+ vm._setData(path, newVal)
+ }
+ }
+
+ /**
+ * @description 获取菜单项
+ * @private
+ */
+ vm._getItem = function (node, up, down) {
+ let items
+ let i
+ if (node.name === 'img') {
+ items = config.img.slice(0)
+ if (!vm.getSrc) {
+ i = items.indexOf('换图')
+ if (i !== -1) {
+ items.splice(i, 1)
+ }
+ i = items.indexOf('超链接')
+ if (i !== -1) {
+ items.splice(i, 1)
+ }
+ i = items.indexOf('预览图')
+ if (i !== -1) {
+ items.splice(i, 1)
+ }
+ }
+ i = items.indexOf('禁用预览')
+ if (i !== -1 && node.attrs.ignore) {
+ items[i] = '启用预览'
+ }
+ } else if (node.name === 'a') {
+ items = config.link.slice(0)
+ if (!vm.getSrc) {
+ i = items.indexOf('更换链接')
+ if (i !== -1) {
+ items.splice(i, 1)
+ }
+ }
+ } else if (node.name === 'video' || node.name === 'audio') {
+ items = config.media.slice(0)
+ i = items.indexOf('封面')
+ if (!vm.getSrc && i !== -1) {
+ items.splice(i, 1)
+ }
+ i = items.indexOf('循环')
+ if (node.attrs.loop && i !== -1) {
+ items[i] = '不循环'
+ }
+ i = items.indexOf('自动播放')
+ if (node.attrs.autoplay && i !== -1) {
+ items[i] = '不自动播放'
+ }
+ } else {
+ items = config.node.slice(0)
+ }
+ if (!up) {
+ i = items.indexOf('上移')
+ if (i !== -1) {
+ items.splice(i, 1)
+ }
+ }
+ if (!down) {
+ i = items.indexOf('下移')
+ if (i !== -1) {
+ items.splice(i, 1)
+ }
+ }
+ return items
+ }
+
+ /**
+ * @description 显示 tooltip
+ * @param {object} obj
+ * @private
+ */
+ vm._tooltip = function (obj) {
+ vm.$set(vm, 'tooltip', {
+ top: obj.top,
+ items: obj.items
+ })
+ vm._tooltipcb = obj.success
+ }
+
+ /**
+ * @description 显示滚动条
+ * @param {object} obj
+ * @private
+ */
+ vm._slider = function (obj) {
+ vm.$set(vm, 'slider', {
+ min: obj.min,
+ max: obj.max,
+ value: obj.value,
+ top: obj.top
+ })
+ vm._slideringcb = obj.changing
+ vm._slidercb = obj.change
+ }
+
+ /**
+ * @description 点击蒙版
+ * @private
+ */
+ vm._maskTap = function () {
+ // 隐藏所有悬浮窗
+ while (vm._mask.length) {
+ (vm._mask.pop())()
+ }
+ if (vm.tooltip) {
+ vm.$set(vm, 'tooltip', null)
+ }
+ if (vm.slider) {
+ vm.$set(vm, 'slider', null)
+ }
+ }
+
+ /**
+ * @description 插入节点
+ * @param {Object} node
+ */
+ function insert (node) {
+ if (vm._edit) {
+ vm._edit.insert(node)
+ } else {
+ const nodes = vm.nodes.slice(0)
+ nodes.push(node)
+ vm._editVal('nodes', vm.nodes, nodes, true)
+ }
+ }
+
+ /**
+ * @description 在光标处插入指定 html 内容
+ * @param {String} html 内容
+ */
+ vm.insertHtml = html => {
+ this.inserting = true
+ const arr = new Parser(vm).parse(html)
+ this.inserting = undefined
+ for (let i = 0; i < arr.length; i++) {
+ insert(arr[i])
+ }
+ }
+
+ /**
+ * @description 在光标处插入图片
+ */
+ vm.insertImg = function () {
+ vm.getSrc && vm.getSrc('img').then(src => {
+ if (typeof src === 'string') {
+ src = [src]
+ }
+ const parser = new Parser(vm)
+ for (let i = 0; i < src.length; i++) {
+ insert({
+ name: 'img',
+ attrs: {
+ src: parser.getUrl(src[i])
+ }
+ })
+ }
+ }).catch(() => { })
+ }
+
+ /**
+ * @description 在光标处插入一个链接
+ */
+ vm.insertLink = function () {
+ vm.getSrc && vm.getSrc('link').then(url => {
+ insert({
+ name: 'a',
+ attrs: {
+ href: url
+ },
+ children: [{
+ type: 'text',
+ text: url
+ }]
+ })
+ }).catch(() => { })
+ }
+
+ /**
+ * @description 在光标处插入一个表格
+ * @param {Number} rows 行数
+ * @param {Number} cols 列数
+ */
+ vm.insertTable = function (rows, cols) {
+ const table = {
+ name: 'table',
+ attrs: {
+ style: 'display:table;width:100%;margin:10px 0;text-align:center;border-spacing:0;border-collapse:collapse;border:1px solid gray'
+ },
+ children: []
+ }
+ for (let i = 0; i < rows; i++) {
+ const tr = {
+ name: 'tr',
+ attrs: {},
+ children: []
+ }
+ for (let j = 0; j < cols; j++) {
+ tr.children.push({
+ name: 'td',
+ attrs: {
+ style: 'padding:2px;border:1px solid gray'
+ },
+ children: [{
+ type: 'text',
+ text: ''
+ }]
+ })
+ }
+ table.children.push(tr)
+ }
+ insert(table)
+ }
+
+ /**
+ * @description 插入视频/音频
+ * @param {Object} node
+ */
+ function insertMedia (node) {
+ if (typeof node.src === 'string') {
+ node.src = [node.src]
+ }
+ const parser = new Parser(vm)
+ // 拼接主域名
+ for (let i = 0; i < node.src.length; i++) {
+ node.src[i] = parser.getUrl(node.src[i])
+ }
+ insert({
+ name: 'div',
+ attrs: {
+ style: 'text-align:center'
+ },
+ children: [node]
+ })
+ }
+
+ /**
+ * @description 在光标处插入一个视频
+ */
+ vm.insertVideo = function () {
+ vm.getSrc && vm.getSrc('video').then(src => {
+ insertMedia({
+ name: 'video',
+ attrs: {
+ controls: 'T'
+ },
+ children: [],
+ src,
+ // #ifdef APP-PLUS
+ html: ``
+ // #endif
+ })
+ }).catch(() => { })
+ }
+
+ /**
+ * @description 在光标处插入一个音频
+ */
+ vm.insertAudio = function () {
+ vm.getSrc && vm.getSrc('audio').then(attrs => {
+ let src
+ if (attrs.src) {
+ src = attrs.src
+ attrs.src = undefined
+ } else {
+ src = attrs
+ attrs = {}
+ }
+ attrs.controls = 'T'
+ insertMedia({
+ name: 'audio',
+ attrs,
+ children: [],
+ src
+ })
+ }).catch(() => { })
+ }
+
+ /**
+ * @description 在光标处插入一段文本
+ */
+ vm.insertText = function () {
+ insert({
+ name: 'p',
+ attrs: {},
+ children: [{
+ type: 'text',
+ text: ''
+ }]
+ })
+ }
+
+ /**
+ * @description 清空内容
+ */
+ vm.clear = function () {
+ vm._maskTap()
+ vm._edit = undefined
+ vm.$set(vm, 'nodes', [{
+ name: 'p',
+ attrs: {},
+ children: [{
+ type: 'text',
+ text: ''
+ }]
+ }])
+ }
+
+ /**
+ * @description 获取编辑后的 html
+ */
+ vm.getContent = function () {
+ let html = '';
+ // 递归遍历获取
+ (function traversal (nodes, table) {
+ for (let i = 0; i < nodes.length; i++) {
+ let item = nodes[i]
+ if (item.type === 'text') {
+ html += item.text.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
').replace(/\xa0/g, ' ') // 编码实体
+ } else {
+ if (item.name === 'img') {
+ item.attrs.i = ''
+ // 还原被转换的 svg
+ if ((item.attrs.src || '').includes('data:image/svg+xml;utf8,')) {
+ html += item.attrs.src.substr(24).replace(/%23/g, '#').replace('
"+re(e.message+"",!0)+"
";throw e}},se.Parser=ee,se.parser=ee.parse,se.Renderer=H,se.TextRenderer=J,se.Lexer=W,se.lexer=W.lex,se.Tokenizer=D,se.Slugger=K,se.parse=se};export default t();
\ No newline at end of file
diff --git a/src/components/mp-html/components/mp-html/mp-html.vue b/src/components/mp-html/components/mp-html/mp-html.vue
new file mode 100644
index 0000000..8476403
--- /dev/null
+++ b/src/components/mp-html/components/mp-html/mp-html.vue
@@ -0,0 +1,579 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{item}}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/mp-html/components/mp-html/node/node.vue b/src/components/mp-html/components/mp-html/node/node.vue
new file mode 100644
index 0000000..9d71b58
--- /dev/null
+++ b/src/components/mp-html/components/mp-html/node/node.vue
@@ -0,0 +1,1119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{n.text}}
+ {{opts[6]||'请输入'}}
+
+ {{n.text}}
+ {{opts[6]||'请输入'}}
+
+
+ \n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/mp-html/components/mp-html/parser.js b/src/components/mp-html/components/mp-html/parser.js
new file mode 100644
index 0000000..62f32ef
--- /dev/null
+++ b/src/components/mp-html/components/mp-html/parser.js
@@ -0,0 +1,1342 @@
+/**
+ * @fileoverview html 解析器
+ */
+
+// 配置
+const config = {
+ // 信任的标签(保持标签名不变)
+ trustTags: makeMap('a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,ruby,rt,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),
+
+ // 块级标签(转为 div,其他的非信任标签转为 span)
+ blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
+
+ // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
+ // 行内标签
+ inlineTags: makeMap('abbr,b,big,code,del,em,i,ins,label,q,small,span,strong,sub,sup'),
+ // #endif
+
+ // 要移除的标签
+ ignoreTags: makeMap('area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'),
+
+ // 自闭合的标签
+ voidTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
+
+ // html 实体
+ entities: {
+ lt: '<',
+ gt: '>',
+ quot: '"',
+ apos: "'",
+ ensp: '\u2002',
+ emsp: '\u2003',
+ nbsp: '\xA0',
+ semi: ';',
+ ndash: '–',
+ mdash: '—',
+ middot: '·',
+ lsquo: '‘',
+ rsquo: '’',
+ ldquo: '“',
+ rdquo: '”',
+ bull: '•',
+ hellip: '…',
+ larr: '←',
+ uarr: '↑',
+ rarr: '→',
+ darr: '↓'
+ },
+
+ // 默认的标签样式
+ tagStyle: {
+ // #ifndef APP-PLUS-NVUE
+ address: 'font-style:italic',
+ big: 'display:inline;font-size:1.2em',
+ caption: 'display:table-caption;text-align:center',
+ center: 'text-align:center',
+ cite: 'font-style:italic',
+ dd: 'margin-left:40px',
+ mark: 'background-color:yellow',
+ pre: 'font-family:monospace;white-space:pre',
+ s: 'text-decoration:line-through',
+ small: 'display:inline;font-size:0.8em',
+ strike: 'text-decoration:line-through',
+ u: 'text-decoration:underline'
+ // #endif
+ },
+
+ // svg 大小写对照表
+ svgDict: {
+ animatetransform: 'animateTransform',
+ lineargradient: 'linearGradient',
+ viewbox: 'viewBox',
+ attributename: 'attributeName',
+ repeatcount: 'repeatCount',
+ repeatdur: 'repeatDur'
+ }
+}
+const tagSelector={}
+const {
+ windowWidth,
+ // #ifdef MP-WEIXIN
+ system
+ // #endif
+} = uni.getSystemInfoSync()
+const blankChar = makeMap(' ,\r,\n,\t,\f')
+let idIndex = 0
+
+// #ifdef H5 || APP-PLUS
+config.ignoreTags.iframe = undefined
+config.trustTags.iframe = true
+config.ignoreTags.embed = undefined
+config.trustTags.embed = true
+// #endif
+// #ifdef APP-PLUS-NVUE
+config.ignoreTags.source = undefined
+config.ignoreTags.style = undefined
+// #endif
+
+/**
+ * @description 创建 map
+ * @param {String} str 逗号分隔
+ */
+function makeMap (str) {
+ const map = Object.create(null)
+ const list = str.split(',')
+ for (let i = list.length; i--;) {
+ map[list[i]] = true
+ }
+ return map
+}
+
+/**
+ * @description 解码 html 实体
+ * @param {String} str 要解码的字符串
+ * @param {Boolean} amp 要不要解码 &
+ * @returns {String} 解码后的字符串
+ */
+function decodeEntity (str, amp) {
+ let i = str.indexOf('&')
+ while (i !== -1) {
+ const j = str.indexOf(';', i + 3)
+ let code
+ if (j === -1) break
+ if (str[i + 1] === '#') {
+ // { 形式的实体
+ code = parseInt((str[i + 2] === 'x' ? '0' : '') + str.substring(i + 2, j))
+ if (!isNaN(code)) {
+ str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1)
+ }
+ } else {
+ // 形式的实体
+ code = str.substring(i + 1, j)
+ if (config.entities[code] || (code === 'amp' && amp)) {
+ str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1)
+ }
+ }
+ i = str.indexOf('&', i + 1)
+ }
+ return str
+}
+
+/**
+ * @description 合并多个块级标签,加快长内容渲染
+ * @param {Array} nodes 要合并的标签数组
+ */
+function mergeNodes (nodes) {
+ let i = nodes.length - 1
+ for (let j = i; j >= -1; j--) {
+ if (j === -1 || nodes[j].c || !nodes[j].name || (nodes[j].name !== 'div' && nodes[j].name !== 'p' && nodes[j].name[0] !== 'h') || (nodes[j].attrs.style || '').includes('inline')) {
+ if (i - j >= 5) {
+ nodes.splice(j + 1, i - j, {
+ name: 'div',
+ attrs: {},
+ children: nodes.slice(j + 1, i + 1)
+ })
+ }
+ i = j - 1
+ }
+ }
+}
+
+/**
+ * @description html 解析器
+ * @param {Object} vm 组件实例
+ */
+function Parser (vm) {
+ this.options = vm || {}
+ this.tagStyle = Object.assign({}, config.tagStyle, this.options.tagStyle)
+ this.imgList = vm.imgList || []
+ this.imgList._unloadimgs = 0
+ this.plugins = vm.plugins || []
+ this.attrs = Object.create(null)
+ this.stack = []
+ this.nodes = []
+ this.pre = (this.options.containerStyle || '').includes('white-space') && this.options.containerStyle.includes('pre') ? 2 : 0
+}
+
+/**
+ * @description 执行解析
+ * @param {String} content 要解析的文本
+ */
+Parser.prototype.parse = function (content) {
+ // 插件处理
+ for (let i = this.plugins.length; i--;) {
+ if (this.plugins[i].onUpdate) {
+ content = this.plugins[i].onUpdate(content, config) || content
+ }
+ }
+
+ new Lexer(this).parse(content)
+ // 出栈未闭合的标签
+ while (this.stack.length) {
+ this.popNode()
+ }
+ if (this.nodes.length > 50) {
+ mergeNodes(this.nodes)
+ }
+ return this.nodes
+}
+
+/**
+ * @description 将标签暴露出来(不被 rich-text 包含)
+ */
+Parser.prototype.expose = function () {
+ // #ifndef APP-PLUS-NVUE
+ for (let i = this.stack.length; i--;) {
+ const item = this.stack[i]
+ if (item.c || item.name === 'a' || item.name === 'video' || item.name === 'audio') return
+ item.c = 1
+ }
+ // #endif
+}
+
+/**
+ * @description 处理插件
+ * @param {Object} node 要处理的标签
+ * @returns {Boolean} 是否要移除此标签
+ */
+Parser.prototype.hook = function (node) {
+ for (let i = this.plugins.length; i--;) {
+ if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) === false) {
+ return false
+ }
+ }
+ return true
+}
+
+/**
+ * @description 将链接拼接上主域名
+ * @param {String} url 需要拼接的链接
+ * @returns {String} 拼接后的链接
+ */
+Parser.prototype.getUrl = function (url) {
+ const domain = this.options.domain
+ if (url[0] === '/') {
+ if (url[1] === '/') {
+ // // 开头的补充协议名
+ url = (domain ? domain.split('://')[0] : 'http') + ':' + url
+ } else if (domain) {
+ // 否则补充整个域名
+ url = domain + url
+ } /* #ifdef APP-PLUS */ else {
+ url = plus.io.convertLocalFileSystemURL(url)
+ } /* #endif */
+ } else if (!url.includes('data:') && !url.includes('://')) {
+ if (domain) {
+ url = domain + '/' + url
+ } /* #ifdef APP-PLUS */ else {
+ url = plus.io.convertLocalFileSystemURL(url)
+ } /* #endif */
+ }
+ return url
+}
+
+/**
+ * @description 解析样式表
+ * @param {Object} node 标签
+ * @returns {Object}
+ */
+Parser.prototype.parseStyle = function (node) {
+ const attrs = node.attrs
+ const list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';'))
+ const styleObj = {}
+ let tmp = ''
+
+ if (attrs.id && !this.xml) {
+ // 暴露锚点
+ if (this.options.useAnchor) {
+ this.expose()
+ } else if (node.name !== 'img' && node.name !== 'a' && node.name !== 'video' && node.name !== 'audio') {
+ attrs.id = undefined
+ }
+ }
+
+ // 转换 width 和 height 属性
+ if (attrs.width) {
+ styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px')
+ attrs.width = undefined
+ }
+ if (attrs.height) {
+ styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px')
+ attrs.height = undefined
+ }
+
+ for (let i = 0, len = list.length; i < len; i++) {
+ const info = list[i].split(':')
+ if (info.length < 2) continue
+ const key = info.shift().trim().toLowerCase()
+ let value = info.join(':').trim()
+ if ((value[0] === '-' && value.lastIndexOf('-') > 0) || value.includes('safe')) {
+ // 兼容性的 css 不压缩
+ tmp += `;${key}:${value}`
+ } else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) {
+ // 重复的样式进行覆盖
+ if (value.includes('url')) {
+ // 填充链接
+ let j = value.indexOf('(') + 1
+ if (j) {
+ while (value[j] === '"' || value[j] === "'" || blankChar[value[j]]) {
+ j++
+ }
+ value = value.substr(0, j) + this.getUrl(value.substr(j))
+ }
+ } else if (value.includes('rpx')) {
+ // 转换 rpx(rich-text 内部不支持 rpx)
+ value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px')
+ }
+ styleObj[key] = value
+ }
+ }
+
+ node.attrs.style = tmp
+ return styleObj
+}
+
+/**
+ * @description 解析到标签名
+ * @param {String} name 标签名
+ * @private
+ */
+Parser.prototype.onTagName = function (name) {
+ this.tagName = this.xml ? name : name.toLowerCase()
+ if (this.tagName === 'svg') {
+ this.xml = (this.xml || 0) + 1 // svg 标签内大小写敏感
+ }
+}
+
+/**
+ * @description 解析到属性名
+ * @param {String} name 属性名
+ * @private
+ */
+Parser.prototype.onAttrName = function (name) {
+ name = this.xml ? name : name.toLowerCase()
+ if (name.substr(0, 5) === 'data-') {
+ if (name === 'data-src' && !this.attrs.src) {
+ // data-src 自动转为 src
+ this.attrName = 'src'
+ } else if (this.tagName === 'img' || this.tagName === 'a') {
+ // a 和 img 标签保留 data- 的属性,可以在 imgtap 和 linktap 事件中使用
+ this.attrName = name
+ } else {
+ // 剩余的移除以减小大小
+ this.attrName = undefined
+ }
+ } else {
+ this.attrName = name
+ this.attrs[name] = 'T' // boolean 型属性缺省设置
+ }
+}
+
+/**
+ * @description 解析到属性值
+ * @param {String} val 属性值
+ * @private
+ */
+Parser.prototype.onAttrVal = function (val) {
+ const name = this.attrName || ''
+ if (name === 'style' || name === 'href') {
+ // 部分属性进行实体解码
+ this.attrs[name] = decodeEntity(val, true)
+ } else if (name.includes('src')) {
+ // 拼接主域名
+ this.attrs[name] = this.getUrl(decodeEntity(val, true))
+ } else if (name) {
+ this.attrs[name] = val
+ }
+}
+
+/**
+ * @description 解析到标签开始
+ * @param {Boolean} selfClose 是否有自闭合标识 />
+ * @private
+ */
+Parser.prototype.onOpenTag = function (selfClose) {
+ // 拼装 node
+ const node = Object.create(null)
+ node.name = this.tagName
+ node.attrs = this.attrs
+ // 避免因为自动 diff 使得 type 被设置为 null 导致部分内容不显示
+ if (this.options.nodes.length) {
+ node.type = 'node'
+ }
+ this.attrs = Object.create(null)
+
+ const attrs = node.attrs
+ const parent = this.stack[this.stack.length - 1]
+ const siblings = parent ? parent.children : this.nodes
+ const close = this.xml ? selfClose : config.voidTags[node.name]
+
+ // 替换标签名选择器
+ if (tagSelector[node.name]) {
+ attrs.class = tagSelector[node.name] + (attrs.class ? ' ' + attrs.class : '')
+ }
+
+ // 转换 embed 标签
+ if (node.name === 'embed') {
+ // #ifndef H5 || APP-PLUS
+ const src = attrs.src || ''
+ // 按照后缀名和 type 将 embed 转为 video 或 audio
+ if (src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8') || (attrs.type || '').includes('video')) {
+ node.name = 'video'
+ } else if (src.includes('.mp3') || src.includes('.wav') || src.includes('.aac') || src.includes('.m4a') || (attrs.type || '').includes('audio')) {
+ node.name = 'audio'
+ }
+ if (attrs.autostart) {
+ attrs.autoplay = 'T'
+ }
+ attrs.controls = 'T'
+ // #endif
+ // #ifdef H5 || APP-PLUS
+ this.expose()
+ // #endif
+ }
+
+ // #ifndef APP-PLUS-NVUE
+ // 处理音视频
+ if (node.name === 'video' || node.name === 'audio') {
+ // 设置 id 以便获取 context
+ if (node.name === 'video' && !attrs.id) {
+ attrs.id = 'v' + idIndex++
+ }
+ // 没有设置 controls 也没有设置 autoplay 的自动设置 controls
+ if (!attrs.controls && !attrs.autoplay) {
+ attrs.controls = 'T'
+ }
+ // 用数组存储所有可用的 source
+ node.src = []
+ if (attrs.src) {
+ node.src.push(attrs.src)
+ attrs.src = undefined
+ }
+ this.expose()
+ }
+ // #endif
+
+ // 处理自闭合标签
+ if (close) {
+ if (!this.hook(node) || config.ignoreTags[node.name]) {
+ // 通过 base 标签设置主域名
+ if (node.name === 'base' && !this.options.domain) {
+ this.options.domain = attrs.href
+ } /* #ifndef APP-PLUS-NVUE */ else if (node.name === 'source' && parent && (parent.name === 'video' || parent.name === 'audio') && attrs.src) {
+ // 设置 source 标签(仅父节点为 video 或 audio 时有效)
+ parent.src.push(attrs.src)
+ } /* #endif */
+ return
+ }
+
+ // 解析 style
+ const styleObj = this.parseStyle(node)
+
+ // 处理图片
+ if (node.name === 'img') {
+ if (attrs.src) {
+ // 标记 webp
+ if (attrs.src.includes('webp')) {
+ node.webp = 'T'
+ }
+ // data url 图片如果没有设置 original-src 默认为不可预览的小图片
+ if (attrs.src.includes('data:') && !attrs['original-src']) {
+ attrs.ignore = 'T'
+ }
+ if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {
+ for (let i = this.stack.length; i--;) {
+ const item = this.stack[i]
+ if (item.name === 'a') {
+ node.a = item.attrs
+ }
+ if (item.name === 'table' && !node.webp && !attrs.src.includes('cloud://')) {
+ if (!styleObj.display || styleObj.display.includes('inline')) {
+ node.t = 'inline-block'
+ } else {
+ node.t = styleObj.display
+ }
+ styleObj.display = undefined
+ }
+ // #ifndef H5 || APP-PLUS
+ const style = item.attrs.style || ''
+ if (style.includes('flex:') && !style.includes('flex:0') && !style.includes('flex: 0') && (!styleObj.width || parseInt(styleObj.width) > 100)) {
+ styleObj.width = '100% !important'
+ styleObj.height = ''
+ for (let j = i + 1; j < this.stack.length; j++) {
+ this.stack[j].attrs.style = (this.stack[j].attrs.style || '').replace('inline-', '')
+ }
+ } else if (style.includes('flex') && styleObj.width === '100%') {
+ for (let j = i + 1; j < this.stack.length; j++) {
+ const style = this.stack[j].attrs.style || ''
+ if (!style.includes(';width') && !style.includes(' width') && style.indexOf('width') !== 0) {
+ styleObj.width = ''
+ break
+ }
+ }
+ } else if (style.includes('inline-block')) {
+ if (styleObj.width && styleObj.width[styleObj.width.length - 1] === '%') {
+ item.attrs.style += ';max-width:' + styleObj.width
+ styleObj.width = ''
+ } else {
+ item.attrs.style += ';max-width:100%'
+ }
+ }
+ // #endif
+ item.c = 1
+ }
+ attrs.i = this.imgList.length.toString()
+ let src = attrs['original-src'] || attrs.src
+ // #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360
+ if (this.imgList.includes(src)) {
+ // 如果有重复的链接则对域名进行随机大小写变换避免预览时错位
+ let i = src.indexOf('://')
+ if (i !== -1) {
+ i += 3
+ let newSrc = src.substr(0, i)
+ for (; i < src.length; i++) {
+ if (src[i] === '/') break
+ newSrc += Math.random() > 0.5 ? src[i].toUpperCase() : src[i]
+ }
+ newSrc += src.substr(i)
+ src = newSrc
+ }
+ }
+ // #endif
+ this.imgList.push(src)
+ if (!node.t) {
+ this.imgList._unloadimgs += 1
+ }
+ // #ifdef H5 || APP-PLUS
+ if (this.options.lazyLoad) {
+ attrs['data-src'] = attrs.src
+ attrs.src = undefined
+ }
+ // #endif
+ }
+ }
+ if (styleObj.display === 'inline') {
+ styleObj.display = ''
+ }
+ // #ifndef APP-PLUS-NVUE
+ if (attrs.ignore) {
+ styleObj['max-width'] = styleObj['max-width'] || '100%'
+ attrs.style += ';-webkit-touch-callout:none'
+ }
+ // #endif
+ // 设置的宽度超出屏幕,为避免变形,高度转为自动
+ if (parseInt(styleObj.width) > windowWidth) {
+ styleObj.height = undefined
+ }
+ // 记录是否设置了宽高
+ if (!isNaN(parseInt(styleObj.width))) {
+ node.w = 'T'
+ }
+ if (!isNaN(parseInt(styleObj.height)) && (!styleObj.height.includes('%') || (parent && (parent.attrs.style || '').includes('height')))) {
+ node.h = 'T'
+ }
+ } else if (node.name === 'svg') {
+ siblings.push(node)
+ this.stack.push(node)
+ this.popNode()
+ return
+ }
+ for (const key in styleObj) {
+ if (styleObj[key]) {
+ attrs.style += `;${key}:${styleObj[key].replace(' !important', '')}`
+ }
+ }
+ attrs.style = attrs.style.substr(1) || undefined
+ // #ifdef (MP-WEIXIN || MP-QQ) && VUE3
+ if (!attrs.style) {
+ delete attrs.style
+ }
+ // #endif
+ } else {
+ if ((node.name === 'pre' || ((attrs.style || '').includes('white-space') && attrs.style.includes('pre'))) && this.pre !== 2) {
+ this.pre = node.pre = 1
+ }
+ node.children = []
+ this.stack.push(node)
+ }
+
+ // 加入节点树
+ siblings.push(node)
+}
+
+/**
+ * @description 解析到标签结束
+ * @param {String} name 标签名
+ * @private
+ */
+Parser.prototype.onCloseTag = function (name) {
+ // 依次出栈到匹配为止
+ name = this.xml ? name : name.toLowerCase()
+ let i
+ for (i = this.stack.length; i--;) {
+ if (this.stack[i].name === name) break
+ }
+ if (i !== -1) {
+ while (this.stack.length > i) {
+ this.popNode()
+ }
+ } else if (name === 'p' || name === 'br') {
+ const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
+ siblings.push({
+ name,
+ attrs: {
+ class: tagSelector[name] || '',
+ style: this.tagStyle[name] || ''
+ }
+ })
+ }
+}
+
+/**
+ * @description 处理标签出栈
+ * @private
+ */
+Parser.prototype.popNode = function () {
+ const editable = this.options.editable
+ const node = this.stack.pop()
+ let attrs = node.attrs
+ const children = node.children
+ const parent = this.stack[this.stack.length - 1]
+ const siblings = parent ? parent.children : this.nodes
+
+ if (!this.hook(node) || config.ignoreTags[node.name]) {
+ // 获取标题
+ if (node.name === 'title' && children.length && children[0].type === 'text' && this.options.setTitle) {
+ uni.setNavigationBarTitle({
+ title: children[0].text
+ })
+ }
+ siblings.pop()
+ return
+ }
+
+ if (node.pre && this.pre !== 2) {
+ // 是否合并空白符标识
+ this.pre = node.pre = undefined
+ for (let i = this.stack.length; i--;) {
+ if (this.stack[i].pre) {
+ this.pre = 1
+ }
+ }
+ }
+
+ const styleObj = {}
+
+ // 转换 svg
+ if (node.name === 'svg') {
+ if (this.xml > 1) {
+ // 多层 svg 嵌套
+ this.xml--
+ return
+ }
+ // #ifdef APP-PLUS-NVUE
+ (function traversal (node) {
+ if (node.name) {
+ // 调整 svg 的大小写
+ node.name = config.svgDict[node.name] || node.name
+ for (const item in node.attrs) {
+ if (config.svgDict[item]) {
+ node.attrs[config.svgDict[item]] = node.attrs[item]
+ node.attrs[item] = undefined
+ }
+ }
+ for (let i = 0; i < (node.children || []).length; i++) {
+ traversal(node.children[i])
+ }
+ }
+ })(node)
+ // #endif
+ // #ifndef APP-PLUS-NVUE
+ let src = ''
+ const style = attrs.style
+ attrs.style = ''
+ attrs.xmlns = 'http://www.w3.org/2000/svg';
+ (function traversal (node) {
+ if (node.type === 'text') {
+ src += node.text
+ return
+ }
+ const name = config.svgDict[node.name] || node.name
+ src += '<' + name
+ for (const item in node.attrs) {
+ const val = node.attrs[item]
+ if (val) {
+ src += ` ${config.svgDict[item] || item}="${val}"`
+ }
+ }
+ if (!node.children) {
+ src += '/>'
+ } else {
+ src += '>'
+ for (let i = 0; i < node.children.length; i++) {
+ traversal(node.children[i])
+ }
+ src += '' + name + '>'
+ }
+ })(node)
+ node.name = 'img'
+ node.attrs = {
+ src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
+ style,
+ ignore: 'T'
+ }
+ node.children = undefined
+ // #endif
+ this.xml = false
+ return
+ }
+
+ // #ifndef APP-PLUS-NVUE
+ // 转换 align 属性
+ if (attrs.align) {
+ if (node.name === 'table') {
+ if (attrs.align === 'center') {
+ styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto'
+ } else {
+ styleObj.float = attrs.align
+ }
+ } else {
+ styleObj['text-align'] = attrs.align
+ }
+ attrs.align = undefined
+ }
+
+ // 转换 dir 属性
+ if (attrs.dir) {
+ styleObj.direction = attrs.dir
+ attrs.dir = undefined
+ }
+
+ // 转换 font 标签的属性
+ if (node.name === 'font') {
+ if (attrs.color) {
+ styleObj.color = attrs.color
+ attrs.color = undefined
+ }
+ if (attrs.face) {
+ styleObj['font-family'] = attrs.face
+ attrs.face = undefined
+ }
+ if (attrs.size) {
+ let size = parseInt(attrs.size)
+ if (!isNaN(size)) {
+ if (size < 1) {
+ size = 1
+ } else if (size > 7) {
+ size = 7
+ }
+ styleObj['font-size'] = ['x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large'][size - 1]
+ }
+ attrs.size = undefined
+ }
+ }
+ // #endif
+
+ // 一些编辑器的自带 class
+ if ((attrs.class || '').includes('align-center')) {
+ styleObj['text-align'] = 'center'
+ }
+
+ Object.assign(styleObj, this.parseStyle(node))
+
+ if (node.name !== 'table' && parseInt(styleObj.width) > windowWidth) {
+ styleObj['max-width'] = '100%'
+ styleObj['box-sizing'] = 'border-box'
+ }
+
+ // #ifndef APP-PLUS-NVUE
+ if (config.blockTags[node.name]) {
+ if (!editable) {
+ node.name = 'div'
+ }
+ } else if (!config.trustTags[node.name] && !this.xml) {
+ // 未知标签转为 span,避免无法显示
+ node.name = 'span'
+ }
+
+ if (node.name === 'a' || node.name === 'ad'
+ // #ifdef H5 || APP-PLUS
+ || node.name === 'iframe' // eslint-disable-line
+ // #endif
+ ) {
+ this.expose()
+ } else if (node.name === 'video') {
+ if ((styleObj.height || '').includes('auto')) {
+ styleObj.height = undefined
+ }
+ /* #ifdef APP-PLUS */
+ let str = ''
+ node.html = str
+ /* #endif */
+ } else if ((node.name === 'ul' || node.name === 'ol') && (node.c || editable)) {
+ // 列表处理
+ const types = {
+ a: 'lower-alpha',
+ A: 'upper-alpha',
+ i: 'lower-roman',
+ I: 'upper-roman'
+ }
+ if (types[attrs.type]) {
+ attrs.style += ';list-style-type:' + types[attrs.type]
+ attrs.type = undefined
+ }
+ for (let i = children.length; i--;) {
+ if (children[i].name === 'li') {
+ children[i].c = 1
+ }
+ }
+ } else if (node.name === 'table') {
+ // 表格处理
+ // cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现
+ let padding = parseFloat(attrs.cellpadding)
+ let spacing = parseFloat(attrs.cellspacing)
+ const border = parseFloat(attrs.border)
+ const bordercolor = styleObj['border-color']
+ const borderstyle = styleObj['border-style']
+ if ((node.c || editable)) {
+ // padding 和 spacing 默认 2
+ if (isNaN(padding)) {
+ padding = 2
+ }
+ if (isNaN(spacing)) {
+ spacing = 2
+ }
+ }
+ if (border) {
+ attrs.style += `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}`
+ }
+ if (node.flag && (node.c || editable)) {
+ // 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
+ styleObj.display = 'grid'
+ if (spacing) {
+ styleObj['grid-gap'] = spacing + 'px'
+ styleObj.padding = spacing + 'px'
+ } else if (border) {
+ // 无间隔的情况下避免边框重叠
+ attrs.style += ';border-left:0;border-top:0'
+ }
+
+ const width = [] // 表格的列宽
+ const trList = [] // tr 列表
+ const cells = [] // 保存新的单元格
+ const map = {}; // 被合并单元格占用的格子
+
+ (function traversal (nodes) {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].name === 'tr') {
+ trList.push(nodes[i])
+ } else {
+ traversal(nodes[i].children || [])
+ }
+ }
+ })(children)
+
+ for (let row = 1; row <= trList.length; row++) {
+ let col = 1
+ for (let j = 0; j < trList[row - 1].children.length; j++) {
+ const td = trList[row - 1].children[j]
+ if (td.name === 'td' || td.name === 'th') {
+ // 这个格子被上面的单元格占用,则列号++
+ while (map[row + '.' + col]) {
+ col++
+ }
+ if (editable) {
+ td.r = row
+ }
+ let style = td.attrs.style || ''
+ let start = style.indexOf('width') ? style.indexOf(';width') : 0
+ // 提取出 td 的宽度
+ if (start !== -1) {
+ let end = style.indexOf(';', start + 6)
+ if (end === -1) {
+ end = style.length
+ }
+ if (!td.attrs.colspan) {
+ width[col] = style.substring(start ? start + 7 : 6, end)
+ }
+ style = style.substr(0, start) + style.substr(end)
+ }
+ // 设置竖直对齐
+ style += ';display:flex'
+ start = style.indexOf('vertical-align')
+ if (start !== -1) {
+ const val = style.substr(start + 15, 10)
+ if (val.includes('middle')) {
+ style += ';align-items:center'
+ } else if (val.includes('bottom')) {
+ style += ';align-items:flex-end'
+ }
+ } else {
+ style += ';align-items:center'
+ }
+ // 设置水平对齐
+ start = style.indexOf('text-align')
+ if (start !== -1) {
+ const val = style.substr(start + 11, 10)
+ if (val.includes('center')) {
+ style += ';justify-content: center'
+ } else if (val.includes('right')) {
+ style += ';justify-content: right'
+ }
+ }
+ style = (border ? `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}` + (spacing ? '' : ';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '') + ';' + style
+ // 处理列合并
+ if (td.attrs.colspan) {
+ style += `;grid-column-start:${col};grid-column-end:${col + parseInt(td.attrs.colspan)}`
+ if (!td.attrs.rowspan) {
+ style += `;grid-row-start:${row};grid-row-end:${row + 1}`
+ }
+ col += parseInt(td.attrs.colspan) - 1
+ }
+ // 处理行合并
+ if (td.attrs.rowspan) {
+ style += `;grid-row-start:${row};grid-row-end:${row + parseInt(td.attrs.rowspan)}`
+ if (!td.attrs.colspan) {
+ style += `;grid-column-start:${col};grid-column-end:${col + 1}`
+ }
+ // 记录下方单元格被占用
+ for (let rowspan = 1; rowspan < td.attrs.rowspan; rowspan++) {
+ for (let colspan = 0; colspan < (td.attrs.colspan || 1); colspan++) {
+ map[(row + rowspan) + '.' + (col - colspan)] = 1
+ }
+ }
+ }
+ if (style) {
+ td.attrs.style = style
+ }
+ cells.push(td)
+ col++
+ }
+ }
+ if (row === 1) {
+ let temp = ''
+ for (let i = 1; i < col; i++) {
+ temp += (width[i] ? width[i] : 'auto') + ' '
+ }
+ styleObj['grid-template-columns'] = temp
+ }
+ }
+ node.children = cells
+ } else {
+ // 没有使用合并单元格的表格通过 table 布局实现
+ if ((node.c || editable)) {
+ styleObj.display = 'table'
+ }
+ if (!isNaN(spacing)) {
+ styleObj['border-spacing'] = spacing + 'px'
+ }
+ if (border || padding) {
+ // 遍历
+ (function traversal (nodes) {
+ for (let i = 0; i < nodes.length; i++) {
+ const td = nodes[i]
+ if (td.name === 'th' || td.name === 'td') {
+ if (border) {
+ td.attrs.style = `border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'};${td.attrs.style || ''}`
+ }
+ if (padding) {
+ td.attrs.style = `padding:${padding}px;${td.attrs.style || ''}`
+ }
+ } else if (td.children) {
+ traversal(td.children)
+ }
+ }
+ })(children)
+ }
+ }
+ // 给表格添加一个单独的横向滚动层
+ if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {
+ const table = Object.assign({}, node)
+ node.name = 'div'
+ node.attrs = {
+ style: 'overflow:auto'
+ }
+ node.children = [table]
+ attrs = table.attrs
+ }
+ } else if ((node.name === 'td' || node.name === 'th') && (attrs.colspan || attrs.rowspan)) {
+ for (let i = this.stack.length; i--;) {
+ if (this.stack[i].name === 'table') {
+ this.stack[i].flag = 1 // 指示含有合并单元格
+ break
+ }
+ }
+ } else if (node.name === 'ruby') {
+ // 转换 ruby
+ node.name = 'span'
+ for (let i = 0; i < children.length - 1; i++) {
+ if (children[i].type === 'text' && children[i + 1].name === 'rt') {
+ children[i] = {
+ name: 'div',
+ attrs: {
+ style: 'display:inline-block;text-align:center'
+ },
+ children: [{
+ name: 'div',
+ attrs: {
+ style: 'font-size:50%;' + (children[i + 1].attrs.style || '')
+ },
+ children: children[i + 1].children
+ }, children[i]]
+ }
+ children.splice(i + 1, 1)
+ }
+ }
+ } else if (!editable && node.c ) {
+ (function traversal (node) {
+ node.c = 2
+ for (let i = node.children.length; i--;) {
+ const child = node.children[i]
+ // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
+ if (child.name && (config.inlineTags[child.name] || ((child.attrs.style || '').includes('inline') && child.children)) && !child.c) {
+ traversal(child)
+ }
+ // #endif
+ if (!child.c || child.name === 'table') {
+ node.c = 1
+ }
+ }
+ })(node)
+ }
+
+ if ((styleObj.display || '').includes('flex') && !(node.c || editable)) {
+ for (let i = children.length; i--;) {
+ const item = children[i]
+ if (item.f) {
+ item.attrs.style = (item.attrs.style || '') + item.f
+ item.f = undefined
+ }
+ }
+ }
+ // flex 布局时部分样式需要提取到 rich-text 外层
+ const flex = parent && ((parent.attrs.style || '').includes('flex') || (parent.attrs.style || '').includes('grid'))
+ // #ifdef MP-WEIXIN
+ // 检查基础库版本 virtualHost 是否可用
+ && !((node.c || editable) && wx.getNFCAdapter) // eslint-disable-line
+ // #endif
+ // #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
+ && !node.c // eslint-disable-line
+ // #endif
+ if (flex) {
+ node.f = ';max-width:100%'
+ }
+
+ if (children.length >= 50 && (node.c || editable) && !(styleObj.display || '').includes('flex')) {
+ mergeNodes(children)
+ }
+ // #endif
+
+ for (const key in styleObj) {
+ if (styleObj[key]) {
+ const val = `;${key}:${styleObj[key].replace(' !important', '')}`
+ /* #ifndef APP-PLUS-NVUE */
+ if (flex && ((key.includes('flex') && key !== 'flex-direction') || key === 'align-self' || key.includes('grid') || styleObj[key][0] === '-' || (key.includes('width') && val.includes('%')))) {
+ node.f += val
+ if (key === 'width') {
+ attrs.style += ';width:100%'
+ }
+ } else /* #endif */ {
+ attrs.style += val
+ }
+ }
+ }
+ attrs.style = attrs.style.substr(1) || undefined
+ // #ifdef (MP-WEIXIN || MP-QQ) && VUE3
+ for (const key in attrs) {
+ if (!attrs[key]) {
+ delete attrs[key]
+ }
+ }
+ // #endif
+}
+
+/**
+ * @description 解析到文本
+ * @param {String} text 文本内容
+ */
+Parser.prototype.onText = function (text) {
+ if (!this.pre) {
+ // 合并空白符
+ let trim = ''
+ let flag
+ for (let i = 0, len = text.length; i < len; i++) {
+ if (!blankChar[text[i]]) {
+ trim += text[i]
+ } else {
+ if (trim[trim.length - 1] !== ' ') {
+ trim += ' '
+ }
+ if (text[i] === '\n' && !flag) {
+ flag = true
+ }
+ }
+ }
+ // 去除含有换行符的空串
+ if (trim === ' ') {
+ if (flag) return
+ // #ifdef VUE3
+ else {
+ const parent = this.stack[this.stack.length - 1]
+ if (parent && parent.name[0] === 't') return
+ }
+ // #endif
+ }
+ text = trim
+ }
+ const node = Object.create(null)
+ node.type = 'text'
+ // #ifdef (MP-BAIDU || MP-ALIPAY || MP-TOUTIAO) && VUE3
+ node.attrs = {}
+ // #endif
+ node.text = decodeEntity(text)
+ if (this.hook(node)) {
+ // #ifdef MP-WEIXIN
+ if (this.options.selectable === 'force' && system.includes('iOS') && !uni.canIUse('rich-text.user-select')) {
+ this.expose()
+ }
+ // #endif
+ const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
+ siblings.push(node)
+ }
+}
+
+/**
+ * @description html 词法分析器
+ * @param {Object} handler 高层处理器
+ */
+function Lexer (handler) {
+ this.handler = handler
+}
+
+/**
+ * @description 执行解析
+ * @param {String} content 要解析的文本
+ */
+Lexer.prototype.parse = function (content) {
+ this.content = content || ''
+ this.i = 0 // 标记解析位置
+ this.start = 0 // 标记一个单词的开始位置
+ this.state = this.text // 当前状态
+ for (let len = this.content.length; this.i !== -1 && this.i < len;) {
+ this.state()
+ }
+}
+
+/**
+ * @description 检查标签是否闭合
+ * @param {String} method 如果闭合要进行的操作
+ * @returns {Boolean} 是否闭合
+ * @private
+ */
+Lexer.prototype.checkClose = function (method) {
+ const selfClose = this.content[this.i] === '/'
+ if (this.content[this.i] === '>' || (selfClose && this.content[this.i + 1] === '>')) {
+ if (method) {
+ this.handler[method](this.content.substring(this.start, this.i))
+ }
+ this.i += selfClose ? 2 : 1
+ this.start = this.i
+ this.handler.onOpenTag(selfClose)
+ if (this.handler.tagName === 'script') {
+ this.i = this.content.indexOf('', this.i)
+ if (this.i !== -1) {
+ this.i += 2
+ this.start = this.i
+ }
+ this.state = this.endTag
+ } else {
+ this.state = this.text
+ }
+ return true
+ }
+ return false
+}
+
+/**
+ * @description 文本状态
+ * @private
+ */
+Lexer.prototype.text = function () {
+ this.i = this.content.indexOf('<', this.i) // 查找最近的标签
+ if (this.i === -1) {
+ // 没有标签了
+ if (this.start < this.content.length) {
+ this.handler.onText(this.content.substring(this.start, this.content.length))
+ }
+ return
+ }
+ const c = this.content[this.i + 1]
+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
+ // 标签开头
+ if (this.start !== this.i) {
+ this.handler.onText(this.content.substring(this.start, this.i))
+ }
+ this.start = ++this.i
+ this.state = this.tagName
+ } else if (c === '/' || c === '!' || c === '?') {
+ if (this.start !== this.i) {
+ this.handler.onText(this.content.substring(this.start, this.i))
+ }
+ const next = this.content[this.i + 2]
+ if (c === '/' && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
+ // 标签结尾
+ this.i += 2
+ this.start = this.i
+ this.state = this.endTag
+ return
+ }
+ // 处理注释
+ let end = '-->'
+ if (c !== '!' || this.content[this.i + 2] !== '-' || this.content[this.i + 3] !== '-') {
+ end = '>'
+ }
+ this.i = this.content.indexOf(end, this.i)
+ if (this.i !== -1) {
+ this.i += end.length
+ this.start = this.i
+ }
+ } else {
+ this.i++
+ }
+}
+
+/**
+ * @description 标签名状态
+ * @private
+ */
+Lexer.prototype.tagName = function () {
+ if (blankChar[this.content[this.i]]) {
+ // 解析到标签名
+ this.handler.onTagName(this.content.substring(this.start, this.i))
+ while (blankChar[this.content[++this.i]]);
+ if (this.i < this.content.length && !this.checkClose()) {
+ this.start = this.i
+ this.state = this.attrName
+ }
+ } else if (!this.checkClose('onTagName')) {
+ this.i++
+ }
+}
+
+/**
+ * @description 属性名状态
+ * @private
+ */
+Lexer.prototype.attrName = function () {
+ let c = this.content[this.i]
+ if (blankChar[c] || c === '=') {
+ // 解析到属性名
+ this.handler.onAttrName(this.content.substring(this.start, this.i))
+ let needVal = c === '='
+ const len = this.content.length
+ while (++this.i < len) {
+ c = this.content[this.i]
+ if (!blankChar[c]) {
+ if (this.checkClose()) return
+ if (needVal) {
+ // 等号后遇到第一个非空字符
+ this.start = this.i
+ this.state = this.attrVal
+ return
+ }
+ if (this.content[this.i] === '=') {
+ needVal = true
+ } else {
+ this.start = this.i
+ this.state = this.attrName
+ return
+ }
+ }
+ }
+ } else if (!this.checkClose('onAttrName')) {
+ this.i++
+ }
+}
+
+/**
+ * @description 属性值状态
+ * @private
+ */
+Lexer.prototype.attrVal = function () {
+ const c = this.content[this.i]
+ const len = this.content.length
+ if (c === '"' || c === "'") {
+ // 有冒号的属性
+ this.start = ++this.i
+ this.i = this.content.indexOf(c, this.i)
+ if (this.i === -1) return
+ this.handler.onAttrVal(this.content.substring(this.start, this.i))
+ } else {
+ // 没有冒号的属性
+ for (; this.i < len; this.i++) {
+ if (blankChar[this.content[this.i]]) {
+ this.handler.onAttrVal(this.content.substring(this.start, this.i))
+ break
+ } else if (this.checkClose('onAttrVal')) return
+ }
+ }
+ while (blankChar[this.content[++this.i]]);
+ if (this.i < len && !this.checkClose()) {
+ this.start = this.i
+ this.state = this.attrName
+ }
+}
+
+/**
+ * @description 结束标签状态
+ * @returns {String} 结束的标签名
+ * @private
+ */
+Lexer.prototype.endTag = function () {
+ const c = this.content[this.i]
+ if (blankChar[c] || c === '>' || c === '/') {
+ this.handler.onCloseTag(this.content.substring(this.start, this.i))
+ if (c !== '>') {
+ this.i = this.content.indexOf('>', this.i)
+ if (this.i === -1) return
+ }
+ this.start = ++this.i
+ this.state = this.text
+ } else {
+ this.i++
+ }
+}
+
+export default Parser
diff --git a/src/components/mp-html/components/mp-html/style/index.js b/src/components/mp-html/components/mp-html/style/index.js
new file mode 100644
index 0000000..abfb371
--- /dev/null
+++ b/src/components/mp-html/components/mp-html/style/index.js
@@ -0,0 +1,129 @@
+/**
+ * @fileoverview style 插件
+ */
+// #ifndef APP-PLUS-NVUE
+import Parser from './parser'
+// #endif
+
+function Style () {
+ this.styles = []
+}
+
+// #ifndef APP-PLUS-NVUE
+Style.prototype.onParse = function (node, vm) {
+ // 获取样式
+ if (node.name === 'style' && node.children.length && node.children[0].type === 'text') {
+ this.styles = this.styles.concat(new Parser().parse(node.children[0].text))
+ } else if (node.name) {
+ // 匹配样式(对非文本标签)
+ // 存储不同优先级的样式 name < class < id < 后代
+ let matched = ['', '', '', '']
+ for (let i = 0, len = this.styles.length; i < len; i++) {
+ const item = this.styles[i]
+ let res = match(node, item.key || item.list[item.list.length - 1])
+ let j
+ if (res) {
+ // 后代选择器
+ if (!item.key) {
+ j = item.list.length - 2
+ for (let k = vm.stack.length; j >= 0 && k--;) {
+ // 子选择器
+ if (item.list[j] === '>') {
+ // 错误情况
+ if (j < 1 || j > item.list.length - 2) break
+ if (match(vm.stack[k], item.list[j - 1])) {
+ j -= 2
+ } else {
+ j++
+ }
+ } else if (match(vm.stack[k], item.list[j])) {
+ j--
+ }
+ }
+ res = 4
+ }
+ if (item.key || j < 0) {
+ // 添加伪类
+ if (item.pseudo && node.children) {
+ let text
+ item.style = item.style.replace(/content:([^;]+)/, (_, $1) => {
+ text = $1.replace(/['"]/g, '')
+ // 处理 attr 函数
+ .replace(/attr\((.+?)\)/, (_, $1) => node.attrs[$1.trim()] || '')
+ // 编码 \xxx
+ .replace(/\\(\w{4})/, (_, $1) => String.fromCharCode(parseInt($1, 16)))
+ return ''
+ })
+ const pseudo = {
+ name: 'span',
+ attrs: {
+ style: item.style
+ },
+ children: [{
+ type: 'text',
+ text
+ }]
+ }
+ if (item.pseudo === 'before') {
+ node.children.unshift(pseudo)
+ } else {
+ node.children.push(pseudo)
+ }
+ } else {
+ matched[res - 1] += item.style + (item.style[item.style.length - 1] === ';' ? '' : ';')
+ }
+ }
+ }
+ }
+ matched = matched.join('')
+ if (matched.length > 2) {
+ node.attrs.style = matched + (node.attrs.style || '')
+ }
+ }
+}
+
+/**
+ * @description 匹配样式
+ * @param {object} node 要匹配的标签
+ * @param {string|string[]} keys 选择器
+ * @returns {number} 0:不匹配;1:name 匹配;2:class 匹配;3:id 匹配
+ */
+function match (node, keys) {
+ function matchItem (key) {
+ if (key[0] === '#') {
+ // 匹配 id
+ if (node.attrs.id && node.attrs.id.trim() === key.substr(1)) return 3
+ } else if (key[0] === '.') {
+ // 匹配 class
+ key = key.substr(1)
+ const selectors = (node.attrs.class || '').split(' ')
+ for (let i = 0; i < selectors.length; i++) {
+ if (selectors[i].trim() === key) return 2
+ }
+ } else if (node.name === key) {
+ // 匹配 name
+ return 1
+ }
+ return 0
+ }
+
+ // 多选择器交集
+ if (keys instanceof Array) {
+ let res = 0
+ for (let j = 0; j < keys.length; j++) {
+ const tmp = matchItem(keys[j])
+ // 任意一个不匹配就失败
+ if (!tmp) return 0
+ // 优先级最大的一个作为最终优先级
+ if (tmp > res) {
+ res = tmp
+ }
+ }
+ return res
+ }
+
+ return matchItem(keys)
+}
+// #endif
+
+export default Style
diff --git a/src/components/mp-html/components/mp-html/style/parser.js b/src/components/mp-html/components/mp-html/style/parser.js
new file mode 100644
index 0000000..b639334
--- /dev/null
+++ b/src/components/mp-html/components/mp-html/style/parser.js
@@ -0,0 +1,175 @@
+const blank = {
+ ' ': true,
+ '\n': true,
+ '\t': true,
+ '\r': true,
+ '\f': true
+}
+
+function Parser () {
+ this.styles = []
+ this.selectors = []
+}
+
+/**
+ * @description 解析 css 字符串
+ * @param {string} content css 内容
+ */
+Parser.prototype.parse = function (content) {
+ new Lexer(this).parse(content)
+ return this.styles
+}
+
+/**
+ * @description 解析到一个选择器
+ * @param {string} name 名称
+ */
+Parser.prototype.onSelector = function (name) {
+ // 不支持的选择器
+ if (name.includes('[') || name.includes('*') || name.includes('@')) return
+ const selector = {}
+ // 伪类
+ if (name.includes(':')) {
+ const info = name.split(':')
+ const pseudo = info.pop()
+ if (pseudo === 'before' || pseudo === 'after') {
+ selector.pseudo = pseudo
+ name = info[0]
+ } else return
+ }
+
+ // 分割交集选择器
+ function splitItem (str) {
+ const arr = []
+ let i, start
+ for (i = 1, start = 0; i < str.length; i++) {
+ if (str[i] === '.' || str[i] === '#') {
+ arr.push(str.substring(start, i))
+ start = i
+ }
+ }
+ if (!arr.length) {
+ return str
+ } else {
+ arr.push(str.substring(start, i))
+ return arr
+ }
+ }
+
+ // 后代选择器
+ if (name.includes(' ')) {
+ selector.list = []
+ const list = name.split(' ')
+ for (let i = 0; i < list.length; i++) {
+ if (list[i].length) {
+ // 拆分子选择器
+ const arr = list[i].split('>')
+ for (let j = 0; j < arr.length; j++) {
+ selector.list.push(splitItem(arr[j]))
+ if (j < arr.length - 1) {
+ selector.list.push('>')
+ }
+ }
+ }
+ }
+ } else {
+ selector.key = splitItem(name)
+ }
+
+ this.selectors.push(selector)
+}
+
+/**
+ * @description 解析到选择器内容
+ * @param {string} content 内容
+ */
+Parser.prototype.onContent = function (content) {
+ // 并集选择器
+ for (let i = 0; i < this.selectors.length; i++) {
+ this.selectors[i].style = content
+ }
+ this.styles = this.styles.concat(this.selectors)
+ this.selectors = []
+}
+
+/**
+ * @description css 词法分析器
+ * @param {object} handler 高层处理器
+ */
+function Lexer (handler) {
+ this.selector = ''
+ this.style = ''
+ this.handler = handler
+}
+
+Lexer.prototype.parse = function (content) {
+ this.i = 0
+ this.content = content
+ this.state = this.blank
+ for (let len = content.length; this.i < len; this.i++) {
+ this.state(content[this.i])
+ }
+}
+
+Lexer.prototype.comment = function () {
+ this.i = this.content.indexOf('*/', this.i) + 1
+ if (!this.i) {
+ this.i = this.content.length
+ }
+}
+
+Lexer.prototype.blank = function (c) {
+ if (!blank[c]) {
+ if (c === '/' && this.content[this.i + 1] === '*') {
+ this.comment()
+ return
+ }
+ this.selector += c
+ this.state = this.name
+ }
+}
+
+Lexer.prototype.name = function (c) {
+ if (c === '/' && this.content[this.i + 1] === '*') {
+ this.comment()
+ return
+ }
+ if (c === '{' || c === ',' || c === ';') {
+ this.handler.onSelector(this.selector.trimEnd())
+ this.selector = ''
+ if (c !== '{') {
+ while (blank[this.content[++this.i]]);
+ }
+ if (this.content[this.i] === '{') {
+ this.floor = 1
+ this.state = this.val
+ } else {
+ this.selector += this.content[this.i]
+ }
+ } else if (blank[c]) {
+ this.selector += ' '
+ } else {
+ this.selector += c
+ }
+}
+
+Lexer.prototype.val = function (c) {
+ if (c === '/' && this.content[this.i + 1] === '*') {
+ this.comment()
+ return
+ }
+ if (c === '{') {
+ this.floor++
+ } else if (c === '}') {
+ this.floor--
+ if (!this.floor) {
+ this.handler.onContent(this.style)
+ this.style = ''
+ this.state = this.blank
+ return
+ }
+ }
+ this.style += c
+}
+
+export default Parser
diff --git a/src/components/mp-html/static/app-plus/mp-html/js/handler.js b/src/components/mp-html/static/app-plus/mp-html/js/handler.js
new file mode 100644
index 0000000..1d986bd
--- /dev/null
+++ b/src/components/mp-html/static/app-plus/mp-html/js/handler.js
@@ -0,0 +1 @@
+"use strict";function t(t){for(var e=Object.create(null),n=t.attributes.length;n--;)e[t.attributes[n].name]=t.attributes[n].value;return e}function e(){a[1]&&(this.src=a[1],this.onerror=null),this.onclick=null,this.ontouchstart=null,uni.postMessage({data:{action:"onError",source:"img",attrs:t(this)}})}function n(){window.unloadimgs-=1,0===window.unloadimgs&&uni.postMessage({data:{action:"onReady"}})}function o(r,s,c){for(var d=0;d0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("navigateTo",{url:encodeURI(n)})},navigateBack:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.delta;a("navigateBack",{delta:parseInt(n)||1})},switchTab:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("switchTab",{url:encodeURI(n)})},reLaunch:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("reLaunch",{url:encodeURI(n)})},redirectTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("redirectTo",{url:encodeURI(n)})},getEnv:function(e){window.plus?e({plus:!0}):e({h5:!0})},postMessage:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};a("postMessage",e.data||{})}},r=/uni-app/i.test(navigator.userAgent),d=/Html5Plus/i.test(navigator.userAgent),s=/complete|loaded|interactive/;var w=window.my&&navigator.userAgent.indexOf("AlipayClient")>-1;var u=window.swan&&window.swan.webView&&/swan/i.test(navigator.userAgent);var c=window.qq&&window.qq.miniProgram&&/QQ/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var g=window.tt&&window.tt.miniProgram&&/toutiaomicroapp/i.test(navigator.userAgent);var v=window.wx&&window.wx.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var p=window.qa&&/quickapp/i.test(navigator.userAgent);for(var l,_=function(){window.UniAppJSBridge=!0,document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady",{bubbles:!0,cancelable:!0}))},f=[function(e){if(r||d)return window.__dcloud_weex_postMessage||window.__dcloud_weex_?document.addEventListener("DOMContentLoaded",e):window.plus&&s.test(document.readyState)?setTimeout(e,0):document.addEventListener("plusready",e),o},function(e){if(v)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.wx.miniProgram},function(e){if(c)return window.QQJSBridge&&window.QQJSBridge.invoke?setTimeout(e,0):document.addEventListener("QQJSBridgeReady",e),window.qq.miniProgram},function(e){if(w){document.addEventListener("DOMContentLoaded",e);var n=window.my;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(u)return document.addEventListener("DOMContentLoaded",e),window.swan.webView},function(e){if(g)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(p){window.QaJSBridge&&window.QaJSBridge.invoke?setTimeout(e,0):document.addEventListener("QaJSBridgeReady",e);var n=window.qa;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){return document.addEventListener("DOMContentLoaded",e),o}],m=0;m.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}
\ No newline at end of file
diff --git a/src/components/uh-album-photo-viewer/uh-album-photo-viewer.vue b/src/components/uh-album-photo-viewer/uh-album-photo-viewer.vue
new file mode 100644
index 0000000..debd5a9
--- /dev/null
+++ b/src/components/uh-album-photo-viewer/uh-album-photo-viewer.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 照片正在努力加载中啦~
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ photo.title }}
+
+
+ {{ photo.takenDate }}
+ {{ photo.location }}
+
+
+ {{ photo.description }}
+
+
+
+
+
+
+
+
+
+
+ {{ photo.title }}
+
+
+ {{ photo.takenDate }}
+ {{ photo.location }}
+
+
+ {{ photo.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/uh-album-unlock-modal/uh-album-unlock-modal.vue b/src/components/uh-album-unlock-modal/uh-album-unlock-modal.vue
new file mode 100644
index 0000000..18125d0
--- /dev/null
+++ b/src/components/uh-album-unlock-modal/uh-album-unlock-modal.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+ 🔒
+
+
+ {{ albumName }}
+
+
+ 此相册已加密,请输入密码查看
+
+
+
+
+
+
+
+
diff --git a/src/components/uh-article-card/uh-article-card.vue b/src/components/uh-article-card/uh-article-card.vue
new file mode 100644
index 0000000..a0b6d76
--- /dev/null
+++ b/src/components/uh-article-card/uh-article-card.vue
@@ -0,0 +1,339 @@
+
+
+
+
+
+
+
+
+
+ 置顶
+ {{ article.spec.title }}
+
+
+ {{ article.status?.excerpt }}
+
+
+
+
+
+
+
diff --git a/src/components/uh-article-min-card/uh-article-min-card.vue b/src/components/uh-article-min-card/uh-article-min-card.vue
new file mode 100644
index 0000000..317813c
--- /dev/null
+++ b/src/components/uh-article-min-card/uh-article-min-card.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+ {{ article.spec.title }}
+
+
+ {{ article.status?.excerpt }}
+
+
+
+
+
+
+
diff --git a/src/components/uh-audio-player/uh-audio-player.vue b/src/components/uh-audio-player/uh-audio-player.vue
new file mode 100644
index 0000000..84f7188
--- /dev/null
+++ b/src/components/uh-audio-player/uh-audio-player.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ name || '音频' }}
+
+
+
+ {{ isPlaying ? '播放中' : (duration ? '已就绪' : '待播放') }}
+
+
+
+
+
+
+
+
diff --git a/src/components/uh-category-mini-card/uh-category-mini-card.vue b/src/components/uh-category-mini-card/uh-category-mini-card.vue
new file mode 100644
index 0000000..b134f61
--- /dev/null
+++ b/src/components/uh-category-mini-card/uh-category-mini-card.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+ {{ category.spec.displayName }}
+
+ 共 {{ category.postCount ?? 0 }} 篇
+
+
+
+
+
diff --git a/src/components/uh-comment-item/uh-comment-item.vue b/src/components/uh-comment-item/uh-comment-item.vue
new file mode 100644
index 0000000..f7e5659
--- /dev/null
+++ b/src/components/uh-comment-item/uh-comment-item.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
diff --git a/src/components/uh-comment-list/uh-comment-list.vue b/src/components/uh-comment-list/uh-comment-list.vue
new file mode 100644
index 0000000..794d7a3
--- /dev/null
+++ b/src/components/uh-comment-list/uh-comment-list.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
diff --git a/src/components/uh-comment-modal/uh-comment-modal.vue b/src/components/uh-comment-modal/uh-comment-modal.vue
new file mode 100644
index 0000000..19386ff
--- /dev/null
+++ b/src/components/uh-comment-modal/uh-comment-modal.vue
@@ -0,0 +1,321 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/uh-heatmap/uh-heatmap.vue b/src/components/uh-heatmap/uh-heatmap.vue
new file mode 100644
index 0000000..be04ef3
--- /dev/null
+++ b/src/components/uh-heatmap/uh-heatmap.vue
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
+ {{ week }}
+
+
+
+
+
+ {{ month.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/uh-notify-dialog/uh-notify-dialog.vue b/src/components/uh-notify-dialog/uh-notify-dialog.vue
new file mode 100644
index 0000000..d5e166a
--- /dev/null
+++ b/src/components/uh-notify-dialog/uh-notify-dialog.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+ 点击跳转内容链接
+
+
+
+
+
+
+
diff --git a/src/components/uh-plugin-unavailable/uh-plugin-unavailable.vue b/src/components/uh-plugin-unavailable/uh-plugin-unavailable.vue
new file mode 100644
index 0000000..03dd947
--- /dev/null
+++ b/src/components/uh-plugin-unavailable/uh-plugin-unavailable.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+ {{ pluginInfo.name }}
+
+
+
+ 未安装/启用插件
+
+
+
+ {{ pluginInfo.desc }}
+
+
+
+ {{ errorText }}
+
+
+
+ 插件地址:{{ pluginInfo.url }}
+
+
+
+
+
+ 复制地址
+
+
+
+
+
+ 复制地址
+
+
+ 提交反馈
+
+
+
+
+
+
+
+ 刷新试试
+
+
+
+
+ 提示:请确保 Halo 博客已安装相关插件
+
+
+
+
+
diff --git a/src/components/uh-restrict-read-skeleton/uh-restrict-read-skeleton.vue b/src/components/uh-restrict-read-skeleton/uh-restrict-read-skeleton.vue
new file mode 100644
index 0000000..1d54c25
--- /dev/null
+++ b/src/components/uh-restrict-read-skeleton/uh-restrict-read-skeleton.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tipText }}
+
+
+
+
+
+
+
diff --git a/src/components/uh-swiper/uh-swiper.vue b/src/components/uh-swiper/uh-swiper.vue
new file mode 100644
index 0000000..a44f871
--- /dev/null
+++ b/src/components/uh-swiper/uh-swiper.vue
@@ -0,0 +1,359 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ date.month }}
+
+
+
+
+ {{ date.monthEn }}
+ {{ date.year }}
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+ ▶
+
+ 视频预览
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/uh-vote-card/uh-vote-card.vue b/src/components/uh-vote-card/uh-vote-card.vue
new file mode 100644
index 0000000..dd7a528
--- /dev/null
+++ b/src/components/uh-vote-card/uh-vote-card.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+ {{ voteData.title }}
+
+
+ {{ voteData.description }}
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
+
+
+
+
+ {{ voteLabel }}
+
+
+
+ {{ voteLabel }}{{ voteResultLabel }}
+
+
+
+
+
+
diff --git a/src/config/appConfig.ts b/src/config/appConfig.ts
new file mode 100644
index 0000000..b6547da
--- /dev/null
+++ b/src/config/appConfig.ts
@@ -0,0 +1,55 @@
+/**
+ * 应用配置默认值(源自旧项目 config/index.js 的 DefaultAppConfigs)
+ * 与 src/api/uni-halo.ts 的 getAppConfigs(plugin-uni-halo/getConfigs)配合,deepMerge 使用
+ * 页面/组件依赖其字段结构(pluginConfig.toolsPlugin.Authorization 等),改动需谨慎
+ */
+import type { IAppConfig } from '@/api/types/uni-halo'
+
+export const DefaultAppConfigs: IAppConfig = {
+ basicConfig: {
+ tokenConfig: {
+ personalToken: '',
+ },
+ },
+ loveConfig: {},
+ imagesConfig: {},
+ authorConfig: {},
+ appConfig: {},
+ pluginConfig: {
+ votePlugin: {},
+ toolsPlugin: {},
+ linksPlugin: {},
+ linksSubmitPlugin: {},
+ doubanPlugin: {
+ position: 'bottom',
+ },
+ },
+ pageConfig: {
+ homeConfig: {
+ pageTitle: '首页',
+ useCategory: true,
+ bannerConfig: {
+ enabled: true,
+ showTitle: true,
+ showIndicator: true,
+ height: '400rpx',
+ dotPosition: 'right',
+ type: 'post',
+ list: [],
+ },
+ },
+ categoryConfig: {
+ type: 'list',
+ },
+ momentConfig: {
+ useTagRandomColor: true,
+ },
+ },
+ auditConfig: {
+ auditModeEnabled: false,
+ auditModeData: {
+ jsonUrl: '',
+ jsonData: '',
+ },
+ },
+}
diff --git a/src/config/appSettings.ts b/src/config/appSettings.ts
new file mode 100644
index 0000000..af06aa2
--- /dev/null
+++ b/src/config/appSettings.ts
@@ -0,0 +1,91 @@
+/**
+ * 应用设置默认值与类型(源自旧项目 utils/app.js 的 _DefaultAppSettings)
+ */
+
+export interface IAppSettings {
+ /** 是否每次启动都显示启动页 */
+ showStartPage: boolean
+ /** 评论头像是否圆形 */
+ isAvatarRadius: boolean
+ banner: {
+ useDot: boolean
+ dotPosition: string
+ }
+ /** 布局配置 */
+ layout: {
+ /** h_row_col1 = 一行一列 / h_row_col2 = 一行两列 */
+ home: string
+ /** lr_image_text=左图右文 / lr_text_image=左文右图 / tb_image_text=上图下文 / tb_text_image=上文下图 / only_text=仅文字 */
+ cardType: string
+ }
+ /** 广告配置 */
+ ad: {
+ /** 屏蔽广告时长,时间到后自动恢复展示(单位小时) */
+ timeout: number
+ /** 是否屏蔽广告 */
+ disabled: boolean
+ }
+ /** 评论弹幕(文章详情) */
+ barrage: {
+ use: boolean
+ /** 弹幕位置(rightToLeft / leftBottom) */
+ type: string
+ }
+ gallery: {
+ /** 是否使用瀑布流 */
+ useWaterfull: boolean
+ }
+ links: {
+ useSimple: boolean
+ useGroup: boolean
+ }
+ about: {
+ /** 显示后台登录入口 */
+ showAdmin: boolean
+ /** 显示所有的统计信息(关于页面) */
+ showAllCount: boolean
+ }
+ /** 文章配置 */
+ article: Record
+ /** 联系博主页面 */
+ contact: {
+ /** 链接是否使用复制的方式,否则直接在内部打开 */
+ isLinkCopy: boolean
+ }
+}
+
+export const DefaultAppSettings: IAppSettings = {
+ showStartPage: false,
+ isAvatarRadius: false,
+ banner: {
+ useDot: true,
+ dotPosition: 'right',
+ },
+ layout: {
+ home: 'h_row_col1',
+ cardType: 'lr_image_text',
+ },
+ ad: {
+ timeout: 3,
+ disabled: false,
+ },
+ barrage: {
+ use: false,
+ type: 'leftBottom',
+ },
+ gallery: {
+ useWaterfull: true,
+ },
+ links: {
+ useSimple: false,
+ useGroup: false,
+ },
+ about: {
+ showAdmin: false,
+ showAllCount: false,
+ },
+ article: {},
+ contact: {
+ isLinkCopy: true,
+ },
+}
diff --git a/src/config/haloGlobal.ts b/src/config/haloGlobal.ts
new file mode 100644
index 0000000..9ae0166
--- /dev/null
+++ b/src/config/haloGlobal.ts
@@ -0,0 +1,14 @@
+/**
+ * Halo 全局配置默认值(源自旧项目 config/halo.config.js)
+ * 与 src/api/uni-halo.ts 的 getHaloGlobalInfo(/actuator/globalinfo)配合,deepMerge 使用
+ */
+import type { IHaloGlobalConfig } from '@/api/types/uni-halo'
+
+export const DefaultHaloGlobalConfigs: IHaloGlobalConfig = {
+ allowAnonymousComments: true,
+ allowRegistration: true,
+ mustVerifyEmailOnRegistration: true,
+ mustVerifyPhoneOnRegistration: false,
+ restrictRegistrationDomain: false,
+ shopDisabled: true,
+}
diff --git a/src/config/markdown.ts b/src/config/markdown.ts
new file mode 100644
index 0000000..a92afb8
--- /dev/null
+++ b/src/config/markdown.ts
@@ -0,0 +1,102 @@
+/**
+ * mp-html 富文本 / markdown 渲染业务配置
+ * 源自旧项目 common/markdown/markdown.config.js,TS 化复刻(业务封装新建,插件本体走 npm mp-html)
+ * 用法:
+ */
+import { checkImageUrl } from '@/utils/url'
+
+export const markdownConfig = {
+ /** 图片域名前缀(mp-html 相对路径补全,源自旧配置 domain: BASE_API) */
+ domain: import.meta.env.VITE_SERVER_BASEURL || '',
+ /** 标签样式定制(表格/引用/标题/代码等) */
+ tagStyle: {
+ table: `
+ table-layout: fixed;
+ border-collapse:collapse;
+ margin-bottom: 18px;
+ overflow: hidden;
+ font-size: 13px;
+ color: var(--routine);
+ background: #f2f6fc;
+ border: 1px solid #dcdcdc;
+ border-radius: 4px;
+ `,
+ th: `
+ padding: 8px;
+ border-right: 1px solid var(--classE);
+ border-bottom: 1px solid var(--classE);
+ `,
+ td: `
+ padding: 8px;
+ border-right: 1px solid var(--classE);
+ border-bottom: 1px solid var(--classE);
+ `,
+ blockquote: `
+ padding: 8px 15px;
+ color: #606266;
+ background: #f2f6fc;
+ border-left: 5px solid #50bfff;
+ border-radius: 4px;
+ line-height: 26px;
+ margin-bottom: 18px;
+ `,
+ ul: 'padding-left: 15px;line-height: 1.85;',
+ ol: 'padding-left: 15px;line-height: 1.85;',
+ li: 'margin-bottom: 12px;line-height: 1.85;',
+ h1: `
+ margin: 30px 0 20px;
+ color: var(--main);
+ line-height: 24px;
+ position: relative;
+ font-size:1.2em;
+ `,
+ h2: `
+ color: var(--main);
+ line-height: 24px;
+ position: relative;
+ margin: 22px 0 16px;
+ font-size: 1.16em;
+ `,
+ h3: `
+ color: var(--main);
+ line-height: 24px;
+ position: relative;
+ margin: 26px 0 18px;
+ font-size: 1.14em;
+ `,
+ h4: `
+ color: var(--main);
+ line-height: 24px;
+ margin-bottom: 18px;
+ position: relative;
+ font-size: 1.12em;
+ `,
+ h5: `
+ color: var(--main);
+ line-height: 24px;
+ margin-bottom: 14px;
+ position: relative;
+ font-size: 1.1em;
+ `,
+ h6: `
+ color: #303133;
+ line-height: 24px;
+ margin-bottom: 14px;
+ position: relative;
+ font-size: 14px;
+ `,
+ p: `
+ line-height: 1.65;
+ margin-bottom: 14px;
+ font-size: 14px;
+ `,
+ code: ' ',
+ strong: 'font-weight: 700;color: rgb(248, 57, 41);',
+ 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;',
+ /** 加载图 / 空图(来自应用配置 imagesConfig) */
+ loadingGif: checkImageUrl(undefined),
+ emptyGif: checkImageUrl(undefined),
+}
diff --git a/src/http/alova.ts b/src/http/alova.ts
index b7b9ff6..2da1a57 100644
--- a/src/http/alova.ts
+++ b/src/http/alova.ts
@@ -1,119 +1,128 @@
-import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
-import type { IResponse } from './types'
-import AdapterUniapp from '@alova/adapter-uniapp'
-import { createAlova } from 'alova'
-import { createServerTokenAuthentication } from 'alova/client'
-import VueHook from 'alova/vue'
-import { toLoginPage } from '@/utils/toLoginPage'
-import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
+import type { uniappRequestAdapter } from '@alova/adapter-uniapp';
+import type { IResponse } from './types';
+import AdapterUniapp from '@alova/adapter-uniapp';
+import { createAlova } from 'alova';
+import { createServerTokenAuthentication } from 'alova/client';
+import VueHook from 'alova/vue';
+import { toLoginPage } from '@/utils/toLoginPage';
+import { ContentTypeEnum, RequestFrom, ResultEnum, ShowMessage } from './tools/enum';
+import { saveCommentCookies } from './tools/commentCookies';
+import { handleCategoryPasswordError } from './tools/categoryPassword';
// 配置动态Tag
export const API_DOMAINS = {
- DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
- SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY,
-}
+ DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
+ SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY
+};
/**
* 创建请求实例
*/
-const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
- typeof VueHook,
- typeof uniappRequestAdapter
->({
- // 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
- refreshTokenOnError: {
- isExpired: (error) => {
- return error.response?.status === ResultEnum.Unauthorized
- },
- handler: async () => {
- try {
- // await authLogin();
- }
- catch (error) {
- // 切换到登录页
- toLoginPage({ mode: 'reLaunch' })
- throw error
- }
- },
- },
-})
+const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({
+ // 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
+ refreshTokenOnError: {
+ isExpired: (error) => {
+ return error.response?.status === ResultEnum.Unauthorized;
+ },
+ handler: async () => {
+ try {
+ // await authLogin();
+ } catch (error) {
+ // 切换到登录页
+ toLoginPage({ mode: 'reLaunch' });
+ throw error;
+ }
+ }
+ }
+});
/**
* alova 请求实例
*/
const alovaInstance = createAlova({
- baseURL: API_DOMAINS.DEFAULT,
- ...AdapterUniapp(),
- timeout: 5000,
- statesHook: VueHook,
+ baseURL: API_DOMAINS.DEFAULT,
+ ...AdapterUniapp(),
+ timeout: 5000,
+ statesHook: VueHook,
- beforeRequest: onAuthRequired((method) => {
- // 设置默认 Content-Type
- method.config.headers = {
- ContentType: ContentTypeEnum.JSON,
- Accept: 'application/json, text/plain, */*',
- ...method.config.headers,
- }
+ beforeRequest: onAuthRequired((method) => {
+ // 设置默认 Content-Type
+ method.config.headers = {
+ ContentType: ContentTypeEnum.JSON,
+ Accept: 'application/json, text/plain, */*',
+ ...method.config.headers
+ };
- const { config } = method
- const ignoreAuth = !config.meta?.ignoreAuth
- console.log('ignoreAuth===>', ignoreAuth)
- // 处理认证信息 自行处理认证问题
- if (ignoreAuth) {
- const token = 'getToken()'
- if (!token) {
- throw new Error('[请求错误]:未登录')
- }
- // method.config.headers.token = token;
- }
+ const { config } = method;
+ const ignoreAuth = !config.meta?.ignoreAuth;
+ console.log('ignoreAuth===>', ignoreAuth);
+ // 处理认证信息 自行处理认证问题
+ if (ignoreAuth) {
+ const token = 'getToken()';
+ if (!token) {
+ throw new Error('[请求错误]:未登录');
+ }
+ // method.config.headers.token = token;
+ }
- // 处理动态域名
- if (config.meta?.domain) {
- method.baseURL = config.meta.domain
- console.log('当前域名', method.baseURL)
- }
- }),
+ if (config.meta?.personalToken) {
+ config.headers['Authorization'] = `Bearer ${config.meta.personalToken}`;
+ }
- responded: onResponseRefreshToken((response, method) => {
- const { config } = method
- const { requestType } = config
- const {
- statusCode,
- data: rawData,
- errMsg,
- } = response as UniNamespace.RequestSuccessCallbackResult
+ // 处理动态域名
+ if (config.meta?.domain) {
+ method.baseURL = config.meta.domain;
+ console.log('当前域名', method.baseURL);
+ }
+ }),
- // 处理特殊请求类型(上传/下载)
- if (requestType === 'upload' || requestType === 'download') {
- return response
- }
+ responded: onResponseRefreshToken((response, method) => {
+ console.log('response===>', response);
+ console.log('method===>', method);
+ const { config } = method;
+ const { requestType } = config;
+ const { statusCode, data: rawData, errMsg, header } = response as UniNamespace.RequestSuccessCallbackResult;
- // 处理 HTTP 状态码错误
- if (statusCode !== 200) {
- const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
- console.error('errorMessage===>', errorMessage)
- uni.showToast({
- title: errorMessage,
- icon: 'error',
- })
- throw new Error(`${errorMessage}:${errMsg}`)
- }
+ // 处理特殊请求类型(上传/下载)
+ if (requestType === 'upload' || requestType === 'download') {
+ return response;
+ }
- // 处理业务逻辑错误
- const { code, message, data } = rawData as IResponse
- // 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
- if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
- if (config.meta?.toast !== false) {
- uni.showToast({
- title: message,
- icon: 'none',
- })
- }
- throw new Error(`请求错误[${code}]:${message}`)
- }
- // 处理成功响应,返回业务数据
- return data
- }),
-})
+ // 保存评论验证码 cookie
+ saveCommentCookies(method.url, header as Record | undefined);
-export const http = alovaInstance
+ // 分类加密密码处理(Halo 私密分类 401/403)
+ // 注意:此处需在 alova 统一 401 刷新之前消费,避免分类密码请求被误判为登录过期
+ const isCategoryConsumed = handleCategoryPasswordError(statusCode, method.url, rawData as { status?: number });
+ if (isCategoryConsumed) {
+ throw new Error('分类访问受限');
+ }
+
+ // 处理 HTTP 状态码错误
+ if (statusCode !== 200) {
+ const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`;
+ console.error('errorMessage===>', errorMessage);
+ uni.showToast({
+ title: errorMessage,
+ icon: 'error'
+ });
+ throw new Error(`${errorMessage}:${errMsg}`);
+ }
+ // 归一化响应结构:Halo 来源的原始数据统一封装为 { code, data, message },
+ // 与标准接口走同一条解析路径,保证响应模型一致
+ let responseData: IResponse;
+ if (config.meta?.requestFrom === RequestFrom.Halo) {
+ responseData = {
+ code: ResultEnum.Success200,
+ data: rawData,
+ message: '请求成功'
+ };
+ } else {
+ responseData = rawData as IResponse;
+ }
+
+ return responseData;
+ })
+});
+
+export const http = alovaInstance;
diff --git a/src/http/interceptor.ts b/src/http/interceptor.ts
index bcd0f87..c106209 100644
--- a/src/http/interceptor.ts
+++ b/src/http/interceptor.ts
@@ -1,69 +1,75 @@
-import type { CustomRequestOptions } from '@/http/types'
-import { useTokenStore } from '@/store'
-import { getEnvBaseUrl } from '@/utils'
-import { stringifyQuery } from './tools/queryString'
+import type { CustomRequestOptions } from '@/http/types';
+import { useTokenStore } from '@/store';
+import { getEnvBaseUrl } from '@/utils';
+import { stringifyQuery } from './tools/queryString';
+import qs from 'qs';
// 请求基准地址
-const baseUrl = getEnvBaseUrl()
+const baseUrl = getEnvBaseUrl();
// 拦截器配置
const httpInterceptor = {
- // 拦截前触发
- invoke(options: CustomRequestOptions) {
- // 如果您使用了alova,则请把下面的代码放开注释
- // alova 执行流程:alova beforeRequest --> 本拦截器 --> alova responded
- // return options
+ // 拦截前触发
+ invoke(options: CustomRequestOptions) {
+ // 如果您使用了alova,则请把下面的代码放开注释
+ // alova 执行流程:alova beforeRequest --> 本拦截器 --> alova responded
+ // return options
- // 非 alova 请求,正常执行
- // 接口请求支持通过 query 参数配置 queryString
- if (options.query) {
- const queryStr = stringifyQuery(options.query)
- if (options.url.includes('?')) {
- options.url += `&${queryStr}`
- }
- else {
- options.url += `?${queryStr}`
- }
- }
- // 非 http 开头需拼接地址
- if (!options.url.startsWith('http')) {
- // #ifdef H5
- if (JSON.parse(import.meta.env.VITE_APP_PROXY_ENABLE)) {
- // 自动拼接代理前缀
- options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
- }
- else {
- options.url = baseUrl + options.url
- }
- // #endif
- // 非H5正常拼接
- // #ifndef H5
- options.url = baseUrl + options.url
- // #endif
- // TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
- }
- // 1. 请求超时
- options.timeout = 60000 // 60s
- // 2. (可选)添加小程序端请求头标识
- options.header = {
- ...options.header,
- }
- // 3. 添加 token 请求头标识
- const tokenStore = useTokenStore()
- const token = tokenStore.updateNowTime().validToken
+ // 非 alova 请求,正常执行
+ // 接口请求支持通过 query 参数配置 queryString
+ if (options.query) {
+ const queryStr = stringifyQuery(options.query)
+ // const queryStr = qs.stringify(options.query, {
+ // allowDots: true,
+ // encodeValuesOnly: true,
+ // skipNulls: true,
+ // encode: true,
+ // arrayFormat: 'repeat'
+ // });
+ if (options.url.includes('?')) {
+ options.url += `&${queryStr}`;
+ } else {
+ options.url += `?${queryStr}`;
+ }
+ }
+ // 非 http 开头需拼接地址
+ if (!options.url.startsWith('http')) {
+ // #ifdef H5
+ if (JSON.parse(import.meta.env.VITE_APP_PROXY_ENABLE)) {
+ // 自动拼接代理前缀
+ options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url;
+ } else {
+ options.url = baseUrl + options.url;
+ }
+ // #endif
+ // 非H5正常拼接
+ // #ifndef H5
+ options.url = baseUrl + options.url;
+ // #endif
+ // TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
+ }
+ // 1. 请求超时
+ options.timeout = 60000; // 60s
+ // 2. (可选)添加小程序端请求头标识
+ options.header = {
+ ...options.header
+ };
+ // 3. 添加 token 请求头标识
+ const tokenStore = useTokenStore();
+ const token = tokenStore.updateNowTime().validToken;
- if (token) {
- options.header.Authorization = `Bearer ${token}`
- }
- return options
- },
-}
+ if (token) {
+ options.header.Authorization = `Bearer ${token}`;
+ }
+ return options;
+ }
+};
export const requestInterceptor = {
- install() {
- // 拦截 request 请求
- uni.addInterceptor('request', httpInterceptor)
- // 拦截 uploadFile 文件上传
- uni.addInterceptor('uploadFile', httpInterceptor)
- },
-}
+ install() {
+ // 拦截 request 请求
+ uni.addInterceptor('request', httpInterceptor);
+ // 拦截 uploadFile 文件上传
+ uni.addInterceptor('uploadFile', httpInterceptor);
+ }
+};
diff --git a/src/http/tools/categoryPassword.ts b/src/http/tools/categoryPassword.ts
new file mode 100644
index 0000000..705de6c
--- /dev/null
+++ b/src/http/tools/categoryPassword.ts
@@ -0,0 +1,97 @@
+/**
+ * 分类加密密码逻辑(源自旧项目 common/http/interceptors.js 的
+ * showCategoryInputPasswordModal / 401/403 分类密码分支)
+ * Halo 私密分类:请求分类文章接口 401/403 时触发密码输入
+ */
+import { delCache, setCache } from '@/utils/storage'
+
+/** 分类密码缓存前缀 */
+const CATEGORY_PWD_PREFIX = 'APP_CATEGORY_PWD_'
+
+/** 分类文章接口路径特征(用于判断请求是否为分类相关) */
+const CATEGORY_API_REG = /\/api\/content\/categories\/.+\/posts/
+
+/**
+ * 从分类接口 URL 提取分类 code
+ */
+function getCategoryNameByUrl(url: string): string {
+ const reg = '(?<=/api/content/categories/).+(?=/posts)'
+ const m = url.match(reg)
+ return m?.[0] || '无分类名'
+}
+
+/**
+ * 分类密码输入弹窗
+ */
+function showCategoryInputPasswordModal(url: string, category: string): void {
+ uni.showModal({
+ title: `[ ${category} ] 分类已加密`,
+ content: '',
+ editable: true,
+ placeholderText: '请输入分类密码后访问',
+ confirmText: '验证密码',
+ cancelText: '暂不访问',
+ showCancel: true,
+ cancelColor: '#999999',
+ confirmColor: '#03a9f4',
+ success: (res) => {
+ if (res.confirm) {
+ if (!res.content) {
+ uni.showToast({
+ title: '提示:请输入密码',
+ icon: 'none',
+ success: () => {
+ setTimeout(() => {
+ showCategoryInputPasswordModal(url, category)
+ }, 800)
+ },
+ })
+ return
+ }
+ // 按分类 code 缓存密码,解决多分类加密时密码串用问题
+ setCache(CATEGORY_PWD_PREFIX + category, res.content)
+ uni.reLaunch({
+ url: '/pages/tabbar/category/category',
+ })
+ }
+ },
+ })
+}
+
+/**
+ * 处理分类加密相关 401/403 响应
+ * @param statusCode HTTP 状态码
+ * @param url 请求 URL
+ * @param data 响应体(含业务 status)
+ * @returns 是否已由本模块消费该错误(调用方应直接终止)
+ */
+export function handleCategoryPasswordError(statusCode: number, url: string, data: { status?: number }): boolean {
+ const isCategoryApi = CATEGORY_API_REG.test(url)
+ if (!isCategoryApi)
+ return false
+
+ const category = getCategoryNameByUrl(url)
+
+ if (statusCode === 401) {
+ // 密码错误:清除该分类密码,下次点击再弹窗
+ delCache(CATEGORY_PWD_PREFIX + category)
+ uni.showToast({
+ title: '提示:密码不正确',
+ icon: 'none',
+ success: () => {
+ setTimeout(() => {
+ showCategoryInputPasswordModal(url, category)
+ }, 800)
+ },
+ })
+ return true
+ }
+
+ if (statusCode === 403) {
+ // 私密分类,需要输入密码
+ showCategoryInputPasswordModal(url, category)
+ return true
+ }
+
+ return false
+}
diff --git a/src/http/tools/commentCookies.ts b/src/http/tools/commentCookies.ts
new file mode 100644
index 0000000..cc92b3d
--- /dev/null
+++ b/src/http/tools/commentCookies.ts
@@ -0,0 +1,32 @@
+/**
+ * 评论验证码 cookie 保存(源自旧项目 common/http/interceptors.js 的 saveCookies)
+ * 仅 APP/微信小程序端需要手动存取 cookie(H5 浏览器自动管理)
+ */
+import { setCache } from '@/utils/storage'
+import { extractCookieItem, getHeaderCaseInsensitive } from '@/utils/cookies'
+
+/** 评论验证码 cookie key */
+export const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha'
+
+/** 需要保存 cookie 的评论相关接口 */
+const MATCH_COMMENT_APIS = [
+ '/apis/api.commentwidget.halo.run/v1alpha1/captcha/-/generate',
+ '/apis/api.halo.run/v1alpha1/comments',
+]
+
+/**
+ * 从响应头保存评论验证码 cookie
+ * @param url 请求地址
+ * @param header 响应头
+ */
+export function saveCommentCookies(url: string, header: Record | undefined): void {
+ // app/小程序 将 cookies 存储起来
+ // #ifdef MP-WEIXIN || APP-PLUS
+ if (MATCH_COMMENT_APIS.some(api => url.includes(api))) {
+ const cookies = getHeaderCaseInsensitive(header, 'set-cookie')
+ if (cookies) {
+ setCache(COMMENT_WIDGET_CAPTCHA_COOKIES, extractCookieItem(cookies, COMMENT_WIDGET_CAPTCHA_COOKIES))
+ }
+ }
+ // #endif
+}
diff --git a/src/http/tools/enum.ts b/src/http/tools/enum.ts
index 2e19f5a..7117324 100644
--- a/src/http/tools/enum.ts
+++ b/src/http/tools/enum.ts
@@ -30,6 +30,16 @@ export enum HttpErrorType {
Network = 'network',
}
+/**
+ * 请求来源(通过请求 meta.requestFrom 标记,决定响应归一化方式)
+ * Halo: Halo 官方/插件接口,响应为原始数据(rawData),拦截器统一封装为 { code, data, message } 后透传
+ * Standard: 标准接口,响应本身即为 { code, data, message } 结构
+ */
+export enum RequestFrom {
+ Halo = 'halo',
+ Standard = 'standard',
+}
+
export function isSuccessResultCode(code: number): boolean {
return [ResultEnum.Success0, ResultEnum.Success200].includes(code)
}
diff --git a/src/locale/en.json b/src/locale/en.json
index f2a758f..b19cd70 100644
--- a/src/locale/en.json
+++ b/src/locale/en.json
@@ -1,8 +1,24 @@
{
"tabbar.home": "Home",
+ "tabbar.category": "Category",
+ "tabbar.gallery": "Gallery",
+ "tabbar.moments": "Moments",
"tabbar.about": "About",
"tabbar.me": "Me",
"tabbar.i18n": "I18n",
+ "page.home.title": "Home",
+ "page.category.title": "Category",
+ "page.gallery.title": "Gallery",
+ "page.moments.title": "Moments",
+ "page.about.title": "About",
+ "common.loading": "Loading...",
+ "common.loadMore": "Pull up to load more",
+ "common.noMore": "No more data~",
+ "common.loadFailed": "Load failed, please pull down to refresh!",
+ "common.loadFailedShort": "Load failed!",
+ "common.loadFailedRetry": "Failed to load data, please try again!",
+ "common.empty": "No data",
+ "common.noMoreData": "No more data",
"i18n.title": "En Title",
"alova.title": "Alova Request",
"weight": "{heavy}KG",
diff --git a/src/locale/zh-Hans.json b/src/locale/zh-Hans.json
index dfb655d..fb6a30b 100644
--- a/src/locale/zh-Hans.json
+++ b/src/locale/zh-Hans.json
@@ -1,8 +1,24 @@
{
"tabbar.home": "首页",
+ "tabbar.category": "分类",
+ "tabbar.gallery": "图库",
+ "tabbar.moments": "瞬间",
"tabbar.about": "关于",
"tabbar.me": "我的",
"tabbar.i18n": "多语言",
+ "page.home.title": "首页",
+ "page.category.title": "分类",
+ "page.gallery.title": "图库",
+ "page.moments.title": "瞬间",
+ "page.about.title": "关于",
+ "common.loading": "加载中...",
+ "common.loadMore": "上拉加载更多",
+ "common.noMore": "呜呜,没有更多数据啦~",
+ "common.loadFailed": "加载失败,请下拉刷新!",
+ "common.loadFailedShort": "加载失败!",
+ "common.loadFailedRetry": "数据加载失败,请重试!",
+ "common.empty": "暂无数据",
+ "common.noMoreData": "没有更多数据了",
"i18n.title": "中文标题",
"alova.title": "Alova 请求",
"weight": "{heavy}公斤",
diff --git a/src/pages-blog/about/about.vue b/src/pages-blog/about/about.vue
new file mode 100644
index 0000000..9108c08
--- /dev/null
+++ b/src/pages-blog/about/about.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+ uni-halo
+
+
+
+
+ {{ link.title }}
+ {{ link.value }}
+
+
+
+
+ 根据 AGPL-3.0 协议开源
+
+ 「 2022 uni-halo 丨 开源项目丨巷子工坊@小莫唐尼 」
+
+
+
+
diff --git a/src/pages-blog/archives/archives.vue b/src/pages-blog/archives/archives.vue
new file mode 100644
index 0000000..8b92cf4
--- /dev/null
+++ b/src/pages-blog/archives/archives.vue
@@ -0,0 +1,380 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.year }}年
+ {{ item.month }}月
+ (共 {{ item.posts.length }} 篇{{ calcAuditModeEnabled ? '内容' : '文章' }})
+
+
+
+
+
+
+
+ {{ post.spec.title }}
+
+
+ {{ post.status?.excerpt }}
+
+
+ 发布时间:{{ formatTime(post.spec.publishTime) }}
+
+
+
+
+
+ 该日期下暂无归档文章!
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/article-detail/article-detail.vue b/src/pages-blog/article-detail/article-detail.vue
new file mode 100644
index 0000000..bfd7324
--- /dev/null
+++ b/src/pages-blog/article-detail/article-detail.vue
@@ -0,0 +1,716 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ result?.spec.title }}
+
+
+
+ 作者:{{ result?.owner?.displayName || bloggerInfo.nickname }}
+ 时间:{{ formatPublishTime(result?.spec.publishTime) }}
+
+
+
+
+
+
+
+
+ {{ result?.stats?.visit ?? 0 }}
+ 阅读
+
+
+ {{ result?.stats?.upvote ?? 0 }}
+ 喜欢
+
+
+ {{ result?.stats?.comment ?? 0 }}
+ 评论
+
+
+ {{ result?.content?.raw.length || 0 }}
+ 字数
+
+
+
+
+
+
+
+
+ 分类:
+ 未选择分类
+
+ {{ item.spec.displayName }}
+
+
+
+ 标签:
+ 未选择标签
+
+ {{ item.spec.displayName }}
+
+
+
+
+ 原文:
+
+
+ {{ originalURL }}
+ 阅读原文
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 版权声明
+
+
+
+ 版权归属:{{ postDetailConfig.copyrightAuthor }}
+
+
+ 版权说明:{{ postDetailConfig.copyrightDesc }}
+
+
+ 侵权处理:{{ postDetailConfig.copyrightViolation }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/articles/articles.vue b/src/pages-blog/articles/articles.vue
new file mode 100644
index 0000000..ccdbe6a
--- /dev/null
+++ b/src/pages-blog/articles/articles.vue
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ isArticle(item) ? '文章' : '瞬间' }}
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/category-detail/category-detail.vue b/src/pages-blog/category-detail/category-detail.vue
new file mode 100644
index 0000000..917d816
--- /dev/null
+++ b/src/pages-blog/category-detail/category-detail.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/contact/contact.vue b/src/pages-blog/contact/contact.vue
new file mode 100644
index 0000000..8ee73b9
--- /dev/null
+++ b/src/pages-blog/contact/contact.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+ {{ bloggerInfo.nickname }}
+
+
+ {{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+ {{ item.value }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/data-visual/data-visual.vue b/src/pages-blog/data-visual/data-visual.vue
new file mode 100644
index 0000000..abdc7ff
--- /dev/null
+++ b/src/pages-blog/data-visual/data-visual.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 标签统计
+ (全部标签的文章数量占比)
+
+
+
+
+
+
+
+
+
+
+
+
+ 分类统计
+ (全部分类的文章数量占比)
+
+
+
+
+
+
+
+
+
+
+
+
+ 文章发布趋势
+ (按日期统计文章发布数量)
+
+
+
+
+
+
+
+
+
+
+
+
+ 评论活跃用户
+ (按评论作者统计评论数量)
+
+
+
+
+
+
+
+
+
+
+
+
+ 热门文章前10
+ (按访问量排序的热门文章)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/disclaimers/disclaimers.vue b/src/pages-blog/disclaimers/disclaimers.vue
new file mode 100644
index 0000000..a27b6af
--- /dev/null
+++ b/src/pages-blog/disclaimers/disclaimers.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+ 《 本博客免责声明 》
+
+
+ 1、本博客属于个人非盈利性质的网站,所有转载的文章都以遵循原作者的版权声明注明了文章来源。
+
+
+ 2、如果原文没有版权声明,按照目前互联网开放的原则,本博客将在不通知作者的情况下转载文章。
+
+
+ 3、如果原文明确注明"禁止转载",本博客将不会转载。
+
+
+ 4、如果本博客转载的文章不符合作者的版权声明或者作者不想让本博客转载您的文章,请邮件告知
+ {{ bloggerInfo.email }}
+ ,博主将会在第一时间删除相关信息!
+
+
+ 5、本博客转载文章仅为留做备份和知识点分享的目的。
+
+
+ 6、本博客将尽力确保所提供信息的准确性及可靠性,但不保证信息的正确性和完整性,且不对因信息的不正确或遗漏导致的任何损失或损害承担相关责任。
+
+
+ 7、本博客所发布、转载的文章,其版权均归原作者所有。如其他自媒体、网站或个人从本博客下载使用,请在转载有关文章时务必尊重该文章的著作权,保留本博客注明的"原文来源"或者自行去原文处复制版权声明,并自负版权等法律责任。
+
+
+ 8、本博客的所有原创文章皆可以任意转载,但转载时务必请注明出处。
+
+
+ 9、尊重原创,知识共享!
+
+
+
+
diff --git a/src/pages-blog/friend-links/friend-links.vue b/src/pages-blog/friend-links/friend-links.vue
new file mode 100644
index 0000000..c117ea4
--- /dev/null
+++ b/src/pages-blog/friend-links/friend-links.vue
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ link.spec.groupName || '暂未分组' }}
+ {{ link.spec.displayName }}
+
+
+ 站点地址:{{ link.spec.url }}
+
+
+ 博客简介:{{ link.spec.description || '这个博主很懒,没写简介~' }}
+
+
+
+
+
+
+
+
+ {{ link.spec.displayName }}
+
+
+ {{ link.spec.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ detail.data.spec.displayName }}
+
+
+ {{ detail.data.spec.groupName }}
+
+
+ {{ detail.data.spec.url }}
+
+
+
+
+ 博客简介:{{ detail.data.spec.description || '这个博主很懒,没写简介~' }}
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
+
diff --git a/src/pages-blog/love/album.vue b/src/pages-blog/love/album.vue
new file mode 100644
index 0000000..8a20f07
--- /dev/null
+++ b/src/pages-blog/love/album.vue
@@ -0,0 +1,273 @@
+
+
+
+
+
+
+
+ 相册正在努力加载中啦~
+
+
+
+
+
+ 刷新试试
+
+
+
+
+
+
+
+
+
+
+ 刷新试试
+
+
+
+
+
+
+
+
+
+
+
+ 🔒
+
+
+ 已加密
+
+
+
+
+
+ {{ item.displayName }}
+
+
+ {{ item.photoCount || 0 }} 张照片
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/love/journey.vue b/src/pages-blog/love/journey.vue
new file mode 100644
index 0000000..f5e2a1b
--- /dev/null
+++ b/src/pages-blog/love/journey.vue
@@ -0,0 +1,281 @@
+
+
+
+
+
+
+ 故事正在努力加载中啦~
+
+
+
+
+
+ 刷新试试
+
+
+
+
+
+
+
+
+ 刷新试试
+
+
+
+
+
+
+
+
+
+ {{ (story as unknown as { spec?: { date?: string } }).spec?.date }}
+
+
+ {{ (story as unknown as { spec?: { title?: string } }).spec?.title || '' }}
+
+
+ {{ (story as unknown as { spec?: { location?: string } }).spec?.location }}
+
+
+
+
+
+
+
+ +{{ ((story as unknown as { spec?: { images?: string[] } }).spec?.images?.length || 0) - 3 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关闭
+
+
+
+
+
+
+
diff --git a/src/pages-blog/love/list.vue b/src/pages-blog/love/list.vue
new file mode 100644
index 0000000..2f5ce59
--- /dev/null
+++ b/src/pages-blog/love/list.vue
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+ 清单正在努力加载中啦~
+
+
+
+
+
+ 刷新试试
+
+
+
+
+
+
+
+ 刷新试试
+
+
+
+
+
+ 看看我们的恋爱清单都完成了哪些吧
+
+
+
+
+
+
+ 未开始
+
+
+ 进行中
+
+
+ 已完成
+
+
+
+
+ {{ item.title }}
+
+
+ {{ item.content }}
+
+
+
+ {{ item.open ? '-' : '+' }}
+
+
+
+
+
+ 计划时间
+
+
+ {{ item.planDate || '-' }}
+
+
+
+
+ 完成时间
+
+
+ {{ item.completeDate || '-' }}
+
+
+
+
+ 完成感想
+
+
+ {{ item.completeRemark || '-' }}
+
+
+
+
+ 回忆图片
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/love/love.vue b/src/pages-blog/love/love.vue
new file mode 100644
index 0000000..80d6093
--- /dev/null
+++ b/src/pages-blog/love/love.vue
@@ -0,0 +1,355 @@
+
+
+
+
+
+
+
+
+
+
+ {{ loveConfig.loveInfo.boyNickname }}
+
+
+
+
+
+
+ {{ loveConfig.loveInfo.girlNickname }}
+
+
+
+
+
+
+
+
+
+ {{ loveConfig.loveDateTitle }}
+
+
+
+ 第
+ {{ loveDayCount.d }}
+ 天
+
+
+ {{ loveDayCount.h }}
+ 小时
+
+
+ {{ loveDayCount.m }}
+ 分钟
+
+
+ {{ loveDayCount.s }}
+ 秒
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ nav.title }}
+
+
+ {{ nav.desc }}
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/moment-detail/moment-detail.vue b/src/pages-blog/moment-detail/moment-detail.vue
new file mode 100644
index 0000000..af217d1
--- /dev/null
+++ b/src/pages-blog/moment-detail/moment-detail.vue
@@ -0,0 +1,299 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ moment.spec.owner?.displayName || bloggerInfo.nickname }}
+
+
+ {{ formatTime(moment.spec.releaseTime) }}
+
+
+
+
+
+
+ 标签列表:
+
+ {{ tag }}
+
+
+
+
+
+
+
+
+
+
+
+ 图片附件:
+
+
+
+
+
+
+
+
+
+
+
+ 音频附件:
+
+
+
+
+
+
+
+
+
+ 视频附件:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/setting/setting.vue b/src/pages-blog/setting/setting.vue
new file mode 100644
index 0000000..443ba51
--- /dev/null
+++ b/src/pages-blog/setting/setting.vue
@@ -0,0 +1,263 @@
+
+
+
+
+
+
+
+
+ 布局
+ 应用以及文章列表布局设置
+
+
+
+
+ 首页文章布局
+
+ {{ homeLayout.selectLabel }}
+
+
+
+
+
+ 文章卡片样式
+
+ {{ articleCardStyle.selectLabel }}
+
+
+
+
+
+
+
+
+
+ 功能
+ 一些常用的功能性设置
+
+
+
+ 图库瀑布流模式
+
+
+
+ 友链简洁模式
+
+
+
+ 是否圆形头像
+
+
+
+ 轮播图指示器
+
+
+
+
+ 指示器位置
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+ 保存设置
+
+
+ 恢复默认设置
+
+
+ 返回
+
+
+
+
+
+
diff --git a/src/pages-blog/submit-link/submit-link.vue b/src/pages-blog/submit-link/submit-link.vue
new file mode 100644
index 0000000..227e8e1
--- /dev/null
+++ b/src/pages-blog/submit-link/submit-link.vue
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+ {{ blogDetail.blogName }}
+
+
+ {{ blogDetail.blogDesc }}
+
+
+
+ 友链详情
+
+
+
+
+
+
+ 友链信息提交
+
+
+
+ 名称
+
+
+
+ 网址
+
+
+
+ Logo
+
+
+
+ 邮箱
+
+
+
+ 友链页面
+
+
+
+ (贵站友情链接页面地址,即包含本站链接的页面)
+
+
+ RSS地址
+
+
+
+ (用于抓取文章)
+
+
+ 网站描述
+
+
+
+
+
+ 提交数据
+
+
+ 友链申请
+
+
+
+
+
+
+
+
+
+
+
+ {{ blogDetail.blogName }}
+
+
+ {{ blogDetail.blogDesc }}
+
+
+
+
+ {{ calcBlogContent }}
+
+
+
+
+ 复制友链交换信息
+
+
+ 关闭
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/tag-detail/tag-detail.vue b/src/pages-blog/tag-detail/tag-detail.vue
new file mode 100644
index 0000000..c1d3285
--- /dev/null
+++ b/src/pages-blog/tag-detail/tag-detail.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/tags/tags.vue b/src/pages-blog/tags/tags.vue
new file mode 100644
index 0000000..42b2cf8
--- /dev/null
+++ b/src/pages-blog/tags/tags.vue
@@ -0,0 +1,22 @@
+
+
+
+
+ 文章标签页面
+
+
+
+
diff --git a/src/pages-blog/vote-detail/vote-detail.vue b/src/pages-blog/vote-detail/vote-detail.vue
new file mode 100644
index 0000000..f851137
--- /dev/null
+++ b/src/pages-blog/vote-detail/vote-detail.vue
@@ -0,0 +1,416 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 投票信息
+
+
+
+ 投票类型:
+ {{ vote.spec?._uh_type }}
+
+
+ 投票状态:
+ {{ vote.spec?._uh_state?.state }}
+
+
+ 投票方式:
+
+ {{ vote.spec?.canAnonymously ? '匿名' : '不匿名' }}
+
+
+
+ 开始时间:{{ formatTime(vote.spec?.startDate) }}
+
+
+ 结束时间:永久有效
+ 结束时间:{{ formatTime(vote.spec?.endDate) }}
+
+
+
+
+
+
+
+ 投票内容
+
+
+ {{ vote.spec?.title }}
+
+
+
+ 投票选项
+ (最多选择 {{ vote.spec?.maxVotes }} 项)
+
+
+
+
+
+
+
+ {{ option.title }}
+
+
+ {{ option._uh_percent }}%
+
+
+
+
+
+
+
+ {{ vote.spec?.type === 'pk' ? `选项${optionIndex + 1}:` : '' }}{{ option.title }}
+
+
+
+
+
+
+
+
+ 投票统计
+
+
+ {{ vote.stats?.voteCount || 0 }} 人已参与
+
+
+
+
+
+
+ 您已参与投票
+
+
+ 投票未开始
+
+
+ 投票已结束
+
+
+ 不支持匿名投票
+
+
+ 提交投票(请选择选项)
+
+
+ 提交投票
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/votes/votes.vue b/src/pages-blog/votes/votes.vue
new file mode 100644
index 0000000..b3ffca0
--- /dev/null
+++ b/src/pages-blog/votes/votes.vue
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/website/website.vue b/src/pages-blog/website/website.vue
new file mode 100644
index 0000000..2844274
--- /dev/null
+++ b/src/pages-blog/website/website.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index 8d2e4c0..b07ce0a 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -1,7 +1,15 @@
-
- uni-halo
- 去首页
+
+
diff --git a/src/pages/start/start.vue b/src/pages/start/start.vue
new file mode 100644
index 0000000..d53c4f0
--- /dev/null
+++ b/src/pages/start/start.vue
@@ -0,0 +1,299 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 「 {{ startConfig.title }} 」
+
+
+
+
+
+
+
+ {{ startConfig.desc1 }}
+
+
+ {{ startConfig.desc2 }}
+
+
+
+ {{ startConfig.btnText || '开始体验' }}
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/tabbar/about/about.vue b/src/pages/tabbar/about/about.vue
index 76a518d..d6af35c 100644
--- a/src/pages/tabbar/about/about.vue
+++ b/src/pages/tabbar/about/about.vue
@@ -1,13 +1,334 @@
-
- 关于页面
+
+
+
+
+
+
+ {{ bloggerInfo.nickname }}
+
+
+ {{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
+
+
+
+
+
+
+
+
+
+
+ {{ statistics.post }}
+
+
+ 内容数量
+
+
+
+
+ {{ statistics.visit }}
+
+
+ 访客数量
+
+
+
+
+ {{ statistics.category }}
+
+
+ 分类总数
+
+
+
+
+
+
+ {{ statistics.comment }}
+
+
+ 评论数量
+
+
+
+
+ {{ statistics.upvote }}
+
+
+ 点赞数量
+
+
+
+
+ {{ statisticsShowMore ? '收起' : '展开' }}
+
+
+
+
+
+
+
+
+
+ {{ nav.title }}
+
+
+ {{ nav.rightText }}
+
+
+
+
+
+
+
+
+ {{ copyrightConfig.content }}
+
+
+
diff --git a/src/pages/tabbar/category/category.vue b/src/pages/tabbar/category/category.vue
index f341897..2425a73 100644
--- a/src/pages/tabbar/category/category.vue
+++ b/src/pages/tabbar/category/category.vue
@@ -1,13 +1,376 @@
-
- 分类
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.spec.displayName }}
+
+
+ 共 {{ item.postCount }} 篇文章
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
+
+ {{ item.spec.displayName }}
+
+
+
+
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/tabbar/gallery/gallery.vue b/src/pages/tabbar/gallery/gallery.vue
index 3371a5b..d76cbc6 100644
--- a/src/pages/tabbar/gallery/gallery.vue
+++ b/src/pages/tabbar/gallery/gallery.vue
@@ -1,13 +1,277 @@
-
- 相册图库
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 刷新试试
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
diff --git a/src/pages/tabbar/home/home.vue b/src/pages/tabbar/home/home.vue
index 76fab60..cd19559 100644
--- a/src/pages/tabbar/home/home.vue
+++ b/src/pages/tabbar/home/home.vue
@@ -1,14 +1,560 @@
-
- 首页
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 快捷导航
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+ 精选分类
+
+
+
+
+
+
+
+ 还没有任何分类~
+
+
+
+
+
+
+
+
+
+
+ 最新列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/tabbar/moments/moments.vue b/src/pages/tabbar/moments/moments.vue
index f43419d..4ce0a33 100644
--- a/src/pages/tabbar/moments/moments.vue
+++ b/src/pages/tabbar/moments/moments.vue
@@ -1,13 +1,379 @@
-
- 瞬间
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ moment.spec.owner?.displayName || bloggerInfo.nickname }}
+
+
+ {{ formatMomentTime(moment.spec.releaseTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tag }}
+
+
+
+
+
+
+
+
+ {{ loadMoreText }}
+
+
+
+
+
+
diff --git a/src/static/wave/wave-1.png b/src/static/wave/wave-1.png
new file mode 100644
index 0000000000000000000000000000000000000000..4aa0758681c072be1d14cf7ab919536474121774
GIT binary patch
literal 4147
zcmeHKeN>Y79)GlzS}V7@v)wI&A!*Ez7Zn60D|Lc~6zf^?3Y0nJJmie$tr66`525nMYSqhQdd
z5V?Q^+v;!#O`z_ChKZ#dXrLbs$-)E#Lh%GY49-7*K-dJuqA@`zbPx*TkHp}J0cau?
z3tjlYKyOkumq-hLZ^0LMM}nowR@Cr#TjxE3{?6ogmlRwtw^?zCNN>BEQSPy@k2X8dJd#g
z{~Vg0zKE8|BQilR3wnQ-SQeKp;h<<7nK(nr;zVR}L~^g^&e*I)TSB3&jy~
z(m|LE6oy%JB}o;_#j;fKAH=b^y%>#z7$)T~5mjjF0d~Wv3
zK&ezBMI@6mL@W*^oCE_!`SJN|B8!1$;n8d+5{t+CBiZZ#E|TGoVDOJn{bMF7*b4pm)PN|s77lIoyTI4n;B_a%m
zA>eRGtRKeF`JfCiTryt$pLEGHm`u!-D;QGFRvuWAmuW=QAL*AdGXGrt%QeYgt9~K5tW<`G#{owz
z3g(ChRTtm?>-f!gn`MR(J+8+%rc`<>L)Ae;|0NI!}Rt+-I^>6)-5
z(;&;Ny3zh1q(kxH-=`)X97A2;Mb!oZKwGY!_nQ*{6g0Xi0N~96i3?ERJt%}A^Pt3!o8|d^HX)7Idb9=ide8MAT;RRE4S$
z>anpF%KAqF%MTMa?s`kJnSKd+*jAYsGrlY7QNJ9e+gp`J1`3YH3HmMJKqNX3v-O0L~Jt)^7Pte7}S?hhT<`z7RltEK~M{NdORXvmBHp%Pk!14~bD!6&29utx3b;HBR>U%)j%y9LkkaAOXo@dlCxliNA
z;msE=ea}~zigiovH{tD32%dL^8}Q*CK?~}O{rNTY(39+8c`U|qI9TzU9j>fM)|GU1
z6}SAFM2t(-HsRZ
zl}=&G231#nv>uA>`1+xDZ%=BOYL;NM%;eOTjNj%*Iu|tll%&*n4BoI#UV#7X?fH)*
zu-lr?~UzJmDz|DCX}wk7Yw8AXj<+2s#!Sl
zma^}*mwMIGrY+6>pQAd>-_{+TeH~Kg^h?f6`@VhOrPUZTrh6bqFtoR4)44OATeE{m
z52F#o<2UtVA>d;drJpaTTP37Y4xg2*?!aI>nw$W-%eyr4$NHg!A05rP=
zeQHyD;>yWE)rdyEQFByMxI57Jb|R_eA*X$(v+&FOO%LjL-CdMxXTttf@tRp8^TDne
zr`(ggx}mM_W-%w17+*Lu2Z|?n_uikXh_*3n;ya5@htQ%3cra0{nnrU>aOv1L7mE#7
zK4x1%k}ZdWb4k9Cy6ncf_$Y*h8oatK7Ywq>dEV%%xbvn(SD7L^V~LDLlz}DurQ=polQ*Hn
zV+i%wK8D~OjDYqs-dd|(N7Z}H+b)Xp^4d|Y&J$qB;v44~X|=Y_qP@BAjvhO{XUcZAVnuWC;b39D)+d{UeApi~o~g1%A++0x
zZ%EqHYTb2}zmA8ie7b&}Er+{rC#*f+dN_E~IT;^vIooHn`I^Euw-20PC5N0S;P^_a
z2(6l0aoUBK*^gVkg5131whV^+b-vVOFSlECC9$!qmYo7|VpY3p9eMmvxqS?7^I5l2
z6L!_JIeDyhQP{q04+n
zOCG-ghjfI@L6K(EcARJLhQ@AB{;s*yyM1GJ`C;mq?%$veI={WWdC9~GZBN)7+pxLP6i2UC=i+R85d-4^r?JK0
zp;C{G4$r~+&9)JlyLV67Pw0M}@jM`S&D_SFh)6Qk9kv_P6;zvWZe+8*qc(&db-8)W
zOXlU6t5aw4JaQF=s$_j#kKrU}S!TMo67p6`rK_Ugi)8SS546&wq??Up6u-0fuTn14
zwWGFp{(WV=VQL$FsLr68I^HxmBlERTR1>FXQCkfkv|I?C<*G*QIf}b@iO86?(eg;Z
zQ}&t5ts_G=n$kW6*Ut!=CPV4J>Gl0UrNl)8;nihdS#8Hl5u=dTn^*{Z@%^4B`|1aA
z%4NXtmgXfu8(=Qa0pBfC(yw{~z%ppB6OaP@fBDzuVSWj#{aaqqwjG~4Uh63lG2!R7
Hr5yY>E8Tkd
literal 0
HcmV?d00001
diff --git a/src/static/wave/wave-2.png b/src/static/wave/wave-2.png
new file mode 100644
index 0000000000000000000000000000000000000000..e7811d03beeb83a23d076d7147b6ae1885c41d83
GIT binary patch
literal 5203
zcmeHLYgAL$w%$k;Eh_cci?uD7Vt`T!Nq8qAU%rbIp~p_ic&@UiS9;Z$l8Y3=RpRLeK&u7++ev2wb(uk(=OWi7+Ha3_)&d&0mYO
z(tuP5S~$hs94(2agcBJ8o*SJhh-bN_@r1w{g8Tx~gmgv{OM;4LakzZ4!$?Du1B%Ne
zJ4AU=a1>!6D}fu5A!2=z5wV$(k;EV{9RmDOerZGiz+*}1s5D+OUrbCRJG{Upg0cBA
z)&cdxM3O{y_|)tW6;0WM3KWP~C@(i}3cSpVYH~?=VCYwkN`s|f2a7T7XkVu3?ELJL&x=B6U1R@R=PaqJmICreOI|f)_
z#OZtqJq^PbuYHXW#1b<^T%m+3;G@il^msvvgzNyAz6^mUd;`lDze*EG7&eVA#Nyp>
z=8#?+QYe3H%HzGU7E3m!0xz%l{##)2=5!$oOJ#`#DIx}IV=9X;S^L@Ba%o@SA~fcpw~Z9nP1C!@V@6fKp)6CG3_zQLLtI@v4qZNu;3uF10dzb?JTQ3Onoa}dKnpTmKHn?wRO
zHyKQbslgji@Ww!IJi*%=mu6xCTOy^4Sf6r$B!8w5vHyv`n4bE_?EhSm
z{Liv~m0U&wozG!`R*Q8o$AdLDrI*rR|GR20=KiIDy*3A3(L8+9j={~FhR)&xXc1`F
zN!NStf+J%d4*GO++VSC@c}>}u+uELX#WY^JwX0>9TinSFaqBEgH|$p5h#**^<>lPv
zL512i^7zk)pOwd)Z2s13|JUVjg|<9AhmMtD-?o1HmRoEQc~)4NGO{>2wQgiOt9>BH
zkKMktWV&_Pl_P!m&Z`E9QROOdEGc;BGi<;yGjv2~1?5>RmO{{bdH-9(^@C)n+3&2X
z5FH$Dc!Ew+s)%Yjtc@s%itJq@*B|2Ad1mJ}Kg&Fi3
zlgc`}ks(FSgQNOMbhpg!n0}B;Ce1z==kh2f{j>D$niC
z*CGj?QJ~wLV@mYDaVxe6x0UE0*N}RjO=Ka#h@aec%*O4qzi)PKE|TAK$LgyEeamm}
zp&7o1BSK6`8X}!02(=y^<90_X0!W<>bJx%T6j5)@9EUb>n0n&xULQ8cuP`>_lo(fM
ziUFe`x~Pw7jEdG<_YnOFwA86ClLjuD!Vlx#ZLw3hsI-=yyJqAI4R_JbWy6m7*T>V4
z67=K_gk<|eLzqj4^tkoI`FD)#SE;mL!V%#nkPL-ZeW7&Ez3R1?$3Bq;D^2~6@{6N~
z=YsM|QM#RuQAuR9nhUpwh^8bxhxET3U|eg=_ynUDP(x;$<1dDPurGwk)FV;w;~f#{qb?YxfG?acP(NePbXd)$J#12sJC^w@E0
z_D|N_ElGTM|KtZU9sSs{>Z;wOC+B{pb)DHb6$9sF*4y6^JH2wuf}Do&Oo#8)3q7Y*e%?8T+^`pt=qf0JB>fZ%JjAaZLQCDla`*-*2mtr9}5T-eUA8yAGcC)1Hwcp
zNG?n&f3&BuADZ59xpjSFh1HqisHQZUVJhOSr3qokd1Xqr%kPuzqfiwh%wG*;{FqlV
zwMKb;%4~7FAXd3>rb$^$9Zeoc&x=Co=!+=bao$doA@{u%s%=&azS>p6?dHzmF@NKg
z?OeT9N8fvw)_Yig8upj#747}dx~^8ZQLev9y(!JKvGi=QBc4~CIBsiL1Dmv9`pR$H
zxYE8V*V36Y6S!(*iS+SFZH6SZZHh!ZX!#_u2kBWcA2LFZD_vo%4p*+Ew?~QwB|S}s
z_0Tjw`2PMA{zZDCv!>bfjr-vWhrdeS&)YbWfMC&+TU~x1f6&_Ox6)Ys
zR$@i4lDDVMQ@uL7%p%(RM`h2Rri*e@^gv9U5onB7X%YQZ*uOwO3ojoctyu5V;W(Ly
z+><mQzU`mf}w@wU4`qUPMmd{42l#cul==IvAL``ezM2MuG)ei0~g&h@jZMwemT*g@rIQ?MZy&y55@t{02pN{Xo*J#V}oaoeM>ed{@u{o!%s
zOr#Ux0zhy*n5j7+zj8`$N&o>|SR#ECS}`^Rw#k}sQ5>N7xW5h^8$qI){t9FU9dK!t
zHX|B2s9ZSB1p>v@>P~fhIB|PV+u5Z*pIOkgQWCOaW
zh-x(WSq~KmIqY9)HX)*%`}YhZ`*UM0qTlyYcgeDx)G4L%0Z=_ycrjj_SRshWggaSJ
zPobegurVq_O!ufa$$;9P-6RkC{f3;+@24sz#)}t02)Z{!l#ZMq-ovq7EUiDMI)Ui1
z$W}yu1MHgSuyj}TfNX~6^80}Yt<|C>(!y;OW8a|HM2NVEN5gVUD0AQux+KhWoqDrB
zJD3i3kL!M#;T-w{P5kWN&{k5r#nAR+9AxWZ+o#7uT}aahT0>E~7^+Gu>#V&7oLn|L
zp{AjD>wX>xbCHhQ@11ni#Yj}z)5y#Y1ROl6AoX#C9)*4lG9z~R{mz3{TY_hmck?yb`VDw|~bK-gcw+y)@xKHh?45!6LUR$E}j(6YXE`(J4jVIn?agHF#y
zd{RLyk6JsBEH8BwqT3Xj{{5)mZQs83u%ortju)j$`mgWc*g=Jzg*E+-RiUD*!=6{(
z?Q5^POS6gSzSOu*8`~vyfT9a>j`XZ(DwmtW2htTQTC(??S5-!eauDmzQ?tJ`uC#!x
z{SGs~K$2uzqe9cqj`~T^x+n!L2{vuvexE}}dO^Q{s#i6Z=#SPMny;dU4*S{M3b|Yb
zskB~PsRkR;&@i{x472~?09+Zi|2d^p&MO|E?^`lK*qqc8ppM~QXvC|JJl%kX6dV$w
zJ6>DC{sJ*;Gj-sn_?1R6$n1;5C2jRzpdXmsGTk8;eVEHJpj`Yr#;-wnDD>oec80cR
z#fu>Y-G(#K^IT9!H3A;f>o42HWt@D*gC7*@rDZ9}UEf1_1!RRjKsi={4ltcG46QVJ
zBxER_ZR1Yg?SQ*a&rMYZT0o;itITI<@!-@pn}&A#z6UWCDNX360)4TD=mC!UufU-U
zMc+wvsv{5Y>>8mM@=N8b>LXMwOD0&J#p9w-#6y|FMl}7U!BF#1<|k?%7e7BcIBFMB
z731HMy$>i5dGVuR<7DNg-b4C_C&U(z>nRSB93e8)@FOqQ$xT6B<&WgT-0u=A&dX*p
z?xz+l(_~~K{h93xpPOOkLYwU?d%B6YW(OjD6XAcWc#E+
zONNaRqF7`))TQIu2fWlDz7bY|t}c???|I57OUX*jpIY*C!CELUW=yF3rbIi-<=hGI
z&CWR^zj#TLe2e`UnA;=zvuJ~+W#&6gN;Z;m&u-8l8|vkTLr|gi0NREny)8JBlY>@w
zC=G$IJ*L#JC+S94qEkyYap&*fL`4TX5=7T%J{5t5DVreZvk3|9HvtU(Q;sDdZL$2H
zTVO(|HX{-Fdi+UdK1ZsrQX3n(@|DB0+Yd8`pUoZ}YzWk}Or0ET_*FwYrmtQYP!)3~
z*A{}JL%WgW60Hobb$gz^1>tDsS|66zWKVZ@&Iy{eHAOVC&+LWp-jNIvk8-hW9+y^o
zmqxy34M86KFGnmOD381nyqLm4eaj)p@}2)5{vXIg-u%J^iFY@{iqaYLYd?5nL{R01
HxLyAMCIO+d
literal 0
HcmV?d00001
diff --git a/src/store/appConfig.ts b/src/store/appConfig.ts
new file mode 100644
index 0000000..8d129c0
--- /dev/null
+++ b/src/store/appConfig.ts
@@ -0,0 +1,86 @@
+/**
+ * 应用配置 store(源自旧项目 store/config.js 的 configs/mockJson 部分)
+ * 注意:旧 fetchConfigs 中会把 basicConfig.tokenConfig 写入缓存,供 getPersonalToken 使用,此处保留
+ */
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { getAppConfigs } from '@/api/uni-halo'
+import { DefaultAppConfigs } from '@/config/appConfig'
+import { deepMerge } from '@/utils/merge'
+import { setCache } from '@/utils/storage'
+import { checkUrl } from '@/utils/url'
+import type { IAppConfig, IMockJson } from '@/api/types/uni-halo'
+
+/** 个人令牌存储 key(与 src/store/token.ts 的 getPersonalToken 保持一致) */
+const APP_TOKENS_KEY = 'APP_TOKENS'
+
+export const useAppConfigStore = defineStore(
+ 'appConfig',
+ () => {
+ const configs = ref(JSON.parse(JSON.stringify(DefaultAppConfigs)))
+ const mockJson = ref({})
+
+ /** 重置为默认配置 */
+ const setDefaultAppSettings = () => {
+ configs.value = JSON.parse(JSON.stringify(DefaultAppConfigs))
+ }
+
+ /** 获取应用配置(与默认值深合并,并存储 token) */
+ const fetchConfigs = async () => {
+ try {
+ const res = await getAppConfigs()
+ const body = res.data as IAppConfig | undefined
+ if (body) {
+ configs.value = deepMerge(JSON.parse(JSON.stringify(DefaultAppConfigs)), body)
+
+ // 存储个人令牌(供 getPersonalToken 使用,如非匿名投票)
+ if (body?.basicConfig?.tokenConfig) {
+ setCache(APP_TOKENS_KEY, body.basicConfig.tokenConfig)
+ }
+ return body
+ }
+ setDefaultAppSettings()
+ return null
+ }
+ catch {
+ setDefaultAppSettings()
+ return null
+ }
+ }
+
+ /** 请求模拟数据(审计模式) */
+ const fetchMockJson = async () => {
+ const mockJsonUrl = checkUrl(configs.value.auditConfig?.auditModeData?.jsonUrl)
+ return new Promise<{ ok: boolean, data: unknown }>((resolve) => {
+ uni.request({
+ url: mockJsonUrl,
+ method: 'GET',
+ success: (res) => {
+ mockJson.value = res.data as IMockJson
+ resolve({ ok: true, data: res.data })
+ },
+ fail: (err) => {
+ resolve({ ok: false, data: err })
+ },
+ })
+ })
+ }
+
+ /** 设置模拟数据(jsonData 本地解析场景) */
+ const setMockJson = (data: IMockJson) => {
+ mockJson.value = data
+ }
+
+ return {
+ configs,
+ mockJson,
+ fetchConfigs,
+ setDefaultAppSettings,
+ fetchMockJson,
+ setMockJson,
+ }
+ },
+ {
+ persist: true,
+ },
+)
diff --git a/src/store/halo.ts b/src/store/halo.ts
new file mode 100644
index 0000000..b59d9d2
--- /dev/null
+++ b/src/store/halo.ts
@@ -0,0 +1,48 @@
+/**
+ * Halo 全局配置 store(源自旧项目 store/config.js 的 haloConfig 部分)
+ */
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { getHaloGlobalInfo } from '@/api/uni-halo'
+import { DefaultHaloGlobalConfigs } from '@/config/haloGlobal'
+import { deepMerge } from '@/utils/merge'
+import type { IHaloGlobalConfig } from '@/api/types/uni-halo'
+
+export const useHaloStore = defineStore(
+ 'halo',
+ () => {
+ const haloConfig = ref({ ...DefaultHaloGlobalConfigs })
+
+ /** 重置为默认配置 */
+ const setDefaultHaloGlobalConfigs = () => {
+ haloConfig.value = JSON.parse(JSON.stringify(DefaultHaloGlobalConfigs))
+ }
+
+ /** 获取 Halo 全局配置(请求失败回退默认值) */
+ const fetchHaloConfigs = async () => {
+ try {
+ const res = await getHaloGlobalInfo()
+ const body = res.data as IHaloGlobalConfig | undefined
+ if (body) {
+ haloConfig.value = deepMerge({ ...DefaultHaloGlobalConfigs }, body)
+ return body
+ }
+ setDefaultHaloGlobalConfigs()
+ return null
+ }
+ catch {
+ setDefaultHaloGlobalConfigs()
+ return null
+ }
+ }
+
+ return {
+ haloConfig,
+ fetchHaloConfigs,
+ setDefaultHaloGlobalConfigs,
+ }
+ },
+ {
+ persist: true,
+ },
+)
diff --git a/src/store/index.ts b/src/store/index.ts
index fe2ead1..da58493 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -15,6 +15,9 @@ setActivePinia(store)
export default store
+export * from './appConfig'
+export * from './halo'
+export * from './setting'
// 模块统一导出
export * from './token'
export * from './user'
diff --git a/src/store/setting.ts b/src/store/setting.ts
new file mode 100644
index 0000000..7d613ec
--- /dev/null
+++ b/src/store/setting.ts
@@ -0,0 +1,35 @@
+/**
+ * 应用设置 store(源自旧项目 store/setting.js)
+ */
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { DefaultAppSettings } from '@/config/appSettings'
+import type { IAppSettings } from '@/config/appSettings'
+
+export const useSettingStore = defineStore(
+ 'setting',
+ () => {
+ const settings = ref(JSON.parse(JSON.stringify(DefaultAppSettings)))
+
+ /** 重置为默认设置 */
+ const updateDefaultAppSettings = () => {
+ settings.value = JSON.parse(JSON.stringify(DefaultAppSettings))
+ }
+
+ /** 检查并设置默认设置(启动时调用,persist 已保证有值,兜底处理) */
+ const checkAndSetDefaultAppSettings = () => {
+ if (!settings.value) {
+ settings.value = JSON.parse(JSON.stringify(DefaultAppSettings))
+ }
+ }
+
+ return {
+ settings,
+ updateDefaultAppSettings,
+ checkAndSetDefaultAppSettings,
+ }
+ },
+ {
+ persist: true,
+ },
+)
diff --git a/src/store/token.ts b/src/store/token.ts
index a24ef32..b752d14 100644
--- a/src/store/token.ts
+++ b/src/store/token.ts
@@ -13,6 +13,19 @@ import {
} from '@/api/login'
import { isDoubleTokenRes, isSingleTokenRes } from '@/api/types/login'
import { useUserStore } from './user'
+import { getCache } from '@/utils/storage'
+
+/** 个人令牌存储 key(源自旧项目 utils/token.js,含 basicConfig.tokenConfig) */
+const APP_TOKENS_KEY = 'APP_TOKENS'
+
+/**
+ * 获取个人令牌(旧项目 getPersonalToken)
+ * 用于需要个人 token 的接口(如非匿名投票 submitVote)
+ * @returns 个人令牌,未配置时为空字符串
+ */
+export function getPersonalToken(): string {
+ return getCache<{ personalToken?: string }>(APP_TOKENS_KEY)?.personalToken || ''
+}
/**
* 是否是双token模式
diff --git a/src/style/uni-components/audio.css b/src/style/uni-components/audio.css
new file mode 100644
index 0000000..0579b65
--- /dev/null
+++ b/src/style/uni-components/audio.css
@@ -0,0 +1,12 @@
+/**
+ * audio 组件默认样式补件
+ * 说明:@dcloudio/uni-components 3.0.0-4070620250821001 的 style/ 目录未提供 audio.css,
+ * vite 编译