From 759373e327b695ae8fb7caac492b5963ce8016ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=8F=E8=8E=AB=E5=94=90=E5=B0=BC?=
Date: Sun, 14 Jun 2026 01:25:15 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=A1=B9=E7=9B=AE=E7=BB=93?=
=?UTF-8?q?=E6=9E=84=E4=B8=8E=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96=E8=B0=83?=
=?UTF-8?q?=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 重构tabbar路由与多语言key,调整首页默认跳转
2. 拆分博客页面到subpkg目录,调整vite分包配置
3. 新增markdown、图片组件、滚动钩子等工具模块
4. 优化api类型定义与工具函数,修复图片缓存逻辑
5. 调整国际化文案与个人页面路径
---
.vscode/settings.json | 3 +-
src/api/halo-plugin/photo.ts | 40 +-
src/api/types/halo.ts | 23 +
.../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 ++
.../components/mp-html/highlight/prism.min.js | 7 +
.../components/mp-html/img-cache/index.js | 138 ++
.../components/mp-html/markdown/index.js | 34 +
.../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-gallery-panel/uh-gallery-panel.vue | 100 ++
.../uh-global-search/uh-global-search.vue | 168 ++-
src/components/uh-image/uh-image.vue | 53 +
src/config/markdown/config.ts | 96 ++
src/config/markdown/style.scss | 155 ++
src/config/useAppConfig.ts | 4 +-
src/hooks/useHaloScroll.ts | 74 +
src/locale/en.json | 2 +-
src/locale/zh-Hans.json | 2 +-
src/pages/gallery/gallery.vue | 335 +++-
src/pages/index/index.vue | 4 +-
src/pages/me/me.vue | 79 -
src/pages/mine/mine.vue | 141 ++
.../about/about.vue | 0
.../page-template/page-template.vue | 0
.../post-detail/post-detail.vue | 4 +
.../i18n/index.vue | 0
.../request-demo/request-demo.vue | 0
.../ucharts/data.json | 0
.../ucharts/index.vue | 0
src/tabbar/TabbarItem.vue | 25 +-
src/tabbar/config.ts | 8 +-
src/utils/randomResources.ts | 9 +-
vite.config.ts | 3 +-
44 files changed, 5517 insertions(+), 190 deletions(-)
create mode 100644 src/api/types/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.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.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-gallery-panel/uh-gallery-panel.vue
create mode 100644 src/components/uh-image/uh-image.vue
create mode 100644 src/config/markdown/config.ts
create mode 100644 src/config/markdown/style.scss
create mode 100644 src/hooks/useHaloScroll.ts
delete mode 100644 src/pages/me/me.vue
create mode 100644 src/pages/mine/mine.vue
rename src/{pages-blog => subpkg-blog}/about/about.vue (100%)
rename src/{pages-blog => subpkg-blog}/page-template/page-template.vue (100%)
rename src/{pages-blog => subpkg-blog}/post-detail/post-detail.vue (64%)
rename src/{pages-demo => subpkg-demo}/i18n/index.vue (100%)
rename src/{pages-demo => subpkg-demo}/request-demo/request-demo.vue (100%)
rename src/{pages-demo => subpkg-demo}/ucharts/data.json (100%)
rename src/{pages-demo => subpkg-demo}/ucharts/index.vue (100%)
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a88242f..501b8ba 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -102,5 +102,6 @@
"i18n-ally.localesPaths": [
"src/locale",
"src/pages/i18n"
- ]
+ ],
+ "i18n-ally.keystyle": "nested"
}
diff --git a/src/api/halo-plugin/photo.ts b/src/api/halo-plugin/photo.ts
index 33bee3d..116d5a1 100644
--- a/src/api/halo-plugin/photo.ts
+++ b/src/api/halo-plugin/photo.ts
@@ -1,11 +1,45 @@
import { http } from '@/http/alova'
+import type { IHaloListResponseBase } from '@/api/types/halo'
+
+export interface IQueryPhotoGroupsRequest {
+ page?: number
+ size?: number
+}
+
+export interface IQueryPhotosRequest {
+ group?: string
+ ungrouped?: boolean
+ tag?: string
+ keyword?: string
+ labelSelector?: string
+ fieldSelector?: string
+ sort?: string
+ page?: number
+ size?: number
+}
+
+export interface IPhoto {
+ metadata: {
+ creationTimestamp: string
+ generateName: string
+ name: string
+ version: string
+ }
+ permalink: string
+ spec: {
+ cover: string
+ displayName: string
+ groupName: string
+ url: string
+ }
+}
/**
* 获取图库分组列表
* @param params 获取图库分组列表请求参数
* @returns 获取图库分组列表响应参数
*/
-export function queryPhotoGroups(params: any) {
+export function queryPhotoGroups(params: IQueryPhotoGroupsRequest) {
return http.Get(`/apis/api.photo.halo.run/v1alpha1/photogroups`, {
params,
meta: {
@@ -33,8 +67,8 @@ export function queryPhotoTags(params: any) {
* @param params 获取图库列表请求参数
* @returns 获取图库列表响应参数
*/
-export function queryPhotos(params: any) {
- return http.Get(`/apis/api.photo.halo.run/v1alpha1/photos`, {
+export function queryPhotos(params: IQueryPhotosRequest) {
+ return http.Get>(`/apis/api.photo.halo.run/v1alpha1/photos`, {
params,
meta: {
isHalo: true,
diff --git a/src/api/types/halo.ts b/src/api/types/halo.ts
new file mode 100644
index 0000000..5148748
--- /dev/null
+++ b/src/api/types/halo.ts
@@ -0,0 +1,23 @@
+/**
+ * Halo 列表响应基础结构
+ * @template T 列表项类型
+ */
+export interface IHaloListResponseBase {
+ page: number
+ size: number
+ total: number
+ items: T[]
+ first: boolean
+ hasNext: boolean
+ hasPrevious: boolean
+ last: boolean
+ totalPages: number
+}
+
+/**
+ * Halo 列表请求基础结构
+ */
+export interface IHaloListRequestBase {
+ page?: number
+ size?: number
+}
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-gallery-panel/uh-gallery-panel.vue b/src/components/uh-gallery-panel/uh-gallery-panel.vue
new file mode 100644
index 0000000..d3c2f37
--- /dev/null
+++ b/src/components/uh-gallery-panel/uh-gallery-panel.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+ {{ props.groupName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载失败,点击刷新试试
+
+
+ 数据已全部加载,没有更多了
+
+
+
+
+
+
+
diff --git a/src/components/uh-global-search/uh-global-search.vue b/src/components/uh-global-search/uh-global-search.vue
index 1be5838..0beaa34 100644
--- a/src/components/uh-global-search/uh-global-search.vue
+++ b/src/components/uh-global-search/uh-global-search.vue
@@ -7,6 +7,9 @@ import type { HaloDocument, SearchOption, SearchResult } from '@halo-dev/api-cli
const { loading, error, data, run } = useRequest(indicesSearch, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
+const showTransition = ref(true)
+const isAutoFocus = ref(false)
+
const scroll = reactive({
top: 0,
})
@@ -69,98 +72,105 @@ const onViewScroll = debounce((e: any) => {
}, 150)
onMounted(() => {
- handleSearch()
+ // handleSearch()
+
+ setTimeout(() => {
+ isAutoFocus.value = true
+ }, 350)
})
-
-
-
-
-
-
-
- 搜索
-
-
-
-
-
-
-
-
-
- 搜索结果
- 共 {{ searchResult.length }} 条
-
-
-
-
-
-
-
- # {{ index + 1 }}
-
-
-
-
-
-
-
-
-
- 来自{{ item.typeCN }}
-
- {{ item.releaseTime }}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
- 点击重试
+ 搜索
-
-
+
+
+
+
+
+
+
+
+ 搜索结果
+ 共 {{ searchResult.length }} 条
+
+
+
+
+
+
+
+ # {{ index + 1 }}
+
+
+
+
+
+
+
+
+
+ 来自{{ item.typeCN }}
+
+ {{ item.releaseTime }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 点击重试
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
diff --git a/src/components/uh-image/uh-image.vue b/src/components/uh-image/uh-image.vue
new file mode 100644
index 0000000..4aadf75
--- /dev/null
+++ b/src/components/uh-image/uh-image.vue
@@ -0,0 +1,53 @@
+
+
+
+ {{ imageStyle }}
+ {{ attrs }}
+
+
diff --git a/src/config/markdown/config.ts b/src/config/markdown/config.ts
new file mode 100644
index 0000000..6243201
--- /dev/null
+++ b/src/config/markdown/config.ts
@@ -0,0 +1,96 @@
+import { useAppConfig } from '../useAppConfig'
+
+export const markdownConfig = {
+ domain: useAppConfig().appBaseUrl.value,
+ 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;',
+
+ loadingGif: '',
+ emptyGif: '',
+}
+
+export default markdownConfig
diff --git a/src/config/markdown/style.scss b/src/config/markdown/style.scss
new file mode 100644
index 0000000..28078a2
--- /dev/null
+++ b/src/config/markdown/style.scss
@@ -0,0 +1,155 @@
+:root {
+ --main: #303133;
+ --theme: #fb6c28;
+ --code-background: #e8f3ff;
+ --radius-inner: 4px;
+ --classA: #dcdfe6;
+ --classB: #e4e7ed;
+ --classC: #ebeef5;
+ --classD: #f2f6fc;
+ --classE: #dcdcdc;
+ --classF: #333;
+ --classG: #dcdcdc;
+ --classH: #e9f2ff;
+ --classI: #5a3713;
+ --classJ: #f9e5fb;
+ --classK: #e4e7ed;
+ --classL: #666;
+ --classM: #2d2e37;
+ --quote: #50bfff;
+ --code: #409eff;
+}
+.uh-markdown {
+ ::v-deep {
+ h1::before,
+ h2::before,
+ h3::before,
+ h4::before,
+ h5::before,
+ h6::before {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle;
+ content: '';
+ margin-right: 6px;
+ background-position: center;
+ }
+ h1::before {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle;
+ content: '¶';
+ top: -4px;
+ margin-right: 12px;
+ font-size: 24px;
+ color: var(--theme);
+ }
+ h2::before {
+ top: -2px;
+ left: 0;
+ width: 20px;
+ height: 20px;
+ background-size: auto 100%;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAVJJREFUWEftl7FKw1AUhv9rbmf1PRw6utkHEOoi6WPoE1ifQJ8gySKCOtRFRBKsDuIiDdZAURBFF50M2sFIzpGogVK03JA2cbh3S/i55zvfvYETgRyLj5erIKOO949FyKm5eHvvHEBV2jyruq1QDQ7n2DNXALEx+D7e2vl6lBYp76scHCzErtmCEPVhqEIAfus8BZk4wM+Zd/46tskDeI0mgLUyAdoAFsoDcM0XCDFdHoDX4FGfbRF3QANoA9qANqANaAPawD82EEWId1sAOJQWz6iO+5nGch41kDw9I3bbYOaTis21wgG4G4AuAzCwXrEoGV6V1ngMJPoPjoB+PzSIa8KBr1QdwFgA6KID7t1AgFYNC5uqxZNcbgB+eASdngHM+9LmpSzF8wFEEagbgHvXoQA3s3aegmYz8P1f4NNVME+39z5e3w4lkSMc3GXtPM1/AjYDFjDGddN5AAAAAElFTkSuQmCC);
+ }
+ h3::before {
+ top: -3px;
+ left: 0;
+ width: 20px;
+ height: 20px;
+ background-size: auto 100%;
+ background-repeat: none;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAH1JREFUWEft1qENgDAQheH/JANg60DAJIgmrMFCzEEYgTFgDyRFIClp0yBf9evl8pl3RsYbp7BjNLHoMptljPiMZH3WAhKQgAQkIIGXQAXUT79cniHaNMa5draliqqojIID86nRHEtvbSqlBSQgAQlIQAISkECRAA746SC5Ad6XpiGnnOGPAAAAAElFTkSuQmCC);
+ }
+ h4::before {
+ top: -2px;
+ width: 22px;
+ height: 22px;
+ color: var(--theme);
+ background-size: auto 100%;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAndJREFUWEftVkty2kAQfT1SBLtwg6AqwzbxCQIniG8QvAmwCp8kRqvgFa5KwHjlkI3NCeIbhJwgXptUgW9AdiDQdGoITqUsjcZyuYqNZ6WSXs88vel+3YQtL9ry+Xgk8KhApALNAe8x5PPbCUrgabdsD2/e+1edEpN4FsIRps7OwT9cXKKHCLw75QIL+V0XRFIUP1dptBx3ChKkxQlw8UnOG5mqLESgdsYZy5dTAE8jg5lH3Ypd5Mlxxl8ttDgGRulcq5iYgApofgn6ILzVBQsSu5/e0OXiqnMOotdatVjsOvkPl4muQIFrp5y1hJzEBA67ZavEk07WX5EexzxM5b1SYgJrFQbBOQDt3wVSuP0qTRfjowsAr3SHODa75HrqqiKX1gdMycjAYa9stU3JCPBhKue1ExNQAY3BakSgl1HBDMykI9z+Ps0WV0eXIITKdhM3c+yUS259FrVPrBM2TlclEnSmY8+S93tV+/yvH+hxxLzv5D11paFltOLmIFD3FzIbtRMzpr2K5arn+bgzJZAWl8631rjby0zAUJI3xnTfkjQTiFEAwHW3bGWNCoCv0zlvjUukgOoJgPymzQGg3itbfVMOMKiezh30ExOIqwIAvwNHZFUVzMcdbbUonGOnsomr4P1XfiFZ/tS6GOOkW7FqJh8gwomz06ol9oE7O6GhH9zLCbfeCxqDoE3AR61sm5nA/3XUZ47pmneYCSLnAeHLCQGZaAvmH72yXdjMA6oTanHpnFfQ5tDmQ+KJ6MZ+jckXY7//k4pWYB7sQVCUccxU3a9teHKcWS7ne0wI4Rhipqv7REZkku8hvhut+CEOidvjkcDWFfgD9RMzMKE7f80AAAAASUVORK5CYII=);
+ }
+ h5::before {
+ top: -1px;
+ left: 0;
+ width: 18px;
+ height: 18px;
+ background-size: 100% 100%;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAC8klEQVRYR+3WP2gTURwH8O/vKnVRRHKXP52cBO3g4p/BqYNIhy4muajUQRBFKjQV1En6ZxJBcmlRCoJDRe3FDiJVwamLS3FwqbgIgss1l2ZQF5XeT3I1Z3NJ7u5dLlAwN9699/t97vfe7/EIO/yhHe5DD9jpCv3fFVTu856+Xzi62Y/35hj9CFPNrlUwWeRJAJfBGADwBcBNI0/PRZFdAdo4xlQLjCqKjBzogat7hZCRAgPghJGBgbFnxglAOvS7b/fLb+q+qnv5BHBCyEBAWTdnANy2IxOtWSC1qsY+1jOFwAVG+gIVvXyHQbcaKuZCxmf5iMQogXFQtEsBeO5JT6BcMu+Bcb1lUhcyqfExACUAB6JEtgUqi+U5JrrmmcyFHCjwSYtQO+tSUSFbAmXdnAdwJVASdyWLPAS2kbFA8xsHNS13EzCmlx8R6KJQcBcypfFpho3cKxRna3ADsgEo6+ZjAKMhgjZ1d2KWR2gTSyD0h4jnIB2gXDIXwciFCPZvSnMl0wwshYrJGDImaMUGKovlLBPVOrDjh8APzFx8zDkjNa7FzYoGZsKT9XEarQOHmei1aJA246cqOWW6/i2l8VMGzoWIPW/k6eq2Ja6UwCz8pw2JGZ8sS8pUz8fWau/jGp+SgLchcGBgZD1Py41NUuoI+ZloV8ZU93+ogZKzfBiWfXAPCgMJU8Y42avQdMzI4ZBfmSizocqrUeJaAmsvBZGGBCtTziXeRY1rCxRAViEhXckqK93AeQJ9kYTvlmVlqmcTdhNEtefc+9X3utVmuX+CkK6oyqtu4nwrWP8bF5IZdGYjJ79wDuMCz4D+XmhFWnZbt7ab5ltBB6mbkyAaBPFCJassuwMmNb4L4EZgXwBc4AoGTZrUeA6A9x1yK6tzzvnFDlxBv0D176kCP2TCpbbjBXCRV9DZk0VeAONCE1IQ1zWg3dlF1sFQHWQIXFeBNrLAw5BwHBZWjQl6E3SbbB8X+R4Mg/Ca0wN2WtFeBTut4B84mFI4VpekyAAAAABJRU5ErkJggg==);
+ }
+ h6::before {
+ top: -1px;
+ left: 0;
+ width: 16px;
+ height: 16px;
+ background-size: auto 100%;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAEI0lEQVRYR+3Xb2wTZRwH8G/vL22RPy5GW8fK6rJpGeFFY9RO3TRRE1HfmEAammEyjZmOSEg1RrPZaTD6xjhU/Ndlf0CZgwmD+qcgZBRIETeqY0Vcnc7pGonhRY2l3m2t5upqjq693l2vcy+8N81zz/NcPvf75fd7rjos8ku3yH34HyjK0PqlDLPzD56vMNL0VHxmpgXAoUIZXKgI3sMSpK+cNTAbylaj/9IkprnLM3+mkg8XQi4EsJ4lSL+ZNbAfVd+JG/XLcSERg3M8IAtZauBtDEkeNdN6fX9NfRqXueQiSwm00wQRMDMGw94snBJkqYBrSYIIXs/ojftqGq6IXHZRFIpkKYC2JSw9SJE663W0nthlqcMawwrJYpVCag1M48pNy1YNvLWRffGNIYSCv6Cnog7rjCtVIbUEXoG7qeqaNMj51F58dWoKPZY62JeWyUJG+ctcIplcIizWCpgTl9EIyODJn9BtceDWq/6B57tO/H4R688fFaZ7AWzWAiiJEyMDx39Ed6UDdyy7Ni/w1ekxbP95VJi/F8CRYoGycGLksaEJdFU6cPdy0zxkR/RbtE6FhPvHATQUm2JFODHSfyyCLmsd7lth/hf53q/jcE8OC+NTAG7PTKiNoCqcGPnpF+PotDrwwNXl+OC3H9A8cVqYPgPgFnFo1QCLwomRg4cvoPMGBzZHTgq3zwKwZ+ddKVATXAZhf+gdnPvuojAUqmJdrspRAtQU93pXEM+8clgwhQHU5itruUBNcTt6TsP9sl8weQC0S/VFOUBNcW/2folt2z+XhZPTZh6kSKJ79aqVRuFszRxfUm8sNbdz9xlsfekz2bhCQBvDMMM8z+ubN92Mjrb71brS+97dM4wtnk8U4SSBBEGErVarzeVywePxoHVLA1pb6lUh3+8bwZMv+BTjpIAVACa9Xq+uqakJ7e3tqpGd/WfR3Jr+81awIJS0mY0A+sLhMGw2W3qfGmTXvhAef/6gapxUBDuqqqoejUQiBvFbKUH2DHyNx54bLAqXF0jT9HBjY6Pd6/XOi7oc5K7936Dp2QNF4/IBrTqdLuLxeIi2tracRSGF/PDgKB55er8muHzATQB2m81m+P1+1NbmPoVyIfsOnUOj+2PNcPmAO1iWbeE4TmexWODz+WQhayrL4No2oCkuJ5Bl2VGO49ZmcisXObdeVSuRaq7ZZ3G10KBTqRQl3pQPGY1GEQgE4HQ6heVDAO5S1cklNmUDXQRB9KZSqXkfESaTCW63G7FYDKFQKDEyMoJoNKqfe/bbAJ7QGpcrxa8xDLOV5/k0kGXZv2ZnZ5FMJtNjmqYvURR1IpFInAcwAeD7ud/pUuDmAY1G41g8Hl9DkmScoqggx3FHAIyJMMlSQfI9V5zKagDC93dsoRFKimQx2dIWOV/U/yn6bx0WyDj8vgLOAAAAAElFTkSuQmCC);
+ }
+ blockquote > p {
+ margin-bottom: 0 !important;
+ margin-top: 0 !important;
+ font-size: 0.9em !important;
+ }
+ code[class='md-code'],
+ code:not([class]) {
+ display: inline-block;
+ font-size: 13px;
+ color: #409eff;
+ margin: 2px 5px;
+ padding: 0 8px;
+ white-space: normal;
+ text-indent: 0;
+ -webkit-user-select: auto;
+ -moz-user-select: auto;
+ -ms-user-select: auto;
+ user-select: auto;
+ vertical-align: baseline;
+ word-break: break-word;
+ background: #e8f3ff;
+ border-radius: 4px;
+ }
+ code[class*='language-'] {
+ display: block;
+ overflow-x: auto;
+ // border-radius: 0 0 8px 8px;
+ white-space: pre-wrap;
+ word-break: break-all;
+ user-select: auto;
+ padding: 12px 12px 14px 18px;
+ margin-bottom: 16px;
+ background: #282c34;
+ color: #abb2bf;
+ border-radius: 4px;
+ text-shadow: 0 1px rgba(0, 0, 0, 0.3);
+ font-family: 'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
+ direction: ltr;
+ text-align: left;
+ white-space: pre;
+ word-spacing: normal;
+ word-break: normal;
+ line-height: 1.5;
+ -moz-tab-size: 2;
+ -o-tab-size: 2;
+ tab-size: 2;
+ -webkit-hyphens: none;
+ -moz-hyphens: none;
+ -ms-hyphens: none;
+ hyphens: none;
+ }
+ table {
+ td {
+ padding: 8px;
+ border-right: 1px solid var(--classE);
+ border-bottom: 1px solid var(--classE);
+ }
+ thead th {
+ font-weight: 500;
+ background: var(--classC);
+ }
+
+ tbody tr {
+ transition: background 0.35s;
+ }
+ }
+ }
+}
diff --git a/src/config/useAppConfig.ts b/src/config/useAppConfig.ts
index 7ca85ac..bc5e630 100644
--- a/src/config/useAppConfig.ts
+++ b/src/config/useAppConfig.ts
@@ -1,5 +1,7 @@
export function useAppConfig() {
+ const appBaseUrl = ref(import.meta.env.VITE_API_BASE_URL)
+
return {
- apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
+ appBaseUrl,
}
}
diff --git a/src/hooks/useHaloScroll.ts b/src/hooks/useHaloScroll.ts
new file mode 100644
index 0000000..dcf6044
--- /dev/null
+++ b/src/hooks/useHaloScroll.ts
@@ -0,0 +1,74 @@
+import type { Ref } from 'vue'
+import { onMounted, ref } from 'vue'
+import type { IHaloListRequestBase, IHaloListResponseBase } from '@/api/types/halo'
+
+interface UseScrollOptions {
+ fetchData: (params: P) => Promise>
+ params: P
+}
+
+interface UseScrollReturn {
+ list: Ref
+ loading: Ref
+ finished: Ref
+ error: Ref
+ refresh: () => Promise
+ loadMore: () => Promise
+}
+
+export function useHaloScroll({
+ fetchData,
+ params,
+}: UseScrollOptions): UseScrollReturn {
+ const response = ref>()
+ const list = ref([]) as Ref
+ const loading = ref(false)
+ const finished = ref(false)
+ const error = ref(null)
+
+ const loadData = async () => {
+ if (loading.value || finished.value) {
+ return
+ }
+
+ loading.value = true
+ error.value = null
+
+ try {
+ response.value = await fetchData(params)
+ finished.value = !response.value?.hasNext
+ list.value.push(...response.value?.items || [])
+ params.page++
+ }
+ catch (err) {
+ error.value = err
+ }
+ finally {
+ loading.value = false
+ }
+ }
+
+ const refresh = async () => {
+ params.page = 1
+ finished.value = false
+ list.value = []
+ await loadData()
+ }
+
+ const loadMore = async () => {
+ await loadData()
+ }
+
+ onMounted(() => {
+ refresh()
+ })
+
+ return {
+ list,
+ loading,
+ finished,
+ error,
+ refresh,
+ loadMore,
+ }
+}
diff --git a/src/locale/en.json b/src/locale/en.json
index f16964f..e5156ed 100644
--- a/src/locale/en.json
+++ b/src/locale/en.json
@@ -4,7 +4,7 @@
"tabbar.moments": "Moments",
"tabbar.links": "Links",
"tabbar.about": "About",
- "tabbar.me": "Me",
+ "tabbar.mine": "Mine",
"tabbar.i18n": "I18n",
"i18n.title": "En Title",
"alova.title": "Alova Request",
diff --git a/src/locale/zh-Hans.json b/src/locale/zh-Hans.json
index 0e1045c..21dba46 100644
--- a/src/locale/zh-Hans.json
+++ b/src/locale/zh-Hans.json
@@ -4,7 +4,7 @@
"tabbar.moments": "瞬间",
"tabbar.about": "关于",
"tabbar.links": "友链",
- "tabbar.me": "我的",
+ "tabbar.mine": "我的",
"tabbar.i18n": "语言",
"i18n.title": "中文标题",
"alova.title": "Alova 请求",
diff --git a/src/pages/gallery/gallery.vue b/src/pages/gallery/gallery.vue
index e205497..3c233ae 100644
--- a/src/pages/gallery/gallery.vue
+++ b/src/pages/gallery/gallery.vue
@@ -1,4 +1,7 @@
-
- 相册页面
+
+
+
+
+
+
+
+
+
+
+
+ 最新精选
+
+
+
+ 显示图片的名称
+
+
+ 显示描述/日期
+
+
+
+
+
+
+
+
+
+ 我的相册
+ 共 30 组记录
+
+
+
+
+
+
+
+
+
+
+ {{ group.displayName }}
+ {{ group.images.length }} 张图片
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.displayName }}
+
+
+ {{ group.images.length }} 张
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 无数据
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index ca5ee6a..280314a 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -14,8 +14,8 @@ definePage({
onLoad(() => {
console.log('index onLoad')
// 默认跳转首页
- uni.switchTab({ url: '/pages/home/home' })
- // uni.switchTab({ url: '/pages/moments/moments' })
+ // uni.switchTab({ url: '/pages/home/home' })
+ uni.switchTab({ url: '/pages/gallery/gallery' })
})
diff --git a/src/pages/me/me.vue b/src/pages/me/me.vue
deleted file mode 100644
index 3deff25..0000000
--- a/src/pages/me/me.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
- {{ userInfo.username ? '已登录' : '未登录' }}
-
-
- {{ JSON.stringify(userInfo, null, 2) }}
-
-
-
-
-
-
-
-
-
-
diff --git a/src/pages/mine/mine.vue b/src/pages/mine/mine.vue
new file mode 100644
index 0000000..1ea524d
--- /dev/null
+++ b/src/pages/mine/mine.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+ {{ userInfo.username ? '已登录' : '未登录' }}
+
+
+ {{ JSON.stringify(userInfo, null, 2) }}
+
+
+
+
+
+ 切换语言
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-blog/about/about.vue b/src/subpkg-blog/about/about.vue
similarity index 100%
rename from src/pages-blog/about/about.vue
rename to src/subpkg-blog/about/about.vue
diff --git a/src/pages-blog/page-template/page-template.vue b/src/subpkg-blog/page-template/page-template.vue
similarity index 100%
rename from src/pages-blog/page-template/page-template.vue
rename to src/subpkg-blog/page-template/page-template.vue
diff --git a/src/pages-blog/post-detail/post-detail.vue b/src/subpkg-blog/post-detail/post-detail.vue
similarity index 64%
rename from src/pages-blog/post-detail/post-detail.vue
rename to src/subpkg-blog/post-detail/post-detail.vue
index 2947ae9..676836b 100644
--- a/src/pages-blog/post-detail/post-detail.vue
+++ b/src/subpkg-blog/post-detail/post-detail.vue
@@ -1,4 +1,6 @@
diff --git a/src/pages-demo/i18n/index.vue b/src/subpkg-demo/i18n/index.vue
similarity index 100%
rename from src/pages-demo/i18n/index.vue
rename to src/subpkg-demo/i18n/index.vue
diff --git a/src/pages-demo/request-demo/request-demo.vue b/src/subpkg-demo/request-demo/request-demo.vue
similarity index 100%
rename from src/pages-demo/request-demo/request-demo.vue
rename to src/subpkg-demo/request-demo/request-demo.vue
diff --git a/src/pages-demo/ucharts/data.json b/src/subpkg-demo/ucharts/data.json
similarity index 100%
rename from src/pages-demo/ucharts/data.json
rename to src/subpkg-demo/ucharts/data.json
diff --git a/src/pages-demo/ucharts/index.vue b/src/subpkg-demo/ucharts/index.vue
similarity index 100%
rename from src/pages-demo/ucharts/index.vue
rename to src/subpkg-demo/ucharts/index.vue
diff --git a/src/tabbar/TabbarItem.vue b/src/tabbar/TabbarItem.vue
index 35573ac..911ea50 100644
--- a/src/tabbar/TabbarItem.vue
+++ b/src/tabbar/TabbarItem.vue
@@ -31,21 +31,19 @@ function isActiveByIndex(index: number) {
return tabbarStore.curIdx === index
}
-let timer: ReturnType | null = null
const isActive = ref(false)
-function activeBgColor() {
- isActive.value = false
- clearTimeout(timer)
- timer = setTimeout(() => {
- isActive.value = true
- }, 30)
+function handleSetActive() {
+ if (tabbarStore.curIdx !== props.index) {
+ return
+ }
+ nextTick(() => {
+ setTimeout(() => {
+ isActive.value = true
+ }, 100)
+ })
}
-watch(() => tabbarStore.curIdx, () => {
- activeBgColor()
-}, { immediate: true })
-
function handleClick(index: number) {
// 点击原来的不做操作
if (index === tabbarStore.curIdx) {
@@ -58,7 +56,6 @@ function handleClick(index: number) {
const url = list[index].pagePath
tabbarStore.setCurIdx(index)
- console.log('handleClick tabbarCacheEnable', tabbarCacheEnable)
if (tabbarCacheEnable) {
uni.switchTab({ url })
}
@@ -66,6 +63,10 @@ function handleClick(index: number) {
uni.navigateTo({ url })
}
}
+
+onShow(() => {
+ handleSetActive()
+})
diff --git a/src/tabbar/config.ts b/src/tabbar/config.ts
index 93a35c8..5c37059 100644
--- a/src/tabbar/config.ts
+++ b/src/tabbar/config.ts
@@ -32,8 +32,8 @@ export const nativeTabbarList: NativeTabBarItem[] = [
{
iconPath: 'static/tabbar/personal.png',
selectedIconPath: 'static/tabbar/personalHL.png',
- pagePath: 'pages/me/me',
- text: '%tabbar.me%',
+ pagePath: 'pages/mine/mine',
+ text: '%tabbar.mine%',
},
]
@@ -65,8 +65,8 @@ export const customTabbarList: CustomTabBarItem[] = [
icon: 'link',
},
{
- pagePath: 'pages/me/me',
- text: '%tabbar.me%',
+ pagePath: 'pages/mine/mine',
+ text: '%tabbar.mine%',
iconType: 'uiLib',
icon: 'user',
},
diff --git a/src/utils/randomResources.ts b/src/utils/randomResources.ts
index f0da009..8c00b5f 100644
--- a/src/utils/randomResources.ts
+++ b/src/utils/randomResources.ts
@@ -1,3 +1,5 @@
+import { sleep } from '@/utils/common'
+
export const randomImageUrl = {
ycy: 'https://t.alcy.cc/ycy',
pc: 'https://t.alcy.cc/pc',
@@ -13,11 +15,12 @@ export const randomVideosUrl = {
acg: 'https://t.alcy.cc/acg',
}
-export function getPicsum(width: number = 800, height?: number) {
+export function getPicSumImages(width: number = 800, height?: number) {
+ const t = `${Date.now()}+${Math.random()}`
if (!height) {
- return `https://picsum.photos/${width}`
+ return `https://picsum.photos/${width}?t=${t}`
}
- return `https://picsum.photos/${width}/${height}`
+ return `https://picsum.photos/${width}/${height}?t=${t}`
}
/**
diff --git a/vite.config.ts b/vite.config.ts
index f09e64f..b807cb6 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -87,8 +87,7 @@ export default defineConfig(({ command, mode }) => {
exclude: ['**/components/**/**.*', '**/sections/**/**.*'],
// pages 目录为 src/pages,分包目录不能配置在pages目录下!!
// 是个数组,可以配置多个,但是不能为pages里面的目录!!
- // "src/pages-demo" 是unibest demo 预留的,方便后续插入demo示例
- subPackages: ['src/pages-demo', 'src/pages-blog'],
+ subPackages: ['src/subpkg-demo', 'src/subpkg-blog'],
dts: 'src/types/uni-pages.d.ts',
}),
// UniOptimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行