瀏覽代碼

first commit

chenyixian 3 月之前
當前提交
572f12f16d
共有 100 個文件被更改,包括 12464 次插入0 次删除
  1. 3 0
      .gitignore
  2. 7 0
      .npmrc
  3. 127 0
      App.vue
  4. 162 0
      README.md
  5. 44 0
      config/globalData.ts
  6. 4 0
      config/index.ts
  7. 643 0
      config/menu.ts
  8. 1 0
      env/.env
  9. 14 0
      env/.env.development
  10. 11 0
      env/.env.production
  11. 6 0
      hook/index.ts
  12. 4 0
      hook/use-menu-click/index.ts
  13. 15 0
      hook/use-on-load/index.ts
  14. 20 0
      index.html
  15. 25 0
      main.js
  16. 69 0
      manifest.json
  17. 34 0
      package.json
  18. 76 0
      pages.json
  19. 33 0
      pages/business/tabbar/homePage/homePage.vue
  20. 9 0
      pages/business/tabbar/personalCenter/personalCenter.vue
  21. 218 0
      pages/business/tabbar/transferPage/transferPage.vue
  22. 758 0
      pagesAdmin/article/business/article/detail/detail.vue
  23. 117 0
      pagesAdmin/article/components/half-screen-dialog/index.vue
  24. 120 0
      pagesAdmin/article/components/rate/index.vue
  25. 9 0
      pagesAdmin/article/components/tree/index.vue
  26. 49 0
      pagesAdmin/article/service/article/index.ts
  27. 1 0
      pagesAdmin/article/service/index.ts
  28. 18 0
      pagesAdmin/satisfaction/business/satisfactionQuestions/fn.ts
  29. 1281 0
      pagesAdmin/satisfaction/business/satisfactionQuestions/satisfactionQuestions.vue
  30. 1 0
      pagesAdmin/satisfaction/service/index.ts
  31. 36 0
      pagesAdmin/satisfaction/service/satisfactionQuestions/index.ts
  32. 343 0
      pagesCrm/business/home/home.vue
  33. 102 0
      pagesCrm/business/schemeDetail/schemeDetail.vue
  34. 73 0
      pagesCrm/business/schemeDetail/template/announcements.vue
  35. 22 0
      pagesCrm/business/schemeDetail/template/common.scss
  36. 207 0
      pagesCrm/business/schemeDetail/template/health-education.vue
  37. 20 0
      pagesCrm/business/schemeDetail/template/props.ts
  38. 206 0
      pagesCrm/business/schemeDetail/template/questionnaire.vue
  39. 203 0
      pagesCrm/business/schemeDetail/template/return-visit.vue
  40. 31 0
      pagesCrm/service/home/index.ts
  41. 2 0
      pagesCrm/service/index.ts
  42. 43 0
      pagesCrm/service/schemeDetail/index.ts
  43. 365 0
      pagesCrm/static/chatRoom.ts
  44. 17 0
      pagesCrm/static/path.ts
  45. 19 0
      pagesCrm/static/schemeDetail.ts
  46. 3 0
      pnpm-workspace.yaml
  47. 二進制
      static/images/healthCard/cardnewbg.png
  48. 二進制
      static/images/healthCard/icon2.png
  49. 二進制
      static/images/healthCard/ksj.png
  50. 二進制
      static/images/healthCard/logo_.png
  51. 二進制
      static/images/tabbar/homePage.png
  52. 二進制
      static/images/tabbar/homePage_ac.png
  53. 二進制
      static/images/tabbar/message.png
  54. 二進制
      static/images/tabbar/message_ac.png
  55. 二進制
      static/images/tabbar/netHosIndex.png
  56. 二進制
      static/images/tabbar/netHosIndex_ac.png
  57. 二進制
      static/images/tabbar/personalCenter.png
  58. 二進制
      static/images/tabbar/personalCenter_ac.png
  59. 9 0
      store/index.ts
  60. 446 0
      theme/common/_app-common.scss
  61. 134 0
      theme/common/var.scss
  62. 51 0
      theme/index.scss
  63. 53 0
      theme/mixins/_var.scss
  64. 1 0
      theme/mixins/config.scss
  65. 18 0
      theme/mixins/function.scss
  66. 13 0
      uni.promisify.adaptor.js
  67. 99 0
      uni.scss
  68. 192 0
      uni_modules/mp-html/README.md
  69. 156 0
      uni_modules/mp-html/changelog.md
  70. 105 0
      uni_modules/mp-html/components/mp-html/aliyun-video/aliyun-video.vue
  71. 7 0
      uni_modules/mp-html/components/mp-html/aliyun-video/index.js
  72. 504 0
      uni_modules/mp-html/components/mp-html/mp-html.vue
  73. 629 0
      uni_modules/mp-html/components/mp-html/node/node.vue
  74. 1402 0
      uni_modules/mp-html/components/mp-html/parser.js
  75. 7 0
      uni_modules/mp-html/components/mp-html/wx-channel-video/index.js
  76. 79 0
      uni_modules/mp-html/components/mp-html/wx-channel-video/wx-channel-video.vue
  77. 79 0
      uni_modules/mp-html/package.json
  78. 0 0
      uni_modules/mp-html/static/app-plus/mp-html/js/handler.js
  79. 0 0
      uni_modules/mp-html/static/app-plus/mp-html/js/uni.webview.min.js
  80. 1 0
      uni_modules/mp-html/static/app-plus/mp-html/local.html
  81. 33 0
      uni_modules/uni-badge/changelog.md
  82. 268 0
      uni_modules/uni-badge/components/uni-badge/uni-badge.vue
  83. 85 0
      uni_modules/uni-badge/package.json
  84. 10 0
      uni_modules/uni-badge/readme.md
  85. 44 0
      uni_modules/uni-icons/changelog.md
  86. 91 0
      uni_modules/uni-icons/components/uni-icons/uni-icons.uvue
  87. 110 0
      uni_modules/uni-icons/components/uni-icons/uni-icons.vue
  88. 664 0
      uni_modules/uni-icons/components/uni-icons/uniicons.css
  89. 二進制
      uni_modules/uni-icons/components/uni-icons/uniicons.ttf
  90. 664 0
      uni_modules/uni-icons/components/uni-icons/uniicons_file.ts
  91. 649 0
      uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js
  92. 111 0
      uni_modules/uni-icons/package.json
  93. 8 0
      uni_modules/uni-icons/readme.md
  94. 8 0
      uni_modules/uni-scss/changelog.md
  95. 1 0
      uni_modules/uni-scss/index.scss
  96. 82 0
      uni_modules/uni-scss/package.json
  97. 4 0
      uni_modules/uni-scss/readme.md
  98. 7 0
      uni_modules/uni-scss/styles/index.scss
  99. 3 0
      uni_modules/uni-scss/styles/setting/_border.scss
  100. 66 0
      uni_modules/uni-scss/styles/setting/_color.scss

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/node_modules
+package-lock.json
+pnpm-lock.yaml

+ 7 - 0
.npmrc

@@ -0,0 +1,7 @@
+shamefully-hoist=true
+auto-install-peers=true
+frozen-lockfile=false
+loglevel="verbose"
+
+registry=https://registry.npmmirror.com/
+@kasite:registry=http://maven.kasitesoft.com/repository/intelmt-npm-group/

+ 127 - 0
App.vue

@@ -0,0 +1,127 @@
+<script>
+import { GLOBALDATA, frontEndConfig, menu } from './config';
+import { common } from '@kasite/uni-app-base/utils';
+import {
+	useFrontEndConfigVersion,
+	useSetFrontEndConfig,
+	useAppStatus,
+	useGetUpdateManager,
+	useSmallProgramLogin,
+	useGetSysAppPageList,
+	usePreserMember,
+	useSetFrontEndForm,
+	useSetMenuList,
+} from './hook';
+import { nextTick } from 'vue';
+
+let app = null;
+
+const beforeMain = async () => {
+	common.showLoading();
+	// 获取前端当前配置信息版本号 并控制是否 前端配置信息是否要重新获取
+	await useFrontEndConfigVersion();
+	// 获取前端配置信息
+	let has = await useSetFrontEndConfig();
+	// 判断获取配置信息是否成功 不成功不让往下
+	if (has) return;
+	// 判断应用状态,非运行中的应用跳转对应状态的缺省页
+	await useAppStatus();
+	common.hideLoading();
+	if (!app.globalData.logSuccess) {
+		main();
+	} else {
+		// 配置判断是否需要互联网医院Websocket
+		if (app.globalData.hasWebsocket) {
+			// 链接websocket
+			connectWebsocket(true);
+		}
+	}
+};
+const main = async function () {
+	let isUpdateManager = false;
+	// #ifndef APP || H5 || WEB
+	// 判断如果是在开发过程 不检查是否存在新版本
+	isUpdateManager =
+		app.globalData.accountInfo.miniProgram.envVersion == 'develop'
+			? false
+			: await useGetUpdateManager();
+	// #endif
+
+	/**如果有新版本 return */
+	if (isUpdateManager) {
+		return;
+	}
+	common.showLoading();
+	// 同步获取设备信息
+	app.globalData.smallPro_systemInfo = JSON.stringify(uni.getSystemInfoSync());
+
+	const resp = await useSmallProgramLogin(app);
+	if (resp) {
+		app.globalData.logSuccess = true;
+		await useGetSysAppPageList();
+		await usePreserMember();
+		// 判断前端配置信息有修改获取form表单存储为空从新获取一遍form表单
+		if (
+			uni.getStorageSync('frontEndConfigRequest') ||
+			common.isEmpty(uni.getStorageSync('formConfigList'))
+		) {
+			await useSetFrontEndForm(frontEndConfig);
+		}
+		// 判断前端配置信息有修改获取菜单存储为空从新获取一遍菜单
+		if (
+			uni.getStorageSync('frontEndConfigRequest') ||
+			common.isEmpty(uni.getStorageSync('menuList'))
+		) {
+			await useSetMenuList(menu);
+		}
+		common.hideLoading();
+		// 配置判断是否需要互联网医院Websocket
+		if (app.globalData.hasWebsocket) {
+			// 链接websocket
+			// connectWebsocket(true);
+		}
+
+		/**判断是否登录成功回调 */
+		if (app.loginReadyCallBack && typeof app.loginReadyCallBack == 'function') {
+			app.loginReadyCallBack();
+		}
+	}
+};
+
+export default {
+	globalData: GLOBALDATA,
+	onLaunch: function (options) {
+		console.log('App Launch');
+		console.log('应用启动路径:', options.path);
+
+		// #ifndef APP || WEB || H5 || MP-BAIDU || MP-TOUTIAO || MP-LARK
+		// 保存 当前账号信息
+		app = this.$scope;
+		app.globalData.accountInfo = uni.getAccountInfoSync();
+		// #endif
+
+		// WEB只需要获取一次
+		// #ifdef WEB
+		app = getApp();
+		beforeMain();
+		// #endif
+	},
+	onShow: function (options) {
+		console.log('App Show');
+		console.log('应用启动路径:', options.path);
+		// #ifndef WEB
+		beforeMain();
+		// #endif
+	},
+	onHide: function () {
+		console.log('App Hide');
+	},
+	methods: {
+		loginReadyCallBack: () => {},
+	},
+};
+</script>
+
+<style lang="scss">
+@import '/theme/index';
+</style>

+ 162 - 0
README.md

@@ -0,0 +1,162 @@
+## 📱 项目特性
+
+- 🚀 基于 Vue 3 + TypeScript 开发
+- 📦 使用 Vite 作为构建工具
+- 🎨 集成 uni-scss 主题系统
+- 🔧 支持多平台发布(微信小程序、微信公众号)
+
+## 🛠️ 技术栈
+
+- **框架**: uni-app (Vue 3)
+- **语言**: TypeScript
+- **构建工具**: Vite
+- **包管理器**: pnpm
+- **样式**: SCSS + uni-scss
+- **组件库**: uni-ui
+
+## 📁 项目结构
+
+```
+uni-app-demo/
+├── config/                 # 配置文件
+│   ├── globalData.ts      # 全局数据配置
+│   ├── index.ts           # 配置入口
+│   └── menu.ts            # 菜单配置
+├── hook/                  # 自定义 Hooks
+│   ├── index.ts
+│   └── use-on-load/       # 页面加载 Hook
+├── pages/                 # 页面文件
+│   ├── business/          # 业务页面
+│   │   └── tabbar/        # TabBar 页面
+│   │       ├── homePage/  # 首页
+│   │       └── personalCenter/ # 个人中心
+│   └── components/        # 页面组件
+├── static/                # 静态资源
+│   └── images/            # 图片资源
+├── theme/                 # 主题样式
+│   ├── common/            # 通用样式变量
+│   ├── mixins/            # SCSS 混入
+│   └── index.scss         # 主题入口
+├── uni_modules/           # uni-ui 组件
+├── utils/                 # 工具函数
+├── App.vue                # 应用入口
+├── main.js                # 主入口文件
+├── manifest.json          # 应用配置
+├── pages.json             # 页面配置
+└── vite.config.js         # Vite 配置
+```
+
+## 🚀 快速开始
+
+### 环境要求
+
+- Node.js >= 18.0.0 | 20.0.0
+- pnpm >= 9.5.0
+
+### 安装依赖
+
+```bash
+# 使用 pnpm 安装依赖
+pnpm install
+```
+
+### 开发运行
+
+```bash
+# 微信小程序开发
+pnpm dev:mp-weixin
+
+# 支付宝小程序开发
+pnpm dev:mp-alipay
+
+# H5 开发
+pnpm dev:h5
+
+# 公众号开发
+pnpm dev:mp-gongzhonghao
+```
+
+### 构建发布
+
+```bash
+# 微信小程序构建
+pnpm build:mp-weixin
+
+# 支付宝小程序构建
+pnpm build:mp-alipay
+
+# H5 构建
+pnpm build:h5
+
+# 公众号构建
+pnpm build:mp-gongzhongha
+```
+
+## 🎨 主题配置
+
+项目使用 uni-scss 主题系统,支持:
+
+- 自定义颜色变量
+- 响应式设计
+- 组件主题定制
+- CSS 变量支持
+
+项目支持主题色自定义,修改 `uni.scss` 文件中的 SCSS 变量:
+
+```scss
+// 主色
+$uni-primary: #409eff;
+// 成功色
+$uni-success: #4cd964;
+// 警告色
+$uni-warning: #f0ad4e;
+// 错误色
+$uni-error: #dd524d;
+```
+
+## 🔧 开发配置
+
+### 代理配置
+
+开发环境已配置 API 代理,代理地址:`https://cs001.kasitesoft.com/`
+
+### 路径别名
+
+- `@` 指向项目根目录
+
+### 全局数据
+
+全局数据配置位于 `config/globalData.ts`,包含:
+
+- 应用 ID 配置
+- 登录状态管理
+- 设备信息存储
+- 渠道配置
+
+## 📦 依赖管理
+
+项目使用 pnpm 作为包管理器,提供以下脚本:
+
+```bash
+# 清理依赖
+pnpm clean
+
+# 清理 node_modules
+pnpm clean:modules
+```
+
+## 📄 如何同步开发 @kasite/uni-app-base
+
+1. 本地创建文件夹 `uni-app-base`
+
+2. 将 `@kasite/uni-app-base` 源代码复制到 `uni-app-base` 文件夹下
+
+3. 修改 `package.json` 文件中的 `@kasite/uni-app-base` 引用为 `workspace:*`
+
+```json
+"dependencies": {
+  "@kasite/uni-app-base": "workspace:*"
+}
+```
+
+4. 运行 `pnpm install` 安装依赖

+ 44 - 0
config/globalData.ts

@@ -0,0 +1,44 @@
+import manifest from '../manifest.json';
+
+let appId = '';
+
+// #ifdef MP-WEIXIN || MP-GONGZHONGHAO
+appId = manifest['mp-weixin']?.appid;
+// #endif
+
+// #ifndef MP-WEIXIN || MP-GONGZHONGHAO
+appId = '';
+// #endif
+
+export default {
+	appId,
+	token: '',
+	accountInfo: {
+		miniProgram: {
+			appId,
+			envVersion: process.env.NODE_ENV === 'development' ? 'develop' : 'release',
+			version: '',
+		},
+	},
+	/** 是否登录成功 */
+	logSuccess: false,
+	/** 配置判断是否需要互联网医院Websocket */
+	hasWebsocket: false,
+	/** 设备信息 */
+	smallPro_systemInfo: {},
+	/**渠道Id */
+	channelId: 'smallpro',
+
+	// /**当前手机绑定的所有就诊人 */
+	// memberList: null,
+	// /**当前就诊人 */
+	// currentUser: null,
+	// /**渠道Id */
+	// channelId: 'smallpro',
+	// /**个性化配置 */
+	// config: {},
+	// /**form表单配置 */
+	// formConfigList: {},
+	// /**session配置信息 */
+	// session: {},
+};

+ 4 - 0
config/index.ts

@@ -0,0 +1,4 @@
+export * from '@kasite/uni-app-base/config';
+
+export { default as GLOBALDATA } from './globalData';
+export * from './menu';

+ 643 - 0
config/menu.ts

@@ -0,0 +1,643 @@
+export const menu = [
+	{
+		MenuName: 'HomePage',
+		IsUse: '1',
+		Children: [
+			{
+				MenuName: '就医服务',
+				IsUse: '1',
+				Children: [
+					{
+						MenuName: '医技预约',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuYygh.png',
+						Topic: '热门专家快速看诊',
+						Url: '/pagesMedicalTech/st1/business/medicalTech/medicalTechList/medicalTechList',
+					},
+					{
+						MenuName: '预约挂号',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuYygh.png',
+						Topic: '热门专家快速看诊',
+						Url: '/pagesPatient/st1/business/yygh/yyghDeptList/yyghDeptList',
+					},
+					{
+						MenuName: '当日挂号',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuDrgh.png',
+						Url: '/pagesPatient/st1/business/yygh/yyghDeptList/yyghDeptList?serviceId=009',
+					},
+					{
+						MenuName: '充值缴费',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuBgcx.png',
+						Topic: '线上自助充值无需排队',
+						Url: '/pagesPatient/st1/business/recharge/rechargeMoney/rechargeMoney?pageType=mzjf',
+					},
+					{
+						MenuName: '报告查询',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuJcbg.png',
+						Children: [
+							{
+								MenuName: '门诊报告查询',
+								IsUse: '1',
+								IsShow: '1',
+								Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/ic_mzbg.png',
+								Url: '/pagesPatient/st1/business/report/reportIndex/reportIndex?queryType=1',
+							},
+							{
+								MenuName: '住院报告查询',
+								IsUse: '1',
+								IsShow: '1',
+								Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/ic_zybg.png',
+								Url: '/pagesPatient/st1/business/report/reportIndex/reportIndex?queryType=2',
+							},
+						],
+						Topic: '检查检验报告单自助查询',
+						Url: '/pagesPatient/st1/business/report/reportSelect/reportSelect',
+					},
+					{
+						MenuName: '自助开单',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuBgcx.png',
+						Topic: '',
+						Url: '/pagesAdmin/selfService/st1/business/selfProject/selfProject',
+					},
+				],
+			},
+			{
+				MenuName: '医院风采',
+				IsUse: '1',
+				Children: [
+					{
+						MenuName: '医院概况',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuYygk.png',
+						Url: '/pagesPatient/st1/business/news/introduction/introduction',
+					},
+					{
+						MenuName: '科室介绍',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuKsjs.png',
+						Url: '/pagesPatient/st1/business/news/deptList/deptList',
+					},
+					{
+						MenuName: '专家介绍',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuLcfb.png',
+						Url: '/pagesPatient/st1/business/news/doctorList/doctorList',
+					},
+				],
+			},
+		],
+	},
+	{
+		MenuName: 'netHosPage',
+		IsUse: '1',
+		Children: [
+			{
+				MenuName: '互联网菜单',
+				IsUse: '1',
+				Children: [
+					{
+						MenuName: '互联网+护理',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuYygh.png',
+						Topic: '',
+						Url: '/pagesNetHos/st1/business/nurse/nurseDeptList/nurseDeptList',
+					},
+					{
+						MenuName: '门诊结算',
+						Topic: ['mzjs'],
+						OtherParam: '',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuBgcx.png',
+						Url: '/pagesNetHos/st1/business/prescription/prescriptionList/prescriptionList',
+					},
+				],
+			},
+		],
+	},
+	{
+		MenuName: 'More',
+		IsUse: '1',
+		Children: [
+			{
+				MenuName: '门诊服务',
+				IsUse: '1',
+				IsShow: '1',
+				Children: [
+					{
+						MenuName: '预约挂号',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/yygh.png',
+						Url: '/pagesPatient/st1/business/yygh/yyghDeptList/yyghDeptList',
+					},
+					{
+						MenuName: '当日挂号',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/drgh.png',
+						Url: '/pagesPatient/st1/business/yygh/yyghDeptList/yyghDeptList?serviceId=009',
+					},
+					{
+						MenuName: '检查预约',
+						IsUse: '0',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/jcyy.png',
+						Url: '',
+					},
+					{
+						MenuName: '签到取号',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/qdqh.png',
+						Url: '/pagesPatient/st1/business/signIn/signInList/signInList',
+					},
+					{
+						MenuName: '候诊查询',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/hzcx.png',
+						Url: '/pagesPatient/st1/business/queue/queueList/queueList',
+					},
+					{
+						MenuName: '门诊充值',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/czjf.png',
+						Url: '/pagesPatient/st1/business/recharge/rechargeMoney/rechargeMoney?pageType=mzjf',
+					},
+					{
+						MenuName: '报告查询',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/bgcx.png',
+						Url: '/pagesPatient/st1/business/report/reportIndex/reportIndex?queryType=1',
+					},
+					{
+						MenuName: '门诊记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/mzjl.png',
+						Url: '/pagesPatient/st1/business/record/cardRecord/cardRecord',
+					},
+					{
+						MenuName: '门诊清单',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/mzqd.png',
+						Url: '/pagesPatient/st1/business/costDetailedList/outpatientCosts/outpatientCosts',
+					},
+					{
+						MenuName: '电子病历',
+						IsUse: '0',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/dzbl.png',
+						Url: '',
+					},
+					{
+						MenuName: '取药凭证',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/qypz.png',
+						Url: '/pagesPatient/st1/business/prescriptionManagement/drugCredentials/drugCredentials',
+					},
+					{
+						MenuName: '价格查询',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/jgcx.png',
+						Children: [
+							{
+								MenuName: '药品查询',
+								IsUse: '1',
+								IsShow: '1',
+								Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/ic_sfxm.png',
+								Url: '/pagesPatient/st1/business/priceInquiry/inquiryList/inquiryList?type=drug',
+							},
+							{
+								MenuName: '收费项目查询',
+								IsUse: '1',
+								IsShow: '1',
+								Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/ic_wsclcx.png',
+								Url: '/pagesPatient/st1/business/priceInquiry/inquiryList/inquiryList?type=pro',
+							},
+						],
+						Url: '/pagesPatient/st1/business/priceInquiry/inquirySelect/inquirySelect',
+					},
+					{
+						MenuName: '就诊指南',
+						MenuType: '1',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/jzzn.png',
+						Url: '/pagesAdmin/article/st1/business/article/list/list?typeTitle=就诊须知',
+					},
+					{
+						MenuName: '门诊宣教',
+						IsUse: '0',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/mzxj.png',
+						Url: '',
+					},
+					{
+						MenuName: '门诊满意度',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/mzmyd.png',
+						Url: '/pagesAdmin/satisfaction/satisfactionHome/satisfactionHome?objType=3',
+					},
+					{
+						MenuName: '门诊结算',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/mzqd.png',
+						Url: '/pagesPatient/st1/business/order/orderPayment/orderPayment',
+					},
+					{
+						MenuName: '在线退费',
+						MenuType: '1',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/zyjf.png',
+						Url: '/pagesPatient/st1/business/outpatient/outpatientRefund/outpatientRefund',
+					},
+					{
+						MenuName: '退费申请',
+						MenuId: '9e365e9bc0f9fc4ae0caa2fcfe445ed5',
+						MenuType: '1',
+						IsUse: '1',
+						OrderCol: 10,
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/mzzn.png',
+						Url: '/pagesPatient/st1/business/refund/refundApplication/refundApplication',
+					},
+					{
+						MenuName: '赠送锦旗',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/mzqd.png',
+						Url: '/pagesAdmin/givePennant/st1/business/pennantList/pennantList',
+					},
+					{
+						MenuName: '多学科联合门诊',
+						OtherParam: '{"pageParam":{"serviceType":"mdtyy","cardType":"1"}}',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/yygh.png',
+						Url: '/pagesAdmin/teamYy/st1/business/mdt/mdtList/mdtList',
+					},
+					{
+						MenuName: '多学科门诊记录',
+						OtherParam: '{"pageParam":{"serviceType":"mdtyyjl","cardType":"1"}}',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/mzqd.png',
+						Url: '/pagesAdmin/teamYy/st1/business/record/recordList/recordList',
+					},
+					{
+						MenuName: '罕见病',
+						OtherParam: '{"pageParam":{"serviceType":"mdtyyjl","cardType":"1"}}',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/drgh.png',
+						Url: '/pagesAdmin/teamYy/st1/business/disease/diseaseIndex/diseaseIndex',
+					},
+				],
+			},
+			{
+				MenuName: '住院服务',
+				IsUse: '1',
+				IsShow: '1',
+				Children: [
+					{
+						MenuName: '住院缴费',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/zyjf.png',
+						Url: '/pagesPatient/st1/business/recharge/rechargeMoney/rechargeMoney?pageType=zyjf',
+					},
+					{
+						MenuName: '住院记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/zyjl.png',
+						Url: '/pagesPatient/st1/business/record/hosRecord/hosRecord',
+					},
+					{
+						MenuName: '住院清单',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/zyqd.png',
+						Url: '/pagesPatient/st1/business/costDetailedList/hospitalCosts/hospitalCosts',
+					},
+					{
+						MenuName: '住院满意度',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/zymyd.png',
+						Url: '/pagesAdmin/satisfaction/satisfactionHome/satisfactionHome?objType=4',
+					},
+					{
+						MenuName: '出院带药',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/zymyd.png',
+						Url: '/pagesPatient/st1/business/prescriptionManagement/dischargeMedication/dischargeMedication',
+					},
+				],
+			},
+			{
+				MenuName: '便民服务',
+				IsUse: '1',
+				IsShow: '1',
+				Children: [
+					{
+						MenuName: '来院导航',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/lydh.png',
+						Url: '',
+					},
+					{
+						MenuName: '院内导航',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/yndh.png',
+						Url: '/pages/st1/business/h5/h5?url=https://www.onemap.top/hospital/H5',
+					},
+					{
+						MenuName: '病案复印',
+						IsUse: '0',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/bafy.png',
+						Url: '',
+					},
+					{
+						MenuName: '影像拷贝',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/bafy.png',
+						Url: '/pagesMyCopy/imagecopy/im_index/index',
+					},
+					{
+						MenuName: '病理借阅',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/bafy.png',
+						Url: '/pagesMyCopy/borrowapply/im_index/index',
+					},
+					{
+						MenuName: '意见反馈',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/yjfk.png',
+						Url: '/pagesFeedback/st1/business/home/home',
+						// "Url": "/pages/st1/business/h5/h5?url=https://cs001.kasitesoft.com/oauth/100123/kasite_demo/login.do?toUrl=DIY_KASITEFEEDBACK"
+					},
+					{
+						MenuName: '健康宣教',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/jkxj.png',
+						Url: '/pagesAdmin/article/st1/business/article/list/list?typeTitle=健康宣教',
+					},
+					{
+						MenuName: '智能问答',
+						MenuId: '94567ae473e917465d78595790ceeced',
+						MenuType: '1',
+						IsUse: '1',
+						OrderCol: 1,
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore/jzzn.png',
+						Url: 'https://miying.qq.com/guide-h5/home?appid=wx3f6fa5f164160c4b&configure=intelligent',
+					},
+					{
+						MenuName: '报告解读',
+						MenuType: '1',
+						IsUse: '1',
+						OrderCol: 1,
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/homePage_menuJcbg.png',
+						// https://miying.qq.com/guide-llm/home 正式环境使用这个地址
+						Url: 'https://dev-guidetaro.wecity.qq.com/guide-llm/home?partnerId=50006594&hospitalId=800001292&configure=report',
+					},
+				],
+			},
+		],
+	},
+	{
+		MenuName: 'PersonalCenter',
+		IsUse: '1',
+		Children: [
+			{
+				MenuName: '服务记录',
+				IsUse: '1',
+				IsShow: '1',
+				Children: [
+					{
+						MenuName: '预约记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/yyjl.png',
+						Url: '/pagesPatient/st1/business/record/appointmentRecord/appointmentRecord',
+					},
+					{
+						MenuName: '充值记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/czjl.png',
+						Url: '/pagesPatient/st1/business/record/topUpRecord/topUpRecord',
+					},
+					{
+						MenuName: '结算记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/jsjl.png',
+						Url: '/pagesPatient/st1/business/record/settlementRecord/settlementRecord',
+					},
+					{
+						MenuName: '门诊记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/mzjl.png',
+						Url: '/pagesPatient/st1/business/record/cardRecord/cardRecord',
+					},
+					{
+						MenuName: '住院记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/zyjl.png',
+						Url: '/pagesPatient/st1/business/record/hosRecord/hosRecord',
+					},
+					{
+						MenuName: '候补记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/hbjl.png',
+						Url: '/pagesPatient/st1/business/record/waitRecord/waitRecord',
+					},
+					{
+						MenuName: '上门护理记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/nurse/hz/smhl.png',
+						Url: '/pagesNetHos/st1/business/toHomeNurse/orderList/orderList',
+					},
+				],
+			},
+			{
+				MenuName: '互联网医院专区',
+				IsUse: '1',
+				IsShow: '1',
+				Children: [
+					{
+						MenuName: '问诊记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/wzjl.png',
+						Url: '/pagesNetHos/st1/business/enquire/enquireList/enquireList',
+					},
+					{
+						MenuName: '视频候诊',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/sphz.png',
+						Url: '/pagesNetHos/st1/business/doctor/waiting/waiting',
+					},
+					{
+						MenuName: '当日处方',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/drcf.png',
+						Url: '/pagesNetHos/st1/business/prescription/newPrescriptionList/newPrescriptionList',
+					},
+					{
+						MenuName: '处方取药',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/drcf.png',
+						Url: '/pagesNetHos/st1/business/prescription/prescriptionRecipe/prescriptionRecipe',
+					},
+					{
+						MenuName: '缴费记录',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/jfjl.png',
+						Url: '/pagesNetHos/st1/business/record/paymentRecord/paymentRecord',
+					},
+					{
+						MenuName: '在线续方',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/zxxf.png',
+						Url: '/pagesNetHos/st1/business/continuation/continuationList/continuationList',
+					},
+				],
+			},
+			{
+				MenuName: '更多服务',
+				IsUse: '1',
+				IsShow: '1',
+				Children: [
+					{
+						MenuName: '意见反馈',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/yjfk.png',
+						Url: '/pagesFeedback/st1/business/home/home',
+					},
+					{
+						MenuName: '地址管理',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/dzgl.png',
+						Url: '/pages/st1/business/addAddress/addressList/addressList',
+					},
+					{
+						MenuName: '我的随访',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/menu/pageMore1.1.0/sfgl.png',
+						Url: '/pagesPersonal/st1/business/patientManagement/selecteCardOrHos/selecteCardOrHos?type=card&pageType=crm',
+					},
+				],
+			},
+		],
+	},
+	{
+		MenuName: 'AddMember',
+		Children: [
+			{
+				MenuName: '微信本人添加方式',
+				IsUse: '1',
+				IsShow: '1',
+				Children: [
+					{
+						MenuName: '医保凭证一键绑定',
+						IsEecommend: '1',
+						MenuSubName: '仅适用于微信账户本人绑卡建档',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/icon/selecteBindCardMode_iconYb.png',
+						Url: '/pagesPersonal/st1/business/patientManagement/medicalCardBind/medicalCardBind',
+					},
+				],
+			},
+			{
+				MenuName: '通用添加方式',
+				IsUse: '1',
+				IsShow: '1',
+				Children: [
+					{
+						MenuName: '手动录入',
+						IsEecommend: '1',
+						MenuSubName: '适用于所有证件类型的用户绑卡建档',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/icon/selecteBindCardMode_iconAdd.png',
+						Url: '/pagesPersonal/st1/business/patientManagement/addMember/addMember',
+					},
+					{
+						MenuName: '卡号添加',
+						IsEecommend: '0',
+						MenuSubName: '适用于已有就诊卡、住院号的用户',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/icon/selecteBindCardMode_iconCard.png',
+						Url: '/pagesPersonal/st1/business/patientManagement/faceChooseVisiCard/faceChooseVisiCard?serviceType=addCard',
+					},
+					{
+						MenuName: '电子健康卡',
+						IsEecommend: '1',
+						MenuSubName: '国家卫生健康委员会兼职',
+						IsUse: '1',
+						IsShow: '1',
+						Icon: 'https://demo.kasitesoft.com/uploadFile/ui/image/icon/selecteBindCardMode_iconHe.png',
+						Url: '/pagesPersonalSlb/st1/business/patientManagement/healthCard/healthCard',
+					},
+				],
+			},
+		],
+	},
+	{
+		MenuName: 'DiyMenu',
+		Children: [
+			{
+				ToUrl: '/pagesPersonal/st1/business/patientManagement/addMember/addMember',
+				FromUrl: '/pagesPersonal/st1/business/patientManagement/addMember/addMember',
+			},
+		],
+	},
+];

+ 1 - 0
env/.env

@@ -0,0 +1 @@
+# .env

+ 14 - 0
env/.env.development

@@ -0,0 +1,14 @@
+# .env.development
+
+# Node.js 运行环境
+NODE_ENV = "development"
+
+# 请求地址  
+# VITE_APP_API_BASE_URL = "https://cs001.kasitesoft.com/"
+VITE_APP_API_BASE_URL = "api/"
+
+# 代理请求地址
+VITE_APP_PROXY_API_BASE_URL = "https://cs001.kasitesoft.com/"
+
+# 图片地址
+VITE_APP_IMAGE_URL = "https://cs001.kasitesoft.com/"

+ 11 - 0
env/.env.production

@@ -0,0 +1,11 @@
+# .env.production
+
+# Node.js 运行环境
+NODE_ENV = "production"
+
+# 请求地址  
+VITE_APP_API_BASE_URL = "https://cs001.kasitesoft.com/"
+# VITE_APP_API_BASE_URL = "api/"
+
+# 图片地址
+VITE_APP_IMAGE_URL = "https://cs001.kasitesoft.com/"

+ 6 - 0
hook/index.ts

@@ -0,0 +1,6 @@
+export * from '@kasite/uni-app-base/hook';
+
+/** 生命周期 */
+export * from './use-on-load';
+
+export * from './use-menu-click';

+ 4 - 0
hook/use-menu-click/index.ts

@@ -0,0 +1,4 @@
+import { menuClick } from '@/utils';
+
+/**  首页 更多 个人中心  菜单点击  */
+export const useMenuClick = menuClick;

+ 15 - 0
hook/use-on-load/index.ts

@@ -0,0 +1,15 @@
+import { onLoad } from '@dcloudio/uni-app';
+
+/** 页面生命周期 - onLoad */
+export const useOnLoad = (callback: Function) => {
+	const app = getApp();
+	onLoad((options) => {
+		if (app.globalData.logSuccess) {
+			callback(options);
+		} else {
+			app.loginReadyCallBack = () => {
+				callback(options);
+			};
+		}
+	});
+};

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+  </body>
+</html>

+ 25 - 0
main.js

@@ -0,0 +1,25 @@
+import App from './App';
+import store from "./store";
+
+// #ifndef VUE3
+import Vue from 'vue'
+import './uni.promisify.adaptor'
+Vue.config.productionTip = false
+App.mpType = 'app'
+const app = new Vue({
+	...App
+})
+app.$mount().use(store)
+// #endif
+
+// #ifdef VUE3
+import {
+	createSSRApp
+} from 'vue'
+export function createApp() {
+	const app = createSSRApp(App).use(store)
+	return {
+		app
+	}
+}
+// #endif

+ 69 - 0
manifest.json

@@ -0,0 +1,69 @@
+{
+    "name" : "uni-app-demo",
+    "appid" : "__UNI__2391417",
+    "description" : "demo",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        "modules" : {},
+        "distribute" : {
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            "ios" : {},
+            "sdkConfigs" : {}
+        }
+    },
+    "quickapp" : {},
+    "mp-weixin" : {
+        "appid" : "wxede0b125eed31b0d",
+        "setting" : {
+            "urlCheck" : false,
+            "es6" : false,
+            "minified" : true
+        },
+        "usingComponents" : true,
+        "optimization" : {
+            "subPackages" : true
+        }
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "3"
+}

+ 34 - 0
package.json

@@ -0,0 +1,34 @@
+{
+	"scripts": {
+		"clean": "pnpm store prune --force && pnpm run clean:modules",
+		"clean:modules": "rimraf pnpm-lock.yaml && rimraf node_modules",
+		"build": "uni build",
+		"build:obfuscated": "uni build --mode production",
+		"dev": "uni",
+		"dev:h5": "uni --platform h5",
+		"dev:mp-weixin": "uni --platform mp-weixin",
+		"dev:mp-alipay": "uni --platform mp-alipay"
+	},
+	"uni-app": {
+		"scripts": {
+			"mp-gongzhonghao": {
+				"title": "公众号",
+				"env": {
+					"UNI_PLATFORM": "h5"
+				},
+				"define": {
+					"MP-GONGZHONGHAO": true
+				}
+			}
+		}
+	},
+	"dependencies": {
+		"@kasite/uni-app-base": "workspace:*",
+		"mui-player": "^1.8.1",
+		"hls.js": "^1.6.13"
+	},
+	"devDependencies": {
+		"javascript-obfuscator": "^4.1.1",
+		"vite-plugin-javascript-obfuscator": "^3.1.0"
+	}
+}

+ 76 - 0
pages.json

@@ -0,0 +1,76 @@
+{
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/business/tabbar/homePage/homePage",
+			"style": {
+				"navigationBarTitleText": "首页"
+			}
+		},
+		{
+			"path": "pages/business/tabbar/personalCenter/personalCenter",
+			"style": {
+				"navigationBarTitleText": "我的"
+			}
+		},
+		{
+			"path": "pages/business/tabbar/transferPage/transferPage",
+			"style": {
+				"navigationBarTitleText": ""
+			}
+		}
+	],
+	"subPackages": [{
+		"root": "pagesAdmin",
+		"pages": [{
+				"path": "article/business/article/detail/detail",
+				"style": {
+					"navigationBarTitleText": "文章详情"
+				}
+			},
+			{
+				"path": "satisfaction/business/satisfactionQuestions/satisfactionQuestions",
+				"style": {
+					"navigationBarTitleText": ""
+				}
+			}
+		]
+	}, {
+		"root": "pagesCrm",
+		"pages": [{
+			"path": "business/home/home",
+			"style": {
+				"navigationBarTitleText": "我的随访",
+				"enablePullDownRefresh": true
+			}
+		}, {
+			"path": "business/schemeDetail/schemeDetail",
+			"style": {
+				"navigationBarTitleText": ""
+			}
+		}]
+	}],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "uni-app 演示",
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"backgroundColor": "#F8F8F8"
+	},
+	"tabBar": {
+		"backgroundColor": "#fff",
+		"borderStyle": "black",
+		"selectedColor": "#55AA56",
+		"color": "#B8BBBF",
+		"list": [{
+			"pagePath": "pages/business/tabbar/homePage/homePage",
+			"iconPath": "static/images/tabbar/homePage.png",
+			"selectedIconPath": "static/images/tabbar/homePage_ac.png",
+			"text": "首页"
+		}, {
+			"pagePath": "pages/business/tabbar/personalCenter/personalCenter",
+			"iconPath": "static/images/tabbar/personalCenter.png",
+			"selectedIconPath": "static/images/tabbar/personalCenter_ac.png",
+			"text": "我的"
+		}]
+	},
+	"uniIdRouter": {}
+}

+ 33 - 0
pages/business/tabbar/homePage/homePage.vue

@@ -0,0 +1,33 @@
+<template>
+	<view class="color-primary p-20">首页</view>
+	<view>{{ text }}</view>
+	<uni-search-bar @confirm="" @input="" />
+	<uni-badge text="1" type="primary" />
+
+	<button @click="toCRM">CRM</button>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import { useOnLoad } from '@/hook';
+import { common } from '@/utils';
+
+const app = getApp();
+
+const text = ref('Hello world');
+const fn = () => {
+	console.log('logSuccess');
+};
+
+const toCRM = () => {
+	common.goToUrl("/pagesCrm/business/home/home")
+};
+
+useOnLoad(fn);
+</script>
+
+<style lang="scss" scoped>
+.color-primary {
+	color: var(--uni-color-primary);
+}
+</style>

+ 9 - 0
pages/business/tabbar/personalCenter/personalCenter.vue

@@ -0,0 +1,9 @@
+<template>
+123
+</template>
+
+<script lang="ts" setup>
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 218 - 0
pages/business/tabbar/transferPage/transferPage.vue

@@ -0,0 +1,218 @@
+<template>
+	<view class="container">
+		<view class="content"> </view>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { useOnLoad } from '@/hook';
+import {
+	useSetMenuList,
+	useLoadViewMenu,
+	useIsToAuthPage,
+	useGetDefaultMember,
+} from '@kasite/uni-app-base';
+import { GetRoomId, GetMemberChatList } from '@kasite/uni-app-base/service/base';
+import { common, menuClick } from '@/utils';
+import { menu } from '@/config';
+
+const app = getApp();
+
+const main = async (options: any = {}) => {
+	let menuList = common.isEmpty(uni.getStorageSync('menuList'))
+		? await useSetMenuList(menu)
+		: uni.getStorageSync('menuList');
+	let skipWay = 'redirectTo'; // 跳转方式 默认关闭当前页面跳转
+	let url = ''; // 页面地址
+	/**
+	 * 用于判断特定页面跳转是否需要查询就诊人
+	 * 为true时 memberId为必须条件 前端才可反向查询当前人的数据
+	 */
+	let isQueryMember = false;
+	/**
+	 * 跳转特定页面需要查询就诊人时 且当前特定页面需要就诊卡或住院号 1:就诊卡 14 住院号
+	 * 当当前页面为一定有卡号或住院号的 一定要传卡号 cardNo
+	 */
+	let cardType;
+	let queryBean; // 跳转特定页面需要一些特定参数
+	// 二维码扫码跳转进入
+	if (options.q) {
+		let url_q = decodeURIComponent(options.q);
+		url_q.replace(/([^?&=]+)=([^&]+)/g, (_, k, v) => (options[k] = v));
+		// 基层预约挂号支付
+		if (url_q && url_q?.split('?')[1]?.split('=')[0] == 'OrderId') {
+			options.type = 'pay';
+			options.orderId = url_q?.split('?')[1]?.split('=')[1];
+		}
+	}
+	if (common.isNotEmpty(options.type)) {
+		if (options.type == 0) {
+			skipWay = 'reLaunch';
+			/** 首页 **/
+			url = `/pages/st1/business/tabbar/homePage/homePage`;
+			options.isViewMenu && (await useLoadViewMenu());
+		} else if (options.type == 1) {
+			skipWay = 'reLaunch';
+			/** 互联网医院 **/
+			url = `/pages/st1/business/tabbar/netHosIndex/netHosIndex`;
+		} else if (options.type == 2) {
+			skipWay = 'reLaunch';
+			/** 消息中心 **/
+			url = `/pages/st1/business/tabbar/MsgContent/MsgContent`;
+		} else if (options.type == 3) {
+			skipWay = 'reLaunch';
+			/** 个人中心 **/
+			url = `/pages/st1/business/tabbar/personalCenter/personalCenter`;
+		} else if (options.type == 4) {
+			/** 订单详情页 **/
+			url = `/pagesPatient/st1/business/pay/payState/payState?orderId=${options.orderId}`;
+		} else if (options.type == 5) {
+			/** AI到诊回调科室排班页 **/
+			let queryBean = {
+				DeptCode: options.deptCode,
+				DeptName: decodeURIComponent(options.deptName),
+			};
+			url = `/pagesPatient/st1/business/yygh/yyghDoctorList/yyghDoctorList?queryBean=${JSON.stringify(
+				queryBean
+			)}`;
+		} else if (options.type == 'satisfactionQuestions') {
+			// CRM随访满意度问卷
+			url = `/pagesAdmin/satisfaction/satisfactionQuestions/satisfactionQuestions?subjectId=${options.subjectId}`;
+		} else if (options.type == 'articleDetail') {
+			// CRM文章详情
+			url = `/pagesAdmin/article/business/article/detail/detail?id=${options.articleId}&msgId=${options.msgId}`;
+		} else if (options.type == 'fuchat') {
+			// HCRM-我的随访
+			const chatData = await this.getMemberChatList(options);
+			if (!chatData) return;
+			app.globalData.currentUser = { memberId: chatData.MemberId };
+			url = `/pagesCrm/st1/business/myFollowUp/followUpList/followUpList?type=${chatData.Type}&roomId=${chatData.Id}&deptName=${chatData.DeptName}&doctorSn=${chatData.Doctorsn}`;
+		} else if (options.type == 'fuvisit') {
+			// HCRM-我的随访
+			const roomIdData = await this.getRoomId(options);
+			options.roomId = roomIdData.RoomId;
+			const chatData = await this.getMemberChatList(options);
+			if (!chatData) return;
+			app.globalData.currentUser = { memberId: chatData.MemberId };
+			url = `/pagesCrm/st1/business/myFollowUp/followUpList/followUpList?type=${chatData.Type}&roomId=${chatData.Id}&deptName=${chatData.DeptName}&doctorSn=${chatData.Doctorsn}`;
+		} else if (options.type == 'article') {
+			// 点击分享的链接跳转文章详情页面
+			url = `/pagesAdmin/teamYy/st1/business/disease/articlenInfo/articlenInfo?heaId=${ata.heaId}`;
+		} else if (options.type == 'pay') {
+			/** 订单支付页 **/
+			url = `/pagesPatient/st1/business/pay/payMent/payMent?orderId=${options.orderId}`;
+		} else if (options.type == 'myddc') {
+			// 满意度调查
+			let subjectId = options.subjectId;
+			let taskId = options.taskId;
+			url = `/pagesAdmin/satisfaction/business/satisfactionQuestions/satisfactionQuestions?subjectId=${subjectId}&taskId=${taskId}`;
+		} else if (options.type == 'overduePerson') {
+			let overdueOptions = common.isJSON(uni.getStorageSync('overdueOptions'))
+				? JSON.parse(uni.getStorageSync('overdueOptions'))
+				: uni.getStorageSync('overdueOptions');
+			const options = overdueOptions.options;
+			if (overdueOptions.route.indexOf('yyghClinicMsg') != -1) {
+				app.globalData.queryBean = overdueOptions.options.queryBean;
+			}
+			overdueOptions.options.queryBean = common.isJSON(overdueOptions.options.queryBean)
+				? overdueOptions.options.queryBean
+				: JSON.stringify(overdueOptions.options.queryBean);
+			const queryString = Object.keys(options)
+				.map((key) => `${key}=${options[key]}`)
+				.join('&');
+			url = `/${overdueOptions.route}?${queryString}`;
+			await checkAuthAndDefaultMember(url);
+			uni.setStorageSync('overdueOptions', '');
+		} else {
+			// 不等于前面特定页面时 走菜单获取 type值为菜单名称
+			const childArray = this.getAllChildren(menuList.map((item) => item.Children));
+			let menuItem = childArray.filter((item) => item.MenuName == options.type);
+			// 判断当前过滤的菜单名称列表不为空时,走定义的菜单跳转
+			if (common.isNotEmpty(menuItem)) {
+				menuItem = menuItem[0];
+				menuClick(menuItem, this, skipWay);
+				return;
+			}
+		}
+	}
+	if (common.isEmpty(url)) {
+		common.showModal(`<<${options.type}>>跳转地址为空,请联系管理员`, () => {
+			common.goToUrl(`/pages/st1/business/tabbar/homePage/homePage`, {
+				skipWay: 'reLaunch',
+			});
+		});
+		return;
+	}
+	common.goToUrl(url, {
+		skipWay: skipWay,
+	});
+};
+/** HCRM-我的随访-获取房间ID */
+const getRoomId = async (data) => {
+	let params = {
+		Doctorsn: data.doctorsn,
+		MemberId: data.memberId,
+	};
+
+	let { resp, resData } = await GetRoomId(params);
+	if (common.isNotEmpty(resp)) {
+		return resp[0];
+	}
+	return null;
+};
+/** HCRM-我的随访-获取会话列表 */
+const getMemberChatList = async (data) => {
+	const params = {
+		MemberId: data.memberId,
+		RoomId: data.roomId,
+		ChatType: '0',
+	};
+	const { resp, resData } = await GetMemberChatList(params);
+	if (common.isNotEmpty(resp[0])) {
+		return resp[0];
+	} else {
+		common.showModal('未查询到记录');
+		return null;
+	}
+};
+/** 判断公众号授权和默认就诊人 */
+const checkAuthAndDefaultMember = async (url) => {
+	/**跳转到挂号页面 需要判断就诊人 和公众号授权*/
+	if (useIsToAuthPage()) {
+		return;
+	}
+	// 获取当前设置的患者信息 判断是否可以无卡预约 取不同的值允许无卡预约或者默认就诊人信息 否者获取就诊卡信息,如当前默认就诊人无就诊卡,跳转选卡界面
+	let lastOperatorResp = await useGetDefaultMember({
+		cardType: '1',
+		url: url,
+		isKeepPage: true,
+		pageType: 'doctorYygh', //个性化预约挂号
+	});
+	// 判断返回的炒作人为null 代表跳转选择界面了
+	if (lastOperatorResp === null) return;
+	if (common.isEmpty(lastOperatorResp)) {
+		app.globalData.toUrl = url; //保存添加成功后跳转地址
+		app.globalData.cardType = app.globalData.withoutCard ? '' : '1'; //保存添加成功,要查询的默认人信息
+		url = `/pagesPersonal/st1/business/patientManagement/addMember/addMember`;
+	}
+};
+/** 菜单数组处理 */
+const getAllChildren = (arr) => {
+	return arr.flatMap((item) => {
+		if (Array.isArray(item)) {
+			return this.getAllChildren(item);
+		} else if (item.Children) {
+			return [item, ...this.getAllChildren(item.Children)];
+		} else {
+			return [item];
+		}
+	});
+};
+
+useOnLoad((options) => {
+	uni.showLoading();
+	main(options);
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 758 - 0
pagesAdmin/article/business/article/detail/detail.vue

@@ -0,0 +1,758 @@
+<template>
+	<view class="container">
+		<view class="content">
+			<view class="title">
+				{{ articleInfo.Title }}
+			</view>
+			<view class="info_row_1 displayFlexRow">
+				<view class="source-type">
+					{{ articleInfo.SourceType == 1 ? '原创' : '转载' }}
+				</view>
+				<view class="author">
+					{{ articleInfo.Author }}
+				</view>
+				<view class="dept_name">
+					<text v-for="item in articleInfo.Depts" :key="item.Key">{{ item.Value }}</text>
+				</view>
+				<view>
+					{{ articleInfo.PublishTime }}
+				</view>
+			</view>
+			<view class="mt-20">
+				<text>分类:</text>
+				<text
+					class="tag"
+					v-for="(item, index) in articleInfo.ArticleTypeList"
+					:key="index"
+					catchtap="toList"
+					data-param-value="{{item.Id}}"
+					data-param-name="TypeId"
+					data-param-label="{{item.Title}}"
+				>
+					#{{ item.Title }}
+				</text>
+			</view>
+			<view class="mt-20">
+				<text>标签:</text>
+				<text
+					class="tag"
+					v-for="(item, index) in articleInfo.Diseases"
+					:key="index"
+					catchtap="toList"
+					data-param-value="{{item.Key}}"
+					data-param-name="DiseaseId"
+					data-param-label="{{item.Value}}"
+				>
+					#{{ item.Value }}
+				</text>
+				<text
+					class="tag"
+					v-for="(item, index) in articleInfo.Tags"
+					:key="index"
+					catchtap="toList"
+					data-param-value="{{item.Key}}"
+					data-param-name="TagId"
+					data-param-label="{{item.Value}}"
+				>
+					#{{ item.Value }}
+				</text>
+			</view>
+			<view class="description">
+				{{ articleInfo.Description }}
+			</view>
+			<view>
+				<mp-html
+					v-if="articleInfo.ContentType != 1"
+					:domain="domain"
+					:content="articleInfo.Content"
+					:token="token"
+				></mp-html>
+			</view>
+			<view class="source_row" v-if="articleInfo.SourceType == 2">
+				<view class="source"> 本文来自【{{ articleInfo.OriginalSource }}】 </view>
+				<view
+					v-if="articleInfo.OriginalLink != ''"
+					class="to_source"
+					data-url="{{articleInfo.OriginalLink}}"
+					bindtap="toSource"
+				>
+					阅读原文
+				</view>
+			</view>
+			<view class="data-bar">
+				<text> 阅读:{{ articleInfo.SeeCount }} </text>
+				<view class="data-bar-btn">
+					<view @click="like">
+						<image
+							:src="articleInfo.MyLike ? icon.article_like_active : icon.article_like"
+							class="icon_like"
+						/>
+						{{ articleInfo.LikeCount > 999 ? '999+' : articleInfo.LikeCount || 0 }}
+					</view>
+					<view>
+						<image :src="icon.article_share" class="icon_share" />
+						<button
+							class="share no-margin no-padding"
+							open-type="share"
+							style="font-weight: 400; font-size: 28rpx"
+						>
+							去分享
+						</button>
+					</view>
+				</view>
+			</view>
+			<!-- 文章评论列表 -->
+			<block v-if="articleInfo.EnableComment">
+				<view class="comment-box">
+					<view style="display: flex; justify-content: space-between">
+						<text>评论 {{ commentCount }}</text>
+						<text
+							v-if="commentList.length > 0"
+							style="color: var(--uni-color-primary)"
+							@click="toComment"
+						>
+							我要评论
+						</text>
+					</view>
+					<view
+						v-if="commentLoad && commentList.length == 0"
+						style="text-align: center; margin-top: 30rpx"
+					>
+						<text>暂无评论</text>
+						<text style="color: var(--uni-color-primary)" @click="toComment">我要评论</text>
+					</view>
+					<block v-if="commentList.length > 0">
+						<view v-for="item in commentList" class="comment-list" :key="item.Id">
+							<view class="comment-item" W>
+								<text class="comment-member-name">{{ item.MemberName }}</text>
+								<text class="comment-time">{{ item.CreateTime }}</text>
+							</view>
+							<view style="margin-right: 40rpx">
+								<zy-rate :max="5" :score="item.Score" :size="20" disabled></zy-rate>
+							</view>
+						</view>
+					</block>
+				</view>
+			</block>
+			<!-- 快捷服务悬浮窗 -->
+			<view
+				@touchstart="serviceDialogTouchStart"
+				@touchmove="serviceDialogTouchMove"
+				@touchend="serviceDialogTouchEnd"
+				class="article-convenient-service-list"
+				id="service-float"
+				:style="
+					serviceDialogXY.x !== null && serviceDialogXY.y !== null
+						? { left: serviceDialogXY.x + 'px', top: serviceDialogXY.y + 'px', bottom: 'auto' }
+						: { left: '25px', bottom: '100px' }
+				"
+				v-if="showConvenientServices"
+			>
+				<block v-for="(item, field) in convenientServices">
+					<view
+						v-if="item.show"
+						class="article-convenient-service-item"
+						style="display: flex; align-items: center"
+						:data-service="field"
+						@click="toServicePage"
+					>
+						<image :src="item.icon" style="width: 40rpx; height: 40rpx; margin-right: 10rpx" />
+						<text>{{ item.title }}</text>
+					</view>
+				</block>
+			</view>
+			<!-- 文章评价弹窗 -->
+			<zy-half-screen-dialog
+				height="25%"
+				extClass="zy_half_screen_dialog"
+				v-model:show="articleCommentDialogVisible"
+				maskClosable
+			>
+				<view class="zy_half_screen_dialog_content">
+					<text>对文章进行评价</text>
+					<view style="display: flex; justify-content: space-between; margin-top: 50rpx">
+						<zy-rate :max="5" v-model:score="commentScore" :size="20"></zy-rate>
+						<view style="width: 200rpx !important">
+							<button
+								class="comment_btn"
+								:disabled="commentCommiting || commentScore == 0"
+								@click="commitComment"
+							>
+								确定
+							</button>
+						</view>
+					</view>
+				</view>
+			</zy-half-screen-dialog>
+		</view>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, reactive, getCurrentInstance, watch, nextTick } from 'vue';
+import { onShareAppMessage, onShareTimeline, onReachBottom } from '@dcloudio/uni-app';
+import { useOnLoad, useDomain, useGetMember, useIsToAuthPage, useGetDefaultMember } from '@/hook';
+import { mapState, mapMutations } from '@kasite/uni-app-base/store';
+import {
+	ArticleDetail,
+	ShareArticle,
+	LikeArticle,
+	GetCommentList,
+	CreateComment,
+} from '../../../service';
+import ZyHalfScreenDialog from '../../../components/half-screen-dialog/index.vue';
+import ZyRate from '../../../components/rate/index.vue';
+import icon from '@/utils/icon';
+import { common, menuClick } from '@/utils';
+
+const app = getApp();
+
+const domain = useDomain();
+const { token, currentUser } = mapState({
+	token: 'token',
+	currentUser: 'currentUser',
+});
+const { setCurrentUser } = mapMutations({
+	setCurrentUser: 'setCurrentUser',
+});
+const id = ref();
+const _msgUUID = ref();
+const articleInfo = ref({} as any);
+const commentList = ref([]);
+const commentCount = ref(0);
+const commentLoad = ref(false);
+const _commentPage = reactive({
+	PIndex: 1,
+	PSize: 10,
+});
+const isLastPage = ref(false);
+const convenientServices = ref({
+	DoctorVideocons: {
+		title: '视频问诊',
+		show: false,
+		pagePath: '',
+		icon: icon.article_video_cons,
+	},
+	DoctorServiceCons: {
+		title: '图文咨询',
+		show: false,
+		pagePath: '',
+		icon: icon.article_service_cons,
+	},
+	DoctorServiceGh: {
+		title: '预约挂号',
+		show: false,
+		pagePath: '',
+		icon: icon.article_service_gh,
+	},
+});
+const doctorDeptList = ref([]);
+const showConvenientServices = ref(false);
+const articleCommentDialogVisible = ref(false);
+const commentScore = ref(0);
+const commentCommiting = ref(false);
+const serviceDialogXY = reactive({
+	x: 25,
+	y: null as number | null,
+});
+
+// 悬浮窗拖动数据与方法
+const instance = getCurrentInstance();
+const serviceDrag = reactive({
+	dragging: false,
+	startX: 0,
+	startY: 0,
+	elemW: 0,
+	elemH: 0,
+	winW: uni.getSystemInfoSync().windowWidth,
+	winH: uni.getSystemInfoSync().windowHeight,
+});
+
+const measureServiceFloat = () => {
+	return new Promise<void>((resolve) => {
+		uni
+			.createSelectorQuery()
+			.in(instance?.proxy as any)
+			.select('#service-float')
+			.boundingClientRect((rect) => {
+				if (rect) {
+					serviceDrag.elemW = rect.width || 0;
+					serviceDrag.elemH = rect.height || 0;
+					// 首次测量时如果还未设置 y,则用当前 rect.top 作为初始 top
+					if (serviceDialogXY.y === null) {
+						serviceDialogXY.x = rect.left || serviceDialogXY.x;
+						serviceDialogXY.y = rect.top || 0;
+					}
+				}
+				resolve();
+			})
+			.exec();
+	});
+};
+
+// 首次显示悬浮窗时,基于默认 bottom:200px 计算等价的 top,避免首次触摸从 bottom 切换到 top 产生位移
+watch(showConvenientServices, async (val) => {
+	if (!val) return;
+	await nextTick();
+	uni
+		.createSelectorQuery()
+		.in(instance?.proxy as any)
+		.select('#service-float')
+		.boundingClientRect((rect) => {
+			if (!rect) return;
+			serviceDrag.elemW = rect.width || 0;
+			serviceDrag.elemH = rect.height || 0;
+			if (serviceDialogXY.y === null) {
+				// 将默认的 bottom:200px 转换为 top 值,保持视觉位置不变
+				const topY = Math.max(0, serviceDrag.winH - (rect.height || 0) - 100);
+				serviceDialogXY.x = serviceDialogXY.x ?? 25;
+				serviceDialogXY.y = topY;
+			}
+		})
+		.exec();
+});
+
+const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val));
+
+function serviceDialogTouchStart(e: any) {
+	serviceDrag.dragging = true;
+	const touch = e.touches && e.touches[0];
+	if (!touch) return;
+	serviceDrag.startX = touch.pageX;
+	serviceDrag.startY = touch.pageY;
+	measureServiceFloat();
+}
+
+function serviceDialogTouchMove(e: any) {
+	if (!serviceDrag.dragging) return;
+	// 阻止页面滚动
+	if (e && typeof e.preventDefault === 'function') e.preventDefault();
+	if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
+	const touch = e.touches && e.touches[0];
+	if (!touch) return;
+	const dx = touch.pageX - serviceDrag.startX;
+	const dy = touch.pageY - serviceDrag.startY;
+	let nextX = serviceDialogXY.x + dx;
+	let nextY = (serviceDialogXY.y ?? 0) + dy;
+	nextX = clamp(nextX, 0, Math.max(0, serviceDrag.winW - serviceDrag.elemW));
+	nextY = clamp(nextY, 0, Math.max(0, serviceDrag.winH - serviceDrag.elemH));
+	serviceDialogXY.x = nextX;
+	serviceDialogXY.y = nextY;
+	serviceDrag.startX = touch.pageX;
+	serviceDrag.startY = touch.pageY;
+}
+
+function serviceDialogTouchEnd() {
+	serviceDrag.dragging = false;
+}
+
+/** 获取文章详情 */
+const getArticleDetail = async () => {
+	const { resp } = await ArticleDetail({
+		Id: id.value,
+		Uuid: _msgUUID.value,
+	});
+	if (resp && resp.length) {
+		const info = resp[0];
+		const content = unescape(info.Content);
+		info.Content = content;
+		info.PublishTime = formatDate2(info.PublishTime);
+		let articleConvenientServices = info.Config?.ConvenientServices || {};
+		if (articleConvenientServices) {
+			for (const field in articleConvenientServices) {
+				convenientServices.value[field].show = articleConvenientServices[field].Checked;
+			}
+		}
+		articleInfo.value = info;
+		doctorDeptList.value = info.DoctorInfo?.DoctorInfo || [];
+		setConvenientServices();
+	}
+};
+/** 格式化日期 */
+const formatDate2 = (date) => {
+	if (!date) {
+		return '';
+	}
+	return date.substring(5, 16);
+};
+const setConvenientServices = () => {
+	showConvenientServices.value =
+		Object.keys(convenientServices.value).find((field) => {
+			return convenientServices.value[field]?.show === true;
+		}) != undefined;
+};
+/** 点赞 */
+const like = async () => {
+	const { Id: id, MyLike: like = false } = articleInfo.value;
+	if (like) {
+		common.showModal('已点赞');
+	} else {
+		const res = await LikeArticle({ Id: id });
+		if (res.resData.RespCode === '10000') {
+			// 点赞成功
+			articleInfo.value.MyLike = true;
+			articleInfo.value.LikeCount++;
+		}
+	}
+};
+/** 获取评论 */
+const getCommentList = async (page) => {
+	const res = await GetCommentList({
+		ArticleId: id.value,
+		Page: page,
+		IsShow: 1,
+	});
+	const { PCount = 0 } = res.resData?.Page;
+	commentCount.value = PCount;
+	commentLoad.value = true;
+	if (PCount <= _commentPage.PSize) {
+		isLastPage.value = true;
+	}
+	return res.resp || [];
+};
+const refershCommentList = async () => {
+	_commentPage.PIndex = 1;
+	const list = await getCommentList(_commentPage);
+	commentList.value = list;
+};
+const loadCommentListNextPage = async () => {
+	_commentPage.PIndex += 1;
+	isLastPage.value = false;
+	const list = await getCommentList(_commentPage);
+	if (list.length < _commentPage.PSize) {
+		isLastPage.value = true;
+	}
+	commentList.value.push(...list);
+	return commentList;
+};
+
+const toComment = () => {
+	if (!currentUser.value) {
+		uni.showToast({
+			title: '请先添加就诊人',
+			icon: 'none',
+		});
+		return;
+	}
+	articleCommentDialogVisible.value = true;
+};
+/** 提交评论 */
+const commitComment = async () => {
+	commentCommiting.value = true;
+	try {
+		const params = {
+			ArticleId: id.value,
+			score: commentScore.value,
+			MemberId: currentUser.value.memberId,
+			MemberName: currentUser.value.memberName,
+		};
+		const res = await CreateComment(params);
+		if (res.resData.RespCode !== '10000') {
+			uni.showToast({
+				title: res.resData.RespMessage,
+				icon: 'error',
+			});
+		} else {
+			uni.showToast({
+				title: '评论成功',
+			});
+			articleCommentDialogVisible.value = false;
+			isLastPage.value = false;
+			commentScore.value = 0;
+			refershCommentList();
+		}
+	} catch (error) {
+		uni.showToast({
+			title: '评论失败',
+			icon: 'error',
+		});
+	} finally {
+		commentCommiting.value = false;
+	}
+};
+
+const toServicePage = (e) => {
+	const service = e.currentTarget.dataset.service;
+
+	if (service === 'DoctorServiceCons') {
+		uni.showActionSheet({
+			alertText: '请选择科室',
+			itemList: articleInfo.value.DoctorInfo?.DoctorInfo?.map((item) => item.DeptName) || [],
+			success: ({ tapIndex }) => {
+				const doctorInfo = articleInfo.value.DoctorInfo.DoctorInfo[tapIndex];
+				// 图文咨询
+				const queryBean = common.stringify(doctorInfo);
+				const url = `/pagesNetHos/st1/business/doctor/supplement/supplement?queryBean=${queryBean}&selectedType=twzx`;
+				let data = {
+					Url: url,
+				};
+				menuClick(data, this);
+			},
+		});
+	} else if (service === 'DoctorVideocons') {
+		uni.showActionSheet({
+			alertText: '请选择科室',
+			itemList: articleInfo.value.DoctorInfo?.DoctorInfo?.map((item) => item.DeptName) || [],
+			success: ({ tapIndex }) => {
+				const doctorInfo = articleInfo.value.DoctorInfo.DoctorInfo[tapIndex];
+				const queryBean = common.stringify(doctorInfo);
+				const url = `/pagesNetHos/st1/business/doctor/supplement/supplement?queryBean=${queryBean}&selectedType=spzx`;
+				let data = {
+					Url: url,
+				};
+				menuClick(data, this);
+			},
+		});
+	} else if (service === 'DoctorServiceGh') {
+		const doctorInfo = articleInfo.value.DoctorInfo.DoctorInfo[0];
+		handleRouter(
+			{
+				DoctorId: doctorInfo.DoctorId,
+				DoctorCode: doctorInfo.DoctorCode,
+				DoctorName: doctorInfo.DoctorName,
+				DoctorUid: doctorInfo.DoctorUid,
+				HosId: doctorInfo.HosId,
+			},
+			'yyghClinicMsg',
+			0
+		);
+	} else {
+		// 服务未开通
+		console.error('服务未开通');
+	}
+};
+const handleRouter = async (queryBean, page, serviceId, selectedIndex) => {
+	app.globalData.queryBean = queryBean;
+	let url = `/pagesPatient/st1/business/yygh/${page}/${page}?serviceId=${serviceId || 0}`;
+	if (selectedIndex != undefined) {
+		url = url + `&selectedIndex=${selectedIndex}`;
+	}
+	if (page == 'yyghClinicMsg') {
+		/**跳转到挂号页面 需要判断就诊人 和公众号授权*/
+		if (useIsToAuthPage()) {
+			return;
+		}
+		// 获取当前设置的患者信息 判断是否可以无卡预约 取不同的值允许无卡预约或者默认就诊人信息 否者获取就诊卡信息,如当前默认就诊人无就诊卡,跳转选卡界面
+		let lastOperatorResp = await useGetDefaultMember({
+			cardType: app.globalData.withoutCard ? '' : '1',
+			url: url,
+			isKeepPage: true,
+		});
+		// 判断返回的操作人为null 代表跳转选择界面了
+		if (lastOperatorResp === null) return;
+		if (common.isEmpty(lastOperatorResp)) {
+			app.globalData.toUrl = url; //保存添加成功后跳转地址
+			app.globalData.cardType = app.globalData.withoutCard ? '' : '1'; //保存添加成功,要查询的默认人信息
+			url = `/pagesPersonal/st1/business/patientManagement/addMember/addMember`;
+		}
+	}
+	common.goToUrl(url);
+};
+
+useOnLoad((options) => {
+	if (uni.onCopyUrl) {
+		uni.onCopyUrl(() => {
+			ShareArticle({ Id: id.value, OptType: 'copy_url' });
+			return { query: `id=${options.id}` };
+		});
+	}
+	id.value = options.id;
+	_msgUUID.value = options.msgId || null;
+	getArticleDetail();
+	refershCommentList();
+	if (common.isEmpty(currentUser.value)) {
+		useGetMember('defaultInfo').then((data) => {
+			setCurrentUser(data);
+		});
+	}
+});
+onReachBottom(() => {
+	if (!isLastPage.value) {
+		loadCommentListNextPage();
+	}
+});
+/** 转发 */
+onShareAppMessage(() => {
+	ShareArticle({ Id: id.value, OptType: 'send' });
+	return {
+		title: articleInfo.value.Title,
+		path: `/pagesAdmin/article/business/article/detail/detail?id=${id.value}`,
+	};
+});
+/** 分享到朋友圈 */
+onShareTimeline(() => {
+	ShareArticle({ Id: id.value, OptType: 'share' });
+	return {
+		title: articleInfo.value.Title,
+	};
+});
+</script>
+
+<style lang="scss" scoped>
+page {
+	height: auto !important;
+}
+view.content {
+	padding: 60rpx 30rpx 120rpx 30rpx;
+	background-color: #ffffff;
+}
+.container view {
+	overflow: visible;
+}
+.title {
+	font-size: 36rpx;
+	color: #222326;
+	font-weight: 600;
+	line-height: 1.2em;
+}
+.info_row_1 {
+	margin-top: 26rpx;
+	justify-content: start;
+	flex-wrap: wrap;
+}
+.info_row_1 > view {
+	font-size: 26rpx;
+	color: #8a8a99;
+	margin-right: 20rpx;
+	flex: 0 0 auto;
+}
+.info_row_1 > view:first-of-type {
+	padding-left: 0;
+}
+.source-type {
+	background-color: #169bd5;
+	color: #ffffff !important;
+	padding: 10rpx 20rpx !important;
+	display: inline-block;
+	border-radius: 8rpx;
+}
+.dept_name {
+	display: flex;
+	flex-wrap: wrap;
+	max-width: 100%;
+}
+.dept_name text:not(:last-of-type):after {
+	content: '、';
+}
+.info_row_2 {
+	margin-top: 36rpx;
+	justify-content: start;
+}
+.info_row_2 > view {
+	border-radius: 6rpx;
+	border: #e8e9f0 solid 1px;
+	color: #8a8a99;
+	font-size: 22rpx;
+	padding: 8rpx 12rpx;
+	margin-right: 12rpx;
+}
+.description {
+	font-size: 30rpx;
+	color: #43434a;
+	text-indent: 2em;
+	margin: 40rpx 0;
+	padding-bottom: 40rpx;
+	border-bottom: #e6e6e6 solid 1rpx;
+	line-height: 1.5em;
+}
+.source_row {
+	margin: 30rpx 0;
+	display: flex;
+	font-size: 28rpx;
+}
+.to_source {
+	color: var(--uni-color-primary);
+}
+.like_count {
+	position: relative;
+	z-index: 1001;
+	font-size: 20rpx;
+	color: #ffffff;
+	background-color: var(--auxiliaryColor);
+	padding: 0.3em;
+	min-width: 3em;
+	border-radius: 1em;
+	border-bottom-left-radius: 0;
+	margin-top: -20rpx;
+	margin-left: -20rpx;
+	margin-bottom: 20rpx;
+}
+.icon_like,
+.icon_share {
+	width: 32rpx;
+	height: 32rpx;
+	margin-right: 14rpx;
+	flex: 0 0 auto;
+}
+.wxParse-p {
+	line-height: 1.5em;
+}
+.data-bar {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-top: 40rpx;
+}
+.data-bar-btn {
+	display: flex;
+	align-items: center;
+}
+.data-bar-btn view {
+	display: flex;
+	align-items: center;
+	margin-left: 30rpx;
+}
+.zy_half_screen_dialog_content {
+	padding: 50rpx 50rpx 0;
+}
+.comment-box {
+	margin-top: 60rpx;
+}
+.comment-list {
+	display: flex;
+	justify-content: space-between;
+	align-items: flex-end;
+	margin-top: 32rpx;
+}
+.comment-item {
+	display: flex;
+	flex-direction: column;
+}
+.comment-member-name {
+	padding-bottom: 16rpx;
+}
+.comment-time {
+	color: #888888;
+	font-size: 24rpx;
+}
+.tag {
+	color: var(--uni-color-primary);
+	display: inline-block;
+	margin-right: 20rpx;
+}
+.article-convenient-service-list {
+	position: fixed;
+	background-color: #000000b0;
+	padding: 24rpx 24rpx;
+	border-radius: 12rpx;
+	font-size: 28rpx;
+	color: #ffffff;
+	/* 允许手势穿透边缘位置,提升拖拽体验 */
+	z-index: 9999;
+}
+.article-convenient-service-item {
+	margin-bottom: 20rpx;
+}
+.article-convenient-service-item:last-of-type {
+	margin-bottom: 0rpx;
+}
+.share::after {
+	border: none;
+}
+.comment_btn {
+	background: var(--uni-color-primary);
+	color: var(--uni-color-white);
+	height: 24px;
+	line-height: 24px;
+}
+</style>

+ 117 - 0
pagesAdmin/article/components/half-screen-dialog/index.vue

@@ -0,0 +1,117 @@
+<template>
+	<view
+		v-if="show"
+		class="zy-half-screen-dialog"
+		:class="extClass"
+		data-target="screen-dialog"
+		@click="tapMask"
+	>
+		<view
+			class="zy-half-screen-dialog-content"
+			:style="{ height: height }"
+			:data-visible="visible"
+			:animation="contentAnimation"
+			@transitionend="updateVisible"
+			@click.stop=""
+		>
+			<slot></slot>
+		</view>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { nextTick, ref, watch } from 'vue';
+
+const props = defineProps({
+	show: {
+		type: Boolean,
+		default: false,
+	},
+	height: {
+		type: String,
+		default: '80%',
+	},
+	extClass: {
+		type: String,
+		default: '',
+	},
+});
+
+const maskClosable = ref(true);
+const duration = 300;
+const contentAnimation = ref(null);
+const visible = ref(false);
+
+const emits = defineEmits(['update:show']);
+
+const tapMask = (e) => {
+	if (e.currentTarget.dataset.target !== 'screen-dialog') {
+		return;
+	}
+	if (maskClosable.value) {
+		close();
+	}
+};
+const updateVisible = (e) => {
+	const { visible } = e.currentTarget.dataset;
+	nextTick(() => {
+		if (!visible) {
+			emits('update:show', visible);
+		}
+	});
+};
+const open = () => {
+	const up = uni.createAnimation({
+		duration,
+	});
+	up.translateY(0).step();
+	contentAnimation.value = up.export();
+	visible.value = true;
+};
+const close = () => {
+	const down = uni.createAnimation({
+		duration,
+	});
+	down.translateY('120%').step();
+	contentAnimation.value = down.export();
+	visible.value = false;
+};
+
+watch(
+	() => props.show,
+	(v) => {
+		visible.value = v;
+		if (v) {
+			nextTick(() => open());
+		} else {
+			nextTick(() => close());
+		}
+	}
+);
+</script>
+
+<style lang="scss" scoped>
+.zy-half-screen-dialog {
+	height: 100%;
+	width: 100%;
+	background-color: #00000083;
+	position: fixed;
+	top: 0;
+	left: 0;
+	z-index: 999999;
+}
+
+.zy-half-screen-dialog-content {
+	position: absolute;
+	bottom: 0;
+	left: 0;
+	width: 100%;
+	background-color: #ffffff;
+	border-top-left-radius: 30rpx;
+	border-top-right-radius: 30rpx;
+	overflow: hidden;
+	min-height: 20%;
+	height: 80%;
+	transform: translateY('100%');
+}
+</style>

+ 120 - 0
pagesAdmin/article/components/rate/index.vue

@@ -0,0 +1,120 @@
+<template>
+	<view class="start-bar" :style="`--rate-size: ${size}rpx`">
+		<block v-for="(item, index) in starts" :key="index">
+			<view
+				class="start"
+				:class="item.active ? 'active' : ''"
+				:data-score="item.score"
+				@click="select"
+			>
+			</view>
+		</block>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
+
+const props = defineProps({
+	// 最大分值
+	max: {
+		type: Number,
+		default: 5,
+	},
+	// 分数值
+	score: {
+		type: Number,
+		default: 0,
+	},
+	// 是否只读
+	disabled: {
+		type: Boolean,
+		default: false,
+	},
+	// 是否显示分数
+	showScore: {
+		type: Boolean,
+		default: false,
+	},
+	size: {
+		type: Number,
+		default: 30,
+	},
+});
+
+const starts = ref([]);
+
+const emits = defineEmits(['update:score', 'change']);
+
+const select = ({ target: { dataset } }) => {
+	if (props.disabled) return;
+	// 这里暂时不支持分数清零
+	starts.value = starts.value.map((item) => {
+		item.active = item.score <= dataset.score;
+		return item;
+	});
+	emits('update:score', dataset.score);
+	emits('change', dataset.score);
+};
+
+watch(
+	() => [props.max, props.score],
+	([max, score]) => {
+		starts.value = [];
+		for (let i = 1; i <= max; i++) {
+			starts.value.push({
+				score: i,
+				active: i <= score,
+			});
+		}
+	},
+	{
+		immediate: true,
+	}
+);
+</script>
+
+<style lang="scss" scoped>
+.start-bar {
+	display: flex;
+	justify-content: space-around;
+	--rate-fill-color: #bebebe;
+	--rate-size: 30rpx;
+}
+.start.active {
+	--rate-fill-color: #f7ba2a;
+}
+.start,
+.start::before,
+.start::after {
+	width: 0;
+	height: 0;
+	border-top: calc(var(--rate-size) / 1.2) solid var(--rate-fill-color);
+	border-right: var(--rate-size) solid transparent;
+	border-left: var(--rate-size) solid transparent;
+	overflow: visible;
+}
+
+.start {
+	margin: calc(var(--rate-size) / 2);
+	position: relative;
+	display: block;
+	color: var(--rate-fill-color);
+}
+
+.start::before,
+.start::after {
+	content: '';
+	display: block;
+	position: absolute;
+	left: calc(var(--rate-size) * -1);
+	top: calc(var(--rate-size) / -1.3);
+}
+
+.start::before {
+	transform: rotate(68deg);
+}
+.start::after {
+	transform: rotate(-68deg);
+}
+</style>

+ 9 - 0
pagesAdmin/article/components/tree/index.vue

@@ -0,0 +1,9 @@
+<template>
+
+</template>
+
+<script lang="ts" setup>
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 49 - 0
pagesAdmin/article/service/article/index.ts

@@ -0,0 +1,49 @@
+import { REQUEST_CONFIG } from '@/config';
+import { request, handle } from '@kasite/uni-app-base';
+
+const doPost = async (url, data, options) => {
+	data.ChannelId = 'smallpro';
+	let resp = handle.promistHandleNew(await request.doPost(url, data, options));
+	return handle.catchPromiseNew(resp, () => resp, options);
+};
+
+/** 文章详情 */
+export const ArticleDetail = async (saveData: any, options: any = {}) => {
+	return doPost(
+		`${REQUEST_CONFIG.BASE_URL}wsgw/article/mgr/ArticleDetail/callApiJSON.do`,
+		saveData,
+		options
+	);
+};
+/** 点赞文章 */
+export const LikeArticle = async (saveData: any, options: any = {}) => {
+	return doPost(
+		`${REQUEST_CONFIG.BASE_URL}wsgw/article/mgr/Like/callApiJSON.do`,
+		saveData,
+		options
+	);
+};
+/** 分享文章 */
+export const ShareArticle = async (saveData: any, options: any = {}) => {
+	return doPost(
+		`${REQUEST_CONFIG.BASE_URL}wsgw/article/mgr/Share/callApiJSON.do`,
+		saveData,
+		options
+	);
+};
+/** 获取评论 */
+export const GetCommentList = async (saveData: any, options: any = {}) => {
+	return doPost(
+		`${REQUEST_CONFIG.BASE_URL}wsgw/article/comment/page/callApiJSON.do`,
+		saveData,
+		options
+	);
+};
+/** 发布评论 */
+export const CreateComment = async (saveData: any, options: any = {}) => {
+	return doPost(
+		`${REQUEST_CONFIG.BASE_URL}wsgw/article/comment/create/callApiJSON.do`,
+		saveData,
+		options
+	);
+};

+ 1 - 0
pagesAdmin/article/service/index.ts

@@ -0,0 +1 @@
+export * from './article';

+ 18 - 0
pagesAdmin/satisfaction/business/satisfactionQuestions/fn.ts

@@ -0,0 +1,18 @@
+const getWidth = (data) => {
+	return 100 / data;
+};
+
+const answer = (item, itemId) => {
+	var flag = false;
+	item.AnswerList.forEach(function (i) {
+		if (i == itemId) {
+			flag = true;
+		}
+	});
+	return flag;
+};
+
+export default {
+	getWidth,
+	answer,
+};

+ 1281 - 0
pagesAdmin/satisfaction/business/satisfactionQuestions/satisfactionQuestions.vue

@@ -0,0 +1,1281 @@
+<template>
+	<view class="container">
+		<scroll-view
+			class="scroll_view"
+			scroll-y
+			scroll-with-animation
+			scroll-anchoring
+			:scroll-into-view="point"
+		>
+			<view class="top_bg_box">
+				<image mode="widthFix" :src="icon.satisfaction.ques_top_bg"></image>
+				<view class="ques_introduce displayFlexCol">
+					<text>{{ quesList.SubjectTitle }}</text>
+					<text>{{ quesList.Remark }}</text>
+				</view>
+			</view>
+			<view class="main_box">
+				<view class="mask_form" v-if="complete"></view>
+				<block v-for="(item, index) in quesList.QuestionList" :key="index">
+					<view
+						class="ques_item_box"
+						:id="'p' + index"
+						:class="item.QuestType == 'SubTitle' ? 'sub_title' : ''"
+					>
+						<view class="ques_title_box">
+							<text class="mustQuest_tag" wx:if="{{item.MustQuest}}">*</text>
+							<text v-if="item.QuestType != 'SubTitle'">{{ item.Sort }}.{{ item.Question }}</text>
+							<text v-if="item.QuestType == 'SubTitle'">{{ item.Num }}、{{ item.Question }}</text>
+						</view>
+						<!-- 填空题 -->
+						<view
+							class="ques_options align_items_left displayFlexCol"
+							v-if="item.QuestType == 'Input'"
+						>
+							<text class="input_label">{{ item.Question }}:</text>
+							<view class="textarea_box">
+								<textarea
+									auto-height
+									:data-index="index"
+									:placeholder="'最多输入' + item.RuleInfo.MaxLength + '个字'"
+									:maxlength="item.RuleInfo.MaxLength"
+									:value="item.AnswerList"
+									@blur="setVal"
+								/>
+							</view>
+						</view>
+						<!-- 矩阵填空题 -->
+						<view
+							class="ques_options align_items_left displayFlexCol"
+							v-if="item.QuestType == 'MatrixInput'"
+						>
+							<block
+								v-for="(childItem, childIndex) in item.MatrixQuestionList"
+								:key="`MatrixInput-${index}-${childIndex}`"
+							>
+								<view class="matrix_input_box margin_bottom_10 displayFlexRow">
+									<text>{{ childItem.Question }}:</text>
+									<view class="textarea_box matrix_textarea_box">
+										<textarea
+											auto-height
+											:data-index="index"
+											:data-childindex="childIndex"
+											:placeholder="'最多输入' + childItem.RuleInfo.MaxLength + '个字'"
+											:maxlength="childItem.RuleInfo.MaxLength"
+											:value="childItem.AnswerList"
+											@input="setVal"
+										/>
+									</view>
+								</view>
+							</block>
+						</view>
+						<!-- 单选/多选题 -->
+						<view
+							class="ques_options displayFlexCol"
+							v-if="item.QuestType == 'Radio' || item.QuestType == 'Checkbox'"
+						>
+							<block
+								v-for="(childItem, childIndex) in item.QuestionItemList"
+								:key="`Checkbox-${index}-${childIndex}`"
+							>
+								<text
+									class="choice_item"
+									:class="fn.answer(item, childItem.ItemId) ? 'active_option' : ''"
+									:data-index="index"
+									:data-childindex="childIndex"
+									@click="choiceOption"
+								>
+									{{ childItem.ItemName }}
+								</text>
+							</block>
+						</view>
+						<!-- 选择题 -->
+						<view class="ques_options displayFlexCol" v-if="item.QuestType == 'Select'">
+							<picker
+								class="picker_box"
+								range-key="ItemName"
+								:range="item.QuestionItemList"
+								:data-index="index"
+								:value="item.AnswerList"
+								@change="bindPickerChange"
+							>
+								<view class="picker displayFlexRow">
+									<text>{{ item.QuestionItemList[item.AnswerList].ItemName }}</text>
+									<image :src="icon.satisfaction.right" mode="" />
+								</view>
+							</picker>
+						</view>
+						<!-- 矩阵单选题/矩阵多选题/矩阵量表 -->
+						<view
+							class="ques_options matrix_border displayFlexCol"
+							v-if="
+								item.QuestType == 'MatrixRadio' ||
+								item.QuestType == 'MatrixCheckbox' ||
+								item.QuestType == 'MatrixScale'
+							"
+						>
+							<view class="matrix_options matrix_options_title displayFlexRow">
+								<block
+									v-for="(childItem, childIndex) in item.MatrixQuestionList[0].QuestionItemList"
+									:key="`${item.QuestType}0-${index}-${childIndex}`"
+								>
+									<text
+										:style="{
+											width: fn.getWidth(item.MatrixQuestionList[0].QuestionItemList.length) + '%',
+										}"
+									>
+										{{ childItem.ItemName }}
+									</text>
+								</block>
+							</view>
+							<block
+								v-for="(childItem, childIndex) in item.MatrixQuestionList"
+								:key="`${item.QuestType}-${index}-${childIndex}`"
+							>
+								<view class="matrix_icon_box displayFlexCol">
+									<text
+										:style="{
+											width: fn.getWidth(item.MatrixQuestionList[0].QuestionItemList.length) + '%',
+										}"
+										>{{ childItem.Question }}</text
+									>
+									<view class="matrix_options displayFlexBetween">
+										<block
+											v-for="(sunItem, sunIndex) in childItem.QuestionItemList"
+											:key="`${item.QuestType}-${index}-${childIndex}-${sunIndex}`"
+										>
+											<view
+												class="displayFlexRow"
+												:data-index="index"
+												:data-childindex="childIndex"
+												:data-sunindex="sunIndex"
+												@click="choiceMatrixOption"
+											>
+												<!-- 矩阵单选/矩阵量表 -->
+												<image
+													:src="
+														fn.answer(childItem, sunItem.ItemId)
+															? icon.satisfaction.circle_active
+															: icon.satisfaction.circle
+													"
+													mode=""
+													v-if="item.QuestType == 'MatrixRadio' || item.QuestType == 'MatrixScale'"
+												/>
+
+												<!-- 矩阵多选 -->
+												<image
+													:src="
+														fn.answer(childItem, sunItem.ItemId)
+															? icon.satisfaction.checkBox_circle_active
+															: icon.satisfaction.checkBox_circle
+													"
+													mode=""
+													v-if="item.QuestType == 'MatrixCheckbox'"
+												/>
+											</view>
+										</block>
+									</view>
+								</view>
+							</block>
+						</view>
+						<!-- 上传文件题 -->
+						<view class="ques_options displayFlexCol" v-if="item.QuestType == 'UploadImage'">
+							<view class="img_list displayFlexRow">
+								<image
+									class="add_img"
+									mode=""
+									:src="icon.satisfaction.add"
+									:data-index="index"
+									@click="choiceFile"
+								/>
+								<block
+									v-for="(imgItem, imgIndex) in item.AnswerList"
+									:key="`UploadImage-${index}-${imgIndex}`"
+								>
+									<view class="add_img">
+										<image class="item_img" :src="imgItem" mode="" />
+										<image
+											class="cha_img"
+											:src="icon.satisfaction.cha_green"
+											mode=""
+											:data-index="index"
+											:data-imgitem="imgItem"
+											@click="closeImg"
+										/>
+									</view>
+								</block>
+							</view>
+							<view class="add_img_tips p_flexStart">
+								<text>限制:</text>
+								<text> 仅支持图片上传、</text>
+								<text>
+									大小不超过{{ item.RuleInfo.FileSize }}M数量不超过{{ item.RuleInfo.FileCount }}个
+								</text>
+							</view>
+						</view>
+						<!-- 量表题 -->
+						<view class="ques_options displayFlexRow" v-if="item.QuestType == 'Scale'">
+							<block
+								v-for="(childItem, childIndex) in item.QuestionItemList"
+								:key="`Scale-${index}-${childIndex}`"
+							>
+								<view
+									class="options_item_box displayFlexCol"
+									:data-index="index"
+									:data-childindex="childIndex"
+									@click="choiceOption"
+								>
+									<text>{{ childItem.ItemName }}</text>
+									<image
+										:src="
+											fn.answer(item, childItem.ItemId)
+												? icon.satisfaction.circle_active
+												: icon.satisfaction.circle
+										"
+										mode=""
+									/>
+								</view>
+							</block>
+						</view>
+					</view>
+				</block>
+			</view>
+		</scroll-view>
+
+		<view class="yjfl" @click="yjfk">
+			<image :src="icon.satisfaction.yjfk" alt="" style="width: 140rpx; height: 140rpx" />
+			<view class="title"> 意见反馈 </view>
+		</view>
+		<view class="footer_box displayFlexRow">
+			<text :class="complete ? 'backgroundCustom_D9' : ''" @click="submit">提交</text>
+		</view>
+
+		<!-- 是否实名弹窗 -->
+		<view class="modal_wrap" v-if="showModal_Anonymous">
+			<view class="modal_box">
+				<view class="modal_tit">是否实名填写</view>
+				<view class="modal_con">
+					<view class="modal_box_b1" @click="isAnonymous(1)">实名填写</view>
+					<view class="modal_box_b1" @click="isAnonymous(2)">匿名填写</view>
+				</view>
+			</view>
+		</view>
+		<!-- 是否实名弹窗 -->
+		<view class="modal_wrap" v-if="showModal_User">
+			<view class="modal_box">
+				<view class="modal_tit">请选择答卷人</view>
+				<view class="modal_con">
+					<view class="modal_user_item" v-if="currentUser.memberName" @click="goSelMember">
+						<view>
+							<view>
+								<text class="modal_t1">{{ currentUser.memberName }}</text>
+								<text class="modal_t2">
+									{{ currentUser.sex == 1 ? '男' : currentUser.sex == 2 ? '女' : '未知' }} |
+									{{ currentUser.age }}岁
+								</text>
+							</view>
+							<view class="modal_t3">{{ currentUser.mobile }}</view>
+						</view>
+						<image class="modal_user_right_img" :src="icon.satisfaction.right"></image>
+					</view>
+					<view class="modal_user_item" wx:else @click="goSelMember">
+						<view>点击选择答卷人</view>
+						<image class="modal_user_right_img" :src="icon.satisfaction.right"></image>
+					</view>
+					<view class="modal_btn_wrap displayFlexBetween" v-if="currentUser.memberName">
+						<view class="modal_btn modal_btn1" @click="goBack">取消</view>
+						<view class="modal_btn modal_btn2" @click="confirmMember">确定</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { reactive, ref } from 'vue';
+import { useOnLoad } from '@/hook';
+import { useDomain } from '@kasite/uni-app-base';
+import { mapGetters } from '@kasite/uni-app-base/store/hook';
+import {
+	QuerySubjectListToChannelTask_V3,
+	QuerySubjectInfoById_V3,
+	UploadZxFile,
+	CommitAnswer_V3,
+} from '../../service';
+import icon from '@/utils/icon';
+import { common } from '@/utils';
+import fn from './fn';
+
+const app = getApp();
+
+let time = null;
+const currentUser = ref({} as any);
+const taskId = ref('');
+const objType = ref('3'); // 3门诊 4住院
+const quesList = ref({} as any);
+const imgList = ref([]);
+const point = ref('');
+const complete = ref(false);
+const anonymousType = ref(0);
+const showModal_Anonymous = ref(false); // 是否实名填写弹窗显示
+const showModal_User = ref(false); // 实名用户选择弹窗显示
+let quesAnswers = reactive({
+	MemberId: '',
+	TaskId: '-1',
+	SysAppId: 'visit',
+	SubjectId: '',
+	UserAgent: '',
+	IP: '',
+	Location: '',
+	Mobile: '',
+	UserName: '',
+	Sex: '',
+	Age: '',
+	ThirdPartyId: '',
+	AnswerUseTime: 0,
+	PushDept: '',
+	PushDeptName: '',
+	BedNo: '',
+	HospitalNo: '',
+	CardNo: '',
+	AnswerList: [],
+});
+
+const { getCurrentUser } = mapGetters({
+	getCurrentUser: 'getCurrentUser',
+});
+
+const main = async (options) => {
+	currentUser.value = getCurrentUser();
+	const params = {
+		SubjectId: options.subjectId,
+		TaskId: options.taskId,
+	};
+	const resp = await QuerySubjectListToChannelTask_V3(params);
+	if (!common.isEmpty(resp)) {
+		objType.value = resp[0].GroupType;
+		//匿名有3种传参 ""(空) "true" "false" 转换字符串为布尔值
+		let anonymous =
+			resp[0].Anonymous === 'false'
+				? false
+				: resp[0].Anonymous === 'true'
+				? true
+				: resp[0].Anonymous || '';
+		// type 1实名 2:匿名 3自行选择
+		anonymousType.value = anonymous === '' ? 3 : anonymous === false ? 1 : 2;
+		showModal_Anonymous.value = anonymous == 3 ? true : false;
+		taskId.value = options.taskId;
+		quesAnswers.SubjectId = options.subjectId;
+		//如果不是执行选择直接调用对应填写按钮
+		if (anonymous != 3) {
+			isAnonymous(anonymous);
+		}
+		querySubjectInfoById_V3();
+		// 开始计算答卷时间
+		getSec();
+	} else {
+		common.showModal('该任务已失效', () => {
+			common.goToUrl(`/pages/business/tabbar/homePage/homePage`, { skipWay: 'reLaunch' });
+		});
+		return;
+	}
+};
+/** 实名/匿名 */
+const isAnonymous = (type) => {
+	//实名
+	if (type == 1) {
+		showModal_Anonymous.value = false;
+		showModal_User.value = true;
+	}
+	//匿名
+	else {
+		currentUser.value = {};
+		showModal_Anonymous.value = false;
+	}
+};
+/** 问卷详情 */
+const querySubjectInfoById_V3 = async () => {
+	const resp = await QuerySubjectInfoById_V3({
+		SubjectId: quesAnswers.SubjectId,
+	});
+	if (common.isNotEmpty(resp)) {
+		if (resp[0].Status != 0) {
+			common.showModal('该问卷已失效', () => {
+				common.navigateBack(1);
+			});
+			return;
+		}
+		if (resp[0].State != 1) {
+			common.showModal('该问卷未发布', () => {
+				common.navigateBack(1);
+			});
+			return;
+		}
+		// 提交答案数据模版
+		quesAnswers.PushDept = resp[0].DeptId;
+		quesAnswers.PushDeptName = resp[0].PushDeptName;
+		resp[0].QuestionList.forEach((item, index) => {
+			item.AnswerList = [];
+			item.MustQuest = item.MustQuest == 'false' ? false : true;
+			item.RuleInfo = item.RuleInfo != '' ? JSON.parse(item.RuleInfo) : '';
+			// 多选框
+			if (item.QuestType == 'Select') {
+				item.AnswerList = 0;
+			}
+			if (common.isNotEmpty(item.MatrixQuestionList)) {
+				item.MatrixQuestionList.forEach((childItem) => {
+					childItem.AnswerList = [];
+					childItem.RuleInfo = childItem.RuleInfo != '' ? JSON.parse(childItem.RuleInfo) : '';
+
+					// 答案列表模版
+					let answerItem = {
+						Answer: [],
+						Blank: index,
+						QuestId: childItem.QuestId,
+						QuestType: item.QuestType,
+						MustQuest: item.MustQuest,
+					};
+					quesAnswers.AnswerList.push(answerItem);
+					if (common.isNotEmpty(childItem.QuestionItemList)) {
+						childItem.QuestionItemList.forEach((sunItem) => {
+							sunItem.Check = false;
+						});
+					}
+				});
+			} else {
+				// 答案列表模版
+				let answerItem = {
+					Answer: [],
+					Blank: index,
+					QuestId: item.QuestId,
+					QuestType: item.QuestType,
+					MustQuest: item.MustQuest,
+				};
+				quesAnswers.AnswerList.push(answerItem);
+			}
+		});
+		resp[0].QuestionList = bySort(resp[0].QuestionList);
+		quesList.value = resp[0];
+		console.log(quesList.value);
+	}
+};
+/** 计算答题时间 */
+const getSec = () => {
+	time = setTimeout(() => {
+		let answerUseTime = quesAnswers.AnswerUseTime;
+		answerUseTime++;
+		quesAnswers.AnswerUseTime = answerUseTime;
+		getSec();
+	}, 1000);
+};
+/** 根据子标题排序 */
+const bySort = (list: any[]) => {
+	let find = 0;
+	let sort = 0;
+	let item = {} as any;
+	for (var t = 0; t < list.length; t++) {
+		item = list[t];
+		//子标题
+		if (item.QuestType == 'SubTitle') {
+			//将子标题底下的题目从1开始排
+			sort = 0;
+			//判断是出现的第几个子标题
+			//num是子标题的排序
+			item.Num = toChinesNum(find + 1);
+			find++;
+		} else {
+			//sort是子标题底下的排序
+			item.Sort = sort + 1;
+			sort++;
+		}
+		item.SortNum = t;
+		if (common.isNotEmpty(item.QuestionItemList)) {
+			item.QuestionItemList.forEach((childItem, childIndex) => {
+				childItem.SortNum = childIndex;
+				if (common.isNotEmpty(childItem.QuestionItem)) {
+					childItem.QuestionItem.forEach((sunItem, sunIndex) => {
+						sunItem.SortNum = sunIndex;
+					});
+				}
+			});
+		}
+	}
+	return list;
+};
+/** 汉字顺序 */
+const toChinesNum = (num) => {
+	var changeNum = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'],
+		newNum = '',
+		arr = num.toString().split('');
+	arr[0] = parseInt(arr[0]) - 1;
+	if (arr[0] == -1 && arr.length == 1) {
+		return '零';
+	}
+	if (arr.length > 1) {
+		arr[1] = parseInt(arr[1]) - 1;
+		if (!arr[0]) {
+			newNum = !arr[0] && arr[1] == -1 ? changeNum[9] : changeNum[9] + changeNum[arr[1]];
+		} else {
+			newNum = changeNum[arr[0]] + changeNum[9] + (changeNum[arr[1]] ? changeNum[arr[1]] : '');
+		}
+	} else {
+		newNum = changeNum[arr[0]];
+	}
+	return newNum;
+};
+
+/** 单选/多选/量表 */
+const choiceOption = (e) => {
+	let index = e.currentTarget.dataset.index;
+	let childIndex = e.currentTarget.dataset.childindex;
+	let answerIndex = 0;
+	let questionList = quesList.value.QuestionList;
+	let answerList = quesAnswers.AnswerList;
+	answerList.forEach((itm, ind) => {
+		if (itm.QuestId == questionList[index].QuestId) {
+			answerIndex = ind;
+		}
+	});
+	// 单选/量表
+	if (questionList[index].QuestType == 'Radio' || questionList[index].QuestType == 'Scale') {
+		if (
+			questionList[index].AnswerList[0] != questionList[index].QuestionItemList[childIndex].ItemId
+		) {
+			questionList[index].AnswerList = [];
+			questionList[index].AnswerList.push(questionList[index].QuestionItemList[childIndex].ItemId);
+		}
+		answerList[answerIndex].Answer = questionList[index].QuestionItemList[childIndex].ItemId;
+	}
+	// 多选
+	else if (questionList[index].QuestType == 'Checkbox') {
+		let flag = false;
+		questionList[index].AnswerList.forEach((item) => {
+			if (item == questionList[index].QuestionItemList[childIndex].ItemId) {
+				flag = true;
+			}
+		});
+
+		if (flag) {
+			questionList[index].AnswerList = questionList[index].AnswerList.filter((item) => {
+				return item != questionList[index].QuestionItemList[childIndex].ItemId;
+			});
+			answerList[answerIndex].Answer = answerList[answerIndex].Answer.filter((fiItem) => {
+				return fiItem != questionList[index].QuestionItemList[childIndex].ItemId;
+			});
+		} else {
+			if (questionList[index].RuleInfo.MaxLength < questionList[index].AnswerList.length + 1) {
+				common.showModal('最多选择' + questionList[index].RuleInfo.MaxLength + '项');
+				return;
+			}
+			questionList[index].AnswerList.push(questionList[index].QuestionItemList[childIndex].ItemId);
+			answerList[answerIndex].Answer.push(questionList[index].QuestionItemList[childIndex].ItemId);
+		}
+	}
+	quesList.value.QuestionList = questionList;
+	quesAnswers.AnswerList = answerList;
+};
+/** 选择题 */
+const bindPickerChange = (e) => {
+	let index = e.currentTarget.dataset.index;
+	let val = e.detail.value;
+	let questionList = quesList.value.QuestionList;
+	let answerList = quesAnswers.AnswerList;
+	let answerIndex = 0;
+	answerList.forEach((itm, ind) => {
+		if (itm.QuestId == questionList[index].QuestId) {
+			answerIndex = ind;
+		}
+	});
+	questionList[index].AnswerList = val;
+	answerList[answerIndex].Answer = questionList[index].QuestionItemList[val].ItemId;
+	quesList.value.QuestionList = questionList;
+	quesAnswers.AnswerList = answerList;
+};
+/** 矩阵单选 */
+const choiceMatrixOption = (e) => {
+	let index = e.currentTarget.dataset.index;
+	let childIndex = e.currentTarget.dataset.childindex;
+	let sunIndex = e.currentTarget.dataset.sunindex;
+	let questionList = quesList.value.QuestionList;
+	let answerList = quesAnswers.AnswerList;
+	let answerIndex = 0;
+	answerList.forEach((itm, ind) => {
+		if (itm.QuestId == questionList[index].MatrixQuestionList[childIndex].QuestId) {
+			answerIndex = ind;
+		}
+	});
+
+	// 矩阵单选/矩阵量表
+	if (
+		questionList[index].QuestType == 'MatrixRadio' ||
+		questionList[index].QuestType == 'MatrixScale'
+	) {
+		if (
+			questionList[index].MatrixQuestionList[childIndex].AnswerList[0] !=
+			questionList[index].MatrixQuestionList[childIndex].QuestionItemList[sunIndex].ItemId
+		) {
+			questionList[index].MatrixQuestionList[childIndex].AnswerList = [];
+			questionList[index].MatrixQuestionList[childIndex].AnswerList.push(
+				questionList[index].MatrixQuestionList[childIndex].QuestionItemList[sunIndex].ItemId
+			);
+		}
+		answerList[answerIndex].Answer =
+			questionList[index].MatrixQuestionList[childIndex].QuestionItemList[sunIndex].ItemId;
+	}
+	// 矩阵多选
+	else if (questionList[index].QuestType == 'MatrixCheckbox') {
+		let flag = false;
+		questionList[index].MatrixQuestionList[childIndex].AnswerList.forEach((item) => {
+			if (
+				item == questionList[index].MatrixQuestionList[childIndex].QuestionItemList[sunIndex].ItemId
+			) {
+				flag = true;
+			}
+		});
+
+		if (flag) {
+			questionList[index].MatrixQuestionList[childIndex].AnswerList = questionList[
+				index
+			].MatrixQuestionList[childIndex].AnswerList.filter((item) => {
+				return (
+					item !=
+					questionList[index].MatrixQuestionList[childIndex].QuestionItemList[sunIndex].ItemId
+				);
+			});
+			answerList[answerIndex].Answer = answerList[answerIndex].Answer.filter((fiItem) => {
+				return (
+					fiItem !=
+					questionList[index].MatrixQuestionList[childIndex].QuestionItemList[sunIndex].ItemId
+				);
+			});
+		} else {
+			questionList[index].MatrixQuestionList[childIndex].AnswerList.push(
+				questionList[index].MatrixQuestionList[childIndex].QuestionItemList[sunIndex].ItemId
+			);
+			answerList[answerIndex].Answer.push(
+				questionList[index].MatrixQuestionList[childIndex].QuestionItemList[sunIndex].ItemId
+			);
+		}
+	}
+	quesList.value.QuestionList = questionList;
+	quesAnswers.AnswerList = answerList;
+};
+/** 选择文件 */
+const choiceFile = (e) => {
+	let index = e.currentTarget.dataset.index;
+	let questionList = quesList.value.QuestionList;
+	let answerList = quesAnswers.AnswerList;
+	let answerIndex = 0;
+	answerList.forEach((itm, ind) => {
+		if (itm.QuestId == questionList[index].QuestId) {
+			answerIndex = ind;
+		}
+	});
+
+	if (answerList[answerIndex].Answer.length >= questionList[index].RuleInfo.FileCount) {
+		common.showModal('最多上传' + questionList[index].RuleInfo.FileCount + '张图片');
+		return;
+	}
+
+	uni.chooseMedia({
+		count: questionList[index].RuleInfo.FileCount,
+		mediaType: ['image'],
+		sourceType: ['album', 'camera'],
+		sizeType: ['compressed'],
+		async success(res) {
+			uni.showLoading({
+				title: '上传中。。。',
+			});
+			for (var i = 0; i < res.tempFiles.length; i++) {
+				let m = 1024 * 1024;
+				if (res.tempFiles[i].size < m) {
+					let imgUrl = (await uploadFile(
+						res.tempFiles[i].tempFilePath,
+						questionList[index].RuleInfo.FileCount,
+						answerList[answerIndex].Answer
+					)) as string;
+					if (common.isNotEmpty(imgUrl)) {
+						imgUrl =
+							imgUrl.indexOf('http') > -1 ? imgUrl : useDomain() + imgUrl.replace(/\\/g, '/');
+						answerList[answerIndex].Answer.push(imgUrl);
+						questionList[index].AnswerList = answerList[answerIndex].Answer;
+						quesList.value.QuestionList = questionList;
+						quesAnswers.AnswerList = answerList;
+						if (res.tempFiles.length - 1 == i) {
+							uni.hideLoading();
+						}
+					}
+				} else {
+					uni.hideLoading();
+					common.showModal('文件不得大于' + questionList[index].RuleInfo.FileSize + 'M');
+					return;
+				}
+			}
+		},
+	});
+};
+/** 上传文件 */
+const uploadFile = (imgItem, fileCount, imgList) => {
+	return new Promise((resolve, reject) => {
+		if (imgList.length >= fileCount) {
+			uni.hideLoading();
+			common.showModal('最多上传' + fileCount + '张图片');
+			return;
+		}
+		// 将图片上传至服务器
+		uni.uploadFile({
+			url: UploadZxFile,
+			filePath: imgItem,
+			name: 'newsFile',
+			formData: {
+				user: 'test',
+			},
+			header: {
+				token: uni.getStorageSync('token'),
+			},
+			data: {},
+			success(res) {
+				const data = JSON.parse(res.data);
+				if (data.RespCode == '10000') {
+					resolve(data.url);
+				} else {
+					common.showModal(data.msg);
+				}
+			},
+		});
+	});
+};
+/** 删除图片 */
+const closeImg = (e) => {
+	let index = e.currentTarget.dataset.index;
+	let imgItem = e.currentTarget.dataset.imgitem;
+	let questionList = quesList.value.QuestionList;
+	let answerList = quesAnswers.AnswerList;
+	let answerIndex = 0;
+	answerList.forEach((itm, ind) => {
+		if (itm.QuestId == questionList[index].QuestId) {
+			answerIndex = ind;
+		}
+	});
+	answerList[answerIndex].Answer = answerList[answerIndex].Answer.filter((item) => {
+		return item != imgItem;
+	});
+
+	questionList[index].AnswerList = answerList[answerIndex].Answer;
+	quesList.value.QuestionList = questionList;
+	quesAnswers.AnswerList = answerList;
+};
+/** 获取input值 */
+const setVal = (e) => {
+	let index = e.currentTarget.dataset.index;
+	let childIndex = e.currentTarget.dataset.childindex;
+	let questionList = quesList.value.QuestionList;
+	let answerList = quesAnswers.AnswerList;
+	let answerIndex = 0;
+	// 矩阵填空题
+	if (questionList[index].QuestType == 'MatrixInput') {
+		if (
+			questionList[index].MatrixQuestionList[childIndex].AnswerList.length ==
+			questionList[index].MatrixQuestionList[childIndex].RuleInfo.MaxLength
+		) {
+			common.showToast(
+				'最多输入' + questionList[index].MatrixQuestionList[childIndex].RuleInfo.MaxLength + '个字'
+			);
+			return;
+		}
+		answerList.forEach((itm, ind) => {
+			if (itm.QuestId == questionList[index].MatrixQuestionList[childIndex].QuestId) {
+				answerIndex = ind;
+			}
+		});
+		questionList[index].MatrixQuestionList[childIndex].AnswerList = e.detail.value;
+	}
+	// 填空题
+	else {
+		if (questionList[index].RuleInfo.DataType != '无') {
+			let dataTypeOptions = questionList[index].RuleInfo.DataTypeOptions;
+			for (var i = 0; i < dataTypeOptions.length; i++) {
+				if (questionList[index].RuleInfo.DataType == dataTypeOptions[i].Value) {
+					const regRule = dataTypeOptions[i].Rule;
+					let reg = null;
+					if (regRule.startsWith('/') && regRule.endsWith('/')) {
+						reg = new RegExp(regRule.slice(1, -1));
+					} else {
+						reg = new RegExp(regRule);
+					}
+					if (!reg.test(e.detail.value)) {
+						common.showModal('请输入' + dataTypeOptions[i].Text);
+						questionList[index].AnswerList = '';
+						answerList[answerIndex].Answer = '';
+
+						this.setData({
+							'quesList.QuestionList': questionList,
+							'quesAnswers.AnswerList': answerList,
+						});
+						return;
+					}
+				}
+			}
+		}
+
+		if (questionList[index].AnswerList.length == questionList[index].RuleInfo.MaxLength) {
+			common.showToast('最多输入' + questionList[index].RuleInfo.MaxLength + '个字');
+			return;
+		}
+		answerList.forEach((itm, ind) => {
+			if (itm.QuestId == questionList[index].QuestId) {
+				answerIndex = ind;
+			}
+		});
+		questionList[index].AnswerList = e.detail.value;
+	}
+
+	answerList[answerIndex].Answer = e.detail.value;
+	quesList.value.QuestionList = questionList;
+	quesAnswers.AnswerList = answerList;
+};
+
+/** 提交 */
+const submit = async () => {
+	uni.showLoading();
+	await common.sleep(1000);
+	if (complete.value) return;
+	const answers = { ...quesAnswers } as any;
+	for (var i = 0; i < answers.AnswerList.length; i++) {
+		let item = answers.AnswerList[i];
+		if (item.MustQuest && common.isEmpty(item.Answer)) {
+			common.showToast('存在未填写的问卷');
+			point.value = 'p' + item.Blank;
+			return;
+		}
+	}
+	answers.AnswerList.forEach((item) => {
+		if (typeof item.Answer == 'object') {
+			item.Answer = item.Answer.join();
+		}
+	});
+	answers.AnswerList = JSON.stringify(quesAnswers.AnswerList);
+	quesAnswers.IP = (await getIP()).cip;
+	quesAnswers.Location = (await getIP()).cname;
+	quesAnswers.UserAgent = app.globalData.smallPro_systemInfo;
+	quesAnswers.Mobile = currentUser.value.mobile;
+	quesAnswers.UserName = currentUser.value.memberName;
+	quesAnswers.MemberId = currentUser.value.memberId;
+	quesAnswers.Sex = currentUser.value.sex;
+	quesAnswers.Age = currentUser.value.age;
+	quesAnswers.ThirdPartyId = currentUser.value.memberId || uni.getStorageSync('openid');
+	quesAnswers.BedNo = '';
+	quesAnswers.HospitalNo = objType.value == '4' ? currentUser.value.cardNo : '';
+	quesAnswers.CardNo = objType.value == '3' ? currentUser.value.cardNo : '';
+	quesAnswers.TaskId = taskId.value;
+	let res = await CommitAnswer_V3(quesAnswers);
+	clearTimeout(time);
+	if (common.isNotEmpty(res)) {
+		common.showModal('提交成功!', () => {
+			common.navigateBack(1);
+		});
+	}
+};
+/** 获取IP */
+const getIP = () => {
+	return new Promise((resolve, reject) => {
+		uni.request({
+			url: 'https://pv.sohu.com/cityjson?ie=utf-8',
+			success: (res: any) => {
+				const result = res.data.substring(res.data.indexOf('{'), res.data.lastIndexOf('}') + 1);
+				const obj = JSON.parse(result);
+				resolve(obj);
+			},
+		});
+	}) as Promise<any>;
+};
+
+const goBack = () => {
+	uni.navigateBack({
+		delta: 1,
+	});
+};
+const yjfk = () => {};
+const goSelMember = () => {};
+const confirmMember = () => {};
+
+useOnLoad((options) => {
+	main(options);
+});
+</script>
+
+<style lang="scss" scoped>
+.align_items_left {
+	align-items: flex-start;
+}
+
+.active_option {
+	background: #18ba89 !important;
+	color: white !important;
+}
+
+.mask_form {
+	width: 100%;
+	height: 100%;
+	position: absolute;
+	top: 0;
+	left: 0;
+	z-index: 100;
+}
+
+.container {
+	overflow: hidden;
+}
+
+.scroll_view {
+	height: 100%;
+}
+
+.top_bg_box {
+	position: relative;
+
+	image {
+		width: 100%;
+		height: 456rpx;
+		position: absolute;
+		top: 0;
+		left: 0;
+		z-index: 1;
+	}
+	.ques_introduce {
+		padding: 46rpx 52rpx 0 52rpx;
+		box-sizing: border-box;
+		position: absolute;
+		top: 0;
+		left: 0;
+		z-index: 2;
+		text:nth-child(1) {
+			font-size: 36rpx;
+			font-weight: bold;
+			color: white;
+			margin-bottom: 20rpx;
+		}
+		text:nth-child(2) {
+			line-height: 45rpx;
+			font-size: 28rpx;
+			color: white;
+		}
+	}
+}
+
+.main_box {
+	padding: 470rpx 30rpx 190rpx 30rpx;
+	box-sizing: border-box;
+	position: relative;
+	z-index: 2;
+}
+
+.ques_item_box {
+	padding: 30rpx;
+	box-sizing: border-box;
+	background: white;
+	border-radius: 20rpx;
+	margin-bottom: 32rpx;
+
+	> .ques_title_box > text {
+		line-height: 50rpx;
+		font-size: 36rpx;
+		font-weight: bold;
+		color: #1c1c1c;
+	}
+	.mustQuest_tag {
+		color: #dc2828;
+	}
+}
+
+.ques_options {
+	margin-top: 34rpx;
+}
+
+.choice_item {
+	width: 100%;
+	line-height: 80rpx;
+	font-size: 30rpx;
+	color: #686868;
+	text-align: center;
+	margin-bottom: 16rpx;
+	border-radius: 10rpx;
+	background: #f8f8fa;
+
+	&:last-child {
+		margin-bottom: 0;
+	}
+}
+
+.input_label {
+	line-height: 40rpx;
+	font-size: 32rpx;
+	color: #474747;
+}
+
+.textarea_box {
+	width: 100%;
+	padding: 20rpx;
+	box-sizing: border-box;
+	border-radius: 10rpx;
+	background: #f8f8fa;
+	margin-top: 22rpx;
+	textarea {
+		width: 100%;
+	}
+}
+
+.matrix_textarea_box {
+	width: 70%;
+}
+
+.picker_box {
+	width: 100%;
+}
+
+.picker {
+	width: 100%;
+	height: 80rpx;
+	padding: 22rpx;
+	box-sizing: border-box;
+	background: #f9f9f9;
+	border-radius: 20rpx;
+	justify-content: space-between;
+
+	text {
+		white-space: nowrap;
+		font-size: 28rpx;
+		color: #686868;
+	}
+	image {
+		width: 12rpx;
+		height: 24rpx;
+	}
+}
+
+.matrix_options {
+	width: 100%;
+	margin-bottom: 34rpx;
+	justify-content: space-between;
+
+	text {
+		font-size: 30rpx;
+		color: #1c1c1c;
+		text-align: center;
+		padding: 20rpx;
+		box-sizing: border-box;
+		word-wrap: break-word;
+		word-break: break-all;
+	}
+}
+
+.matrix_border {
+	border: 1px solid #f0f0f0;
+}
+
+.matrix_options_title {
+	background: #f5f5f5;
+}
+
+.matrix_icon_box {
+	width: 100%;
+	align-items: flex-start;
+	text {
+		font-size: 30rpx;
+		color: #1c1c1c;
+		margin-bottom: 36rpx;
+		padding: 0 20rpx;
+		box-sizing: border-box;
+	}
+	image {
+		width: 40rpx;
+		height: 40rpx;
+	}
+}
+
+.sub_title {
+	background: none;
+}
+
+.img_list {
+	width: 100%;
+	margin-bottom: 24rpx;
+	justify-content: flex-start;
+	flex-wrap: wrap;
+	.item_img {
+		width: 100%;
+		height: 100%;
+	}
+}
+
+.add_img {
+	width: 32%;
+	height: 200rpx;
+	margin-right: 2%;
+	margin-bottom: 3%;
+	position: relative;
+	&:nth-child(3n) {
+		margin-right: 0;
+	}
+}
+
+.add_img_tips {
+	width: 100%;
+	text {
+		font-size: 30rpx;
+		color: #474747;
+	}
+}
+
+.cha_img {
+	width: 50rpx;
+	height: 50rpx;
+	position: absolute;
+	top: -15rpx;
+	right: -5rpx;
+}
+
+.options_item_box {
+	width: 100%;
+	padding: 0 10rpx;
+	box-sizing: border-box;
+	margin-bottom: 20rpx;
+	justify-content: space-between;
+
+	text {
+		font-size: 28rpx;
+		color: #686868;
+		word-wrap: break-word;
+		word-break: break-all;
+	}
+	image {
+		width: 40rpx;
+		height: 40rpx;
+		margin: 20rpx 0;
+	}
+}
+
+.matrix_input_box {
+	width: 100%;
+
+	text {
+		width: 30%;
+		white-space: nowrap;
+	}
+	input {
+		width: 70%;
+	}
+}
+
+.footer_box {
+	width: 100%;
+	padding: 30rpx 30rpx 60rpx 30rpx;
+	box-sizing: border-box;
+	background: white;
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	z-index: 10;
+
+	text {
+		width: 100%;
+		line-height: 88rpx;
+		font-size: 36rpx;
+		color: white;
+		text-align: center;
+		border-radius: 88rpx;
+		background: #18ba89;
+		display: block;
+	}
+}
+
+.modal_wrap {
+	width: 100%;
+	height: 100%;
+	background-color: rgba(0, 0, 0, 0.6);
+	position: fixed;
+	left: 0;
+	top: 0;
+	z-index: 99;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.modal_box {
+	width: 600rpx;
+	max-height: 600rpx;
+	background-color: #fff;
+	border-radius: 5px;
+	padding: 40rpx 40rpx 60rpx;
+}
+.modal_tit {
+	text-align: center;
+	font-size: 30rpx;
+}
+.modal_box_b1 {
+	background-color: #f2f2f2;
+	border-radius: 5px;
+	line-height: 80rpx;
+	margin-top: 40rpx;
+	/* padding-left: 40rpx; */
+	text-align: center;
+}
+.modal_user_item {
+	background: #f2f2f2;
+	padding: 30rpx;
+	margin-top: 40rpx;
+	position: relative;
+}
+.modal_user_right_img {
+	width: 12rpx;
+	height: 21rpx;
+	position: absolute;
+	right: 30rpx;
+	top: 0;
+	bottom: 0;
+	margin: auto 0;
+}
+.modal_t1 {
+	font-weight: bold;
+	display: inline-block;
+	margin-right: 20rpx;
+	font-size: 32rpx;
+}
+.modal_t3 {
+	margin-top: 20rpx;
+}
+.modal_btn {
+	width: 46%;
+	line-height: 75rpx;
+	text-align: center;
+	border-radius: 50rpx;
+	margin-top: 40rpx;
+}
+.modal_btn1 {
+	background: #aaaaaa;
+	color: #fff;
+}
+.modal_btn2 {
+	background: #61c88f;
+	color: #fff;
+}
+.yjfl {
+	position: fixed;
+	bottom: 340rpx;
+	right: 0;
+	z-index: 100;
+	width: 160rpx;
+	text-align: center;
+
+	.title {
+		color: #fff;
+		position: absolute;
+		bottom: 10rpx;
+		right: 4rpx;
+		width: 100%;
+		text-align: center;
+		font-size: 24rpx;
+	}
+}
+</style>

+ 1 - 0
pagesAdmin/satisfaction/service/index.ts

@@ -0,0 +1 @@
+export * from './satisfactionQuestions';

+ 36 - 0
pagesAdmin/satisfaction/service/satisfactionQuestions/index.ts

@@ -0,0 +1,36 @@
+import { REQUEST_CONFIG } from '@/config';
+import { request, handle } from '@kasite/uni-app-base';
+
+/** 上传图片 */
+export const UploadZxFile = `${REQUEST_CONFIG.BASE_URL}upload/uploadZxFile.do`;
+
+/** 问卷任务-查询由渠道任务发布的满意度调查问卷列表 */
+export const QuerySubjectListToChannelTask_V3 = async (queryData) => {
+	let resp = handle.promistHandle(
+		await request.doPost(
+			`${REQUEST_CONFIG.BASE_URL}wsgw/surveyV3/SurveyWs/QuerySubjectListToChannelTask/callApiJSON.do`,
+			queryData
+		)
+	);
+	return handle.catchPromise(resp, () => resp);
+};
+/** 问卷详情 */
+export const QuerySubjectInfoById_V3 = async (queryData) => {
+	let resp = handle.promistHandle(
+		await request.doPost(
+			`${REQUEST_CONFIG.BASE_URL}wsgw/surveyV3/SurveyWs/QuerySubjectInfoById/callApiJSON.do`,
+			queryData
+		)
+	);
+	return handle.catchPromise(resp, () => resp);
+};
+/** 提交答案 */
+export const CommitAnswer_V3 = async (queryData) => {
+	let resp = handle.promistHandle(
+		await request.doPost(
+			`${REQUEST_CONFIG.BASE_URL}wsgw/surveyV3/SurveyWs/CommitAnswer_V3/callApiJSON.do`,
+			queryData
+		)
+	);
+	return handle.catchPromise(resp, () => resp);
+};

+ 343 - 0
pagesCrm/business/home/home.vue

@@ -0,0 +1,343 @@
+<template>
+	<view class="container">
+		<view class="filtration_scroll_wrapper ptb-12 plr-16">
+			<view class="filtration--item" :class="{ actived: !deptId }" @click="selectedDept()">
+				全部
+			</view>
+			<view
+				v-for="item in deptList"
+				:key="item.DeptId"
+				class="filtration--item"
+				:class="{ actived: deptId == item.DeptId }"
+				@click="selectedDept(item)"
+			>
+				{{ item.DeptName }}
+			</view>
+		</view>
+
+		<view class="timer-list">
+			<scroll-view
+				v-if="dateline.length"
+				scroll-y
+				lower-threshold="100"
+				bindscrolltolower="getPatientGroupPlanList"
+			>
+				<view class="timer" v-for="item in dateline">
+					<view class="date">
+						<text>{{ item.year }}</text>
+						<text class="divider">/</text>
+						<text>{{ item.mouth }}</text>
+						<text class="divider">/</text>
+						<text>{{ item.day }}</text>
+					</view>
+
+					<view class="list">
+						<view
+							v-for="(data, index) in datelineData[item.date]"
+							:key="data.PlanUuid"
+							class="wrapper"
+							:class="{ unread: !data.IsRead }"
+							@click="toSchemeDetail(data, data.TempType, index)"
+						>
+							<view>
+								<view class="dept">{{ data.DeptName }}</view>
+								<view class="desc">{{ schemeTypeEnum[data.TempType] }}</view>
+							</view>
+						</view>
+					</view>
+				</view>
+			</scroll-view>
+			<noData wx:else value="暂无数据"></noData>
+		</view>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { getCurrentInstance, nextTick, ref } from 'vue';
+import { useStore } from 'vuex';
+import { useOnLoad } from '@/hook';
+import { mapState, mapGetters, mapMutations } from '@kasite/uni-app-base/store';
+import { common } from '@kasite/uni-app-base';
+import { schemeTypeEnum } from '../../static/schemeDetail';
+import { lastMsgContentExtract } from '../../static/chatRoom';
+import { GetPatientExecPlanDeptList, GetPatientExecPlanList } from '../../service';
+
+const { proxy } = getCurrentInstance();
+const store = useStore();
+
+const cardInfo = ref({} as any);
+const deptList = ref([]);
+const deptId = ref('');
+const dateline = ref([]);
+const datelineData = ref({});
+const pIndex = ref(1);
+const noMore = ref(false);
+
+const { memberList, currentUser = {} } = mapState({
+	memberList: 'memberList',
+	currentUser: 'currentUser',
+});
+const { setCurrentUser } = mapMutations({
+	setCurrentUser: 'setCurrentUser',
+});
+
+const main = (options) => {
+	/**默认查全部记录 */
+	refresh(options);
+};
+/** 刷新 */
+const refresh = async (options: any = {}) => {
+	if (!currentUser.value) {
+		let [user = {}] = memberList.value.filter((item) => {
+			if (options.memberId) {
+				return item.memberId == options.memberId;
+			} else {
+				return !!item.userMemberList[0].isDefaultMember;
+			}
+		});
+		if (common.isEmpty(user)) user = memberList.value[0];
+		setCurrentUser(user);
+		cardInfo.value = user;
+		console.log(user);
+	} else {
+		cardInfo.value = currentUser.value;
+	}
+
+	noMore.value = false;
+	getPatientExecPlanDeptList();
+	// getMemberChatList()
+	await getPatientExecPlanList();
+	uni.stopPullDownRefresh();
+};
+/** 查询患者执行计划科室列表 */
+const getPatientExecPlanDeptList = async () => {
+	if (!cardInfo.value.memberId) return;
+	const resp = await GetPatientExecPlanDeptList({
+		MemberId: cardInfo.value.memberId,
+	});
+	deptList.value = resp;
+};
+/** 查询患者组内执行计划列表 */
+const getPatientExecPlanList = async (e = undefined) => {
+	if (!cardInfo.value.memberId) return;
+	if (noMore.value) return;
+	let PIndex = pIndex.value;
+	if (!e) {
+		PIndex = 1;
+	} else {
+		PIndex++;
+	}
+	const resp = await GetPatientExecPlanList({
+		MemberId: cardInfo.value.memberId,
+		DeptId: deptId.value,
+		Page: {
+			PIndex,
+			PSize: 10,
+		},
+	});
+	let data = e ? datelineData.value : {};
+	let line = e
+		? dateline.value.map((item) => item.date).concat(resp.map((item) => item.ExecDate))
+		: resp.map((item) => item.ExecDate);
+	resp.map((item) => {
+		if (!data[item.ExecDate]) {
+			data[item.ExecDate] = [item];
+		} else {
+			data[item.ExecDate].push(item);
+		}
+	});
+	line = Array.from(new Set(line))
+		.sort((a, b) => {
+			return new Date(b) - new Date(a);
+		})
+		.map((item: string) => {
+			const [year, mouth, day] = item.split('-');
+			return {
+				date: item,
+				year,
+				mouth,
+				day,
+			};
+		});
+	pIndex.value = PIndex;
+	noMore.value = !resp.length;
+	dateline.value = line;
+	datelineData.value = data;
+};
+/** 选择科室 */
+const selectedDept = (item: any = {}) => {
+	const { DeptCode, DeptId = '' } = item;
+	deptId.value = DeptId;
+	noMore.value = false;
+	nextTick(() => getPatientExecPlanList());
+};
+/** 查看方案详情 */
+const toSchemeDetail = (item, type, index) => {
+	const path = `/pagesCrm/business/schemeDetail/schemeDetail?type=${type}&planuuid=${item.PlanUuid}&date=${item.ExecDate}&index=${index}&id=${item.Id}`;
+	common.goToUrl(path);
+};
+const changeItemReadStatus = (date, index, status) => {
+	datelineData.value[date][index].IsRead = status;
+};
+
+useOnLoad((options) => {
+	main(options);
+	// 兜底:监听事件总线的状态更新
+	uni.$on('changeItemReadStatus', ({ date, index, status }) => {
+		if (datelineData.value[date] && datelineData.value[date][index]) {
+			datelineData.value[date][index].IsRead = status;
+		}
+	});
+});
+
+defineExpose({
+	changeItemReadStatus,
+});
+</script>
+
+<style lang="scss" scoped>
+.container {
+	display: flex;
+	flex-direction: column;
+	padding-bottom: calc(constant(safe-area-inset-bottom) + 20rpx);
+	padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
+}
+
+.filtration {
+	padding: 0 20rpx;
+}
+
+.filtration_scroll_wrapper {
+	display: flex;
+	overflow: auto;
+}
+
+.filtration--item {
+	padding: 20rpx 40rpx;
+	background-color: #d3d3d3;
+	border-radius: 8rpx;
+	word-break: keep-all;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+.filtration--item + .filtration--item {
+	margin-left: 20rpx;
+}
+
+.filtration--item.actived {
+	background-color: var(--uni-color-primary);
+	color: #fff;
+	font-weight: bold;
+}
+
+.timer-list {
+	height: 100%;
+	overflow: auto;
+}
+
+.timer + .timer {
+	margin-top: 20rpx;
+}
+
+.date {
+	--height: 46rpx;
+	margin: 20rpx;
+	padding: 0 20rpx 0 56rpx;
+	position: relative;
+	font-weight: bold;
+	height: var(--height);
+	align-items: center;
+	background-color: #e8e8e8;
+	border-radius: var(--height);
+	display: inline-flex;
+}
+
+.date::before {
+	content: '';
+	height: 12rpx;
+	width: 12rpx;
+	border-radius: 100%;
+	border: 16rpx solid #b7bbca;
+	position: absolute;
+	left: 0;
+	z-index: 1;
+}
+
+.date::after {
+	content: '';
+	width: 1rpx;
+	height: calc(100% + 20rpx);
+	position: absolute;
+	left: 20rpx;
+	bottom: -100%;
+	background-color: #dee0e9;
+}
+
+.date .divider {
+	color: var(--uni-color-primary);
+	margin: 0 8rpx;
+}
+
+.timer .wrapper {
+	padding: 24rpx 24rpx 24rpx 76rpx;
+	position: relative;
+	align-items: center;
+	background-color: #fff;
+	display: flex;
+	justify-content: space-between;
+}
+
+.wrapper::before {
+	content: '';
+	height: 18rpx;
+	width: 18rpx;
+	border-radius: 100%;
+	background-color: #dee0e9;
+	position: absolute;
+	left: 32rpx;
+}
+
+.wrapper::after {
+	content: '';
+	width: 1rpx;
+	height: calc(100% + 20rpx);
+	position: absolute;
+	left: calc(32rpx + 8rpx);
+	top: 0;
+	background-color: #dee0e9;
+}
+
+.wrapper:last-child::after {
+	height: 50%;
+}
+
+.wrapper + .wrapper {
+	margin-top: 20rpx;
+}
+
+.arrow_right {
+	width: 30rpx;
+	height: 30rpx;
+}
+
+.dept {
+	color: #909399;
+	margin-bottom: 24rpx;
+	font-size: 26rpx;
+	position: relative;
+	display: inline-block;
+}
+
+.unread .dept::before {
+	content: '';
+	height: 14rpx;
+	width: 14rpx;
+	border-radius: 100%;
+	position: absolute;
+	right: -20rpx;
+	top: -5rpx;
+	background-color: #fa4844;
+}
+</style>

+ 102 - 0
pagesCrm/business/schemeDetail/schemeDetail.vue

@@ -0,0 +1,102 @@
+<template>
+	<template v-if="option.type == schemeTypeEnum['服务告知']">
+		<announcementsVue
+			:content="content"
+			:date="option.date"
+			:index="option.index"
+		></announcementsVue>
+	</template>
+	<template v-if="option.type == schemeTypeEnum['注意事项']">
+		<announcements-vue
+			:content="content"
+			:date="option.date"
+			:index="option.index"
+		></announcements-vue>
+	</template>
+	<template v-if="option.type == schemeTypeEnum['复诊提醒']">
+		<return-visit-vue
+			:content="content"
+			:date="option.date"
+			:index="option.index"
+		></return-visit-vue>
+	</template>
+	<template v-if="option.type == schemeTypeEnum['宣教']">
+		<health-education-vue
+			:content="content"
+			:date="option.date"
+			:index="option.index"
+		></health-education-vue>
+	</template>
+	<template v-if="option.type == schemeTypeEnum['问卷']">
+		<questionnaire-vue
+			:content="content"
+			:date="option.date"
+			:index="option.index"
+		></questionnaire-vue>
+	</template>
+</template>
+
+<script lang="ts" setup>
+import { getCurrentInstance, nextTick, reactive, ref, computed } from 'vue';
+import { useStore } from 'vuex';
+import { useOnLoad } from '@/hook';
+import { schemeTypeEnum } from '../../static/schemeDetail';
+import AnnouncementsVue from './template/announcements.vue';
+import HealthEducationVue from './template/health-education.vue';
+import QuestionnaireVue from './template/questionnaire.vue';
+import ReturnVisitVue from './template/return-visit.vue';
+import { GetPatientGroupPlanDetailList, GetPatientGroupPlanDetail } from '../../service';
+
+defineOptions({
+	components: {
+		AnnouncementsVue,
+		HealthEducationVue,
+		QuestionnaireVue,
+		ReturnVisitVue,
+	},
+});
+
+const option = reactive({
+	type: '',
+	planuuid: '',
+	id: '',
+	memberName: '',
+	date: '',
+	index: '',
+});
+const content = ref([]);
+
+/** 查询患者组内执行计划详情列表 */
+const getPatientGroupPlanDetailList = async () => {
+	const params = {
+		PlanUuid: option.planuuid,
+		TempType: option.type,
+	};
+	const resp = await GetPatientGroupPlanDetailList(params);
+	content.value = resp;
+};
+/** 查询患者执行计划详情 */
+const getPatientGroupPlanDetail = async () => {
+	const params = {
+		Id: option.id,
+	};
+	const resp = await GetPatientGroupPlanDetail(params);
+	content.value = resp;
+};
+
+useOnLoad((options: any) => {
+	uni.setNavigationBarTitle({
+		title: schemeTypeEnum[options.type],
+	});
+	Object.keys(options).map((key) => {
+		option[key] = options[key];
+	});
+	if ([schemeTypeEnum['宣教'], schemeTypeEnum['问卷']].includes(option.type)) {
+		getPatientGroupPlanDetail();
+	} else {
+		getPatientGroupPlanDetailList();
+	}
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 73 - 0
pagesCrm/business/schemeDetail/template/announcements.vue

@@ -0,0 +1,73 @@
+<template>
+	<view>
+		<view class="header">
+			<view>{{ memberName }}</view>
+			<text class="warning">计划阅读:{{ option.ExecDate }}前</text>
+			<text class="warning" wx:if="{{option.IsRead}}">实际阅读:{{ option.ReadTime }}</text>
+		</view>
+		<view class="body">
+			<view class="title">注意事项</view>
+			<rich-text>
+				{{ pushContent }}
+			</rich-text>
+		</view>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
+import Props from './props';
+import { PatientRead } from '../../../service';
+
+const props = defineProps(Props);
+
+const list = ref([]);
+const option = ref({} as any);
+const isAllRead = ref(1);
+const pushContent = ref('');
+
+const init = async (datas) => {
+	if (datas && !datas.length) return;
+	pushContent.value = datas[0].PushContent;
+	option.value = datas[0];
+	let readRes = datas[0].IsRead;
+	if (!readRes) {
+		const resp = await PatientRead({
+			Id: datas[0].Id,
+		});
+		readRes = !!resp;
+		const pages = getCurrentPages();
+		const homePage = pages.find((p: any) => p && p.route && p.route.indexOf('pagesCrm/business/home/home') !== -1);
+		const target: any = homePage || (pages.length >= 2 ? pages[pages.length - 2] : undefined);
+		const vm = target && (target as any).$vm;
+		if (vm && typeof vm.changeItemReadStatus === 'function') {
+			vm.changeItemReadStatus(props.date, props.index, 1);
+		} else {
+			uni.$emit('changeItemReadStatus', { date: props.date, index: props.index, status: 1 });
+		}
+	}
+};
+
+watch(
+	() => props.content,
+	(v) => {
+		init(v);
+	},
+	{
+		immediate: true,
+		deep: true,
+	}
+);
+</script>
+
+<style lang="scss" scoped>
+@import './common.scss';
+
+.title {
+	font-family: PingFang-SC, PingFang-SC;
+	font-weight: bold;
+	font-size: 36rpx;
+	text-align: center;
+	padding: 42rpx 0;
+}
+</style>

+ 22 - 0
pagesCrm/business/schemeDetail/template/common.scss

@@ -0,0 +1,22 @@
+.header {
+    height: 60rpx;
+    width: 100%;
+    padding: 0 30rpx;
+    font-size: 22rpx;
+    font-weight: bold;
+    background: #FFF0DA;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}
+
+.body {
+    padding: 0 30rpx;
+    font-size: 30rpx;
+    color: #434349;
+    background: #fff;
+}
+
+.warning {
+    color: #D18D09
+}

+ 207 - 0
pagesCrm/business/schemeDetail/template/health-education.vue

@@ -0,0 +1,207 @@
+<template>
+	<view>
+		<view class="header">
+			<view>{{ memberName }}</view>
+			<text class="warning">计划阅读:{{ option.ExecDate }}前</text>
+			<text class="warning" v-if="option.IsRead">
+				实际阅读:{{ isAllRead ? '完成' : '未完成' }}
+			</text>
+		</view>
+		<view class="body">
+			<view v-for="(item, index) in list" :key="index" class="article">
+				<view class="status" :class="item.IsRead ? 'read' : 'unread'">
+					{{ item.IsRead ? '已阅读' : '未阅读' }}
+				</view>
+				<view class="title">
+					<text>{{ index + 1 }}、</text>
+					<text>{{ item.PushContent.Title }}</text>
+				</view>
+				<view class="btn" @click="toArticle(item)">
+					<text>立即阅读</text>
+					<view class="arrow-right"></view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
+import Props from './props';
+import { PatientRead } from '../../../service';
+import { common } from '@kasite/uni-app-base';
+import path from '../../../static/path';
+
+const props = defineProps(Props);
+
+const list = ref([]);
+const option = ref({} as any);
+const isAllRead = ref(0);
+
+const init = async (datas) => {
+	if (datas && !datas.length) return;
+	datas.forEach((item) => {
+		try {
+			if (typeof item.PushContent == 'string') {
+				item.PushContent = JSON.parse(item.PushContent);
+			}
+		} catch (e) {}
+	});
+	let isRead = 1;
+	for (let i = 0; i < datas.length; i++) {
+		if (!datas[i].IsRead) {
+			isRead = 0;
+			break;
+		}
+	}
+	list.value = datas;
+	option.value = datas[0];
+	isAllRead.value = isRead;
+};
+
+const toArticle = async (row) => {
+	const {
+		PushContent: { Title, Id },
+		Id: planid,
+	} = row;
+	const index = list.value.findIndex((item) => item.Id == planid);
+	const plan = list.value[index];
+	let readRes = plan.IsRead;
+	if (!plan.IsRead) {
+		const resp = await PatientRead({
+			Id: planid,
+		});
+		readRes = !!resp;
+	}
+	list.value[index].IsRead = readRes;
+	const isAllRead = list.value.reduce((res, item) => {
+		return res && item.IsRead;
+	}, true);
+	const pages = getCurrentPages();
+	const homePage = pages.find((p: any) => p && p.route && p.route.indexOf('pagesCrm/business/home/home') !== -1);
+	const target: any = homePage || (pages.length >= 2 ? pages[pages.length - 2] : undefined);
+	const vm = target && (target as any).$vm;
+	if (vm && typeof vm.changeItemReadStatus === 'function') {
+		vm.changeItemReadStatus(props.date, props.index, isAllRead);
+	} else {
+		uni.$emit('changeItemReadStatus', { date: props.date, index: props.index, status: isAllRead });
+	}
+	common.goToUrl(path.article(Id));
+};
+
+watch(
+	() => props.content,
+	(v) => {
+		init(v);
+	},
+	{
+		immediate: true,
+		deep: true,
+	}
+);
+</script>
+
+<style lang="scss" scoped>
+@import './common.scss';
+
+.body {
+	padding: 30rpx;
+}
+
+.article {
+	height: 108rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 40rpx 22rpx;
+	background: #f7f7fb;
+	border-radius: 16rpx 16rpx 16rpx 16rpx;
+	margin-bottom: 29rpx;
+	position: relative;
+}
+
+.title {
+	font-size: 28rpx;
+	color: #434349;
+	position: relative;
+	z-index: 1;
+}
+
+.btn {
+	padding: 20rpx 10rpx;
+	font-size: 28rpx;
+	font-weight: normal;
+	color: var(--uni-color-primary);
+	background: transparent;
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	gap: 8rpx;
+}
+
+.status {
+	--is-read: #00a9a9;
+	--un-read: #f95d3b;
+	font-size: 20rpx;
+	color: #fefffe;
+	padding: 6rpx 13rpx;
+	position: absolute;
+	top: 0;
+	left: 0;
+	border-radius: 20rpx 20rpx 20rpx 0;
+	z-index: 0;
+}
+
+.status::before {
+	content: '';
+	width: 16rpx;
+	height: 16rpx;
+	position: absolute;
+	bottom: -16rpx;
+	left: 0;
+}
+
+.status::after {
+	content: '';
+	width: 32rpx;
+	height: 32rpx;
+	background-color: #f7f7fb;
+	position: absolute;
+	bottom: -32rpx;
+	left: 0;
+	border-radius: 32rpx;
+}
+
+.status.read,
+.status.read::before {
+	background-color: var(--is-read);
+}
+
+.status.unread,
+.status.unread::before {
+	background-color: var(--un-read);
+}
+
+.arrow-right {
+	--width: 13rpx;
+	--height: 10rpx;
+	display: inline-block;
+	width: calc(var(--width));
+	height: calc(var(--height) * 2);
+	border-top: var(--height) solid transparent;
+	border-bottom: var(--height) solid transparent;
+	border-left: var(--width) solid var(--uni-color-primary);
+	position: relative;
+}
+
+.arrow-right::after {
+	content: '';
+	border-top: var(--height) solid transparent;
+	border-bottom: var(--height) solid transparent;
+	border-left: var(--width) solid #f7f7fb;
+	position: absolute;
+	left: -14rpx;
+	top: 50%;
+	transform: translateY(-50%);
+}
+</style>

+ 20 - 0
pagesCrm/business/schemeDetail/template/props.ts

@@ -0,0 +1,20 @@
+const props = {
+	memberName: {
+		type: String,
+		default: '',
+	},
+	content: {
+		type: Array,
+		default: () => [],
+	},
+	date: {
+		type: String,
+		default: '',
+	},
+	index: {
+		type: [Number, String],
+		default: 0,
+	},
+};
+
+export default props;

+ 206 - 0
pagesCrm/business/schemeDetail/template/questionnaire.vue

@@ -0,0 +1,206 @@
+<template>
+	<view>
+		<view class="header">
+			<view>{{ memberName }}</view>
+			<text class="warning">计划阅读:{{ option.ExecDate }}前</text>
+			<!-- <text class="warning" wx:if="{{option.IsRead}}">实际阅读:{{isAllRead? '完成' : '未完成'}}</text> -->
+		</view>
+		<view class="body">
+			<view v-for="(item, index) in list" :key="index" class="article">
+				<view class="status" :class="item.IsRead ? 'read' : 'unread'">{{
+					item.IsRead ? '已阅读' : '未阅读'
+				}}</view>
+				<view class="title">
+					<text>{{ index + 1 }}、</text>
+					<text>{{ item.PushContent.Title }}</text>
+				</view>
+				<view class="btn" @click="toArticle(item)">
+					<text>立即填写</text>
+					<view class="arrow-right"></view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
+import Props from './props';
+import { PatientRead } from '../../../service';
+import { common } from '@kasite/uni-app-base';
+import path from '../../../static/path';
+
+const props = defineProps(Props);
+
+const list = ref([]);
+const option = ref({} as any);
+const isAllRead = ref(0);
+
+const init = async (datas) => {
+	if (datas && !datas.length) return;
+	datas.forEach((item) => {
+		try {
+			if (typeof item.PushContent == 'string') {
+				item.PushContent = JSON.parse(item.PushContent);
+			}
+		} catch (e) {}
+	});
+	let isRead = 1;
+	for (let i = 0; i < datas.length; i++) {
+		if (!datas[i].IsRead) {
+			isRead = 0;
+			break;
+		}
+	}
+	list.value = datas;
+	option.value = datas[0];
+	isAllRead.value = isRead;
+};
+
+const toArticle = async (row) => {
+	const {
+		PushContent: { Title, Id, SubTaskId },
+		Id: planid,
+	} = row;
+	const index = list.value.findIndex((item) => item.Id == planid);
+	const plan = list.value[index];
+	let readRes = plan.IsRead;
+	if (!plan.IsRead) {
+		const resp = await PatientRead({
+			Id: planid,
+		});
+		readRes = !!resp;
+	}
+	list.value[index].IsRead = readRes;
+	const isAllRead = list.value.reduce((res, item) => {
+		return res && item.IsRead;
+	}, true);
+	const pages = getCurrentPages();
+	const homePage = pages.find((p: any) => p && p.route && p.route.indexOf('pagesCrm/business/home/home') !== -1);
+	const target: any = homePage || (pages.length >= 2 ? pages[pages.length - 2] : undefined);
+	const vm = target && (target as any).$vm;
+	if (vm && typeof vm.changeItemReadStatus === 'function') {
+		vm.changeItemReadStatus(props.date, props.index, isAllRead);
+	} else {
+		uni.$emit('changeItemReadStatus', { date: props.date, index: props.index, status: isAllRead });
+	}
+	common.goToUrl(path.questionnaire({ id: Id, subTaskId: SubTaskId }));
+};
+
+watch(
+	() => props.content,
+	(v) => {
+		init(v);
+	},
+	{
+		immediate: true,
+		deep: true,
+	}
+);
+</script>
+
+<style lang="scss" scoped>
+@import './common.scss';
+
+.body {
+	padding: 30rpx;
+}
+
+.article {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 40rpx 22rpx;
+	background: #f7f7fb;
+	border-radius: 16rpx 16rpx 16rpx 16rpx;
+	margin-bottom: 29rpx;
+	position: relative;
+}
+
+.title {
+	font-size: 28rpx;
+	color: #434349;
+	position: relative;
+	z-index: 1;
+}
+
+.btn {
+	padding: 20rpx 10rpx;
+	font-size: 28rpx;
+	font-weight: normal;
+	color: var(--uni-color-primary);
+	background: transparent;
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	gap: 8rpx;
+	min-width: 160rpx;
+	width: 160rpx;
+}
+
+.status {
+	--is-read: #00a9a9;
+	--un-read: #f95d3b;
+	font-size: 20rpx;
+	color: #fefffe;
+	padding: 6rpx 13rpx;
+	position: absolute;
+	top: 0;
+	left: 0;
+	border-radius: 20rpx 20rpx 20rpx 0;
+	z-index: 0;
+}
+
+.status::before {
+	content: '';
+	width: 16rpx;
+	height: 16rpx;
+	position: absolute;
+	bottom: -16rpx;
+	left: 0;
+}
+
+.status::after {
+	content: '';
+	width: 32rpx;
+	height: 32rpx;
+	background-color: #f7f7fb;
+	position: absolute;
+	bottom: -32rpx;
+	left: 0;
+	border-radius: 32rpx;
+}
+
+.status.read,
+.status.read::before {
+	background-color: var(--is-read);
+}
+
+.status.unread,
+.status.unread::before {
+	background-color: var(--un-read);
+}
+
+.arrow-right {
+	--width: 13rpx;
+	--height: 10rpx;
+	display: inline-block;
+	width: calc(var(--width));
+	height: calc(var(--height) * 2);
+	border-top: var(--height) solid transparent;
+	border-bottom: var(--height) solid transparent;
+	border-left: var(--width) solid var(--uni-color-primary);
+	position: relative;
+}
+
+.arrow-right::after {
+	content: '';
+	border-top: var(--height) solid transparent;
+	border-bottom: var(--height) solid transparent;
+	border-left: var(--width) solid #f7f7fb;
+	position: absolute;
+	left: -14rpx;
+	top: 50%;
+	transform: translateY(-50%);
+}
+</style>

+ 203 - 0
pagesCrm/business/schemeDetail/template/return-visit.vue

@@ -0,0 +1,203 @@
+<template>
+	<view>
+		<view class="header">
+			<view>{{ memberName }}</view>
+			<text class="warning">请根据计划安排到院{{ activeTypeName }}</text>
+		</view>
+		<view class="body">
+			<view
+				v-for="item in pushContent"
+				:key="item.Id"
+				:class="{ 'is-active': item.IsActive }"
+				class="time-line"
+			>
+				<view class="dot">
+					<view class="location" v-if="item.IsActive"></view>
+				</view>
+				<view class="wrapper">
+					<view class="timestamp">
+						<view>{{ item.ExecDate }}</view>
+						<view>{{ item.OutPatientPushTypeName }}</view>
+					</view>
+					<text style="white-space: pre-wrap" class="content">{{ item.PushContent }}</text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
+import Props from './props';
+import { PatientRead } from '../../../service';
+import { common } from '@kasite/uni-app-base';
+
+const props = defineProps(Props);
+
+const option = ref({} as any);
+const pushContent = ref([]);
+const activeTypeName = ref('');
+
+const init = async (datas) => {
+	if (datas && !datas.length) return;
+	const today = common.dateFormat(new Date()).formatYear;
+	activeTypeName.value = '';
+	datas.forEach((item) => {
+		item.IsActive = item.ExecDate == today;
+		if (item.IsActive) activeTypeName.value = item.OutPatientPushTypeName;
+	});
+	let readRes = datas[0].IsRead;
+	if (!readRes) {
+		const resp = await PatientRead({
+			Id: datas[0].Id,
+		});
+		readRes = !!resp;
+	const pages = getCurrentPages();
+	const homePage = pages.find((p: any) => p && p.route && p.route.indexOf('pagesCrm/business/home/home') !== -1);
+	const target: any = homePage || (pages.length >= 2 ? pages[pages.length - 2] : undefined);
+	const vm = target && (target as any).$vm;
+	if (vm && typeof vm.changeItemReadStatus === 'function') {
+		vm.changeItemReadStatus(props.date, props.index, 1);
+	} else {
+		uni.$emit('changeItemReadStatus', { date: props.date, index: props.index, status: 1 });
+	}
+	}
+	pushContent.value = datas;
+	option.value = datas[0];
+};
+
+watch(
+	() => props.content,
+	(v) => {
+		init(v);
+	},
+	{
+		immediate: true,
+		deep: true,
+	}
+);
+</script>
+
+<style lang="scss" scoped>
+@import './common.scss';
+
+.body {
+	padding: 30rpx;
+}
+
+.time-line {
+	padding-left: 71rpx;
+	position: relative;
+	margin-bottom: 38rpx;
+}
+
+.timestamp {
+	font-weight: bold;
+	font-size: 32rpx;
+	color: #212326;
+	line-height: 48rpx;
+	display: flex;
+	gap: 20rpx;
+	margin-bottom: 22rpx;
+}
+
+.dot {
+	width: 26rpx;
+	height: 26rpx;
+	background: #fff;
+	border: 4rpx solid #d8d8d8;
+	border-radius: 100%;
+	position: absolute;
+	left: 20rpx;
+	top: 5rpx;
+	z-index: 1;
+}
+
+.time-line::after {
+	content: '';
+	width: 6rpx;
+	height: calc(100% + 38rpx);
+	background: #d8d8d8;
+	position: absolute;
+	top: 5rpx;
+	left: 30rpx;
+	z-index: 0;
+}
+
+.content {
+	background: #f7f7fb;
+	border-radius: 6rpx;
+	padding: 20rpx;
+	font-size: 28rpx;
+	color: #434349;
+	line-height: 48rpx;
+	display: block;
+}
+
+.time-line:last-child::after {
+	display: none;
+}
+
+/** 激活状态 */
+.time-line.is-active .timestamp {
+	color: var(--dominantColor);
+}
+
+.time-line.is-active .dot {
+	width: 38rpx;
+	height: 38rpx;
+	border: none;
+	background-color: var(--dominantColor);
+	left: 14rpx;
+	top: -2rpx;
+}
+
+.time-line.is-active .dot::after {
+	--width: 6rpx;
+	content: '';
+	position: absolute;
+	border-top: var(--width) solid var(--dominantColor);
+	border-left: var(--width) solid transparent;
+	border-right: var(--width) solid transparent;
+	border-bottom: var(--width) solid transparent;
+	left: 50%;
+	transform: translateX(-50%);
+	bottom: -14rpx;
+}
+
+.location {
+	width: 20rpx;
+	height: 20rpx;
+	background: #fff;
+	border-radius: 100%;
+	position: absolute;
+	left: 50%;
+	transform: translateX(-50%);
+	top: 6rpx;
+}
+
+.location::before {
+	content: '';
+	width: 8rpx;
+	height: 8rpx;
+	background: var(--dominantColor);
+	border-radius: 100%;
+	position: absolute;
+	left: 50%;
+	top: 50%;
+	transform: translateX(-50%) translateY(-50%);
+}
+
+.location::after {
+	--width: 8rpx;
+	content: '';
+	position: absolute;
+	border-top: calc(var(--width) + 4rpx) solid #fff;
+	border-left: var(--width) solid transparent;
+	border-right: var(--width) solid transparent;
+	border-bottom: calc(var(--width) + 4rpx) solid transparent;
+	left: 50%;
+	transform: translateX(-50%);
+	bottom: -18rpx;
+}
+</style>

+ 31 - 0
pagesCrm/service/home/index.ts

@@ -0,0 +1,31 @@
+import { REQUEST_CONFIG } from '@/config';
+import { request, handle } from '@kasite/uni-app-base';
+
+/**
+ * 查询患者执行计划科室列表
+ * @param MemberId String 患者ID
+ */
+export const GetPatientExecPlanDeptList = async (queryData: any) => {
+	let resp = handle.promistHandle(
+		await request.doPost(
+			`${REQUEST_CONFIG.BASE_URL}wsgw/hcrm/GroupBusiness/GetPatientExecPlanDeptList/callApiJSON.do`,
+			queryData
+		)
+	);
+	return handle.catchPromise(resp, () => resp);
+};
+
+/**
+ * 查询患者组内执行计划列表
+ * @param MemberId  String 患者ID(多个id用英文,隔开)
+ * @param DeptId    String 科室ID
+ */
+export const GetPatientExecPlanList = async (queryData: any) => {
+	let resp = handle.promistHandle(
+		await request.doPost(
+			`${REQUEST_CONFIG.BASE_URL}wsgw/hcrm/GroupBusiness/GetPatientExecPlanList/callApiJSON.do`,
+			queryData
+		)
+	);
+	return handle.catchPromise(resp, () => resp);
+};

+ 2 - 0
pagesCrm/service/index.ts

@@ -0,0 +1,2 @@
+export * from './home';
+export * from './schemeDetail';

+ 43 - 0
pagesCrm/service/schemeDetail/index.ts

@@ -0,0 +1,43 @@
+import { REQUEST_CONFIG } from '@/config';
+import { request, handle } from '@kasite/uni-app-base';
+
+/**
+ * 患者已读
+ * @param {String} Id
+ */
+export const PatientRead = async (queryData: any) => {
+	let resp = handle.promistHandle(
+		await request.doPost(
+			`${REQUEST_CONFIG.BASE_URL}wsgw/hcrm/GroupBusiness/PatientRead/callApiJSON.do`,
+			queryData
+		)
+	);
+	return handle.catchPromise(resp, () => resp);
+};
+/**
+ * 查询患者组内执行计划详情列表
+ * @param PlanUuid  String  计划Id
+ * @param TempType  Number  模板类型:1-服务告知,2-出院注意事项,3-复诊计划,4-宣教计划
+ */
+export const GetPatientGroupPlanDetailList = async (queryData: any) => {
+	let resp = handle.promistHandle(
+		await request.doPost(
+			`${REQUEST_CONFIG.BASE_URL}wsgw/hcrm/GroupBusiness/GetPatientGroupPlanDetailList/callApiJSON.do`,
+			queryData
+		)
+	);
+	return handle.catchPromise(resp, () => resp);
+};
+/**
+ * 查询患者执行计划详情
+ * @param Id  String  执行计划id
+ */
+export const GetPatientGroupPlanDetail = async (queryData: any) => {
+	let resp = handle.promistHandle(
+		await request.doPost(
+			`${REQUEST_CONFIG.BASE_URL}wsgw/hcrm/GroupBusiness/GetPatientGroupPlanDetail/callApiJSON.do`,
+			queryData
+		)
+	);
+	return handle.catchPromise(resp, () => resp);
+};

+ 365 - 0
pagesCrm/static/chatRoom.ts

@@ -0,0 +1,365 @@
+/** 消息类别的code */
+export const msgTypeEnum = {
+	//#region 在线互动
+	文本消息: 1000,
+	图片消息: 1001,
+	文本消息: 1000,
+	图片消息: 1001,
+	文件消息: 1002,
+	视频消息: 1003,
+	患教资料: 1004,
+	问卷量表: 1005,
+
+	患教已读: 1104,
+	问卷已填写: 1105,
+
+	申请: 2000,
+	申请通过: 2001,
+	申请拒绝: 2002,
+	申请取消: 2003,
+	申请超时: 2004,
+
+	结束服务: 3000,
+	//#endregion
+
+	//#region 随访消息
+	随访群发消息: 0,
+	随访问卷通知: 1,
+	随访文章通知: 2,
+	随访复诊提醒: 3,
+	随访用药提醒: 4,
+	随访换药提醒: 5,
+	随访手术提醒: 6,
+	随访注意事项: 7,
+	随访检查预约提醒: 8,
+	随访报告已出提醒: 9,
+	随访体征上传提醒: 10,
+	//#endregion
+};
+
+/** 列表显示的消息 */
+class ContentExtractFactory {
+	//#region 在线互动
+	[msgTypeEnum.文本消息](content) {
+		const obj = this.parse(content);
+		return decodeURI(obj.content);
+	}
+	[msgTypeEnum.图片消息]() {
+		return '[图片消息]';
+	}
+	[msgTypeEnum.视频消息]() {
+		return '[视频通话]';
+	}
+	[msgTypeEnum.患教资料]() {
+		return '[健康宣教]';
+	}
+	[msgTypeEnum.问卷量表]() {
+		return '[问卷量表]';
+	}
+	[msgTypeEnum.申请]() {
+		return '[申请交流互动]';
+	}
+	[msgTypeEnum.申请通过](content) {
+		const obj = this.parse(content);
+		return `[申请通过]:${obj.content}`;
+	}
+	[msgTypeEnum.申请拒绝](content) {
+		const obj = this.parse(content);
+		return `[申请被拒绝]:您好,您的互动申请暂未通过,${obj.content}`;
+	}
+	[msgTypeEnum.申请取消](content) {
+		const obj = this.parse(content);
+		return `[申请取消]:${obj.content}`;
+	}
+	[msgTypeEnum.申请超时](content) {
+		const obj = this.parse(content);
+		return `[申请超时]:${obj.content}`;
+	}
+	[msgTypeEnum.结束服务](content) {
+		const obj = this.parse(content);
+		return `[系统]:${obj.content}`;
+	}
+
+	[msgTypeEnum.问卷已填写](content) {
+		const obj = this.parse(content);
+		return obj.content;
+	}
+	[msgTypeEnum.患教已读]() {
+		return '[健康宣教]';
+	}
+	//#endregion
+
+	//#region 随访消息
+	[msgTypeEnum.随访群发消息](content) {
+		const obj = this.parse(content);
+		return `[通知]:${obj.txt}`;
+	}
+	[msgTypeEnum.随访问卷通知]() {
+		return `[问卷量表]`;
+	}
+	[msgTypeEnum.随访文章通知]() {
+		return `[健康宣教]`;
+	}
+	[msgTypeEnum.随访复诊提醒](content) {
+		const obj = this.parse(content);
+		return `[复诊提醒]:${obj.txt}`;
+	}
+	[msgTypeEnum.随访用药提醒](content) {
+		const obj = this.parse(content);
+		return `[用药提醒]:${obj.txt}`;
+	}
+	[msgTypeEnum.随访换药提醒](content) {
+		const obj = this.parse(content);
+		return `[换药提醒]:${obj.txt}`;
+	}
+	[msgTypeEnum.随访手术提醒](content) {
+		const obj = this.parse(content);
+		return `[手术提醒]:${obj.txt}`;
+	}
+	[msgTypeEnum.随访注意事项](content) {
+		const obj = this.parse(content);
+		return `[注意事项]:${obj.txt}`;
+	}
+	[msgTypeEnum.随访检查预约提醒](content) {
+		const obj = this.parse(content);
+		return `[检查预约提醒]:${obj.txt}`;
+	}
+	[msgTypeEnum.随访报告已出提醒](content) {
+		const obj = this.parse(content);
+		return `[报告已出提醒]:${obj.txt}`;
+	}
+	[msgTypeEnum.随访体征上传提醒](content) {
+		const obj = this.parse(content);
+		return `[体征上传提醒]:${obj.txt}`;
+	}
+	//#endregion
+	parse(content) {
+		if (typeof content == 'string') {
+			try {
+				const data = JSON.parse(content);
+				return data;
+			} catch (error) {
+				return content;
+			}
+		}
+	}
+}
+
+/** 消息内容提取 */
+export const lastMsgContentExtract = (msg) => {
+	const factory = new ContentExtractFactory();
+	return factory[msg.contentType] ? factory[msg.contentType](msg.content) : '';
+};
+
+/** 消息内容组件 */
+export const ContentComponents = {
+	//#region 在线互动
+	[msgTypeEnum['文本消息']]: 'textMsg',
+	[msgTypeEnum['图片消息']]: 'imgMsg',
+	[msgTypeEnum['视频消息']]: 'videoCallMsg',
+	[msgTypeEnum['患教资料']]: 'articleMsg',
+	[msgTypeEnum['问卷量表']]: 'questionnaireMsg',
+	[msgTypeEnum['申请']]: 'applyMsg',
+	[msgTypeEnum['申请通过']]: 'textMsg',
+	[msgTypeEnum['申请拒绝']]: 'textMsg',
+	[msgTypeEnum['申请取消']]: 'systemMsg',
+	[msgTypeEnum['申请超时']]: 'systemMsg',
+	[msgTypeEnum['结束服务']]: 'systemMsg',
+
+	[msgTypeEnum['患教已读']]: 'articleMsg',
+	[msgTypeEnum['问卷已填写']]: 'questionnaireMsg',
+	//#endregion
+
+	//#region 随访消息
+	[msgTypeEnum['随访群发消息']]: 'textMsg',
+	[msgTypeEnum['随访问卷通知']]: 'questionnaireMsg',
+	[msgTypeEnum['随访文章通知']]: 'articleMsg',
+	[msgTypeEnum['随访复诊提醒']]: 'textMsg',
+	[msgTypeEnum['随访用药提醒']]: 'textMsg',
+	[msgTypeEnum['随访换药提醒']]: 'textMsg',
+	[msgTypeEnum['随访手术提醒']]: 'textMsg',
+	[msgTypeEnum['随访注意事项']]: 'textMsg',
+	[msgTypeEnum['随访检查预约提醒']]: 'textMsg',
+	[msgTypeEnum['随访报告已出提醒']]: 'textMsg',
+	[msgTypeEnum['随访体征上传提醒']]: 'textMsg',
+	//#endregion
+};
+
+/** 消息内容组件内容prop */
+const ContentProps = {
+	//#region 在线互动
+	[msgTypeEnum['文本消息']]: {
+		content: 'Content.content',
+	},
+	[msgTypeEnum['图片消息']]: {
+		content: 'Content.content',
+	},
+	[msgTypeEnum['视频消息']]: {
+		content: 'Content.content',
+	},
+	[msgTypeEnum['患教资料']]: {
+		title: 'Content.title',
+		id: 'Content.id',
+		read: 'Content.read',
+		subTaskId: 'Content.subTaskId',
+	},
+	[msgTypeEnum['患教已读']]: {
+		title: 'Content.title',
+		id: 'Content.id',
+		read: 'Content.read',
+		subTaskId: 'Content.subTaskId',
+	},
+	[msgTypeEnum['问卷量表']]: {
+		title: 'Content.title',
+		id: 'Content.subjectId',
+		subTaskId: 'Content.subTaskId',
+	},
+	[msgTypeEnum['问卷已填写']]: {
+		id: 'Content.subjectId',
+		title: 'Content.title',
+		content: 'Content.content',
+		sampleId: 'Content.sampleId',
+		recordId: 'Content.recordId',
+		subTaskId: 'Content.subTaskId',
+	},
+	[msgTypeEnum['申请']]: {
+		content: 'Content.content',
+		imgList: 'Content.imgList',
+		id: 'Id',
+	},
+	[msgTypeEnum['申请通过']]: {
+		content: 'Content.content',
+	},
+	[msgTypeEnum['申请拒绝']]: {
+		content: 'Content.content',
+		format(obj) {
+			return `您好,您的互动申请暂未通过,${obj.content}`;
+		},
+	},
+	[msgTypeEnum['申请超时']]: {
+		content: 'Content.content',
+	},
+	[msgTypeEnum['申请取消']]: {
+		content: 'Content.content',
+	},
+	[msgTypeEnum['结束服务']]: {
+		content: 'Content.content',
+	},
+	//#endregion
+
+	//#region 随访消息
+	[msgTypeEnum['随访群发消息']]: {
+		content: 'Content.txt',
+	},
+	[msgTypeEnum['随访问卷通知']]: {
+		title: 'Content.txt',
+		id: 'Content.id',
+		subTaskId: 'Content.subTaskId',
+	},
+	[msgTypeEnum['随访文章通知']]: {
+		title: 'Content.txt',
+		id: 'Content.id',
+		msgId: 'Content.msgId',
+		subTaskId: 'Content.subTaskId',
+	},
+	[msgTypeEnum['随访复诊提醒']]: {
+		content: 'Content.txt',
+	},
+	[msgTypeEnum['随访用药提醒']]: {
+		content: 'Content.txt',
+	},
+	[msgTypeEnum['随访换药提醒']]: {
+		content: 'Content.txt',
+	},
+	[msgTypeEnum['随访手术提醒']]: {
+		content: 'Content.txt',
+	},
+	[msgTypeEnum['随访注意事项']]: {
+		content: 'Content.txt',
+	},
+	[msgTypeEnum['随访检查预约提醒']]: {
+		content: 'Content.txt',
+	},
+	[msgTypeEnum['随访报告已出提醒']]: {
+		content: 'Content.txt',
+	},
+	[msgTypeEnum['随访体征上传提醒']]: {
+		content: 'Content.txt',
+	},
+	//#endregion
+};
+
+/** 转换消息 */
+export const parseMessageEntry = (message, type) => {
+	const obj = {};
+	const props = ContentProps[type];
+	if (!props) return;
+	let hasFormat = typeof props['format'] == 'function';
+	Object.keys(props).map((key) => {
+		if (key != 'format') {
+			const path = props[key].split('.');
+			if (path.length <= 1) {
+				obj[key] = message[path];
+			} else {
+				obj[key] = path.reduce((item, p) => {
+					if (item instanceof Array) {
+						const [v] = item.filter((i) => i.key == p);
+						return v?.value;
+					} else {
+						try {
+							if (typeof item[p] == 'string') {
+								const value = JSON.parse(item[p]);
+								typeof value == 'object' && (item[p] = value);
+							}
+							return item[p];
+						} catch (error) {
+							return item[p];
+						}
+					}
+				}, message);
+			}
+		}
+	});
+	hasFormat && (obj['content'] = props['format'](obj));
+	return obj;
+};
+
+/** 会话记录排序*/
+export const msgListSort = (
+	list,
+	props = {
+		sendTime: 'CreateTime',
+	}
+) => {
+	// 时间排序  从小到大  最新的在后面
+	list.sort((a, b) => {
+		return new Date(a[props.sendTime]).getTime() - new Date(b[props.sendTime]).getTime();
+	});
+	return list;
+};
+/** 判断消息是否显示时间 */
+export const msgJudgeTime = (
+	list,
+	lastMsg = null,
+	props = {
+		sendTime: 'CreateTime',
+	}
+) => {
+	for (let i = 0; i < list.length; i++) {
+		if (i == 0) {
+			list[i].showSendTime = lastMsg
+				? compareTime(list[i][props.sendTime], lastMsg[props.sendTime])
+				: true;
+		} else {
+			list[i].showSendTime = compareTime(list[i][props.sendTime], list[i - 1][props.sendTime]);
+		}
+		list[i].isNew = !!lastMsg;
+	}
+	return list;
+};
+/** 对比时间 */
+export const compareTime = (startTime, endTime) => {
+	const times = new Date(startTime).getTime();
+	const preTimes = new Date(endTime).getTime();
+	return times - preTimes >= 5 * 60 * 1000;
+};

+ 17 - 0
pagesCrm/static/path.ts

@@ -0,0 +1,17 @@
+const path = {
+	/** 跳转患教详情 */
+	article: (id) => {
+		return `/pages/business/tabbar/transferPage/transferPage?type=articleDetail&articleId=${id}`;
+	},
+	/** 跳转问卷详情 */
+	questionnaire: ({ id, sampleId, recordId, subTaskId, Id }: any) => {
+		return `/pages/business/tabbar/transferPage/transferPage?type=myddc&subjectId=${id}&sampleId=${sampleId}&recordId=${
+			recordId || Id
+		}&taskId=${subTaskId}`;
+	},
+	/** 线上护理咨询 */
+	/** 线下护理门诊预约 */
+	/** 护理上门预约 */
+};
+
+export default path;

+ 19 - 0
pagesCrm/static/schemeDetail.ts

@@ -0,0 +1,19 @@
+const schemeType = {
+	1: '服务告知',
+	2: '注意事项',
+	3: '复诊提醒',
+	4: '宣教',
+	5: '问卷',
+};
+
+const createEnum = (obj: any) => {
+	if (obj instanceof Object) {
+		Object.keys(obj).map((key) => {
+			const value = obj[key];
+			obj[value] = key;
+		});
+	}
+	return obj;
+};
+
+export const schemeTypeEnum = createEnum(schemeType);

+ 3 - 0
pnpm-workspace.yaml

@@ -0,0 +1,3 @@
+packages:
+  - 'uni-app-base'
+

二進制
static/images/healthCard/cardnewbg.png


二進制
static/images/healthCard/icon2.png


二進制
static/images/healthCard/ksj.png


二進制
static/images/healthCard/logo_.png


二進制
static/images/tabbar/homePage.png


二進制
static/images/tabbar/homePage_ac.png


二進制
static/images/tabbar/message.png


二進制
static/images/tabbar/message_ac.png


二進制
static/images/tabbar/netHosIndex.png


二進制
static/images/tabbar/netHosIndex_ac.png


二進制
static/images/tabbar/personalCenter.png


二進制
static/images/tabbar/personalCenter_ac.png


+ 9 - 0
store/index.ts

@@ -0,0 +1,9 @@
+import { createStore } from 'vuex';
+import global from '@kasite/uni-app-base/store/global';
+
+const store = createStore({
+	...global,
+	modules: {},
+});
+
+export default store;

+ 446 - 0
theme/common/_app-common.scss

@@ -0,0 +1,446 @@
+/* 从小程序 app.wxss 迁移过来的全局公用样式(partial)
+   统一在 theme/index.scss 中通过 @import 'mixins/app-common'; 引入
+   说明:该文件以 "_" 开头表示 Sass partial,不会单独生成 CSS
+*/
+
+page {
+	height: 100vh;
+	/*主色 */
+	--dominantColor: #74b72f;
+	/*辅色 */
+	--auxiliaryColor: #f08400;
+}
+/* 背景色 */
+
+.backgroundCustom,
+.p_bgcolor {
+	background: var(--dominantColor) !important;
+	color: #fff !important;
+}
+.backgroundCustom_F08 {
+	background: var(--auxiliaryColor) !important;
+	color: #fff !important;
+}
+
+.backgroundCustom_D9 {
+	background-color: #d9d9d9 !important;
+	color: #fff !important;
+}
+
+/* 字体颜色 */
+
+.colorCustom,
+.p_color {
+	color: var(--dominantColor) !important;
+}
+.colorCustom_F08 {
+	color: var(--auxiliaryColor) !important;
+}
+.colorCustom_999 {
+	color: #999 !important;
+}
+.colorRed {
+	color: #fa4844 !important;
+}
+/* 边框颜色 */
+.boderColorCustom {
+	border-color: var(--dominantColor) !important;
+}
+.boderColorCustom::before {
+	border-color: var(--dominantColor) !important;
+}
+.boderColorCustom_F08 {
+	border-color: var(--auxiliaryColor) !important;
+}
+.boderColorCustom_F08::before {
+	border-color: var(--auxiliaryColor) !important;
+}
+/* 初始化 */
+
+.container {
+	height: 100%;
+	box-sizing: border-box;
+	background-color: #f1f1f6;
+	color: #000;
+	font-size: 28rpx;
+	font-family: Source Han Sans CN;
+}
+
+.content {
+	display: inline-block;
+	width: 100%;
+	background-color: #f1f1f6;
+}
+
+.content_inner {
+	position: relative;
+	z-index: 1;
+}
+button {
+	width: 100% !important;
+	padding: 0 !important;
+	margin: 0 !important;
+}
+button:after {
+	border: 0;
+	border-radius: 0;
+}
+
+image {
+	display: block;
+	width: 100%;
+	height: 100%;
+}
+
+view,
+image,
+text,
+navigator,
+form {
+	box-sizing: border-box;
+	line-height: 1em;
+}
+
+.fixed {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+}
+
+input {
+	font-family: PingFang-SC-Medium;
+	color: #303133;
+	font-size: 30rpx;
+	min-height: 0;
+	width: 100%;
+}
+
+.placeholder {
+	color: #ccc;
+	font-family: SourceHanSansCN-Regular;
+	font-size: 30rpx;
+}
+
+.flexCenter {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.flexBetween {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+}
+.flexColumnCenter {
+	display: flex;
+	align-items: center;
+	flex-direction: column;
+	justify-content: center;
+}
+
+.public_dialog {
+	background-color: rgba(1, 1, 1, 0.6);
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	z-index: 11;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+/* 弹性布局-横向 */
+.displayFlexRow {
+	display: flex;
+	flex-direction: row;
+	justify-content: center;
+	align-items: center;
+}
+
+/* 弹性布局-纵向 */
+.displayFlexCol {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+}
+
+/* 弹性布局-横向两端对齐 */
+.displayFlexBetween {
+	display: flex;
+	flex-wrap: wrap;
+	justify-content: space-between;
+	align-items: center;
+}
+
+/* 弹性布局-靠左 */
+.displayFlexLeft {
+	display: flex;
+	flex-wrap: wrap;
+	justify-content: left;
+	align-items: center;
+}
+
+/* 提示 */
+.public_tip {
+	padding: 40rpx 30rpx;
+	font-size: 26rpx;
+	font-family: Source Han Sans CN;
+	font-weight: 400;
+	color: rgba(166, 166, 166, 1);
+	line-height: 40rpx;
+}
+/* 滑块 */
+.public_switch {
+	transform: scale(0.7);
+	margin-right: -20rpx;
+}
+
+/* 向右 图片 */
+.public_right_img {
+	width: 12rpx;
+	height: 21rpx;
+	position: absolute;
+	right: 0;
+	top: 0;
+	bottom: 0;
+	margin: auto 0;
+}
+.public_right_img30 {
+	right: 30rpx;
+}
+.arrow {
+	width: 12rpx;
+	height: 21rpx;
+	margin-right: 30rpx;
+}
+
+/* 长按钮 */
+/* 
+<view class="public_btn_con">
+<view class="public_btn backgroundCustom">添加就诊卡</view>
+</view>
+*/
+.public_btn_con {
+	position: fixed;
+	left: 0;
+	bottom: 0;
+	width: 100%;
+	height: 180rpx;
+	padding: 30rpx 30rpx 52rpx;
+	background-color: #f1f1f6;
+}
+
+.public_btn {
+	border-radius: 49rpx;
+	height: 88rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 34rpx;
+	font-weight: 500;
+}
+.bg_white {
+	background: #fff !important;
+}
+
+/* 头部组件固定 */
+.userInfoTopFixe {
+	position: fixed;
+	top: 0;
+	width: 100%;
+	z-index: 3;
+}
+
+/* 边框 */
+.border_top {
+	position: relative;
+}
+.border_top::before {
+	content: ' ';
+	position: absolute;
+	left: 0;
+	top: 0;
+	width: 100%;
+	height: 1px;
+	border-top: 1px solid #eee;
+	-webkit-transform-origin: 0 0;
+	transform-origin: 0 0;
+	-webkit-transform: scaleY(0.5);
+	transform: scaleY(0.5);
+}
+
+.border_bottom {
+	position: relative;
+}
+.border_bottom::before {
+	content: ' ';
+	position: absolute;
+	left: 0;
+	bottom: 0;
+	width: 100%;
+	height: 1px;
+	border-top: 1px solid #eee;
+	-webkit-transform-origin: 0 0;
+	transform-origin: 0 0;
+	-webkit-transform: scaleY(0.5);
+	transform: scaleY(0.5);
+}
+
+.border_left {
+	position: relative;
+}
+.border_left::after {
+	content: ' ';
+	position: absolute;
+	left: 0;
+	bottom: 0;
+	width: 1px;
+	height: 100%;
+	border-left: 1px solid #eee;
+	-webkit-transform-origin: 0 0;
+	transform-origin: 0 0;
+	-webkit-transform: scaleX(0.5);
+	transform: scaleX(0.5);
+}
+
+.border_right {
+	position: relative;
+}
+.border_right::after {
+	content: ' ';
+	position: absolute;
+	right: 0;
+	bottom: 0;
+	width: 1px;
+	height: 100%;
+	border-right: 1px solid #eee;
+	-webkit-transform-origin: 0 0;
+	transform-origin: 0 0;
+	-webkit-transform: scaleX(0.5);
+	transform: scaleX(0.5);
+}
+
+.border {
+	position: relative;
+}
+.border::before {
+	content: '';
+	position: absolute;
+	border: 1px solid #ccc;
+	top: -50%;
+	left: -50%;
+	width: 200%;
+	height: 200%;
+	border-radius: 50px;
+	transform: scale(0.5);
+	box-sizing: border-box;
+}
+
+.border_fillet {
+	position: relative;
+}
+.border_fillet::after {
+	content: '';
+	position: absolute;
+	top: -50%;
+	bottom: -50%;
+	left: -50%;
+	right: -50%;
+	width: 200%;
+	height: 200%;
+	-webkit-transform: scale(0.5);
+	transform: scale(0.5);
+	border: solid 1px var(--dominantColor);
+	border-radius: 600rpx;
+	box-sizing: border-box;
+}
+
+/* 下拉加载更多样式 */
+.public_loadAllTip {
+	text-align: center;
+	padding: 35rpx 0;
+	color: #999;
+}
+
+/* 暂无数据 */
+.noData {
+	width: 100%;
+	padding-top: 0;
+	position: absolute;
+	top: 50%;
+	margin-top: -250rpx;
+}
+
+/* 遮罩 */
+.mask {
+	width: 100%;
+	height: 100%;
+	background: rgba(0, 0, 0, 0.6);
+	position: fixed;
+	top: 0;
+	left: 0;
+	z-index: 3;
+}
+
+/* 互联网专用 */
+/* 弹性布局-横向 */
+.p_flexCenter {
+	display: flex;
+	flex-direction: row;
+	justify-content: center;
+	align-items: center;
+}
+
+/* 弹性布局-纵向 */
+.p_flexCenterCol {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+}
+
+/* 弹性布局-横向两端对齐 */
+.p_flexBetween {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+
+/* 文本溢出省略 */
+.ellipsis {
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+/* 弹性布局-起始对齐 */
+.displayFlexStart {
+	display: flex;
+	flex-direction: row;
+	justify-content: flex-start;
+	align-items: center;
+}
+
+/* 特殊色调 */
+.backgroundCustom_E35779 {
+	background: #e35779 !important;
+	color: #fff !important;
+}
+.p_color_E35779 {
+	color: #e35779 !important;
+}
+
+/* 压缩图片canvas(隐藏到视口外) */
+.press_canvas {
+	position: absolute;
+	top: -1000px;
+	background-color: gray;
+}
+
+uni-video {
+	width: 100% !important;
+}

+ 134 - 0
theme/common/var.scss

@@ -0,0 +1,134 @@
+@use 'sass:map';
+@use 'sass:math';
+@use 'sass:color';
+
+@use '../mixins/var' as *;
+
+$types: primary, success, warning, error, info;
+
+// Color
+$colors: () !default;
+$colors: map.deep-merge(
+	(
+		'white': $uni-white,
+		'black': $uni-black,
+		'primary': (
+			base: $uni-primary,
+		),
+		'success': (
+			base: $uni-success,
+		),
+		'warning': (
+			base: $uni-warning,
+		),
+		'error': (
+			base: $uni-error,
+		),
+		'info': (
+			base: $uni-info,
+		),
+	),
+	$colors
+);
+
+$color-white: map.get($colors, 'white') !default;
+$color-black: map.get($colors, 'black') !default;
+
+// https://sass-lang.com/documentation/values/maps#immutability
+// mix colors with white/black to generate light/dark level
+@mixin set-color-mix-level($type, $number, $mode: 'light', $mix-color: $uni-white) {
+	$colors: map.deep-merge(
+		(
+			#{$type}: (
+					'#{$mode}-#{$number}':
+						color.mix(
+							$mix-color,
+							map.get($colors, $type, 'base'),
+							math.percentage(math.div($number, 10))
+						),
+				)
+		),
+		$colors
+	) !global;
+}
+
+// --uni-primary-light-1
+@each $type in $types {
+	@for $i from 1 through 9 {
+		@include set-color-mix-level($type, $i, 'light', $uni-white);
+	}
+}
+// --uni-primary-dark-2
+@each $type in $types {
+	@include set-color-mix-level($type, 2, 'dark', $uni-black);
+}
+
+// TextColor
+$text-color: () !default;
+$text-color: map.merge(
+	(
+		'primary': $uni-text-color,
+		'regular': $uni-text-color-regular,
+		'secondary': $uni-text-color-grey,
+		'placeholder': $uni-text-color-placeholder,
+		'disabled': $uni-text-color-disable,
+	),
+	$text-color
+);
+
+// FontSize
+$font-size: () !default;
+$font-size: map.merge(
+	(
+		'sm': $uni-font-size-sm,
+		'base': $uni-font-size-base,
+		'lg': $uni-font-size-lg,
+	),
+	$font-size
+);
+
+// Padding / Margin
+$padding-maring-direction: (
+	'': '',
+	't': 'top',
+	'r': 'right',
+	'b': 'bottom',
+	'l': 'left',
+	'tb': (
+		't': 'top',
+		'b': 'bottom',
+	),
+	'lr': (
+		'l': 'left',
+		'r': 'right',
+	),
+);
+@mixin each-padding-margin-direction($abbr, $type, $dAbbr, $direction) {
+	@each $number in $uni-padding-margin-size {
+		@if ($number == 0) {
+		} @else {
+			.#{$abbr}#{$dAbbr}-#{$number} {
+				@if (type-of($direction) == map) {
+					@each $v, $d in $direction {
+						#{$type}-#{$d}: #{$number}rpx;
+					}
+				} @else if($direction== '') {
+					#{$type}: #{$number}rpx;
+				} @else {
+					#{$type}-#{$direction}: #{$number}rpx;
+				} 
+			}
+		}
+	}
+}
+
+@mixin set-padding-margin-size-class() {
+	@each $abbr, $type in ('m': 'margin', 'p': 'padding') {
+		.no-#{$type} {
+			#{$type}: 0 !important;
+		}
+		@each $dAbbr, $d in $padding-maring-direction {
+			@include each-padding-margin-direction($abbr, $type, $dAbbr, $d);
+		}
+	}
+}

+ 51 - 0
theme/index.scss

@@ -0,0 +1,51 @@
+@import 'common/var';
+@import 'mixins/var';
+@import 'common/app-common';
+
+view,
+image,
+text,
+navigator,
+form {
+	box-sizing: border-box;
+}
+
+:root,
+page {
+	@include set-css-var-value('color-white', $color-white);
+	@include set-css-var-value('color-black', $color-black);
+
+	// Typography
+	@include set-component-css-var('font-size', $font-size);
+}
+
+:root,
+page {
+	height: 100vh;
+	color-scheme: light;
+
+	// --uni-color-#{$type}
+	// --uni-color-#{$type}-light-{$i}
+	@each $type in $types {
+		@include set-css-color-type($colors, $type);
+	}
+	// --uni-text-color-#{$type}
+	@include set-component-css-var('text-color', $text-color);
+}
+
+// .uni-primary / .uni-bg-primary
+@each $type in $types {
+	@include set-css-color-class($colors, $type);
+}
+
+// .p-10 .m-10
+@include set-padding-margin-size-class();
+
+.container {
+	height: 100%;
+	box-sizing: border-box;
+	background-color: #f1f1f6;
+	color: #000;
+	font-size: 28rpx;
+	font-family: Source Han Sans CN;
+}

+ 53 - 0
theme/mixins/_var.scss

@@ -0,0 +1,53 @@
+@use 'sass:map';
+@use 'sass:color';
+
+@use 'config' as *;
+@use 'function' as *;
+
+// set css var value, because we need translate value to string
+// for example:
+// @include set-css-var-value(('color', 'primary'), red);
+// --uni-color-primary: red;
+@mixin set-css-var-value($name, $value) {
+	#{joinVarName($name)}: #{$value};
+}
+
+// @include set-css-var-type('color', 'primary', $map);
+// --uni-color-primary: #{map.get($map, 'primary')};
+@mixin set-css-var-type($name, $type, $variables) {
+	#{getCssVarName($name, $type)}: #{map.get($variables, $type)};
+}
+
+@mixin set-css-color-type($colors, $type) {
+	@include set-css-var-value(('color', $type), map.get($colors, $type, 'base'));
+
+	@each $i in (3, 5, 7, 8, 9) {
+		@include set-css-var-value(
+			('color', $type, 'light', $i),
+			map.get($colors, $type, 'light-#{$i}')
+		);
+	}
+
+	@include set-css-var-value(('color', $type, 'dark-2'), map.get($colors, $type, 'dark-2'));
+}
+
+// set all css var for component by map
+@mixin set-component-css-var($name, $variables) {
+	@each $attribute, $value in $variables {
+		@if $attribute == 'default' {
+			#{getCssVarName($name)}: #{$value};
+		} @else {
+			#{getCssVarName($name, $attribute)}: #{$value};
+		}
+	}
+}
+
+// .uni-primary / .uni-bg-primary
+@mixin set-css-color-class($colors, $type) {
+	.#{$namespace}-#{$type} {
+		color: map.get($colors, $type, 'base');
+	}
+	.#{$namespace}-bg-#{$type} {
+		color: map.get($colors, $type, 'base');
+	}
+}

+ 1 - 0
theme/mixins/config.scss

@@ -0,0 +1 @@
+$namespace: 'uni';

+ 18 - 0
theme/mixins/function.scss

@@ -0,0 +1,18 @@
+@use 'config';
+
+// join var name
+// joinVarName(('button', 'text-color')) => '--uni-button-text-color'
+@function joinVarName($list) {
+	$name: '--' + config.$namespace;
+	@each $item in $list {
+		@if $item != '' {
+			$name: $name + '-' + $item;
+		}
+	}
+	@return $name;
+}
+
+// getCssVarName('button', 'text-color') => '--uni-button-text-color'
+@function getCssVarName($args...) {
+	@return joinVarName($args);
+}

+ 13 - 0
uni.promisify.adaptor.js

@@ -0,0 +1,13 @@
+uni.addInterceptor({
+  returnValue (res) {
+    if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
+      return res;
+    }
+    return new Promise((resolve, reject) => {
+      res.then((res) => {
+        if (!res) return resolve(res) 
+        return res[0] ? reject(res[0]) : resolve(res[1])
+      });
+    });
+  },
+});

+ 99 - 0
uni.scss

@@ -0,0 +1,99 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+// 主色
+$uni-primary: #409eff;
+// 辅助色
+$uni-success: #4cd964;
+// 警告色
+$uni-warning: #f0ad4e;
+// 错误色
+$uni-error: #dd524d;
+// 描述色
+$uni-info: #909399;
+$uni-white: #fff;
+$uni-black: #000;
+
+/* 行为相关颜色 */
+$uni-color-primary: #2979ff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color: #333; //基本色
+$uni-text-color-regular: #666; //普通色
+$uni-text-color-inverse: #fff; //反色
+$uni-text-color-grey: #999; //辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable: #c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color: #ffffff;
+$uni-bg-color-grey: #f8f8f8;
+$uni-bg-color-hover: #f1f1f1; //点击状态颜色
+$uni-bg-color-mask: rgba(0, 0, 0, 0.4); //遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color: #c8c7cc;
+
+/* padding / margin */
+@function make-range($from, $to) {
+  $list: ();
+  @for $i from $from through $to {
+    $list: append($list, $i);
+  }
+  @return $list;
+}
+$uni-padding-margin-size: make-range(0, 100);
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm: 12px;
+$uni-font-size-base: 14px;
+$uni-font-size-lg: 16px;
+
+/* 图片尺寸 */
+$uni-img-size-sm: 20px;
+$uni-img-size-base: 26px;
+$uni-img-size-lg: 40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2c405a; // 文章标题颜色
+$uni-font-size-title: 20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle: 26px;
+$uni-color-paragraph: #3f536e; // 文章段落颜色
+$uni-font-size-paragraph: 15px;

+ 192 - 0
uni_modules/mp-html/README.md

@@ -0,0 +1,192 @@
+## 为减小组件包的大小,默认组件包中不包含编辑、latex 公式等扩展功能,需要使用扩展功能的请参考下方的 插件扩展 栏的说明
+
+## 功能介绍
+- 全端支持(含 `v3、NVUE`)
+- 支持丰富的标签(包括 `table`、`video`、`svg` 等)
+- 支持丰富的事件效果(自动预览图片、链接处理等)
+- 支持设置占位图(加载中、出错时、预览时)
+- 支持锚点跳转、长按复制等丰富功能
+- 支持大部分 *html* 实体
+- 丰富的插件(关键词搜索、内容编辑、`latex` 公式等)
+- 效率高、容错性强且轻量化
+
+查看 [功能介绍](https://jin-yufeng.github.io/mp-html/#/overview/feature) 了解更多
+
+## 使用方法
+- `uni_modules` 方式  
+  1. 点击右上角的 `使用 HBuilder X 导入插件` 按钮直接导入项目或点击 `下载插件 ZIP` 按钮下载插件包并解压到项目的 `uni_modules/mp-html` 目录下  
+  2. 在需要使用页面的 `(n)vue` 文件中添加  
+     ```html
+     <!-- 不需要引入,可直接使用 -->
+     <mp-html :content="html" />
+     ```
+     ```javascript
+     export default {
+       data() {
+         return {
+           html: '<div>Hello World!</div>'
+         }
+       }
+     }
+     ```
+  3. 需要更新版本时在 `HBuilder X` 中右键 `uni_modules/mp-html` 目录选择 `从插件市场更新` 即可  
+
+- 源码方式  
+  1. 从 [github](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 或 [gitee](https://gitee.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 下载源码  
+     插件市场的 **非 uni_modules 版本** 无法更新,不建议从插件市场获取  
+  2. 在需要使用页面的 `(n)vue` 文件中添加  
+     ```html
+     <mp-html :content="html" />
+     ```
+     ```javascript
+     import mpHtml from '@/components/mp-html/mp-html'
+     export default {
+       // HBuilderX 2.5.5+ 可以通过 easycom 自动引入
+       components: {
+         mpHtml
+       },
+       data() {
+         return {
+           html: '<div>Hello World!</div>'
+         }
+       }
+     }
+     ```
+
+- npm 方式  
+  1. 在项目根目录下执行  
+     ```bash
+     npm install mp-html
+     ```
+  2. 在需要使用页面的 `(n)vue` 文件中添加  
+     ```html
+     <mp-html :content="html" />
+     ```
+     ```javascript
+     import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html'
+     export default {
+       // 不可省略
+       components: {
+         mpHtml
+       },
+       data() {
+         return {
+           html: '<div>Hello World!</div>'
+         }
+       }
+     }
+     ```
+  3. 需要更新版本时执行以下命令即可  
+     ```bash
+     npm update mp-html
+     ```
+  
+  使用 *cli* 方式运行的项目,通过 *npm* 方式引入时,需要在 *vue.config.js* 中配置 *transpileDependencies*,详情可见 [#330](https://github.com/jin-yufeng/mp-html/issues/330#issuecomment-913617687)  
+  如果在 **nvue** 中使用还要将 `dist/uni-app/static` 目录下的内容拷贝到项目的 `static` 目录下,否则无法运行  
+
+查看 [快速开始](https://jin-yufeng.github.io/mp-html/#/overview/quickstart) 了解更多
+
+## 组件属性
+
+| 属性 | 类型 | 默认值 | 说明 |
+|:---:|:---:|:---:|---|
+| container-style | String |  | 容器的样式([2.1.0+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v210)) |
+| content | String |  | 用于渲染的 html 字符串 |
+| copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 |
+| domain | String |  | 主域名(用于链接拼接) |
+| error-img | String |  | 图片出错时的占位图链接 |
+| lazy-load | Boolean | false | 是否开启图片懒加载 |
+| loading-img | String |  | 图片加载过程中的占位图链接 |
+| pause-video | Boolean | true | 是否在播放一个视频时自动暂停其他视频 |
+| preview-img | Boolean | true | 是否允许图片被点击时自动预览 |
+| scroll-table | Boolean | false | 是否给每个表格添加一个滚动层使其能单独横向滚动 |
+| selectable | Boolean | false | 是否开启文本长按复制 |
+| set-title | Boolean | true | 是否将 title 标签的内容设置到页面标题 |
+| show-img-menu | Boolean | true | 是否允许图片被长按时显示菜单 |
+| tag-style | Object |  | 设置标签的默认样式 |
+| use-anchor | Boolean | false | 是否使用锚点链接 |
+
+查看 [属性](https://jin-yufeng.github.io/mp-html/#/basic/prop) 了解更多
+
+## 组件事件
+
+| 名称 | 触发时机 |
+|:---:|---|
+| load | dom 树加载完毕时 |
+| ready | 图片加载完毕时 |
+| error | 发生渲染错误时 |
+| imgtap | 图片被点击时 |
+| linktap | 链接被点击时 |
+| play | 音视频播放时 |
+
+查看 [事件](https://jin-yufeng.github.io/mp-html/#/basic/event) 了解更多
+
+## api
+组件实例上提供了一些 `api` 方法可供调用
+
+| 名称 | 作用 |
+|:---:|---|
+| in | 将锚点跳转的范围限定在一个 scroll-view 内 |
+| navigateTo | 锚点跳转 |
+| getText | 获取文本内容 |
+| getRect | 获取富文本内容的位置和大小 |
+| setContent | 设置富文本内容 |
+| imgList | 获取所有图片的数组 |
+| pauseMedia | 暂停播放音视频([2.2.2+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v222)) |
+| setPlaybackRate | 设置音视频播放速率([2.4.0+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v240)) |
+
+查看 [api](https://jin-yufeng.github.io/mp-html/#/advanced/api) 了解更多
+
+## 插件扩展  
+除基本功能外,本组件还提供了丰富的扩展,可按照需要选用
+
+| 名称 | 作用 |
+|:---:|---|
+| audio | 音乐播放器 |
+| editable | 富文本 **编辑**([示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip)) |
+| emoji | 解析 emoji |
+| highlight | 代码块高亮显示 |
+| markdown | 渲染 markdown |
+| search | 关键词搜索 |
+| style | 匹配 style 标签中的样式 |
+| txv-video | 使用腾讯视频 |
+| img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) |
+| latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) |
+
+从插件市场导入的包中 **不含有** 扩展插件,使用插件需通过微信小程序 `富文本插件` 获取或参考以下方法进行打包:  
+1. 获取完整组件包  
+   ```bash
+   npm install mp-html
+   ```
+2. 编辑 `tools/config.js` 中的 `plugins` 项,选择需要的插件  
+3. 生成新的组件包  
+   在 `node_modules/mp-html` 目录下执行  
+   ```bash
+   npm install
+   npm run build:uni-app
+   ```
+4. 拷贝 `dist/uni-app` 中的内容到项目根目录  
+
+查看 [插件](https://jin-yufeng.github.io/mp-html/#/advanced/plugin) 了解更多
+
+## 关于 nvue
+`nvue` 使用原生渲染,不支持部分 `css` 样式,为实现和 `html` 相同的效果,组件内部通过 `web-view` 进行渲染,性能上差于原生,根据 `weex` 官方建议,`web` 标签仅应用在非常规的降级场景。因此,如果通过原生的方式(如 `richtext`)能够满足需要,则不建议使用本组件,如果有较多的富文本内容,则可以直接使用 `vue` 页面  
+由于渲染方式与其他端不同,有以下限制:  
+1. 不支持 `lazy-load` 属性
+2. 视频不支持全屏播放
+3. 如果在 `flex-direction: row` 的容器中使用,需要给组件设置宽度或设置 `flex: 1` 占满剩余宽度
+
+纯 `nvue` 模式下,[此问题](https://ask.dcloud.net.cn/question/119678) 修复前,不支持通过 `uni_modules` 引入,需要本地引入(将 [dist/uni-app](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 中的内容拷贝到项目根目录下)  
+
+
+## 问题反馈
+遇到问题时,请先查阅 [常见问题](https://jin-yufeng.github.io/mp-html/#/question/faq) 和 [issue](https://github.com/jin-yufeng/mp-html/issues) 中是否已有相同的问题  
+可通过 [issue](https://github.com/jin-yufeng/mp-html/issues/new/choose) 、插件问答或发送邮件到 [mp_html@126.com](mailto:mp_html@126.com) 提问,不建议在评论区提问(不方便回复)  
+提问请严格按照 [issue 模板](https://github.com/jin-yufeng/mp-html/issues/new/choose) ,描述清楚使用环境、`html` 内容或可复现的 `demo` 项目以及复现方式,对于 **描述不清**、**无法复现** 或重复的问题将不予回复  
+
+欢迎加入 `QQ` 交流群:  
+群1(已满):`699734691`  
+群2(已满):`778239129`  
+群3:`960265313`  
+
+查看 [问题反馈](https://jin-yufeng.github.io/mp-html/#/question/feedback) 了解更多

+ 156 - 0
uni_modules/mp-html/changelog.md

@@ -0,0 +1,156 @@
+## v2.5.1(2025-04-20)
+1. `U` 适配鸿蒙 `APP` [详细](https://github.com/jin-yufeng/mp-html/issues/615)
+2. `U` 微信小程序替换废弃 `api` `getSystemInfoSync` [详细](https://github.com/jin-yufeng/mp-html/issues/613)
+3. `F` 修复了 `app` 端播放视频可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/617)
+4. `F` 修复了 `latex` 插件可能出现 `xxx can be used only in display mode` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/632)
+5. `F` 修复了 `uni-app` 包 `latex` 公式可能不显示的问题  [#599](https://github.com/jin-yufeng/mp-html/issues/599)、[#627](https://github.com/jin-yufeng/mp-html/issues/627)
+## v2.5.0(2024-04-22)
+1. `U` `play` 事件增加返回 `src` 等信息 [详细](https://github.com/jin-yufeng/mp-html/issues/526)
+2. `U` `preview-img` 属性支持设置为 `all` 开启 `base64` 图片预览 [详细](https://github.com/jin-yufeng/mp-html/issues/536)
+3. `U` `editable` 插件增加简易模式(点击文字直接编辑)
+4. `U` `latex` 插件支持块级公式 [详细](https://github.com/jin-yufeng/mp-html/issues/582)
+5. `F` 修复了表格部分情况下背景丢失的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/587)
+6. `F` 修复了部分 `svg` 无法显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/591)
+7. `F` 修复了 `h5` 和 `app` 端部分情况下样式无法识别的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/518)
+8. `F` 修复了 `latex` 插件部分情况下显示不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/580)
+9. `F` 修复了 `editable` 插件表格无法删除的问题
+10. `F` 修复了 `editable` 插件 `vue3` `h5` 端点击图片报错的问题
+11. `F` 修复了 `editable` 插件点击表格没有菜单栏的问题
+## v2.4.3(2024-01-21)
+1. `A` 增加 [card](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#card) 插件 [详细](https://github.com/jin-yufeng/mp-html/pull/533) by [@whoooami](https://github.com/whoooami)
+2. `F` 修复了 `svg` 中包含 `foreignobject` 可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/523)
+3. `F` 修复了合并单元格的表格部分情况下显示不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/561)
+4. `F` 修复了 `img` 标签设置 `object-fit` 无效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/567)
+5. `F` 修复了 `latex` 插件公式会换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/540) 
+6. `F` 修复了 `editable` 和 `audio` 插件共用时点击 `audio` 无法编辑的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/529) by [@whoooami](https://github.com/whoooami)
+7. `F` 修复了微信小程序部分情况下图片会报错 `replace of undefined` 的问题
+8. `F` 修复了快手小程序图片不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/571)
+## v2.4.2(2023-05-14)
+1. `A` `editable` 插件支持修改文字颜色 [详细](https://github.com/jin-yufeng/mp-html/issues/254)
+2. `F` 修复了 `svg` 中有 `style` 不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/505)
+3. `F` 修复了使用旧版编译器可能报错 `Bad attr nodes` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/472)
+4. `F` 修复了 `app` 端可能出现无法读取 `lazyLoad` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/513)
+5. `F` 修复了 `editable` 插件在点击换图时未拼接 `domain` 的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/497) by [@TwoKe945](https://github.com/TwoKe945)
+6. `F` 修复了 `latex` 插件部分情况下不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/515) 
+7. `F` 修复了 `editable` 插件点击音视频时其他标签框不消失的问题
+## v2.4.1(2022-12-25)
+1. `F` 修复了没有图片时 `ready` 事件可能不触发的问题
+2. `F` 修复了加载过程中可能出现 `Root label not found` 错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/470)
+3. `F` 修复了 `audio` 插件退出页面可能会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/457)
+4. `F` 修复了 `vue3` 运行到 `app` 在 `HBuilder X 3.6.10` 以上报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/480)
+5. `F` 修复了 `nvue` 端链接中包含 `%22` 时可能无法显示的问题
+6. `F` 修复了 `vue3` 使用 `highlight` 插件可能报错的问题
+## v2.4.0(2022-08-27)
+1. `A` 增加了 [setPlaybackRate](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#setPlaybackRate) 的 `api`,可以设置音视频的播放速率 [详细](https://github.com/jin-yufeng/mp-html/issues/452)
+2. `A` 示例小程序代码开源 [详细](https://github.com/jin-yufeng/mp-html-demo)
+3. `U` 优化 `ready` 事件触发时机,未设置懒加载的情况下基本可以准确触发 [详细](https://github.com/jin-yufeng/mp-html/issues/195)
+4. `U` `highlight` 插件在编辑状态下不进行高亮处理,便于编辑
+5. `F` 修复了 `flex` 布局下图片大小可能不正确的问题
+6. `F` 修复了 `selectable` 属性没有设置 `force` 也可能出现渲染异常的问题
+7. `F` 修复了表格中的图片大小可能不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/448)
+8. `F` 修复了含有合并单元格的表格可能无法设置竖直对齐的问题
+9. `F` 修复了 `editable` 插件在 `scroll-view` 中使用时工具条位置可能不正确的问题
+10. `F` 修复了 `vue3` 使用 [search](advanced/plugin#search) 插件可能导致错误换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/449)
+## v2.3.2(2022-08-13)
+1. `A` 增加 [latex](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#latex) 插件,可以渲染数学公式 [详细](https://github.com/jin-yufeng/mp-html/pull/447) by [@Zeng-J](https://github.com/Zeng-J)
+2. `U` 优化根节点下有很多标签的长内容渲染速度
+3. `U` `highlight` 插件适配 `lang-xxx` 格式
+4. `F` 修复了 `table` 标签设置 `border` 属性后可能无法修改边框样式的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/439) by [@zouxingjie](https://github.com/zouxingjie)
+5. `F` 修复了 `editable` 插件输入连续空格无效的问题
+6. `F` 修复了 `vue3` 图片设置 `inline` 会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/438)
+7. `F` 修复了 `vue3` 使用 `table` 可能报错的问题
+## v2.3.1(2022-05-20)
+1. `U` `app` 端支持使用本地图片
+2. `U` 优化了微信小程序 `selectable` 属性在 `ios` 端的处理 [详细](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable)
+3. `F` 修复了 `editable` 插件不在顶部时 `tooltip` 位置可能错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/430)
+4. `F` 修复了 `vue3` 运行到微信小程序可能报错丢失内容的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/414)
+5. `F` 修复了 `vue3` 部分标签可能被错误换行的问题
+6. `F` 修复了 `editable` 插件 `app` 端插入视频无法预览的问题
+## v2.3.0(2022-04-01)
+1. `A` 增加了 `play` 事件,音视频播放时触发,可用于与页面其他音视频进行互斥播放 [详细](basic/event#play)
+2. `U` `show-img-menu` 属性支持控制预览时是否长按弹出菜单
+3. `U` 优化 `wxs` 处理,提高渲染性能 [详细](https://developers.weixin.qq.com/community/develop/article/doc/0006cc2b204740f601bd43fa25a413)  
+4. `U` `video` 标签支持 `object-fit` 属性
+5. `U` 增加支持一些常用实体编码 [详细](https://github.com/jin-yufeng/mp-html/issues/418)
+6. `F` 修复了图片仅设置高度可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/410)
+7. `F` 修复了 `video` 标签高度设置为 `auto` 不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/411)
+8. `F` 修复了使用 `grid` 布局时可能样式错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/413)
+9. `F` 修复了含有合并单元格的表格部分情况下显示异常的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/417)
+10. `F` 修复了 `editable` 插件连续插入内容时顺序不正确的问题
+11. `F` 修复了 `uni-app` 包 `vue3` 使用 `audio` 插件报错的问题
+12. `F` 修复了 `uni-app` 包 `highlight` 插件使用自定义的 `prism.min.js` 报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/416)
+## v2.2.2(2022-02-26)
+1. `A` 增加了 [pauseMedia](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#pauseMedia) 的 `api`,可用于暂停播放音视频 [详细](https://github.com/jin-yufeng/mp-html/issues/317)
+2. `U` 优化了长内容的加载速度  
+3. `U` 适配 `vue3` [#389](https://github.com/jin-yufeng/mp-html/issues/389)、[#398](https://github.com/jin-yufeng/mp-html/pull/398) by [@zhouhuafei](https://github.com/zhouhuafei)、[#400](https://github.com/jin-yufeng/mp-html/issues/400)
+4. `F` 修复了小程序端图片高度设置为百分比时可能不显示的问题
+5. `F` 修复了 `highlight` 插件部分情况下可能显示不完整的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/403)
+## v2.2.1(2021-12-24)
+1. `A` `editable` 插件增加上下移动标签功能
+2. `U` `editable` 插件支持在文本中间光标处插入内容
+3. `F` 修复了 `nvue` 端设置 `margin` 后可能导致高度不正确的问题
+4. `F` 修复了 `highlight` 插件使用压缩版的 `prism.css` 可能导致背景失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/367)
+5. `F` 修复了编辑状态下使用 `emoji` 插件内容为空时可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/371)
+6. `F` 修复了使用 `editable` 插件后将 `selectable` 属性设置为 `force` 不生效的问题
+## v2.2.0(2021-10-12)
+1. `A` 增加 `customElements` 配置项,便于添加自定义功能性标签 [详细](https://github.com/jin-yufeng/mp-html/issues/350)
+2. `A` `editable` 插件增加切换音视频自动播放状态的功能 [详细](https://github.com/jin-yufeng/mp-html/pull/341) by [@leeseett](https://github.com/leeseett)
+3. `A` `editable` 插件删除媒体标签时触发 `remove` 事件,便于删除已上传的文件
+4. `U` `editable` 插件 `insertImg` 方法支持同时插入多张图片 [详细](https://github.com/jin-yufeng/mp-html/issues/342)
+5. `U` `editable` 插入图片和音视频时支持拼接 `domian` 主域名
+6. `F` 修复了内部链接参数中包含 `://` 时被认为是外部链接的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/356)
+7. `F` 修复了部分 `svg` 标签名或属性名大小写不正确时不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/351)
+8. `F` 修复了 `nvue` 页面运行到非 `app` 平台时可能样式错误的问题
+## v2.1.5(2021-08-13)
+1. `A` 增加支持标签的 `dir` 属性
+2. `F` 修复了 `ruby` 标签文字与拼音没有居中对齐的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/325)
+3. `F` 修复了音视频标签内有 `a` 标签时可能无法播放的问题
+4. `F` 修复了 `externStyle` 中的 `class` 名包含下划线或数字时可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
+5. `F` 修复了 `h5` 端引入 `externStyle` 可能不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
+## v2.1.4(2021-07-14)
+1. `F` 修复了 `rt` 标签无法设置样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/318)
+2. `F` 修复了表格中有单元格同时合并行和列时可能显示不正确的问题
+3. `F` 修复了 `app` 端无法关闭图片长按菜单的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/322)
+4. `F` 修复了 `editable` 插件只能添加图片链接不能修改的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/312) by [@leeseett](https://github.com/leeseett)
+## v2.1.3(2021-06-12)
+1. `A` `editable` 插件增加 `insertTable` 方法
+2. `U` `editable` 插件支持编辑表格中的空白单元格 [详细](https://github.com/jin-yufeng/mp-html/issues/310)
+3. `F` 修复了 `externStyle` 中使用伪类可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/298)
+4. `F` 修复了多个组件同时使用时 `tag-style` 属性时可能互相影响的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/305) by [@woodguoyu](https://github.com/woodguoyu)
+5. `F` 修复了包含 `linearGradient` 的 `svg` 可能无法显示的问题
+6. `F` 修复了编译到头条小程序时可能报错的问题
+7. `F` 修复了 `nvue` 端不触发 `click` 事件的问题
+8. `F` 修复了 `editable` 插件尾部插入时无法撤销的问题
+9. `F` 修复了 `editable` 插件的 `insertHtml` 方法只能在末尾插入的问题
+10. `F` 修复了 `editable` 插件插入音频不显示的问题
+## v2.1.2(2021-04-24)
+1. `A` 增加了 [img-cache](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#img-cache) 插件,可以在 `app` 端缓存图片 [详细](https://github.com/jin-yufeng/mp-html/issues/292) by [@PentaTea](https://github.com/PentaTea)
+2. `U` 支持通过 `container-style` 属性设置 `white-space` 来保留连续空格和换行符 [详细](https://jin-yufeng.gitee.io/mp-html/#/question/faq#space)
+3. `U` 代码风格符合 [standard](https://standardjs.com) 标准
+4. `U` `editable` 插件编辑状态下支持预览视频 [详细](https://github.com/jin-yufeng/mp-html/issues/286)
+5. `F` 修复了 `svg` 标签内嵌 `svg` 时无法显示的问题
+6. `F` 修复了编译到支付宝和头条小程序时部分区域不可复制的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/291)
+## v2.1.1(2021-04-09)
+1. 修复了对 `p` 标签设置 `tag-style` 可能不生效的问题
+2. 修复了 `svg` 标签中的文本无法显示的问题
+3. 修复了使用 `editable` 插件编辑表格时可能报错的问题
+4. 修复了使用 `highlight` 插件运行到头条小程序时可能没有样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/280)
+5. 修复了使用 `editable` 插件 `editable` 属性为 `false` 时会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/284)
+6. 修复了 `style` 插件连续子选择器失效的问题
+7. 修复了 `editable` 插件无法修改图片和字体大小的问题
+## v2.1.0.2(2021-03-21)
+修复了 `nvue` 端使用可能报错的问题
+## v2.1.0(2021-03-20)
+1. `A` 增加了 [container-style](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#container-style) 属性 [详细](https://gitee.com/jin-yufeng/mp-html/pulls/1)
+2. `A` 增加支持 `strike` 标签
+3. `A` `editable` 插件增加 `placeholder` 属性 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
+4. `A` `editable` 插件增加 `insertHtml` 方法 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
+5. `U` 外部样式支持标签名选择器 [详细](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart#setting)
+6. `F` 修复了 `nvue` 端部分情况下可能不显示的问题
+## v2.0.5(2021-03-12)
+1. `U` [linktap](https://jin-yufeng.gitee.io/mp-html/#/basic/event#linktap) 事件增加返回内部文本内容 `innerText` [详细](https://github.com/jin-yufeng/mp-html/issues/271)
+2. `U` [selectable](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable) 属性设置为 `force` 时能够在微信 `iOS` 端生效(文本块会变成 `inline-block`) [详细](https://github.com/jin-yufeng/mp-html/issues/267)
+3. `F` 修复了部分情况下竖向无法滚动的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/182)
+4. `F` 修复了多次修改富文本数据时部分内容可能不显示的问题
+5. `F` 修复了 [腾讯视频](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#txv-video) 插件可能无法播放的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/265)
+6. `F` 修复了 [highlight](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#highlight) 插件没有设置高亮语言时没有应用默认样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/276) by [@fuzui](https://github.com/fuzui)

+ 105 - 0
uni_modules/mp-html/components/mp-html/aliyun-video/aliyun-video.vue

@@ -0,0 +1,105 @@
+<template>
+  <view :class="loadingClass" v-if="!url">{{ loadingText }}</view>
+  <block v-else>
+    <!-- #ifdef MP-WEIXIN -->
+    <video :src="url"></video>
+    <!-- #endif -->
+
+    <!-- #ifdef H5 || WEB || MP-GONGZHONGHAO -->
+    <div id="mui-player"></div>
+    <!-- #endif -->
+  </block>
+</template>
+
+<script>
+import { nextTick } from "vue";
+import { handle, request } from "@kasite/uni-app-base";
+import { REQUEST_CONFIG } from "@kasite/uni-app-base/config";
+import "mui-player/dist/mui-player.min.css";
+import MuiPlayer from "mui-player";
+import Hls from "hls.js";
+
+export default {
+  props: {
+    // 视频ID
+    videoId: {
+      type: [String, null],
+      default: null,
+    },
+  },
+  data() {
+    return {
+      url: "",
+      loadingText: "视频解码中",
+      loadingClass: "aliyun-video-loading-text",
+    };
+  },
+  methods: {
+    async doPost(url, data, options) {
+      let resp = handle.promistHandleNew(await request.doPost(url, data, options));
+      return handle.catchPromiseNew(resp, () => resp, options);
+    },
+    async getVideoUrlByIdasync(id) {
+      if (!id) return;
+      const { resp } = await this.doPost(`${REQUEST_CONFIG.BASE_URL}wsgw/article/video/GetVideoUrl/callApiJSON.do`, { ID: id });
+      const url = resp && resp.length ? resp[0].Url : "";
+      if (!url) {
+        this.loadingClass = "aliyun-video-loading-fail-text";
+        this.loadingText = "视频未完成解码,请刷新页面或稍后再试";
+      } else {
+        this.url = url;
+        //#ifdef H5 || WEB || MP-GONGZHONGHAO
+        nextTick(() => this.mpPlayerInit());
+        //#endif
+      }
+    },
+    mpPlayerInit() {
+      const dom = this.$el;
+      var mp = new MuiPlayer({
+        container: dom,
+        src: this.url,
+        parse: {
+          type: "hls",
+          loader: Hls,
+          config: {
+            // hls config to:https://github.com/video-dev/hls.js/blob/HEAD/docs/API.md#fine-tuning
+            debug: false,
+          },
+        },
+        // pageHead: false,
+        height: "220px",
+      });
+    },
+  },
+  mounted() {
+    this.getVideoUrlByIdasync(this.videoId);
+  },
+};
+</script>
+
+<style>
+.aliyun-video-loading-fail-text {
+  color: red;
+}
+.aliyun-video-loading-text::after {
+  animation: aliyun-video-loading-dots 2s linear infinite;
+  content: "";
+}
+@keyframes aliyun-video-loading-dots {
+  0% {
+    content: ".";
+  }
+
+  30% {
+    content: "..";
+  }
+
+  60% {
+    content: "...";
+  }
+
+  90% {
+    content: "";
+  }
+}
+</style>

+ 7 - 0
uni_modules/mp-html/components/mp-html/aliyun-video/index.js

@@ -0,0 +1,7 @@
+/**
+ * @fileoverview AliyunVideo 插件
+ */
+function AliyunVideo (vm) {
+}
+
+export default AliyunVideo

+ 504 - 0
uni_modules/mp-html/components/mp-html/mp-html.vue

@@ -0,0 +1,504 @@
+<template>
+  <view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle">
+    <slot v-if="!nodes[0]" />
+    <!-- #ifndef APP-PLUS-NVUE -->
+    <node v-else :token="token" :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" />
+    <!-- #endif -->
+    <!-- #ifdef APP-PLUS-NVUE -->
+    <web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
+    <!-- #endif -->
+  </view>
+</template>
+
+<script>
+/**
+ * mp-html v2.5.1
+ * @description 富文本组件
+ * @tutorial https://github.com/jin-yufeng/mp-html
+ * @property {String} container-style 容器的样式
+ * @property {String} content 用于渲染的 html 字符串
+ * @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
+ * @property {String} domain 主域名,用于拼接链接
+ * @property {String} error-img 图片出错时的占位图链接
+ * @property {Boolean} lazy-load 是否开启图片懒加载
+ * @property {string} loading-img 图片加载过程中的占位图链接
+ * @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
+ * @property {Boolean} preview-img 是否允许图片被点击时自动预览
+ * @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
+ * @property {Boolean | String} selectable 是否开启长按复制
+ * @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
+ * @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
+ * @property {Object} tag-style 标签的默认样式
+ * @property {Boolean | Number} use-anchor 是否使用锚点链接
+ * @event {Function} load dom 结构加载完毕时触发
+ * @event {Function} ready 所有图片加载完毕时触发
+ * @event {Function} imgtap 图片被点击时触发
+ * @event {Function} linktap 链接被点击时触发
+ * @event {Function} play 音视频播放时触发
+ * @event {Function} error 媒体加载出错时触发
+ */
+// #ifndef APP-PLUS-NVUE
+import node from './node/node'
+// #endif
+import Parser from './parser'
+import aliyunVideo from './aliyun-video/index.js'
+import wxChannelVideo from './wx-channel-video/index.js'
+const plugins=[aliyunVideo,wxChannelVideo,]
+// #ifdef APP-PLUS-NVUE
+const dom = weex.requireModule('dom')
+// #endif
+export default {
+  name: 'mp-html',
+  data () {
+    return {
+      nodes: [],
+      // #ifdef APP-PLUS-NVUE
+      height: 3
+      // #endif
+    }
+  },
+  props: {
+    containerStyle: {
+      type: String,
+      default: ''
+    },
+    content: {
+      type: String,
+      default: ''
+    },
+    copyLink: {
+      type: [Boolean, String],
+      default: true
+    },
+    domain: String,
+    errorImg: {
+      type: String,
+      default: ''
+    },
+    lazyLoad: {
+      type: [Boolean, String],
+      default: false
+    },
+    loadingImg: {
+      type: String,
+      default: ''
+    },
+    pauseVideo: {
+      type: [Boolean, String],
+      default: true
+    },
+    previewImg: {
+      type: [Boolean, String],
+      default: true
+    },
+    scrollTable: [Boolean, String],
+    selectable: [Boolean, String],
+    setTitle: {
+      type: [Boolean, String],
+      default: true
+    },
+    showImgMenu: {
+      type: [Boolean, String],
+      default: true
+    },
+    tagStyle: Object,
+    useAnchor: [Boolean, Number],
+    token: {
+      type: String,
+      default: ''
+    },
+  },
+  // #ifdef VUE3
+  emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
+  // #endif
+  // #ifndef APP-PLUS-NVUE
+  components: {
+    node
+  },
+  // #endif
+  watch: {
+    content (content) {
+      this.setContent(content)
+    }
+  },
+  created () {
+    this.plugins = []
+    for (let i = plugins.length; i--;) {
+      this.plugins.push(new plugins[i](this))
+    }
+  },
+  mounted () {
+    if (this.content && !this.nodes.length) {
+      this.setContent(this.content)
+    }
+  },
+  beforeDestroy () {
+    this._hook('onDetached')
+  },
+  methods: {
+    /**
+     * @description 将锚点跳转的范围限定在一个 scroll-view 内
+     * @param {Object} page scroll-view 所在页面的示例
+     * @param {String} selector scroll-view 的选择器
+     * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
+     */
+    in (page, selector, scrollTop) {
+      // #ifndef APP-PLUS-NVUE
+      if (page && selector && scrollTop) {
+        this._in = {
+          page,
+          selector,
+          scrollTop
+        }
+      }
+      // #endif
+    },
+
+    /**
+     * @description 锚点跳转
+     * @param {String} id 要跳转的锚点 id
+     * @param {Number} offset 跳转位置的偏移量
+     * @returns {Promise}
+     */
+    navigateTo (id, offset) {
+      return new Promise((resolve, reject) => {
+        if (!this.useAnchor) {
+          reject(Error('Anchor is disabled'))
+          return
+        }
+        offset = offset || parseInt(this.useAnchor) || 0
+        // #ifdef APP-PLUS-NVUE
+        if (!id) {
+          dom.scrollToElement(this.$refs.web, {
+            offset
+          })
+          resolve()
+        } else {
+          this._navigateTo = {
+            resolve,
+            reject,
+            offset
+          }
+          this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
+        }
+        // #endif
+        // #ifndef APP-PLUS-NVUE
+        let deep = ' '
+        // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
+        deep = '>>>'
+        // #endif
+        const selector = uni.createSelectorQuery()
+          // #ifndef MP-ALIPAY
+          .in(this._in ? this._in.page : this)
+          // #endif
+          .select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
+        if (this._in) {
+          selector.select(this._in.selector).scrollOffset()
+            .select(this._in.selector).boundingClientRect()
+        } else {
+          // 获取 scroll-view 的位置和滚动距离
+          selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
+        }
+        selector.exec(res => {
+          if (!res[0]) {
+            reject(Error('Label not found'))
+            return
+          }
+          const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
+          if (this._in) {
+            // scroll-view 跳转
+            this._in.page[this._in.scrollTop] = scrollTop
+          } else {
+            // 页面跳转
+            uni.pageScrollTo({
+              scrollTop,
+              duration: 300
+            })
+          }
+          resolve()
+        })
+        // #endif
+      })
+    },
+
+    /**
+     * @description 获取文本内容
+     * @return {String}
+     */
+    getText (nodes) {
+      let text = '';
+      (function traversal (nodes) {
+        for (let i = 0; i < nodes.length; i++) {
+          const node = nodes[i]
+          if (node.type === 'text') {
+            text += node.text.replace(/&amp;/g, '&')
+          } else if (node.name === 'br') {
+            text += '\n'
+          } else {
+            // 块级标签前后加换行
+            const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
+            if (isBlock && text && text[text.length - 1] !== '\n') {
+              text += '\n'
+            }
+            // 递归获取子节点的文本
+            if (node.children) {
+              traversal(node.children)
+            }
+            if (isBlock && text[text.length - 1] !== '\n') {
+              text += '\n'
+            } else if (node.name === 'td' || node.name === 'th') {
+              text += '\t'
+            }
+          }
+        }
+      })(nodes || this.nodes)
+      return text
+    },
+
+    /**
+     * @description 获取内容大小和位置
+     * @return {Promise}
+     */
+    getRect () {
+      return new Promise((resolve, reject) => {
+        uni.createSelectorQuery()
+          // #ifndef MP-ALIPAY
+          .in(this)
+          // #endif
+          .select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
+      })
+    },
+
+    /**
+     * @description 暂停播放媒体
+     */
+    pauseMedia () {
+      for (let i = (this._videos || []).length; i--;) {
+        this._videos[i].pause()
+      }
+      // #ifdef APP-PLUS
+      const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
+      // #ifndef APP-PLUS-NVUE
+      let page = this.$parent
+      while (!page.$scope) page = page.$parent
+      page.$scope.$getAppWebview().evalJS(command)
+      // #endif
+      // #ifdef APP-PLUS-NVUE
+      this.$refs.web.evalJs(command)
+      // #endif
+      // #endif
+    },
+
+    /**
+     * @description 设置媒体播放速率
+     * @param {Number} rate 播放速率
+     */
+    setPlaybackRate (rate) {
+      this.playbackRate = rate
+      for (let i = (this._videos || []).length; i--;) {
+        this._videos[i].playbackRate(rate)
+      }
+      // #ifdef APP-PLUS
+      const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
+      // #ifndef APP-PLUS-NVUE
+      let page = this.$parent
+      while (!page.$scope) page = page.$parent
+      page.$scope.$getAppWebview().evalJS(command)
+      // #endif
+      // #ifdef APP-PLUS-NVUE
+      this.$refs.web.evalJs(command)
+      // #endif
+      // #endif
+    },
+
+    /**
+     * @description 设置内容
+     * @param {String} content html 内容
+     * @param {Boolean} append 是否在尾部追加
+     */
+    setContent (content, append) {
+      if (!append || !this.imgList) {
+        this.imgList = []
+      }
+      const nodes = new Parser(this).parse(content)
+      // #ifdef APP-PLUS-NVUE
+      if (this._ready) {
+        this._set(nodes, append)
+      }
+      // #endif
+      this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
+
+      // #ifndef APP-PLUS-NVUE
+      this._videos = []
+      this.$nextTick(() => {
+        this._hook('onLoad')
+        this.$emit('load')
+      })
+
+      if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
+        // 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
+        let height = 0
+        const callback = rect => {
+          if (!rect || !rect.height) rect = {}
+          // 350ms 总高度无变化就触发 ready 事件
+          if (rect.height === height) {
+            this.$emit('ready', rect)
+          } else {
+            height = rect.height
+            setTimeout(() => {
+              this.getRect().then(callback).catch(callback)
+            }, 350)
+          }
+        }
+        this.getRect().then(callback).catch(callback)
+      } else {
+        // 未设置懒加载,等待所有图片加载完毕
+        if (!this.imgList._unloadimgs) {
+          this.getRect().then(rect => {
+            this.$emit('ready', rect)
+          }).catch(() => {
+            this.$emit('ready', {})
+          })
+        }
+      }
+      // #endif
+    },
+
+    /**
+     * @description 调用插件钩子函数
+     */
+    _hook (name) {
+      for (let i = plugins.length; i--;) {
+        if (this.plugins[i][name]) {
+          this.plugins[i][name]()
+        }
+      }
+    },
+
+    // #ifdef APP-PLUS-NVUE
+    /**
+     * @description 设置内容
+     */
+    _set (nodes, append) {
+      this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
+    },
+
+    /**
+     * @description 接收到 web-view 消息
+     */
+    _onMessage (e) {
+      const message = e.detail.data[0]
+      switch (message.action) {
+        // web-view 初始化完毕
+        case 'onJSBridgeReady':
+          this._ready = true
+          if (this.nodes) {
+            this._set(this.nodes)
+          }
+          break
+        // 内容 dom 加载完毕
+        case 'onLoad':
+          this.height = message.height
+          this._hook('onLoad')
+          this.$emit('load')
+          break
+        // 所有图片加载完毕
+        case 'onReady':
+          this.getRect().then(res => {
+            this.$emit('ready', res)
+          }).catch(() => {
+            this.$emit('ready', {})
+          })
+          break
+        // 总高度发生变化
+        case 'onHeightChange':
+          this.height = message.height
+          break
+        // 图片点击
+        case 'onImgTap':
+          this.$emit('imgtap', message.attrs)
+          if (this.previewImg) {
+            uni.previewImage({
+              current: parseInt(message.attrs.i),
+              urls: this.imgList
+            })
+          }
+          break
+        // 链接点击
+        case 'onLinkTap': {
+          const href = message.attrs.href
+          this.$emit('linktap', message.attrs)
+          if (href) {
+            // 锚点跳转
+            if (href[0] === '#') {
+              if (this.useAnchor) {
+                dom.scrollToElement(this.$refs.web, {
+                  offset: message.offset
+                })
+              }
+            } else if (href.includes('://')) {
+              // 打开外链
+              if (this.copyLink) {
+                plus.runtime.openWeb(href)
+              }
+            } else {
+              uni.navigateTo({
+                url: href,
+                fail () {
+                  uni.switchTab({
+                    url: href
+                  })
+                }
+              })
+            }
+          }
+          break
+        }
+        case 'onPlay':
+          this.$emit('play')
+          break
+        // 获取到锚点的偏移量
+        case 'getOffset':
+          if (typeof message.offset === 'number') {
+            dom.scrollToElement(this.$refs.web, {
+              offset: message.offset + this._navigateTo.offset
+            })
+            this._navigateTo.resolve()
+          } else {
+            this._navigateTo.reject(Error('Label not found'))
+          }
+          break
+        // 点击
+        case 'onClick':
+          this.$emit('tap')
+          this.$emit('click')
+          break
+        // 出错
+        case 'onError':
+          this.$emit('error', {
+            source: message.source,
+            attrs: message.attrs
+          })
+      }
+    }
+    // #endif
+  }
+}
+</script>
+
+<style>
+/* #ifndef APP-PLUS-NVUE */
+/* 根节点样式 */
+._root {
+  padding: 1px 0;
+  overflow-x: auto;
+  overflow-y: hidden;
+  -webkit-overflow-scrolling: touch;
+}
+
+/* 长按复制 */
+._select {
+  user-select: text;
+}
+/* #endif */
+</style>

+ 629 - 0
uni_modules/mp-html/components/mp-html/node/node.vue

@@ -0,0 +1,629 @@
+<template>
+  <view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
+    <block v-for="(n, i) in nodes" v-bind:key="i">
+      <!-- 图片 -->
+      <!-- 占位图 -->
+      <image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
+      <!-- 显示图片 -->
+      <!-- #ifdef H5 || (APP-PLUS && VUE2) -->
+      <img v-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- #ifndef H5 || (APP-PLUS && VUE2) -->
+      <!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
+      <rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" />
+      <!-- #endif -->
+      <!-- #ifdef APP-HARMONY -->
+      <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+ctrl[i]+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
+      <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- #ifdef MP-KUAISHOU -->
+      <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src" :lazy-load="opts[0]" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap"></image>
+      <!-- #endif -->
+      <!-- #ifdef APP-PLUS && VUE3 -->
+      <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||''))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- 文本 -->
+      <!-- #ifdef MP-WEIXIN -->
+      <text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
+      <!-- #endif -->
+      <!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
+      <text v-else-if="n.text" decode>{{n.text}}</text>
+      <!-- #endif -->
+      <text v-else-if="n.name==='br'">{{'\n'}}</text>
+      <!-- 链接 -->
+      <view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
+        <node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
+      </view>
+      <!-- 视频 -->
+      <!-- #ifdef APP-PLUS -->
+      <view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" :data-i="i" @vplay.stop="play" />
+      <!-- #endif -->
+      <!-- #ifndef APP-PLUS -->
+      <video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="urlWithToken(n.src[ctrl[i]||0])" :data-i="i" @play="play" @error="mediaError" />
+      <template v-else-if="n.name==='channel-video'">
+				<wx-channel-video :feed-id="n.attrs['data-feed-id']" :finder-user-name="n.attrs['data-finder-user-name']" :src="n.attrs['data-url']" />
+			</template>
+      <template v-else-if="n.name==='aliyun-video'">
+				<aliyun-video :video-id="n.attrs['data-id']" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :data-i="i" @play="play" @error="mediaError" />
+			</template>
+      <!-- #endif -->
+      <!-- #ifdef H5 || APP-PLUS -->
+      <iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
+      <embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
+      <!-- #endif -->
+      <!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
+      <!-- 音频 -->
+      <audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="urlWithToken(n.src[ctrl[i]||0])" :data-i="i" @play="play" @error="mediaError" />
+      <!-- #endif -->
+      <view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
+        <node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
+        <view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
+          <node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
+          <block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
+            <view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
+              <node :childs="tr.children" :opts="opts" />
+            </view>
+            <view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
+              <view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
+                <node :childs="td.children" :opts="opts" />
+              </view>
+            </view>
+          </block>
+        </view>
+      </view>
+      <aliyun-video v-else-if="n.name=='aliyun-video'" :video-id="n.attrs['data-id']" :class="n.attrs.class" :style="n.attrs.style" :mode="opts[5]" :src="n.attrs.src" :title="n.attrs.title" :desc="n.attrs.desc" :url="n.attrs.url" :color="n.attrs.color" :bgcolor="n.attrs.bgcolor" :border="n.attrs.border" :name="n.attrs.name" :data-i="i" data-source="card" /><wx-channel-video v-else-if="n.name=='channel-video'" data-type='channelVideoQrCode' />
+      <!-- 富文本 -->
+      <!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
+      <rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
+      <!-- #endif -->
+      <!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
+      <rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
+      <!-- #endif -->
+      <!-- 继续递归 -->
+      <view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
+        <node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
+      </view>
+      <node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
+    </block>
+  </view>
+</template>
+<script module="handler" lang="wxs">
+// 行内标签列表
+var inlineTags = {
+  abbr: true,
+  b: true,
+  big: true,
+  code: true,
+  del: true,
+  em: true,
+  i: true,
+  ins: true,
+  label: true,
+  q: true,
+  small: true,
+  span: true,
+  strong: true,
+  sub: true,
+  sup: true
+}
+/**
+ * @description 判断是否为行内标签
+ */
+module.exports = {
+  isInline: function (tagName, style) {
+    return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
+  }
+}
+</script>
+<script>
+import aliyunVideo from '../aliyun-video/aliyun-video'
+import wxChannelVideo from '../wx-channel-video/wx-channel-video'
+
+import node from './node'
+export default {
+  name: 'node',
+  options: {
+    // #ifdef MP-WEIXIN
+    virtualHost: true,
+    // #endif
+    // #ifdef MP-TOUTIAO
+    addGlobalClass: false
+    // #endif
+  },
+  data () {
+    return {
+      ctrl: {},
+      nodes: [],
+      // #ifdef MP-WEIXIN
+      isiOS: uni.getDeviceInfo().system.includes('iOS')
+      // #endif
+    }
+  },
+  props: {
+    name: String,
+    attrs: {
+      type: Object,
+      default () {
+        return {}
+      }
+    },
+    childs: Array,
+    opts: Array,
+    token: String
+  },
+  watch: {
+    childs: {
+		  handler (nodes) {
+        // 列表缩短会刷新整个列表,因此进行空填充
+        while (this.nodes.length > nodes.length) {
+			    nodes.push({})
+		    }
+        this.nodes = nodes
+      },
+	    immediate: true
+	  }
+  },
+  components: {
+aliyunVideo,
+wxChannelVideo,
+
+    // #ifndef ((H5 || APP-PLUS) && VUE3) || APP-HARMONY
+    node
+    // #endif
+  },
+  mounted () {
+    this.$nextTick(() => {
+      for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; this.root = this.root.$parent);
+    })
+    // #ifdef H5 || APP-PLUS
+    if (this.opts[0]) {
+      let i
+      for (i = this.childs.length; i--;) {
+        if (this.childs[i].name === 'img') break
+      }
+      if (i !== -1) {
+        this.observer = uni.createIntersectionObserver(this).relativeToViewport({
+          top: 500,
+          bottom: 500
+        })
+        this.observer.observe('._img', res => {
+          if (res.intersectionRatio) {
+            this.$set(this.ctrl, 'load', 1)
+            this.observer.disconnect()
+          }
+        })
+      }
+    }
+    // #endif
+  },
+  beforeDestroy () {
+    // #ifdef H5 || APP-PLUS
+    if (this.observer) {
+      this.observer.disconnect()
+    }
+    // #endif
+  },
+  methods:{
+    // #ifdef MP-WEIXIN
+    toJSON () { return this },
+    // #endif
+    /**
+     * @description 播放视频事件
+     * @param {Event} e
+     */
+    play (e) {
+      const i = e.currentTarget.dataset.i
+      const node = this.childs[i]
+      this.root.$emit('play', {
+        source: node.name,
+        attrs: {
+          ...node.attrs,
+          src: node.src[this.ctrl[i] || 0]
+        }
+      })
+      // #ifndef APP-PLUS
+      if (this.root.pauseVideo) {
+        let flag = false
+        const id = e.target.id
+        for (let i = this.root._videos.length; i--;) {
+          if (this.root._videos[i].id === id) {
+            flag = true
+          } else {
+            this.root._videos[i].pause() // 自动暂停其他视频
+          }
+        }
+        // 将自己加入列表
+        if (!flag) {
+          const ctx = uni.createVideoContext(id
+            // #ifndef MP-BAIDU
+            , this
+            // #endif
+          )
+          ctx.id = id
+          if (this.root.playbackRate) {
+            ctx.playbackRate(this.root.playbackRate)
+          }
+          this.root._videos.push(ctx)
+        }
+      }
+      // #endif
+    },
+
+    /**
+     * @description 图片点击事件
+     * @param {Event} e
+     */
+    imgTap (e) {
+      const node = this.childs[e.currentTarget.dataset.i]
+      if (node.a) {
+        this.linkTap(node.a)
+        return
+      }
+      if (node.attrs.ignore) return
+      // #ifdef H5 || APP-PLUS
+      node.attrs.src = node.attrs.src || node.attrs['data-src']
+      // #endif
+      // #ifndef APP-HARMONY
+      this.root.$emit('imgtap', node.attrs)
+      // #endif
+      // #ifdef APP-HARMONY
+      this.root.$emit('imgtap', {
+        ...node.attrs
+      })
+      // #endif
+      // 自动预览图片
+      if (this.root.previewImg) {
+        uni.previewImage({
+          // #ifdef MP-WEIXIN
+          showmenu: this.root.showImgMenu,
+          // #endif
+          // #ifdef MP-ALIPAY
+          enablesavephoto: this.root.showImgMenu,
+          enableShowPhotoDownload: this.root.showImgMenu,
+          // #endif
+          current: parseInt(node.attrs.i),
+          urls: this.root.imgList
+        })
+      }
+    },
+
+    /**
+     * @description 图片长按
+     */
+    imgLongTap (e) {
+      // #ifdef APP-PLUS
+      const attrs = this.childs[e.currentTarget.dataset.i].attrs
+      if (this.opts[3] && !attrs.ignore) {
+        uni.showActionSheet({
+          itemList: ['保存图片'],
+          success: () => {
+            const save = path => {
+              uni.saveImageToPhotosAlbum({
+                filePath: path,
+                success () {
+                  uni.showToast({
+                    title: '保存成功'
+                  })
+                }
+              })
+            }
+            if (this.root.imgList[attrs.i].startsWith('http')) {
+              uni.downloadFile({
+                url: this.root.imgList[attrs.i],
+                success: res => save(res.tempFilePath)
+              })
+            } else {
+              save(this.root.imgList[attrs.i])
+            }
+          }
+        })
+      }
+      // #endif
+    },
+
+    /**
+     * @description 图片加载完成事件
+     * @param {Event} e
+     */
+    imgLoad (e) {
+      const i = e.currentTarget.dataset.i
+      /* #ifndef H5 || (APP-PLUS && VUE2) */
+      if (!this.childs[i].w) {
+        // 设置原宽度
+        this.$set(this.ctrl, i, e.detail.width)
+      } else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
+        // 加载完毕,取消加载中占位图
+        this.$set(this.ctrl, i, 1)
+      }
+      this.checkReady()
+    },
+
+    /**
+     * @description 检查是否所有图片加载完毕
+     */
+    checkReady () {
+      if (this.root && !this.root.lazyLoad) {
+        this.root._unloadimgs -= 1
+        if (!this.root._unloadimgs) {
+          setTimeout(() => {
+            this.root.getRect().then(rect => {
+              this.root.$emit('ready', rect)
+            }).catch(() => {
+              this.root.$emit('ready', {})
+            })
+          }, 350)
+        }
+      }
+    },
+
+    /**
+     * @description 链接点击事件
+     * @param {Event} e
+     */
+    linkTap (e) {
+      const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
+      const attrs = node.attrs || e
+      const href = attrs.href
+      this.root.$emit('linktap', Object.assign({
+        innerText: this.root.getText(node.children || []) // 链接内的文本内容
+      }, attrs))
+      if (href) {
+        if (href[0] === '#') {
+          // 跳转锚点
+          this.root.navigateTo(href.substring(1)).catch(() => { })
+        } else if (href.split('?')[0].includes('://')) {
+          // 复制外部链接
+          if (this.root.copyLink) {
+            // #ifdef H5
+            window.open(href)
+            // #endif
+            // #ifdef MP
+            uni.setClipboardData({
+              data: href,
+              success: () =>
+                uni.showToast({
+                  title: '链接已复制'
+                })
+            })
+            // #endif
+            // #ifdef APP-PLUS
+            plus.runtime.openWeb(href)
+            // #endif
+          }
+        } else {
+          // 跳转页面
+          uni.navigateTo({
+            url: href,
+            fail () {
+              uni.switchTab({
+                url: href,
+                fail () { }
+              })
+            }
+          })
+        }
+      }
+    },
+
+    /**
+     * @description 错误事件
+     * @param {Event} e
+     */
+    mediaError (e) {
+      const i = e.currentTarget.dataset.i
+      const node = this.childs[i]
+      // 加载其他源
+      if (node.name === 'video' || node.name === 'audio') {
+        let index = (this.ctrl[i] || 0) + 1
+        if (index > node.src.length) {
+          index = 0
+        }
+        if (index < node.src.length) {
+          this.$set(this.ctrl, i, index)
+          return
+        }
+      } else if (node.name === 'img') {
+        // #ifdef H5 && VUE3
+        if (this.opts[0] && !this.ctrl.load) return
+        // #endif
+        // 显示错误占位图
+        if (this.opts[2]) {
+          this.$set(this.ctrl, i, -1)
+        }
+        this.checkReady()
+      }
+      if (this.root) {
+        this.root.$emit('error', {
+          source: node.name,
+          attrs: node.attrs,
+          // #ifndef H5 && VUE3
+          errMsg: e.detail.errMsg
+          // #endif
+        })
+      }
+    },
+    urlWithToken (url) {
+      if (this.root) {
+        const hasParams = url.includes('?')
+        return `${url}${this.root.token ? `${hasParams ? '&': '?'}token=${this.root.token}` : ''}` 
+      } else {
+        return url
+      }
+		}
+  },
+}
+</script>
+<style>
+/* a 标签默认效果 */
+._a {
+  padding: 1.5px 0 1.5px 0;
+  color: #366092;
+  word-break: break-all;
+}
+
+/* a 标签点击态效果 */
+._hover {
+  text-decoration: underline;
+  opacity: 0.7;
+}
+
+/* 图片默认效果 */
+._img {
+  max-width: 100%;
+  -webkit-touch-callout: none;
+}
+
+/* 内部样式 */
+
+._block {
+  display: block;
+}
+
+._b,
+._strong {
+  font-weight: bold;
+}
+
+._code {
+  font-family: monospace;
+}
+
+._del {
+  text-decoration: line-through;
+}
+
+._em,
+._i {
+  font-style: italic;
+}
+
+._h1 {
+  font-size: 2em;
+}
+
+._h2 {
+  font-size: 1.5em;
+}
+
+._h3 {
+  font-size: 1.17em;
+}
+
+._h5 {
+  font-size: 0.83em;
+}
+
+._h6 {
+  font-size: 0.67em;
+}
+
+._h1,
+._h2,
+._h3,
+._h4,
+._h5,
+._h6 {
+  display: block;
+  font-weight: bold;
+}
+
+._image {
+  height: 1px;
+}
+
+._ins {
+  text-decoration: underline;
+}
+
+._li {
+  display: list-item;
+}
+
+._ol {
+  list-style-type: decimal;
+}
+
+._ol,
+._ul {
+  display: block;
+  padding-left: 40px;
+  margin: 1em 0;
+}
+
+._q::before {
+  content: '"';
+}
+
+._q::after {
+  content: '"';
+}
+
+._sub {
+  font-size: smaller;
+  vertical-align: sub;
+}
+
+._sup {
+  font-size: smaller;
+  vertical-align: super;
+}
+
+._thead,
+._tbody,
+._tfoot {
+  display: table-row-group;
+}
+
+._tr {
+  display: table-row;
+}
+
+._td,
+._th {
+  display: table-cell;
+  vertical-align: middle;
+}
+
+._th {
+  font-weight: bold;
+  text-align: center;
+}
+
+._ul {
+  list-style-type: disc;
+}
+
+._ul ._ul {
+  margin: 0;
+  list-style-type: circle;
+}
+
+._ul ._ul ._ul {
+  list-style-type: square;
+}
+
+._abbr,
+._b,
+._code,
+._del,
+._em,
+._i,
+._ins,
+._label,
+._q,
+._span,
+._strong,
+._sub,
+._sup {
+  display: inline;
+}
+
+/* #ifdef APP-PLUS */
+._video {
+  width: 300px;
+  height: 225px;
+}
+/* #endif */
+</style>

+ 1402 - 0
uni_modules/mp-html/components/mp-html/parser.js

@@ -0,0 +1,1402 @@
+/**
+ * @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,channel-video,aliyun-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',
+    foreignobject: 'foreignObject'
+  }
+}
+const tagSelector={}
+let windowWidth, system
+// #ifdef MP-WEIXIN
+if (uni.canIUse('getWindowInfo')) {
+  windowWidth = uni.getWindowInfo().windowWidth
+  system = uni.getDeviceInfo().system
+} else {
+// #endif
+  const systemInfo = uni.getSystemInfoSync()
+  windowWidth = systemInfo.windowWidth
+  // #ifdef MP-WEIXIN
+  system = systemInfo.system
+}
+// #endif
+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 要不要解码 &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] === '#') {
+      // &#123; 形式的实体
+      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 {
+      // &nbsp; 形式的实体
+      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 标签内大小写敏感
+    config.ignoreTags.style = undefined // svg 标签内 style 可用
+  }
+}
+
+/**
+ * @description 解析到属性名
+ * @param {String} name 属性名
+ * @private
+ */
+Parser.prototype.onAttrName = function (name) {
+  name = this.xml ? name : name.toLowerCase()
+  // #ifdef (VUE3 && (H5 || APP-PLUS)) || APP-PLUS-NVUE
+  if (name.includes('?') || name.includes(';')) {
+    this.attrName = undefined
+    return
+  }
+  // #endif
+  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 if(this.tagName === 'aliyun-video' || this.tagName === 'channel-video') {
+			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:') && this.options.previewImg !== 'all' && !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'
+      }
+      if (node.w && node.h && styleObj['object-fit']) {
+        if (styleObj['object-fit'] === 'contain') {
+          node.m = 'aspectFit'
+        } else if (styleObj['object-fit'] === 'cover') {
+          node.m = 'aspectFill'
+        }
+      }
+    } 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 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
+      if (name === 'foreignObject') {
+        for (const child of (node.children || [])) {
+          if (child.attrs && !child.attrs.xmlns) {
+            child.attrs.xmlns = 'http://www.w3.org/1999/xhtml'
+            break
+          }
+        }
+      }
+      src += '<' + name
+      for (const item in node.attrs) {
+        const val = node.attrs[item]
+        if (val) {
+          src += ` ${config.svgDict[item] || item}="${val.replace(/"/g, '')}"`
+        }
+      }
+      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
+    config.ignoreTags.style = true
+    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]) {
+    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 = '<video style="width:100%;height:100%"'
+    for (const item in attrs) {
+      if (attrs[item]) {
+        str += ' ' + item + '="' + attrs[item] + '"'
+      }
+    }
+    if (this.options.pauseVideo) {
+      str += ' onplay="this.dispatchEvent(new CustomEvent(\'vplay\',{bubbles:!0}));for(var e=document.getElementsByTagName(\'video\'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()"'
+    }
+    str += '>'
+    for (let i = 0; i < node.src.length; i++) {
+      str += '<source src="' + node.src[i] + '">'
+    }
+    str += '</video>'
+    node.html = str
+    /* #endif */
+  } else if ((node.name === 'ul' || node.name === 'ol') && node.c) {
+    // 列表处理
+    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) {
+      // 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) {
+      // 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
+      styleObj.display = 'grid'
+      if (styleObj['border-collapse'] === 'collapse') {
+        styleObj['border-collapse'] = undefined
+        spacing = 0
+      }
+      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 if (nodes[i].name === 'colgroup') {
+            let colI = 1
+            for (const col of (nodes[i].children || [])) {
+              if (col.name === 'col') {
+                const style = col.attrs.style || ''
+                const start = style.indexOf('width') ? style.indexOf(';width') : 0
+                // 提取出宽度
+                if (start !== -1) {
+                  let end = style.indexOf(';', start + 6)
+                  if (end === -1) {
+                    end = style.length
+                  }
+                  width[colI] = style.substring(start ? start + 7 : 6, end)
+                }
+                colI += 1
+              }
+            }
+          } 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++
+            }
+            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) {
+        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 === 'tbody' || node.name === 'tr') && node.flag && node.c) {
+    node.flag = undefined;
+    (function traversal (nodes) {
+      for (let i = 0; i < nodes.length; i++) {
+        if (nodes[i].name === 'td') {
+          // 颜色样式设置给单元格避免丢失
+          for (const style of ['color', 'background', 'background-color']) {
+            if (styleObj[style]) {
+              nodes[i].attrs.style = style + ':' + styleObj[style] + ';' + (nodes[i].attrs.style || '')
+            }
+          }
+        } else {
+          traversal(nodes[i].children || [])
+        }
+      }
+    })(children)
+  } 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].name === 'tbody' || this.stack[i].name === 'tr') {
+        this.stack[i].flag = 1 // 指示含有合并单元格
+      }
+    }
+  } 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 (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) {
+    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 && 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 && !(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

+ 7 - 0
uni_modules/mp-html/components/mp-html/wx-channel-video/index.js

@@ -0,0 +1,7 @@
+/**
+ * @fileoverview ChannelVideo 插件
+ */
+function ChannelVideo (vm) {
+}
+
+export default ChannelVideo

+ 79 - 0
uni_modules/mp-html/components/mp-html/wx-channel-video/wx-channel-video.vue

@@ -0,0 +1,79 @@
+<template>
+  <!-- #ifdef MP-WEIXIN -->
+  <block v-if="type === 'channelVideoQrCode'">
+    <view class="channel-video-qrcode">
+      <view>
+        <image style="width: 240rpx; height: 240rpx" :src="src"></image>
+        <view>扫码查看视频内容</view>
+      </view>
+    </view>
+  </block>
+  <block v-else>
+    <view class="channel-video">
+      <channel-video :feed-id="feedId" :finder-user-name="finderUserName"></channel-video>
+      <view style="color: var(--uni-color-primary)" :data-feed-id="feedId" :data-finder-user-name="finderUserName" @click="toPlayChannelVideo"
+        >点击观看视频</view
+      >
+    </view>
+  </block>
+  <!-- #endif -->
+  <!-- #ifndef MP-WEIXIN -->
+  <view class="channel-video-qrcode">
+    <view>
+      <image style="width: 240rpx; height: 240rpx" :src="src"></image>
+      <view>扫码查看视频内容</view>
+    </view>
+  </view>
+  <!-- #endif -->
+</template>
+
+<script>
+import { handle, request } from "@kasite/uni-app-base";
+import { REQUEST_CONFIG } from "@kasite/uni-app-base/config";
+export default {
+  props: {
+    type: {
+      type: String,
+      default: "",
+    },
+    src: {
+      type: String,
+      default: "",
+    },
+    feedId: {
+      type: String,
+      default: "",
+    },
+    finderUserName: {
+      type: String,
+      default: "",
+    },
+  },
+  data() {
+    return {};
+  },
+  methods: {
+    toPlayChannelVideo(e) {
+      const { feedId, finderUserName } = e.currentTarget.dataset;
+      uni.openChannelsActivity({ finderUserName, feedId });
+    },
+  },
+  mounted() {},
+};
+</script>
+
+<style>
+.channel-video {
+  display: flex;
+  justify-content: center;
+}
+.channel-video-qrcode {
+  display: flex;
+  justify-content: center;
+}
+.channel-video-qrcode > view {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+</style>

+ 79 - 0
uni_modules/mp-html/package.json

@@ -0,0 +1,79 @@
+{
+    "id": "mp-html",
+    "displayName": "mp-html 富文本组件【全端支持,支持编辑、latex等扩展】",
+    "version": "v2.5.1",
+    "description": "一个强大的富文本组件,高效轻量,功能丰富",
+    "keywords": [
+        "富文本",
+        "编辑器",
+        "html",
+        "rich-text",
+        "editor"
+    ],
+    "repository": "https://github.com/jin-yufeng/mp-html",
+    "dcloudext": {
+        "sale": {
+            "regular": {
+                "price": "0.00"
+            },
+            "sourcecode": {
+                "price": "0.00"
+            }
+        },
+        "contact": {
+            "qq": ""
+        },
+        "declaration": {
+            "ads": "无",
+            "data": "无",
+            "permissions": "无"
+        },
+        "npmurl": "https://www.npmjs.com/package/mp-html",
+        "type": "component-vue"
+    },
+    "uni_modules": {
+        "platforms": {
+            "cloud": {
+                "tcb": "y",
+                "aliyun": "y",
+                "alipay": "n"
+            },
+            "client": {
+                "App": {
+                    "app-vue": "y",
+                    "app-nvue": "y",
+                    "app-harmony": "u",
+                    "app-uvue": "u"
+                },
+                "H5-mobile": {
+                    "Safari": "y",
+                    "Android Browser": "y",
+                    "微信浏览器(Android)": "y",
+                    "QQ浏览器(Android)": "y"
+                },
+                "H5-pc": {
+                    "Chrome": "y",
+                    "IE": "u",
+                    "Edge": "y",
+                    "Firefox": "y",
+                    "Safari": "y"
+                },
+                "小程序": {
+                    "微信": "y",
+                    "阿里": "y",
+                    "百度": "y",
+                    "字节跳动": "y",
+                    "QQ": "y"
+                },
+                "快应用": {
+                    "华为": "y",
+                    "联盟": "y"
+                },
+                "Vue": {
+                    "vue2": "y",
+                    "vue3": "y"
+                }
+            }
+        }
+    }
+}

文件差異過大導致無法顯示
+ 0 - 0
uni_modules/mp-html/static/app-plus/mp-html/js/handler.js


文件差異過大導致無法顯示
+ 0 - 0
uni_modules/mp-html/static/app-plus/mp-html/js/uni.webview.min.js


+ 1 - 0
uni_modules/mp-html/static/app-plus/mp-html/local.html

@@ -0,0 +1 @@
+<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>body,html{width:100%;height:100%;overflow-x:scroll;overflow-y:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}</style></head><body><div id="content" style="overflow:hidden"></div><script type="text/javascript" src="./js/uni.webview.min.js"></script><script type="text/javascript" src="./js/handler.js"></script></body>

+ 33 - 0
uni_modules/uni-badge/changelog.md

@@ -0,0 +1,33 @@
+## 1.2.2(2023-01-28)
+- 修复 运行/打包 控制台警告问题
+## 1.2.1(2022-09-05)
+- 修复 当 text 超过 max-num 时,badge 的宽度计算是根据 text 的长度计算,更改为 css 计算实际展示宽度,详见:[https://ask.dcloud.net.cn/question/150473](https://ask.dcloud.net.cn/question/150473)
+## 1.2.0(2021-11-19)
+- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-badge](https://uniapp.dcloud.io/component/uniui/uni-badge)
+## 1.1.7(2021-11-08)
+- 优化 升级ui
+- 修改 size 属性默认值调整为 small
+- 修改 type 属性,默认值调整为 error,info 替换 default
+## 1.1.6(2021-09-22)
+- 修复 在字节小程序上样式不生效的 bug
+## 1.1.5(2021-07-30)
+- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+## 1.1.4(2021-07-29)
+- 修复 去掉 nvue 不支持css 的 align-self 属性,nvue 下不暂支持 absolute 属性
+## 1.1.3(2021-06-24)
+- 优化 示例项目
+## 1.1.1(2021-05-12)
+- 新增 组件示例地址
+## 1.1.0(2021-05-12)
+- 新增 uni-badge 的 absolute 属性,支持定位
+- 新增 uni-badge 的 offset 属性,支持定位偏移
+- 新增 uni-badge 的 is-dot 属性,支持仅显示有一个小点
+- 新增 uni-badge 的 max-num 属性,支持自定义封顶的数字值,超过 99 显示99+
+- 优化 uni-badge 属性 custom-style, 支持以对象形式自定义样式
+## 1.0.7(2021-05-07)
+- 修复 uni-badge 在 App 端,数字小于10时不是圆形的bug
+- 修复 uni-badge 在父元素不是 flex 布局时,宽度缩小的bug
+- 新增 uni-badge 属性 custom-style, 支持自定义样式
+## 1.0.6(2021-02-04)
+- 调整为uni_modules目录规范

+ 268 - 0
uni_modules/uni-badge/components/uni-badge/uni-badge.vue

@@ -0,0 +1,268 @@
+<template>
+	<view class="uni-badge--x">
+		<slot />
+		<text v-if="text" :class="classNames" :style="[positionStyle, customStyle, dotStyle]"
+			class="uni-badge" @click="onClick()">{{displayValue}}</text>
+	</view>
+</template>
+
+<script>
+	/**
+	 * Badge 数字角标
+	 * @description 数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=21
+	 * @property {String} text 角标内容
+	 * @property {String} size = [normal|small] 角标内容
+	 * @property {String} type = [info|primary|success|warning|error] 颜色类型
+	 * 	@value info 灰色
+	 * 	@value primary 蓝色
+	 * 	@value success 绿色
+	 * 	@value warning 黄色
+	 * 	@value error 红色
+	 * @property {String} inverted = [true|false] 是否无需背景颜色
+	 * @property {Number} maxNum 展示封顶的数字值,超过 99 显示 99+
+	 * @property {String} absolute = [rightTop|rightBottom|leftBottom|leftTop] 开启绝对定位, 角标将定位到其包裹的标签的四角上
+	 * 	@value rightTop 右上
+	 * 	@value rightBottom 右下
+	 * 	@value leftTop 左上
+	 * 	@value leftBottom 左下
+	 * @property {Array[number]} offset	距定位角中心点的偏移量,只有存在 absolute 属性时有效,例如:[-10, -10] 表示向外偏移 10px,[10, 10] 表示向 absolute 指定的内偏移 10px
+	 * @property {String} isDot = [true|false] 是否显示为一个小点
+	 * @event {Function} click 点击 Badge 触发事件
+	 * @example <uni-badge text="1"></uni-badge>
+	 */
+
+	export default {
+		name: 'UniBadge',
+		emits: ['click'],
+		props: {
+			type: {
+				type: String,
+				default: 'error'
+			},
+			inverted: {
+				type: Boolean,
+				default: false
+			},
+			isDot: {
+				type: Boolean,
+				default: false
+			},
+			maxNum: {
+				type: Number,
+				default: 99
+			},
+			absolute: {
+				type: String,
+				default: ''
+			},
+			offset: {
+				type: Array,
+				default () {
+					return [0, 0]
+				}
+			},
+			text: {
+				type: [String, Number],
+				default: ''
+			},
+			size: {
+				type: String,
+				default: 'small'
+			},
+			customStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			}
+		},
+		data() {
+			return {};
+		},
+		computed: {
+			width() {
+				return String(this.text).length * 8 + 12
+			},
+			classNames() {
+				const {
+					inverted,
+					type,
+					size,
+					absolute
+				} = this
+				return [
+					inverted ? 'uni-badge--' + type + '-inverted' : '',
+					'uni-badge--' + type,
+					'uni-badge--' + size,
+					absolute ? 'uni-badge--absolute' : ''
+				].join(' ')
+			},
+			positionStyle() {
+				if (!this.absolute) return {}
+				let w = this.width / 2,
+					h = 10
+				if (this.isDot) {
+					w = 5
+					h = 5
+				}
+				const x = `${- w  + this.offset[0]}px`
+				const y = `${- h + this.offset[1]}px`
+
+				const whiteList = {
+					rightTop: {
+						right: x,
+						top: y
+					},
+					rightBottom: {
+						right: x,
+						bottom: y
+					},
+					leftBottom: {
+						left: x,
+						bottom: y
+					},
+					leftTop: {
+						left: x,
+						top: y
+					}
+				}
+				const match = whiteList[this.absolute]
+				return match ? match : whiteList['rightTop']
+			},
+			dotStyle() {
+				if (!this.isDot) return {}
+				return {
+					width: '10px',
+					minWidth: '0',
+					height: '10px',
+					padding: '0',
+					borderRadius: '10px'
+				}
+			},
+			displayValue() {
+				const {
+					isDot,
+					text,
+					maxNum
+				} = this
+				return isDot ? '' : (Number(text) > maxNum ? `${maxNum}+` : text)
+			}
+		},
+		methods: {
+			onClick() {
+				this.$emit('click');
+			}
+		}
+	};
+</script>
+
+<style lang="scss" >
+	$uni-primary: #2979ff !default;
+	$uni-success: #4cd964 !default;
+	$uni-warning: #f0ad4e !default;
+	$uni-error: #dd524d !default;
+	$uni-info: #909399 !default;
+
+
+	$bage-size: 12px;
+	$bage-small: scale(0.8);
+
+	.uni-badge--x {
+		/* #ifdef APP-NVUE */
+		// align-self: flex-start;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: inline-block;
+		/* #endif */
+		position: relative;
+	}
+
+	.uni-badge--absolute {
+		position: absolute;
+	}
+
+	.uni-badge--small {
+		transform: $bage-small;
+		transform-origin: center center;
+	}
+
+	.uni-badge {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		overflow: hidden;
+		box-sizing: border-box;
+		font-feature-settings: "tnum";
+		min-width: 20px;
+		/* #endif */
+		justify-content: center;
+		flex-direction: row;
+		height: 20px;
+		padding: 0 4px;
+		line-height: 18px;
+		color: #fff;
+		border-radius: 100px;
+		background-color: $uni-info;
+		background-color: transparent;
+		border: 1px solid #fff;
+		text-align: center;
+		font-family: 'Helvetica Neue', Helvetica, sans-serif;
+		font-size: $bage-size;
+		/* #ifdef H5 */
+		z-index: 999;
+		cursor: pointer;
+		/* #endif */
+
+		&--info {
+			color: #fff;
+			background-color: $uni-info;
+		}
+
+		&--primary {
+			background-color: $uni-primary;
+		}
+
+		&--success {
+			background-color: $uni-success;
+		}
+
+		&--warning {
+			background-color: $uni-warning;
+		}
+
+		&--error {
+			background-color: $uni-error;
+		}
+
+		&--inverted {
+			padding: 0 5px 0 0;
+			color: $uni-info;
+		}
+
+		&--info-inverted {
+			color: $uni-info;
+			background-color: transparent;
+		}
+
+		&--primary-inverted {
+			color: $uni-primary;
+			background-color: transparent;
+		}
+
+		&--success-inverted {
+			color: $uni-success;
+			background-color: transparent;
+		}
+
+		&--warning-inverted {
+			color: $uni-warning;
+			background-color: transparent;
+		}
+
+		&--error-inverted {
+			color: $uni-error;
+			background-color: transparent;
+		}
+
+	}
+</style>

+ 85 - 0
uni_modules/uni-badge/package.json

@@ -0,0 +1,85 @@
+{
+  "id": "uni-badge",
+  "displayName": "uni-badge 数字角标",
+  "version": "1.2.2",
+  "description": "数字角标(徽章)组件,在元素周围展示消息提醒,一般用于列表、九宫格、按钮等地方。",
+  "keywords": [
+    "",
+    "badge",
+    "uni-ui",
+    "uniui",
+    "数字角标",
+    "徽章"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": ""
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": ["uni-scss"],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "y",
+          "联盟": "y"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 10 - 0
uni_modules/uni-badge/readme.md

@@ -0,0 +1,10 @@
+## Badge 数字角标
+> **组件名:uni-badge**
+> 代码块: `uBadge`
+
+数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景,
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-badge)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 
+
+

+ 44 - 0
uni_modules/uni-icons/changelog.md

@@ -0,0 +1,44 @@
+## 2.0.12(2025-08-26)
+- 优化 uni-app x 下 size 类型问题
+## 2.0.11(2025-08-18)
+- 修复 图标点击事件返回
+## 2.0.9(2024-01-12)
+fix: 修复图标大小默认值错误的问题
+## 2.0.8(2023-12-14)
+- 修复 项目未使用 ts 情况下,打包报错的bug
+## 2.0.7(2023-12-14)
+- 修复 size 属性为 string 时,不加单位导致尺寸异常的bug
+## 2.0.6(2023-12-11)
+- 优化 兼容老版本icon类型,如 top ,bottom 等
+## 2.0.5(2023-12-11)
+- 优化 兼容老版本icon类型,如 top ,bottom 等
+## 2.0.4(2023-12-06)
+- 优化 uni-app x 下示例项目图标排序
+## 2.0.3(2023-12-06)
+- 修复 nvue下引入组件报错的bug
+## 2.0.2(2023-12-05)
+-优化 size 属性支持单位
+## 2.0.1(2023-12-05)
+- 新增 uni-app x 支持定义图标
+## 1.3.5(2022-01-24)
+- 优化 size 属性可以传入不带单位的字符串数值
+## 1.3.4(2022-01-24)
+- 优化 size 支持其他单位
+## 1.3.3(2022-01-17)
+- 修复 nvue 有些图标不显示的bug,兼容老版本图标
+## 1.3.2(2021-12-01)
+- 优化 示例可复制图标名称
+## 1.3.1(2021-11-23)
+- 优化 兼容旧组件 type 值
+## 1.3.0(2021-11-19)
+- 新增 更多图标
+- 优化 自定义图标使用方式
+- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons)
+## 1.1.7(2021-11-08)
+## 1.2.0(2021-07-30)
+- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+## 1.1.5(2021-05-12)
+- 新增 组件示例地址
+## 1.1.4(2021-02-05)
+- 调整为uni_modules目录规范

+ 91 - 0
uni_modules/uni-icons/components/uni-icons/uni-icons.uvue

@@ -0,0 +1,91 @@
+<template>
+	<text class="uni-icons" :style="styleObj">
+		<slot>{{unicode}}</slot>
+	</text>
+</template>
+
+<script>
+	import { fontData, IconsDataItem } from './uniicons_file'
+
+	/**
+	 * Icons 图标
+	 * @description 用于展示 icon 图标
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=28
+	 * @property {Number} size 图标大小
+	 * @property {String} type 图标图案,参考示例
+	 * @property {String} color 图标颜色
+	 * @property {String} customPrefix 自定义图标
+	 * @event {Function} click 点击 Icon 触发事件
+	 */
+	export default {
+		name: "uni-icons",
+		props: {
+			type: {
+				type: String,
+				default: ''
+			},
+			color: {
+				type: String,
+				default: '#333333'
+			},
+			size: {
+        type: [Number, String],
+        default: 16
+			},
+			fontFamily: {
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {};
+		},
+		computed: {
+			unicode() : string {
+				let codes = fontData.find((item : IconsDataItem) : boolean => { return item.font_class == this.type })
+				if (codes !== null) {
+					return codes.unicode
+				}
+				return ''
+			},
+			iconSize() : string {
+				const size = this.size
+				if (typeof size == 'string') {
+				  const reg = /^[0-9]*$/g
+				  return reg.test(size as string) ? '' + size + 'px' : '' + size;
+				  // return '' + this.size
+				}
+				return this.getFontSize(size as number)
+			},
+			styleObj() : UTSJSONObject {
+				if (this.fontFamily !== '') {
+					return { color: this.color, fontSize: this.iconSize, fontFamily: this.fontFamily }
+				}
+				return { color: this.color, fontSize: this.iconSize }
+			}
+		},
+		created() { },
+		methods: {
+			/**
+			 * 字体大小
+			 */
+			getFontSize(size : number) : string {
+				return size + 'px';
+			},
+		},
+	}
+</script>
+
+<style scoped>
+	@font-face {
+		font-family: UniIconsFontFamily;
+		src: url('./uniicons.ttf');
+	}
+
+	.uni-icons {
+		font-family: UniIconsFontFamily;
+		font-size: 18px;
+		font-style: normal;
+		color: #333;
+	}
+</style>

+ 110 - 0
uni_modules/uni-icons/components/uni-icons/uni-icons.vue

@@ -0,0 +1,110 @@
+<template>
+	<!-- #ifdef APP-NVUE -->
+	<text :style="styleObj" class="uni-icons" @click="_onClick">{{unicode}}</text>
+	<!-- #endif -->
+	<!-- #ifndef APP-NVUE -->
+	<text :style="styleObj" class="uni-icons" :class="['uniui-'+type,customPrefix,customPrefix?type:'']" @click="_onClick">
+		<slot></slot>
+	</text>
+	<!-- #endif -->
+</template>
+
+<script>
+	import { fontData } from './uniicons_file_vue.js';
+
+	const getVal = (val) => {
+		const reg = /^[0-9]*$/g
+		return (typeof val === 'number' || reg.test(val)) ? val + 'px' : val;
+	}
+
+	// #ifdef APP-NVUE
+	var domModule = weex.requireModule('dom');
+	import iconUrl from './uniicons.ttf'
+	domModule.addRule('fontFace', {
+		'fontFamily': "uniicons",
+		'src': "url('" + iconUrl + "')"
+	});
+	// #endif
+
+	/**
+	 * Icons 图标
+	 * @description 用于展示 icons 图标
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=28
+	 * @property {Number} size 图标大小
+	 * @property {String} type 图标图案,参考示例
+	 * @property {String} color 图标颜色
+	 * @property {String} customPrefix 自定义图标
+	 * @event {Function} click 点击 Icon 触发事件
+	 */
+	export default {
+		name: 'UniIcons',
+		emits: ['click'],
+		props: {
+			type: {
+				type: String,
+				default: ''
+			},
+			color: {
+				type: String,
+				default: '#333333'
+			},
+			size: {
+				type: [Number, String],
+				default: 16
+			},
+			customPrefix: {
+				type: String,
+				default: ''
+			},
+			fontFamily: {
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {
+				icons: fontData
+			}
+		},
+		computed: {
+			unicode() {
+				let code = this.icons.find(v => v.font_class === this.type)
+				if (code) {
+					return code.unicode
+				}
+				return ''
+			},
+			iconSize() {
+				return getVal(this.size)
+			},
+			styleObj() {
+				if (this.fontFamily !== '') {
+					return `color: ${this.color}; font-size: ${this.iconSize}; font-family: ${this.fontFamily};`
+				}
+				return `color: ${this.color}; font-size: ${this.iconSize};`
+			}
+		},
+		methods: {
+			_onClick(e) {
+				this.$emit('click', e)
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	/* #ifndef APP-NVUE */
+	@import './uniicons.css';
+
+	@font-face {
+		font-family: uniicons;
+		src: url('./uniicons.ttf');
+	}
+
+	/* #endif */
+	.uni-icons {
+		font-family: uniicons;
+		text-decoration: none;
+		text-align: center;
+	}
+</style>

+ 664 - 0
uni_modules/uni-icons/components/uni-icons/uniicons.css

@@ -0,0 +1,664 @@
+
+.uniui-cart-filled:before {
+  content: "\e6d0";
+}
+
+.uniui-gift-filled:before {
+  content: "\e6c4";
+}
+
+.uniui-color:before {
+  content: "\e6cf";
+}
+
+.uniui-wallet:before {
+  content: "\e6b1";
+}
+
+.uniui-settings-filled:before {
+  content: "\e6ce";
+}
+
+.uniui-auth-filled:before {
+  content: "\e6cc";
+}
+
+.uniui-shop-filled:before {
+  content: "\e6cd";
+}
+
+.uniui-staff-filled:before {
+  content: "\e6cb";
+}
+
+.uniui-vip-filled:before {
+  content: "\e6c6";
+}
+
+.uniui-plus-filled:before {
+  content: "\e6c7";
+}
+
+.uniui-folder-add-filled:before {
+  content: "\e6c8";
+}
+
+.uniui-color-filled:before {
+  content: "\e6c9";
+}
+
+.uniui-tune-filled:before {
+  content: "\e6ca";
+}
+
+.uniui-calendar-filled:before {
+  content: "\e6c0";
+}
+
+.uniui-notification-filled:before {
+  content: "\e6c1";
+}
+
+.uniui-wallet-filled:before {
+  content: "\e6c2";
+}
+
+.uniui-medal-filled:before {
+  content: "\e6c3";
+}
+
+.uniui-fire-filled:before {
+  content: "\e6c5";
+}
+
+.uniui-refreshempty:before {
+  content: "\e6bf";
+}
+
+.uniui-location-filled:before {
+  content: "\e6af";
+}
+
+.uniui-person-filled:before {
+  content: "\e69d";
+}
+
+.uniui-personadd-filled:before {
+  content: "\e698";
+}
+
+.uniui-arrowthinleft:before {
+  content: "\e6d2";
+}
+
+.uniui-arrowthinup:before {
+  content: "\e6d3";
+}
+
+.uniui-arrowthindown:before {
+  content: "\e6d4";
+}
+
+.uniui-back:before {
+  content: "\e6b9";
+}
+
+.uniui-forward:before {
+  content: "\e6ba";
+}
+
+.uniui-arrow-right:before {
+  content: "\e6bb";
+}
+
+.uniui-arrow-left:before {
+  content: "\e6bc";
+}
+
+.uniui-arrow-up:before {
+  content: "\e6bd";
+}
+
+.uniui-arrow-down:before {
+  content: "\e6be";
+}
+
+.uniui-arrowthinright:before {
+  content: "\e6d1";
+}
+
+.uniui-down:before {
+  content: "\e6b8";
+}
+
+.uniui-bottom:before {
+  content: "\e6b8";
+}
+
+.uniui-arrowright:before {
+  content: "\e6d5";
+}
+
+.uniui-right:before {
+  content: "\e6b5";
+}
+
+.uniui-up:before {
+  content: "\e6b6";
+}
+
+.uniui-top:before {
+  content: "\e6b6";
+}
+
+.uniui-left:before {
+  content: "\e6b7";
+}
+
+.uniui-arrowup:before {
+  content: "\e6d6";
+}
+
+.uniui-eye:before {
+  content: "\e651";
+}
+
+.uniui-eye-filled:before {
+  content: "\e66a";
+}
+
+.uniui-eye-slash:before {
+  content: "\e6b3";
+}
+
+.uniui-eye-slash-filled:before {
+  content: "\e6b4";
+}
+
+.uniui-info-filled:before {
+  content: "\e649";
+}
+
+.uniui-reload:before {
+  content: "\e6b2";
+}
+
+.uniui-micoff-filled:before {
+  content: "\e6b0";
+}
+
+.uniui-map-pin-ellipse:before {
+  content: "\e6ac";
+}
+
+.uniui-map-pin:before {
+  content: "\e6ad";
+}
+
+.uniui-location:before {
+  content: "\e6ae";
+}
+
+.uniui-starhalf:before {
+  content: "\e683";
+}
+
+.uniui-star:before {
+  content: "\e688";
+}
+
+.uniui-star-filled:before {
+  content: "\e68f";
+}
+
+.uniui-calendar:before {
+  content: "\e6a0";
+}
+
+.uniui-fire:before {
+  content: "\e6a1";
+}
+
+.uniui-medal:before {
+  content: "\e6a2";
+}
+
+.uniui-font:before {
+  content: "\e6a3";
+}
+
+.uniui-gift:before {
+  content: "\e6a4";
+}
+
+.uniui-link:before {
+  content: "\e6a5";
+}
+
+.uniui-notification:before {
+  content: "\e6a6";
+}
+
+.uniui-staff:before {
+  content: "\e6a7";
+}
+
+.uniui-vip:before {
+  content: "\e6a8";
+}
+
+.uniui-folder-add:before {
+  content: "\e6a9";
+}
+
+.uniui-tune:before {
+  content: "\e6aa";
+}
+
+.uniui-auth:before {
+  content: "\e6ab";
+}
+
+.uniui-person:before {
+  content: "\e699";
+}
+
+.uniui-email-filled:before {
+  content: "\e69a";
+}
+
+.uniui-phone-filled:before {
+  content: "\e69b";
+}
+
+.uniui-phone:before {
+  content: "\e69c";
+}
+
+.uniui-email:before {
+  content: "\e69e";
+}
+
+.uniui-personadd:before {
+  content: "\e69f";
+}
+
+.uniui-chatboxes-filled:before {
+  content: "\e692";
+}
+
+.uniui-contact:before {
+  content: "\e693";
+}
+
+.uniui-chatbubble-filled:before {
+  content: "\e694";
+}
+
+.uniui-contact-filled:before {
+  content: "\e695";
+}
+
+.uniui-chatboxes:before {
+  content: "\e696";
+}
+
+.uniui-chatbubble:before {
+  content: "\e697";
+}
+
+.uniui-upload-filled:before {
+  content: "\e68e";
+}
+
+.uniui-upload:before {
+  content: "\e690";
+}
+
+.uniui-weixin:before {
+  content: "\e691";
+}
+
+.uniui-compose:before {
+  content: "\e67f";
+}
+
+.uniui-qq:before {
+  content: "\e680";
+}
+
+.uniui-download-filled:before {
+  content: "\e681";
+}
+
+.uniui-pyq:before {
+  content: "\e682";
+}
+
+.uniui-sound:before {
+  content: "\e684";
+}
+
+.uniui-trash-filled:before {
+  content: "\e685";
+}
+
+.uniui-sound-filled:before {
+  content: "\e686";
+}
+
+.uniui-trash:before {
+  content: "\e687";
+}
+
+.uniui-videocam-filled:before {
+  content: "\e689";
+}
+
+.uniui-spinner-cycle:before {
+  content: "\e68a";
+}
+
+.uniui-weibo:before {
+  content: "\e68b";
+}
+
+.uniui-videocam:before {
+  content: "\e68c";
+}
+
+.uniui-download:before {
+  content: "\e68d";
+}
+
+.uniui-help:before {
+  content: "\e679";
+}
+
+.uniui-navigate-filled:before {
+  content: "\e67a";
+}
+
+.uniui-plusempty:before {
+  content: "\e67b";
+}
+
+.uniui-smallcircle:before {
+  content: "\e67c";
+}
+
+.uniui-minus-filled:before {
+  content: "\e67d";
+}
+
+.uniui-micoff:before {
+  content: "\e67e";
+}
+
+.uniui-closeempty:before {
+  content: "\e66c";
+}
+
+.uniui-clear:before {
+  content: "\e66d";
+}
+
+.uniui-navigate:before {
+  content: "\e66e";
+}
+
+.uniui-minus:before {
+  content: "\e66f";
+}
+
+.uniui-image:before {
+  content: "\e670";
+}
+
+.uniui-mic:before {
+  content: "\e671";
+}
+
+.uniui-paperplane:before {
+  content: "\e672";
+}
+
+.uniui-close:before {
+  content: "\e673";
+}
+
+.uniui-help-filled:before {
+  content: "\e674";
+}
+
+.uniui-paperplane-filled:before {
+  content: "\e675";
+}
+
+.uniui-plus:before {
+  content: "\e676";
+}
+
+.uniui-mic-filled:before {
+  content: "\e677";
+}
+
+.uniui-image-filled:before {
+  content: "\e678";
+}
+
+.uniui-locked-filled:before {
+  content: "\e668";
+}
+
+.uniui-info:before {
+  content: "\e669";
+}
+
+.uniui-locked:before {
+  content: "\e66b";
+}
+
+.uniui-camera-filled:before {
+  content: "\e658";
+}
+
+.uniui-chat-filled:before {
+  content: "\e659";
+}
+
+.uniui-camera:before {
+  content: "\e65a";
+}
+
+.uniui-circle:before {
+  content: "\e65b";
+}
+
+.uniui-checkmarkempty:before {
+  content: "\e65c";
+}
+
+.uniui-chat:before {
+  content: "\e65d";
+}
+
+.uniui-circle-filled:before {
+  content: "\e65e";
+}
+
+.uniui-flag:before {
+  content: "\e65f";
+}
+
+.uniui-flag-filled:before {
+  content: "\e660";
+}
+
+.uniui-gear-filled:before {
+  content: "\e661";
+}
+
+.uniui-home:before {
+  content: "\e662";
+}
+
+.uniui-home-filled:before {
+  content: "\e663";
+}
+
+.uniui-gear:before {
+  content: "\e664";
+}
+
+.uniui-smallcircle-filled:before {
+  content: "\e665";
+}
+
+.uniui-map-filled:before {
+  content: "\e666";
+}
+
+.uniui-map:before {
+  content: "\e667";
+}
+
+.uniui-refresh-filled:before {
+  content: "\e656";
+}
+
+.uniui-refresh:before {
+  content: "\e657";
+}
+
+.uniui-cloud-upload:before {
+  content: "\e645";
+}
+
+.uniui-cloud-download-filled:before {
+  content: "\e646";
+}
+
+.uniui-cloud-download:before {
+  content: "\e647";
+}
+
+.uniui-cloud-upload-filled:before {
+  content: "\e648";
+}
+
+.uniui-redo:before {
+  content: "\e64a";
+}
+
+.uniui-images-filled:before {
+  content: "\e64b";
+}
+
+.uniui-undo-filled:before {
+  content: "\e64c";
+}
+
+.uniui-more:before {
+  content: "\e64d";
+}
+
+.uniui-more-filled:before {
+  content: "\e64e";
+}
+
+.uniui-undo:before {
+  content: "\e64f";
+}
+
+.uniui-images:before {
+  content: "\e650";
+}
+
+.uniui-paperclip:before {
+  content: "\e652";
+}
+
+.uniui-settings:before {
+  content: "\e653";
+}
+
+.uniui-search:before {
+  content: "\e654";
+}
+
+.uniui-redo-filled:before {
+  content: "\e655";
+}
+
+.uniui-list:before {
+  content: "\e644";
+}
+
+.uniui-mail-open-filled:before {
+  content: "\e63a";
+}
+
+.uniui-hand-down-filled:before {
+  content: "\e63c";
+}
+
+.uniui-hand-down:before {
+  content: "\e63d";
+}
+
+.uniui-hand-up-filled:before {
+  content: "\e63e";
+}
+
+.uniui-hand-up:before {
+  content: "\e63f";
+}
+
+.uniui-heart-filled:before {
+  content: "\e641";
+}
+
+.uniui-mail-open:before {
+  content: "\e643";
+}
+
+.uniui-heart:before {
+  content: "\e639";
+}
+
+.uniui-loop:before {
+  content: "\e633";
+}
+
+.uniui-pulldown:before {
+  content: "\e632";
+}
+
+.uniui-scan:before {
+  content: "\e62a";
+}
+
+.uniui-bars:before {
+  content: "\e627";
+}
+
+.uniui-checkbox:before {
+  content: "\e62b";
+}
+
+.uniui-checkbox-filled:before {
+  content: "\e62c";
+}
+
+.uniui-shop:before {
+  content: "\e62f";
+}
+
+.uniui-headphones:before {
+  content: "\e630";
+}
+
+.uniui-cart:before {
+  content: "\e631";
+}

二進制
uni_modules/uni-icons/components/uni-icons/uniicons.ttf


+ 664 - 0
uni_modules/uni-icons/components/uni-icons/uniicons_file.ts

@@ -0,0 +1,664 @@
+
+export type IconsData = {
+	id : string
+	name : string
+	font_family : string
+	css_prefix_text : string
+	description : string
+	glyphs : Array<IconsDataItem>
+}
+
+export type IconsDataItem = {
+	font_class : string
+	unicode : string
+}
+
+
+export const fontData = [
+  {
+    "font_class": "arrow-down",
+    "unicode": "\ue6be"
+  },
+  {
+    "font_class": "arrow-left",
+    "unicode": "\ue6bc"
+  },
+  {
+    "font_class": "arrow-right",
+    "unicode": "\ue6bb"
+  },
+  {
+    "font_class": "arrow-up",
+    "unicode": "\ue6bd"
+  },
+  {
+    "font_class": "auth",
+    "unicode": "\ue6ab"
+  },
+  {
+    "font_class": "auth-filled",
+    "unicode": "\ue6cc"
+  },
+  {
+    "font_class": "back",
+    "unicode": "\ue6b9"
+  },
+  {
+    "font_class": "bars",
+    "unicode": "\ue627"
+  },
+  {
+    "font_class": "calendar",
+    "unicode": "\ue6a0"
+  },
+  {
+    "font_class": "calendar-filled",
+    "unicode": "\ue6c0"
+  },
+  {
+    "font_class": "camera",
+    "unicode": "\ue65a"
+  },
+  {
+    "font_class": "camera-filled",
+    "unicode": "\ue658"
+  },
+  {
+    "font_class": "cart",
+    "unicode": "\ue631"
+  },
+  {
+    "font_class": "cart-filled",
+    "unicode": "\ue6d0"
+  },
+  {
+    "font_class": "chat",
+    "unicode": "\ue65d"
+  },
+  {
+    "font_class": "chat-filled",
+    "unicode": "\ue659"
+  },
+  {
+    "font_class": "chatboxes",
+    "unicode": "\ue696"
+  },
+  {
+    "font_class": "chatboxes-filled",
+    "unicode": "\ue692"
+  },
+  {
+    "font_class": "chatbubble",
+    "unicode": "\ue697"
+  },
+  {
+    "font_class": "chatbubble-filled",
+    "unicode": "\ue694"
+  },
+  {
+    "font_class": "checkbox",
+    "unicode": "\ue62b"
+  },
+  {
+    "font_class": "checkbox-filled",
+    "unicode": "\ue62c"
+  },
+  {
+    "font_class": "checkmarkempty",
+    "unicode": "\ue65c"
+  },
+  {
+    "font_class": "circle",
+    "unicode": "\ue65b"
+  },
+  {
+    "font_class": "circle-filled",
+    "unicode": "\ue65e"
+  },
+  {
+    "font_class": "clear",
+    "unicode": "\ue66d"
+  },
+  {
+    "font_class": "close",
+    "unicode": "\ue673"
+  },
+  {
+    "font_class": "closeempty",
+    "unicode": "\ue66c"
+  },
+  {
+    "font_class": "cloud-download",
+    "unicode": "\ue647"
+  },
+  {
+    "font_class": "cloud-download-filled",
+    "unicode": "\ue646"
+  },
+  {
+    "font_class": "cloud-upload",
+    "unicode": "\ue645"
+  },
+  {
+    "font_class": "cloud-upload-filled",
+    "unicode": "\ue648"
+  },
+  {
+    "font_class": "color",
+    "unicode": "\ue6cf"
+  },
+  {
+    "font_class": "color-filled",
+    "unicode": "\ue6c9"
+  },
+  {
+    "font_class": "compose",
+    "unicode": "\ue67f"
+  },
+  {
+    "font_class": "contact",
+    "unicode": "\ue693"
+  },
+  {
+    "font_class": "contact-filled",
+    "unicode": "\ue695"
+  },
+  {
+    "font_class": "down",
+    "unicode": "\ue6b8"
+  },
+	{
+	  "font_class": "bottom",
+	  "unicode": "\ue6b8"
+	},
+  {
+    "font_class": "download",
+    "unicode": "\ue68d"
+  },
+  {
+    "font_class": "download-filled",
+    "unicode": "\ue681"
+  },
+  {
+    "font_class": "email",
+    "unicode": "\ue69e"
+  },
+  {
+    "font_class": "email-filled",
+    "unicode": "\ue69a"
+  },
+  {
+    "font_class": "eye",
+    "unicode": "\ue651"
+  },
+  {
+    "font_class": "eye-filled",
+    "unicode": "\ue66a"
+  },
+  {
+    "font_class": "eye-slash",
+    "unicode": "\ue6b3"
+  },
+  {
+    "font_class": "eye-slash-filled",
+    "unicode": "\ue6b4"
+  },
+  {
+    "font_class": "fire",
+    "unicode": "\ue6a1"
+  },
+  {
+    "font_class": "fire-filled",
+    "unicode": "\ue6c5"
+  },
+  {
+    "font_class": "flag",
+    "unicode": "\ue65f"
+  },
+  {
+    "font_class": "flag-filled",
+    "unicode": "\ue660"
+  },
+  {
+    "font_class": "folder-add",
+    "unicode": "\ue6a9"
+  },
+  {
+    "font_class": "folder-add-filled",
+    "unicode": "\ue6c8"
+  },
+  {
+    "font_class": "font",
+    "unicode": "\ue6a3"
+  },
+  {
+    "font_class": "forward",
+    "unicode": "\ue6ba"
+  },
+  {
+    "font_class": "gear",
+    "unicode": "\ue664"
+  },
+  {
+    "font_class": "gear-filled",
+    "unicode": "\ue661"
+  },
+  {
+    "font_class": "gift",
+    "unicode": "\ue6a4"
+  },
+  {
+    "font_class": "gift-filled",
+    "unicode": "\ue6c4"
+  },
+  {
+    "font_class": "hand-down",
+    "unicode": "\ue63d"
+  },
+  {
+    "font_class": "hand-down-filled",
+    "unicode": "\ue63c"
+  },
+  {
+    "font_class": "hand-up",
+    "unicode": "\ue63f"
+  },
+  {
+    "font_class": "hand-up-filled",
+    "unicode": "\ue63e"
+  },
+  {
+    "font_class": "headphones",
+    "unicode": "\ue630"
+  },
+  {
+    "font_class": "heart",
+    "unicode": "\ue639"
+  },
+  {
+    "font_class": "heart-filled",
+    "unicode": "\ue641"
+  },
+  {
+    "font_class": "help",
+    "unicode": "\ue679"
+  },
+  {
+    "font_class": "help-filled",
+    "unicode": "\ue674"
+  },
+  {
+    "font_class": "home",
+    "unicode": "\ue662"
+  },
+  {
+    "font_class": "home-filled",
+    "unicode": "\ue663"
+  },
+  {
+    "font_class": "image",
+    "unicode": "\ue670"
+  },
+  {
+    "font_class": "image-filled",
+    "unicode": "\ue678"
+  },
+  {
+    "font_class": "images",
+    "unicode": "\ue650"
+  },
+  {
+    "font_class": "images-filled",
+    "unicode": "\ue64b"
+  },
+  {
+    "font_class": "info",
+    "unicode": "\ue669"
+  },
+  {
+    "font_class": "info-filled",
+    "unicode": "\ue649"
+  },
+  {
+    "font_class": "left",
+    "unicode": "\ue6b7"
+  },
+  {
+    "font_class": "link",
+    "unicode": "\ue6a5"
+  },
+  {
+    "font_class": "list",
+    "unicode": "\ue644"
+  },
+  {
+    "font_class": "location",
+    "unicode": "\ue6ae"
+  },
+  {
+    "font_class": "location-filled",
+    "unicode": "\ue6af"
+  },
+  {
+    "font_class": "locked",
+    "unicode": "\ue66b"
+  },
+  {
+    "font_class": "locked-filled",
+    "unicode": "\ue668"
+  },
+  {
+    "font_class": "loop",
+    "unicode": "\ue633"
+  },
+  {
+    "font_class": "mail-open",
+    "unicode": "\ue643"
+  },
+  {
+    "font_class": "mail-open-filled",
+    "unicode": "\ue63a"
+  },
+  {
+    "font_class": "map",
+    "unicode": "\ue667"
+  },
+  {
+    "font_class": "map-filled",
+    "unicode": "\ue666"
+  },
+  {
+    "font_class": "map-pin",
+    "unicode": "\ue6ad"
+  },
+  {
+    "font_class": "map-pin-ellipse",
+    "unicode": "\ue6ac"
+  },
+  {
+    "font_class": "medal",
+    "unicode": "\ue6a2"
+  },
+  {
+    "font_class": "medal-filled",
+    "unicode": "\ue6c3"
+  },
+  {
+    "font_class": "mic",
+    "unicode": "\ue671"
+  },
+  {
+    "font_class": "mic-filled",
+    "unicode": "\ue677"
+  },
+  {
+    "font_class": "micoff",
+    "unicode": "\ue67e"
+  },
+  {
+    "font_class": "micoff-filled",
+    "unicode": "\ue6b0"
+  },
+  {
+    "font_class": "minus",
+    "unicode": "\ue66f"
+  },
+  {
+    "font_class": "minus-filled",
+    "unicode": "\ue67d"
+  },
+  {
+    "font_class": "more",
+    "unicode": "\ue64d"
+  },
+  {
+    "font_class": "more-filled",
+    "unicode": "\ue64e"
+  },
+  {
+    "font_class": "navigate",
+    "unicode": "\ue66e"
+  },
+  {
+    "font_class": "navigate-filled",
+    "unicode": "\ue67a"
+  },
+  {
+    "font_class": "notification",
+    "unicode": "\ue6a6"
+  },
+  {
+    "font_class": "notification-filled",
+    "unicode": "\ue6c1"
+  },
+  {
+    "font_class": "paperclip",
+    "unicode": "\ue652"
+  },
+  {
+    "font_class": "paperplane",
+    "unicode": "\ue672"
+  },
+  {
+    "font_class": "paperplane-filled",
+    "unicode": "\ue675"
+  },
+  {
+    "font_class": "person",
+    "unicode": "\ue699"
+  },
+  {
+    "font_class": "person-filled",
+    "unicode": "\ue69d"
+  },
+  {
+    "font_class": "personadd",
+    "unicode": "\ue69f"
+  },
+  {
+    "font_class": "personadd-filled",
+    "unicode": "\ue698"
+  },
+  {
+    "font_class": "personadd-filled-copy",
+    "unicode": "\ue6d1"
+  },
+  {
+    "font_class": "phone",
+    "unicode": "\ue69c"
+  },
+  {
+    "font_class": "phone-filled",
+    "unicode": "\ue69b"
+  },
+  {
+    "font_class": "plus",
+    "unicode": "\ue676"
+  },
+  {
+    "font_class": "plus-filled",
+    "unicode": "\ue6c7"
+  },
+  {
+    "font_class": "plusempty",
+    "unicode": "\ue67b"
+  },
+  {
+    "font_class": "pulldown",
+    "unicode": "\ue632"
+  },
+  {
+    "font_class": "pyq",
+    "unicode": "\ue682"
+  },
+  {
+    "font_class": "qq",
+    "unicode": "\ue680"
+  },
+  {
+    "font_class": "redo",
+    "unicode": "\ue64a"
+  },
+  {
+    "font_class": "redo-filled",
+    "unicode": "\ue655"
+  },
+  {
+    "font_class": "refresh",
+    "unicode": "\ue657"
+  },
+  {
+    "font_class": "refresh-filled",
+    "unicode": "\ue656"
+  },
+  {
+    "font_class": "refreshempty",
+    "unicode": "\ue6bf"
+  },
+  {
+    "font_class": "reload",
+    "unicode": "\ue6b2"
+  },
+  {
+    "font_class": "right",
+    "unicode": "\ue6b5"
+  },
+  {
+    "font_class": "scan",
+    "unicode": "\ue62a"
+  },
+  {
+    "font_class": "search",
+    "unicode": "\ue654"
+  },
+  {
+    "font_class": "settings",
+    "unicode": "\ue653"
+  },
+  {
+    "font_class": "settings-filled",
+    "unicode": "\ue6ce"
+  },
+  {
+    "font_class": "shop",
+    "unicode": "\ue62f"
+  },
+  {
+    "font_class": "shop-filled",
+    "unicode": "\ue6cd"
+  },
+  {
+    "font_class": "smallcircle",
+    "unicode": "\ue67c"
+  },
+  {
+    "font_class": "smallcircle-filled",
+    "unicode": "\ue665"
+  },
+  {
+    "font_class": "sound",
+    "unicode": "\ue684"
+  },
+  {
+    "font_class": "sound-filled",
+    "unicode": "\ue686"
+  },
+  {
+    "font_class": "spinner-cycle",
+    "unicode": "\ue68a"
+  },
+  {
+    "font_class": "staff",
+    "unicode": "\ue6a7"
+  },
+  {
+    "font_class": "staff-filled",
+    "unicode": "\ue6cb"
+  },
+  {
+    "font_class": "star",
+    "unicode": "\ue688"
+  },
+  {
+    "font_class": "star-filled",
+    "unicode": "\ue68f"
+  },
+  {
+    "font_class": "starhalf",
+    "unicode": "\ue683"
+  },
+  {
+    "font_class": "trash",
+    "unicode": "\ue687"
+  },
+  {
+    "font_class": "trash-filled",
+    "unicode": "\ue685"
+  },
+  {
+    "font_class": "tune",
+    "unicode": "\ue6aa"
+  },
+  {
+    "font_class": "tune-filled",
+    "unicode": "\ue6ca"
+  },
+  {
+    "font_class": "undo",
+    "unicode": "\ue64f"
+  },
+  {
+    "font_class": "undo-filled",
+    "unicode": "\ue64c"
+  },
+  {
+    "font_class": "up",
+    "unicode": "\ue6b6"
+  },
+	{
+	  "font_class": "top",
+	  "unicode": "\ue6b6"
+	},
+  {
+    "font_class": "upload",
+    "unicode": "\ue690"
+  },
+  {
+    "font_class": "upload-filled",
+    "unicode": "\ue68e"
+  },
+  {
+    "font_class": "videocam",
+    "unicode": "\ue68c"
+  },
+  {
+    "font_class": "videocam-filled",
+    "unicode": "\ue689"
+  },
+  {
+    "font_class": "vip",
+    "unicode": "\ue6a8"
+  },
+  {
+    "font_class": "vip-filled",
+    "unicode": "\ue6c6"
+  },
+  {
+    "font_class": "wallet",
+    "unicode": "\ue6b1"
+  },
+  {
+    "font_class": "wallet-filled",
+    "unicode": "\ue6c2"
+  },
+  {
+    "font_class": "weibo",
+    "unicode": "\ue68b"
+  },
+  {
+    "font_class": "weixin",
+    "unicode": "\ue691"
+  }
+] as IconsDataItem[]
+
+// export const fontData = JSON.parse<IconsDataItem>(fontDataJson)

+ 649 - 0
uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js

@@ -0,0 +1,649 @@
+
+export const fontData = [
+  {
+    "font_class": "arrow-down",
+    "unicode": "\ue6be"
+  },
+  {
+    "font_class": "arrow-left",
+    "unicode": "\ue6bc"
+  },
+  {
+    "font_class": "arrow-right",
+    "unicode": "\ue6bb"
+  },
+  {
+    "font_class": "arrow-up",
+    "unicode": "\ue6bd"
+  },
+  {
+    "font_class": "auth",
+    "unicode": "\ue6ab"
+  },
+  {
+    "font_class": "auth-filled",
+    "unicode": "\ue6cc"
+  },
+  {
+    "font_class": "back",
+    "unicode": "\ue6b9"
+  },
+  {
+    "font_class": "bars",
+    "unicode": "\ue627"
+  },
+  {
+    "font_class": "calendar",
+    "unicode": "\ue6a0"
+  },
+  {
+    "font_class": "calendar-filled",
+    "unicode": "\ue6c0"
+  },
+  {
+    "font_class": "camera",
+    "unicode": "\ue65a"
+  },
+  {
+    "font_class": "camera-filled",
+    "unicode": "\ue658"
+  },
+  {
+    "font_class": "cart",
+    "unicode": "\ue631"
+  },
+  {
+    "font_class": "cart-filled",
+    "unicode": "\ue6d0"
+  },
+  {
+    "font_class": "chat",
+    "unicode": "\ue65d"
+  },
+  {
+    "font_class": "chat-filled",
+    "unicode": "\ue659"
+  },
+  {
+    "font_class": "chatboxes",
+    "unicode": "\ue696"
+  },
+  {
+    "font_class": "chatboxes-filled",
+    "unicode": "\ue692"
+  },
+  {
+    "font_class": "chatbubble",
+    "unicode": "\ue697"
+  },
+  {
+    "font_class": "chatbubble-filled",
+    "unicode": "\ue694"
+  },
+  {
+    "font_class": "checkbox",
+    "unicode": "\ue62b"
+  },
+  {
+    "font_class": "checkbox-filled",
+    "unicode": "\ue62c"
+  },
+  {
+    "font_class": "checkmarkempty",
+    "unicode": "\ue65c"
+  },
+  {
+    "font_class": "circle",
+    "unicode": "\ue65b"
+  },
+  {
+    "font_class": "circle-filled",
+    "unicode": "\ue65e"
+  },
+  {
+    "font_class": "clear",
+    "unicode": "\ue66d"
+  },
+  {
+    "font_class": "close",
+    "unicode": "\ue673"
+  },
+  {
+    "font_class": "closeempty",
+    "unicode": "\ue66c"
+  },
+  {
+    "font_class": "cloud-download",
+    "unicode": "\ue647"
+  },
+  {
+    "font_class": "cloud-download-filled",
+    "unicode": "\ue646"
+  },
+  {
+    "font_class": "cloud-upload",
+    "unicode": "\ue645"
+  },
+  {
+    "font_class": "cloud-upload-filled",
+    "unicode": "\ue648"
+  },
+  {
+    "font_class": "color",
+    "unicode": "\ue6cf"
+  },
+  {
+    "font_class": "color-filled",
+    "unicode": "\ue6c9"
+  },
+  {
+    "font_class": "compose",
+    "unicode": "\ue67f"
+  },
+  {
+    "font_class": "contact",
+    "unicode": "\ue693"
+  },
+  {
+    "font_class": "contact-filled",
+    "unicode": "\ue695"
+  },
+  {
+    "font_class": "down",
+    "unicode": "\ue6b8"
+  },
+	{
+	  "font_class": "bottom",
+	  "unicode": "\ue6b8"
+	},
+  {
+    "font_class": "download",
+    "unicode": "\ue68d"
+  },
+  {
+    "font_class": "download-filled",
+    "unicode": "\ue681"
+  },
+  {
+    "font_class": "email",
+    "unicode": "\ue69e"
+  },
+  {
+    "font_class": "email-filled",
+    "unicode": "\ue69a"
+  },
+  {
+    "font_class": "eye",
+    "unicode": "\ue651"
+  },
+  {
+    "font_class": "eye-filled",
+    "unicode": "\ue66a"
+  },
+  {
+    "font_class": "eye-slash",
+    "unicode": "\ue6b3"
+  },
+  {
+    "font_class": "eye-slash-filled",
+    "unicode": "\ue6b4"
+  },
+  {
+    "font_class": "fire",
+    "unicode": "\ue6a1"
+  },
+  {
+    "font_class": "fire-filled",
+    "unicode": "\ue6c5"
+  },
+  {
+    "font_class": "flag",
+    "unicode": "\ue65f"
+  },
+  {
+    "font_class": "flag-filled",
+    "unicode": "\ue660"
+  },
+  {
+    "font_class": "folder-add",
+    "unicode": "\ue6a9"
+  },
+  {
+    "font_class": "folder-add-filled",
+    "unicode": "\ue6c8"
+  },
+  {
+    "font_class": "font",
+    "unicode": "\ue6a3"
+  },
+  {
+    "font_class": "forward",
+    "unicode": "\ue6ba"
+  },
+  {
+    "font_class": "gear",
+    "unicode": "\ue664"
+  },
+  {
+    "font_class": "gear-filled",
+    "unicode": "\ue661"
+  },
+  {
+    "font_class": "gift",
+    "unicode": "\ue6a4"
+  },
+  {
+    "font_class": "gift-filled",
+    "unicode": "\ue6c4"
+  },
+  {
+    "font_class": "hand-down",
+    "unicode": "\ue63d"
+  },
+  {
+    "font_class": "hand-down-filled",
+    "unicode": "\ue63c"
+  },
+  {
+    "font_class": "hand-up",
+    "unicode": "\ue63f"
+  },
+  {
+    "font_class": "hand-up-filled",
+    "unicode": "\ue63e"
+  },
+  {
+    "font_class": "headphones",
+    "unicode": "\ue630"
+  },
+  {
+    "font_class": "heart",
+    "unicode": "\ue639"
+  },
+  {
+    "font_class": "heart-filled",
+    "unicode": "\ue641"
+  },
+  {
+    "font_class": "help",
+    "unicode": "\ue679"
+  },
+  {
+    "font_class": "help-filled",
+    "unicode": "\ue674"
+  },
+  {
+    "font_class": "home",
+    "unicode": "\ue662"
+  },
+  {
+    "font_class": "home-filled",
+    "unicode": "\ue663"
+  },
+  {
+    "font_class": "image",
+    "unicode": "\ue670"
+  },
+  {
+    "font_class": "image-filled",
+    "unicode": "\ue678"
+  },
+  {
+    "font_class": "images",
+    "unicode": "\ue650"
+  },
+  {
+    "font_class": "images-filled",
+    "unicode": "\ue64b"
+  },
+  {
+    "font_class": "info",
+    "unicode": "\ue669"
+  },
+  {
+    "font_class": "info-filled",
+    "unicode": "\ue649"
+  },
+  {
+    "font_class": "left",
+    "unicode": "\ue6b7"
+  },
+  {
+    "font_class": "link",
+    "unicode": "\ue6a5"
+  },
+  {
+    "font_class": "list",
+    "unicode": "\ue644"
+  },
+  {
+    "font_class": "location",
+    "unicode": "\ue6ae"
+  },
+  {
+    "font_class": "location-filled",
+    "unicode": "\ue6af"
+  },
+  {
+    "font_class": "locked",
+    "unicode": "\ue66b"
+  },
+  {
+    "font_class": "locked-filled",
+    "unicode": "\ue668"
+  },
+  {
+    "font_class": "loop",
+    "unicode": "\ue633"
+  },
+  {
+    "font_class": "mail-open",
+    "unicode": "\ue643"
+  },
+  {
+    "font_class": "mail-open-filled",
+    "unicode": "\ue63a"
+  },
+  {
+    "font_class": "map",
+    "unicode": "\ue667"
+  },
+  {
+    "font_class": "map-filled",
+    "unicode": "\ue666"
+  },
+  {
+    "font_class": "map-pin",
+    "unicode": "\ue6ad"
+  },
+  {
+    "font_class": "map-pin-ellipse",
+    "unicode": "\ue6ac"
+  },
+  {
+    "font_class": "medal",
+    "unicode": "\ue6a2"
+  },
+  {
+    "font_class": "medal-filled",
+    "unicode": "\ue6c3"
+  },
+  {
+    "font_class": "mic",
+    "unicode": "\ue671"
+  },
+  {
+    "font_class": "mic-filled",
+    "unicode": "\ue677"
+  },
+  {
+    "font_class": "micoff",
+    "unicode": "\ue67e"
+  },
+  {
+    "font_class": "micoff-filled",
+    "unicode": "\ue6b0"
+  },
+  {
+    "font_class": "minus",
+    "unicode": "\ue66f"
+  },
+  {
+    "font_class": "minus-filled",
+    "unicode": "\ue67d"
+  },
+  {
+    "font_class": "more",
+    "unicode": "\ue64d"
+  },
+  {
+    "font_class": "more-filled",
+    "unicode": "\ue64e"
+  },
+  {
+    "font_class": "navigate",
+    "unicode": "\ue66e"
+  },
+  {
+    "font_class": "navigate-filled",
+    "unicode": "\ue67a"
+  },
+  {
+    "font_class": "notification",
+    "unicode": "\ue6a6"
+  },
+  {
+    "font_class": "notification-filled",
+    "unicode": "\ue6c1"
+  },
+  {
+    "font_class": "paperclip",
+    "unicode": "\ue652"
+  },
+  {
+    "font_class": "paperplane",
+    "unicode": "\ue672"
+  },
+  {
+    "font_class": "paperplane-filled",
+    "unicode": "\ue675"
+  },
+  {
+    "font_class": "person",
+    "unicode": "\ue699"
+  },
+  {
+    "font_class": "person-filled",
+    "unicode": "\ue69d"
+  },
+  {
+    "font_class": "personadd",
+    "unicode": "\ue69f"
+  },
+  {
+    "font_class": "personadd-filled",
+    "unicode": "\ue698"
+  },
+  {
+    "font_class": "personadd-filled-copy",
+    "unicode": "\ue6d1"
+  },
+  {
+    "font_class": "phone",
+    "unicode": "\ue69c"
+  },
+  {
+    "font_class": "phone-filled",
+    "unicode": "\ue69b"
+  },
+  {
+    "font_class": "plus",
+    "unicode": "\ue676"
+  },
+  {
+    "font_class": "plus-filled",
+    "unicode": "\ue6c7"
+  },
+  {
+    "font_class": "plusempty",
+    "unicode": "\ue67b"
+  },
+  {
+    "font_class": "pulldown",
+    "unicode": "\ue632"
+  },
+  {
+    "font_class": "pyq",
+    "unicode": "\ue682"
+  },
+  {
+    "font_class": "qq",
+    "unicode": "\ue680"
+  },
+  {
+    "font_class": "redo",
+    "unicode": "\ue64a"
+  },
+  {
+    "font_class": "redo-filled",
+    "unicode": "\ue655"
+  },
+  {
+    "font_class": "refresh",
+    "unicode": "\ue657"
+  },
+  {
+    "font_class": "refresh-filled",
+    "unicode": "\ue656"
+  },
+  {
+    "font_class": "refreshempty",
+    "unicode": "\ue6bf"
+  },
+  {
+    "font_class": "reload",
+    "unicode": "\ue6b2"
+  },
+  {
+    "font_class": "right",
+    "unicode": "\ue6b5"
+  },
+  {
+    "font_class": "scan",
+    "unicode": "\ue62a"
+  },
+  {
+    "font_class": "search",
+    "unicode": "\ue654"
+  },
+  {
+    "font_class": "settings",
+    "unicode": "\ue653"
+  },
+  {
+    "font_class": "settings-filled",
+    "unicode": "\ue6ce"
+  },
+  {
+    "font_class": "shop",
+    "unicode": "\ue62f"
+  },
+  {
+    "font_class": "shop-filled",
+    "unicode": "\ue6cd"
+  },
+  {
+    "font_class": "smallcircle",
+    "unicode": "\ue67c"
+  },
+  {
+    "font_class": "smallcircle-filled",
+    "unicode": "\ue665"
+  },
+  {
+    "font_class": "sound",
+    "unicode": "\ue684"
+  },
+  {
+    "font_class": "sound-filled",
+    "unicode": "\ue686"
+  },
+  {
+    "font_class": "spinner-cycle",
+    "unicode": "\ue68a"
+  },
+  {
+    "font_class": "staff",
+    "unicode": "\ue6a7"
+  },
+  {
+    "font_class": "staff-filled",
+    "unicode": "\ue6cb"
+  },
+  {
+    "font_class": "star",
+    "unicode": "\ue688"
+  },
+  {
+    "font_class": "star-filled",
+    "unicode": "\ue68f"
+  },
+  {
+    "font_class": "starhalf",
+    "unicode": "\ue683"
+  },
+  {
+    "font_class": "trash",
+    "unicode": "\ue687"
+  },
+  {
+    "font_class": "trash-filled",
+    "unicode": "\ue685"
+  },
+  {
+    "font_class": "tune",
+    "unicode": "\ue6aa"
+  },
+  {
+    "font_class": "tune-filled",
+    "unicode": "\ue6ca"
+  },
+  {
+    "font_class": "undo",
+    "unicode": "\ue64f"
+  },
+  {
+    "font_class": "undo-filled",
+    "unicode": "\ue64c"
+  },
+  {
+    "font_class": "up",
+    "unicode": "\ue6b6"
+  },
+	{
+	  "font_class": "top",
+	  "unicode": "\ue6b6"
+	},
+  {
+    "font_class": "upload",
+    "unicode": "\ue690"
+  },
+  {
+    "font_class": "upload-filled",
+    "unicode": "\ue68e"
+  },
+  {
+    "font_class": "videocam",
+    "unicode": "\ue68c"
+  },
+  {
+    "font_class": "videocam-filled",
+    "unicode": "\ue689"
+  },
+  {
+    "font_class": "vip",
+    "unicode": "\ue6a8"
+  },
+  {
+    "font_class": "vip-filled",
+    "unicode": "\ue6c6"
+  },
+  {
+    "font_class": "wallet",
+    "unicode": "\ue6b1"
+  },
+  {
+    "font_class": "wallet-filled",
+    "unicode": "\ue6c2"
+  },
+  {
+    "font_class": "weibo",
+    "unicode": "\ue68b"
+  },
+  {
+    "font_class": "weixin",
+    "unicode": "\ue691"
+  }
+]
+
+// export const fontData = JSON.parse<IconsDataItem>(fontDataJson)

+ 111 - 0
uni_modules/uni-icons/package.json

@@ -0,0 +1,111 @@
+{
+  "id": "uni-icons",
+  "displayName": "uni-icons 图标",
+  "version": "2.0.12",
+  "description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。",
+  "keywords": [
+    "uni-ui",
+    "uniui",
+    "icon",
+    "图标"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": "^3.2.14",
+    "uni-app": "^4.08",
+    "uni-app-x": "^4.61"
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+  "dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
+    "type": "component-vue",
+    "darkmode": "x",
+    "i18n": "x",
+    "widescreen": "x"
+  },
+  "uni_modules": {
+    "dependencies": [
+      "uni-scss"
+    ],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "x",
+        "aliyun": "x",
+        "alipay": "x"
+      },
+      "client": {
+        "uni-app": {
+          "vue": {
+            "vue2": "√",
+            "vue3": "√"
+          },
+          "web": {
+            "safari": "√",
+            "chrome": "√"
+          },
+          "app": {
+            "vue": "√",
+            "nvue": "-",
+            "android": {
+                "extVersion": "",
+                "minVersion": "29"
+            },
+            "ios": "√",
+            "harmony": "√"
+          },
+          "mp": {
+            "weixin": "√",
+            "alipay": "√",
+            "toutiao": "√",
+            "baidu": "√",
+            "kuaishou": "-",
+            "jd": "-",
+            "harmony": "-",
+            "qq": "√",
+            "lark": "-"
+          },
+          "quickapp": {
+            "huawei": "√",
+            "union": "√"
+          }
+        },
+        "uni-app-x": {
+          "web": {
+            "safari": "√",
+            "chrome": "√"
+          },
+          "app": {
+            "android": {
+                "extVersion": "",
+                "minVersion": "29"
+            },
+            "ios": "√",
+            "harmony": "√"
+          },
+          "mp": {
+            "weixin": "√"
+          }
+        }
+      }
+    }
+  }
+}

+ 8 - 0
uni_modules/uni-icons/readme.md

@@ -0,0 +1,8 @@
+## Icons 图标
+> **组件名:uni-icons**
+> 代码块: `uIcons`
+
+用于展示 icons 图标 。
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-icons)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 

+ 8 - 0
uni_modules/uni-scss/changelog.md

@@ -0,0 +1,8 @@
+## 1.0.3(2022-01-21)
+- 优化 组件示例
+## 1.0.2(2021-11-22)
+- 修复 / 符号在 vue 不同版本兼容问题引起的报错问题
+## 1.0.1(2021-11-22)
+- 修复 vue3中scss语法兼容问题
+## 1.0.0(2021-11-18)
+- init

+ 1 - 0
uni_modules/uni-scss/index.scss

@@ -0,0 +1 @@
+@import './styles/index.scss';

+ 82 - 0
uni_modules/uni-scss/package.json

@@ -0,0 +1,82 @@
+{
+  "id": "uni-scss",
+  "displayName": "uni-scss 辅助样式",
+  "version": "1.0.3",
+  "description": "uni-sass是uni-ui提供的一套全局样式 ,通过一些简单的类名和sass变量,实现简单的页面布局操作,比如颜色、边距、圆角等。",
+  "keywords": [
+    "uni-scss",
+    "uni-ui",
+    "辅助样式"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": "^3.1.0"
+  },
+  "dcloudext": {
+    "category": [
+        "JS SDK",
+        "通用 SDK"
+    ],
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "u"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "n",
+          "联盟": "n"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 4 - 0
uni_modules/uni-scss/readme.md

@@ -0,0 +1,4 @@
+`uni-sass` 是 `uni-ui`提供的一套全局样式 ,通过一些简单的类名和`sass`变量,实现简单的页面布局操作,比如颜色、边距、圆角等。
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-sass)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 

+ 7 - 0
uni_modules/uni-scss/styles/index.scss

@@ -0,0 +1,7 @@
+@import './setting/_variables.scss';
+@import './setting/_border.scss';
+@import './setting/_color.scss';
+@import './setting/_space.scss';
+@import './setting/_radius.scss';
+@import './setting/_text.scss';
+@import './setting/_styles.scss';

+ 3 - 0
uni_modules/uni-scss/styles/setting/_border.scss

@@ -0,0 +1,3 @@
+.uni-border {
+	border: 1px $uni-border-1 solid;
+}

+ 66 - 0
uni_modules/uni-scss/styles/setting/_color.scss

@@ -0,0 +1,66 @@
+
+// TODO 暂时不需要 class ,需要用户使用变量实现 ,如果使用类名其实并不推荐
+// @mixin get-styles($k,$c) {
+// 	@if $k == size or $k == weight{
+// 		font-#{$k}:#{$c}
+// 	}@else{
+// 		#{$k}:#{$c}
+// 	}
+// }
+$uni-ui-color:(
+	// 主色
+	primary: $uni-primary,
+	primary-disable: $uni-primary-disable,
+	primary-light: $uni-primary-light,
+	// 辅助色
+	success: $uni-success,
+	success-disable: $uni-success-disable,
+	success-light: $uni-success-light,
+	warning: $uni-warning,
+	warning-disable: $uni-warning-disable,
+	warning-light: $uni-warning-light,
+	error: $uni-error,
+	error-disable: $uni-error-disable,
+	error-light: $uni-error-light,
+	info: $uni-info,
+	info-disable: $uni-info-disable,
+	info-light: $uni-info-light,
+	// 中性色
+	main-color: $uni-main-color,
+	base-color: $uni-base-color,
+	secondary-color: $uni-secondary-color,
+	extra-color: $uni-extra-color,
+	// 背景色
+	bg-color: $uni-bg-color,
+	// 边框颜色
+	border-1: $uni-border-1,
+	border-2: $uni-border-2,
+	border-3: $uni-border-3,
+	border-4: $uni-border-4,
+	// 黑色
+	black:$uni-black,
+	// 白色
+	white:$uni-white,
+	// 透明
+	transparent:$uni-transparent
+) !default;
+@each $key, $child in $uni-ui-color {
+	.uni-#{"" + $key} {
+		color: $child;
+	}
+	.uni-#{"" + $key}-bg {
+		background-color: $child;
+	}
+}
+.uni-shadow-sm {
+	box-shadow: $uni-shadow-sm;
+}
+.uni-shadow-base {
+	box-shadow: $uni-shadow-base;
+}
+.uni-shadow-lg {
+	box-shadow: $uni-shadow-lg;
+}
+.uni-mask {
+	background-color:$uni-mask;
+}

部分文件因文件數量過多而無法顯示