zhangyaowen před 3 týdny
rodič
revize
bb8d309cf1

+ 2 - 2
.env

@@ -3,13 +3,13 @@ SHOPRO_VERSION=v2.4.1
 
 # 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
 SHOPRO_BASE_URL=https://zjkdywd.com:9018
-# SHOPRO_BASE_URL=https://192.168.10.17:9095
+# SHOPRO_BASE_URL=http://192.168.10.25:9095
 # SHOPRO_BASE_URL=http://42.194.163.46:9095
 
 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
 # 正式环境
 SHOPRO_DEV_BASE_URL=https://zjkdywd.com:9018
-# SHOPRO_DEV_BASE_URL=https://192.168.10.17:9095
+# SHOPRO_DEV_BASE_URL=http://192.168.10.25:9095
 # SHOPRO_DEV_BASE_URL=http://42.194.163.46:9095
 
 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc

+ 215 - 0
components/easy-hover/easy-hover.vue

@@ -0,0 +1,215 @@
+<template>
+	<view class="hover-wrapper"
+		:style="{ 'width': width + 'rpx', 'height': height + 'rpx', 'border-radius': circle ? '50%' : '0', 'top': top + 'px', 'left': left + 'px' }"
+		@touchmove.prevent="touchmove" @touchend="touchend" @tap="doTap">
+		<image :src="iconUrl" mode="aspectFill" class="icon"
+			:style="{ 'width': width + 'rpx', 'height': height + 'rpx', 'border-radius': circle ? '50%' : '0' }"></image>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'easy-hover',
+	props: {
+		/**
+		 * 初始化方向
+		 */
+		initSide: {
+			type: String,
+			default: 'right'
+		},
+		/**
+		 * 初始化距离上部距离rpx
+		 */
+		initMarginTop: {
+			type: Number,
+			default: 100
+		},
+		/**
+		 * 图标地址
+		 */
+		iconUrl: {
+			type: String,
+			default: ''
+		},
+		/**
+		 * 宽度
+		 */
+		width: {
+			type: Number,
+			default: 200
+		},
+		/**
+		 * 高度
+		 */
+		height: {
+			type: Number,
+			default: 200
+		},
+		/**
+		 * 是否是圆形
+		 */
+		circle: {
+			type: Boolean,
+			default: false
+		},
+		/**
+		 * 是否贴边
+		 */
+		stickSide: {
+			type: Boolean,
+			default: true
+		}
+
+	},
+	data() {
+		return {
+			screenWidthMax: 0,
+			screenHeightMax: 0,
+			widthMiddle: 0,
+			xOffset: 0,
+			yOffset: 0,
+			menuHeight: 0,
+			isMove: false,
+			top: 0,
+			left: 0,
+			animation: {},
+			animationData: {}
+		}
+	},
+	methods: {
+		/**
+		 * 初始化参数封装,主要处理界面参数
+		 */
+		initParams() {
+			try {
+				// 获取窗口信息
+				let systemInfo = {}
+				try {
+					systemInfo = uni.getSystemInfoSync()
+				} catch (e) {
+					// 获取系统信息失败时使用默认值
+					systemInfo = {
+						windowWidth: 375,
+						windowHeight: 667,
+						statusBarHeight: 20
+					}
+				}
+
+				this.screenWidthMax = systemInfo.windowWidth
+				this.screenHeightMax = systemInfo.windowHeight
+				this.widthMiddle = systemInfo.windowWidth / 2
+
+				// #ifdef H5
+				this.menuHeight = 0
+				// #endif
+				// #ifdef MP-WEIXIN
+				// 获取微信小程序胶囊按钮位置信息
+				const menuButtonInfo = wx.getMenuButtonBoundingClientRect()
+				this.menuHeight = menuButtonInfo.top || 0
+				// #endif
+				// #ifndef H5 || MP-WEIXIN
+				this.menuHeight = systemInfo.statusBarHeight || 0
+				// #endif
+
+				//计算偏移量
+				this.xOffset = this.width * systemInfo.windowWidth / 750
+				this.yOffset = this.height * systemInfo.windowWidth / 750
+				//计算top和left
+				this.top = this.menuHeight + (this.initMarginTop * systemInfo.windowWidth / 750)
+				this.left = this.initSide === 'left' ? (this.xOffset / 2) : this.initSide === 'right' ? (this.screenWidthMax - this
+					.xOffset / 2) : 0
+			} catch (e) {
+				console.error('获取系统信息失败:', e)
+			}
+		},
+		/**
+		 * 长按拖动
+		 */
+		touchmove(e) {
+			this.isMove = true;
+			let touch = e.touches[0] || e.changedTouches[0];
+			this.left = touch.clientX;
+			this.top = touch.clientY;
+		},
+		/**
+		 * 长按结束
+		 * 计算贴什么边,如果开启贴边则计算,否则不计算
+		 * 计算时注意下边和右边要减去一半的偏移量
+		 * 贴边计算时因为质心为中心,需要加上偏移量
+		 */
+		touchend(e) {
+			//超过边界放置于边界,不属于贴边,属于通用
+			let touch = e.touches[0] || e.changedTouches[0];
+			//开启贴边,贴边原则,只要一遍碰到边就,只要过中线也贴
+			if (this.stickSide) {
+				//x方向小于贴边
+				if (touch.clientX < this.xOffset / 2) {
+					this.left = this.xOffset / 2
+				}
+				//x方向大于贴边
+				if (touch.clientX < this.screenWidthMax - this.xOffset / 2) {
+					this.left = this.screenWidthMax - this.xOffset / 2
+				}
+				//x中线贴边
+				if (touch.clientX < this.widthMiddle) {
+					this.left = this.xOffset / 2
+				}
+				if (touch.clientX > this.widthMiddle) {
+					this.left = this.screenWidthMax - this.xOffset / 2
+				}
+				//y方向小于贴边
+				if (touch.clientY < this.yOffset) {
+					this.top = this.menuHeight
+				}
+				//y方向大于贴边
+				if (touch.clientY > this.screenHeightMax - this.yOffset) {
+					//需要计算偏移量
+					this.top = this.screenHeightMax - this.yOffset
+				}
+
+			} else {
+				if (touch.clientX < 0) {
+					this.left = this.xOffset / 2
+				}
+				if (touch.clientX > this.screenWidthMax) {
+					this.left = this.screenWidthMax - this.xOffset / 2
+				}
+				if (touch.clientY < 0) {
+					this.top = this.menuHeight
+				}
+				if (touch.clientY > this.screenHeightMax) {
+					//需要计算偏移量
+					this.top = this.screenHeightMax - this.yOffset
+				}
+			}
+
+		},
+		/**
+		 * 显示动画
+		 */
+		doAnimation(param, derection) { },
+		/**
+		 * 被点击
+		 */
+		doTap() {
+			this.$emit('taped')
+		}
+	},
+	created() {
+		this.initParams()
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.hover-wrapper {
+	z-index: 1000;
+	position: fixed;
+	overflow: hidden;
+	display: flex;
+	transform: translate(-50%, 0);
+	justify-content: center;
+	align-items: center;
+}
+</style>

+ 12 - 11
pages.json

@@ -74,17 +74,6 @@
         "group": "客服"
       }
     },
-    {
-      "path": "pages/ai/index",
-      "style": {
-        "navigationBarTitleText": "ai"
-      },
-      "meta": {
-        "sync": true,
-        "title": "ai",
-        "group": "ai"
-      }
-    },
     {
       "path": "pages/index/search",
       "style": {
@@ -735,6 +724,18 @@
             "title": "客服",
             "group": "客服"
           }
+        },
+        {
+          "path": "ai/index",
+          "style": {
+            "navigationBarTitleText": "ai"
+          },
+          "meta": {
+            "auth": true,
+            "sync": true,
+            "title": "ai",
+            "group": "ai"
+          }
         }
       ]
     },

+ 0 - 71
pages/ai/index.vue

@@ -1,71 +0,0 @@
-<!-- 文章展示 -->
-<template>
-  <s-layout :bgStyle="{ color: 'transparent' }" :title="state.title" class="set-wrap">
-    <view>
-      <!-- #ifdef H5 -->
-      <!-- <iframe src="http://42.194.163.46:9005/chatbot/Daahm2N28l24ECJx"
-        style="width: 100%; height: calc(100vh - 50px);"></iframe> -->
-      <!-- #endif -->
-
-      <!-- #ifdef MP-WEIXIN -->
-      <!-- <web-view src="http://42.194.163.46:9005/chatbot/Daahm2N28l24ECJx"
-        style="width: 100vw; height: 100vh;"></web-view> -->
-      <!-- #endif -->
-    </view>
-  </s-layout>
-  <view>
-  </view>
-</template>
-
-<script setup>
-import { onLoad } from '@dcloudio/uni-app';
-import { reactive, ref } from 'vue';
-import ArticleApi from '@/sheep/api/promotion/article';
-const webviewstyle = ref({
-  width: "100%",
-  height: "100%",
-  top: "100px",
-});
-const state = reactive({
-  title: '',
-  content: '',
-});
-
-async function getRichTextContent(id, title) {
-  const { code, data } = await ArticleApi.getArticle(id, title);
-  if (code !== 0) {
-    return;
-  }
-  state.content = data.content;
-  // 标题不一致时,修改标题
-  if (state.title !== data.title) {
-    state.title = data.title;
-    uni.setNavigationBarTitle({
-      title: state.title,
-    });
-  }
-}
-
-onLoad((options) => {
-  if (options.title) {
-    state.title = options.title;
-    uni.setNavigationBarTitle({
-      title: state.title,
-    });
-  }
-  getRichTextContent(options.id, options.title);
-});
-
-</script>
-
-<style lang="scss" scoped>
-.set-title {
-  margin: 0 30rpx;
-}
-
-iframe {
-  border: none;
-}
-
-.richtext {}
-</style>

+ 149 - 0
pages/chat/ai/eventSource.vue

@@ -0,0 +1,149 @@
+<!--
+ * @Author: HTangtang 1539880046@qq.com
+ * @Date: 2025-01-21 14:41:20
+ * @LastEditors: HTangtang 1539880046@qq.com
+ * @LastEditTime: 2025-03-06 17:49:40
+ * @FilePath: \dam_app_project\pages\smartAi\components\eventSource.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <!-- #ifdef H5 -->
+  <view id="event-source" :props="mergeProps" :change:props="eventSource.renderPropsChange" v-show="false">
+  </view>
+  <!-- #endif -->
+
+  <!-- #ifdef MP-WEIXIN -->
+  <view id="event-source" v-show="false">
+  </view>
+  <!-- #endif -->
+</template>
+
+<script>
+export default {
+  props: {
+    url: {
+      type: String,
+      default: "",
+    },
+    options: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      isSend: false
+    }
+  },
+  computed: {
+    // 合并传入renderjs的数据
+    mergeProps({ url, options, isSend }) {
+      return {
+        url,
+        options,
+        isSend
+      };
+    },
+  },
+  methods: {
+    // 发送
+    send() {
+      // #ifdef H5
+      this.isSend = true;
+      this.$nextTick(() => {
+        this.isSend = false;
+      });
+      // #endif
+
+      // #ifdef MP-WEIXIN
+      const { url, options = {} } = this;
+      const { headers, method, body } = options;
+
+      this.$emit("callback", { type: "onopen", msg: "Connection opened." });
+
+      uni.request({
+        url: url,
+        method: method || 'POST',
+        header: headers,
+        data: JSON.parse(body),
+        success: (res) => {
+          if (res.statusCode === 200) {
+            // 看起来返回的res.data是一个字符串,需要先处理掉开头的"data:"
+            // const rawData = res.data.replace(/^data:/, '');
+            // 然后解析JSON
+            console.log('Raw data:', res.data.split('data:').filter(item => item.trim() !== ''));
+            res.data.split('data:').filter(item => item.trim() !== '').forEach(item => {
+              const parsedData = JSON.parse(item);
+              console.log(item, parsedData, 5454545)
+              this.$emit("callback", { type: "onmessage", msg: "Message received.", data: JSON.stringify(parsedData) });  // 发送解析后的数据
+            });
+            // const parsedData = JSON.parse(rawData);
+            // this.$emit("callback", {
+            //   type: "onmessage",
+            //   msg: "Message received.",
+            //   data: JSON.stringify(parsedData)  // 发送解析后的数据
+            // });
+          } else {
+            this.$emit("callback", { type: "onerror", msg: "Request failed.", data: JSON.stringify(res) });
+          }
+        },
+        fail: (error) => {
+          this.$emit("callback", { type: "onerror", msg: "Request error.", data: JSON.stringify(error) });
+        },
+        complete: () => {
+          this.$emit("callback", { type: "onclose", msg: "Connection closed." });
+        }
+      });
+      // #endif
+    },
+    // 处理renderjs发回的数据
+    emits(e) {
+      this.$emit("callback", { ...e });
+    },
+  },
+};
+</script>
+
+<script module="eventSource" lang="renderjs">
+// #ifdef H5
+import { fetchEventSource } from "@microsoft/fetch-event-source";
+export default {
+  methods: {
+    // 传入数据变更
+    renderPropsChange(nVal) {
+      const { url, isSend } = nVal || {};
+      if (!isSend) return;
+      if (!url) return this.handleEmitData({ type: "tip", msg: "URL cannot be empty." });
+      this.$nextTick(() => {
+        this.handleSSE(nVal);
+      });
+    },
+    // 发送数据到service层
+    handleEmitData(data = {}) {
+      this.$ownerInstance.callMethod('emits', data);
+    },
+    // 处理SSE (H5)
+    handleSSE(opts = {}) {
+      const that = this;
+      if (!('EventSource' in window)) return this.handleEmitData({ type: "tip", msg: "The current device does not support EventSource." });
+      const { url, options = {} } = opts || {};
+      fetchEventSource(url, {
+        ...options,
+        async onopen() {
+          that.handleEmitData({ type: "onopen", msg: "EventSource onopen." });
+        },
+        onmessage(res) {
+          that.handleEmitData({ type: "onmessage", msg: "EventSource onmessage.", data: res.data });
+        },
+        onclose() {
+          that.handleEmitData({ type: "onclose", msg: "EventSource onclose." });
+        },
+        onerror(error) {
+          that.handleEmitData({ type: "onerror", msg: "EventSource onerror.", data: JSON.stringify(error) });
+        }
+      });
+    }
+  }
+}
+// #endif
+</script>

+ 408 - 0
pages/chat/ai/index.vue

@@ -0,0 +1,408 @@
+<template>
+  <s-layout ref="layoutRef" class="chat-wrap" title="ai" navbar="inner">
+    <EasyHover :iconUrl="sheep.$url.static('/static/home/xq.png')" :circle="false" :height="80" :initMarginTop="1000"
+      :width="80" @taped="triged" :stickSide="true" initSide="right">
+    </EasyHover>
+    <!-- <view class="dropdownClass">
+      <u-dropdown>
+        <u-dropdown-item v-model="value1" :title="options1[value1 - 1].label" :options="options1"></u-dropdown-item>
+      </u-dropdown>
+    </view> -->
+    <!--  覆盖头部导航栏背景颜色  -->
+    <view class="page-bg" :style="{ height: sys_navBar + 'px' }"></view>
+    <!--  聊天区域  -->
+    <MessageList ref="messageListRef" :queryShow="false">
+      <template #bottom>
+        <message-input :loading="loadingInput" v-model="chat.msg" @on-tools="onTools"
+          @send-message="onSendMessage"></message-input>
+      </template>
+    </MessageList>
+    <!--  聊天工具  -->
+    <tools-popup :show-tools="chat.showTools" :tools-mode="chat.toolsMode" @close="handleToolsClose" @on-emoji="onEmoji"
+      @image-select="onSelect" @on-show-select="onShowSelect">
+      <message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
+    </tools-popup>
+    <!--  产品订单选择  -->
+    <SelectPopup :mode="chat.selectMode" :show="chat.showSelect" @select="onSelect" @close="chat.showSelect = false" />
+    <EventSource ref="EventSourceRef" :url="eventSourceUrl" :options="eventSourceOptions" @callback="handleCallback" />
+    <template #right>
+      <view class="lsdhClass">
+        <view class="lsdhDetailClass addClass" style="" @click="addChat">
+          <u-icon name="plus"></u-icon>
+          新增对话
+        </view>
+
+        <view @click="detailCLick(item)" class="lsdhDetailClass" v-for="item in lsjlList" :key="item">
+          {{ item.title }}
+        </view>
+      </view>
+    </template>
+  </s-layout>
+</template>
+
+<script setup>
+import MessageList from '@/pages/chat/components/messageList.vue';
+import EventSource from './eventSource.vue';
+import { reactive, ref, toRefs, computed, provide, onMounted } from 'vue';
+import sheep from '@/sheep';
+import ToolsPopup from '@/pages/chat/components/toolsPopup.vue';
+import MessageInput from '@/pages/chat/components/messageInput.vue';
+import SelectPopup from '@/pages/chat/components/select-popup.vue';
+import EasyHover from '@/components/easy-hover/easy-hover.vue'
+
+import {
+  KeFuMessageContentTypeEnum
+} from '../util/constants';
+import FileApi from '@/sheep/api/infra/file';
+import KeFuApi from '@/sheep/api/promotion/kefu';
+import { useWebSocket } from '@/sheep/hooks/useWebSocket';
+import { jsonParse } from '@/sheep/util';
+import { onLoad } from '@dcloudio/uni-app';
+
+
+const EventSourceRef = ref(null); //AIref
+const chatMsgData = ref({});
+const messageListRef = ref();
+const eventSourceUrl = import.meta.env.SHOPRO_BASE_URL + '/app-api/ai/chat/message/dify-stream'; //ai客服URL
+// const eventSourceUrl = "https://192.168.10.17:9095/app-api/infra/ai-dify/chat-messages-stream"; //ai客服URL
+// ai客服流式上传参数
+const eventSourceOptions = computed(() => {
+  return {
+    headers: {
+      'content-type': 'application/json',
+      Accept: 'text/event-stream',
+      'tenant-id': 1,
+      authorization: uni.getStorageSync('token'),
+    },
+    method: 'POST',
+    body: JSON.stringify({
+      contentType: 1,
+      content: chatMsgData.value.content || chat.msg,
+      relUserId: route.value.relUserId,
+      stateId: messageListRef.value ? messageListRef.value.messageList.length + 2 : 2,
+      conversationId: reateDifyParams.value.id,
+      difyConversationId: difyConversationId.value,
+      // type: "律师咨询",
+      // query: chat.msg
+    }), // 请求体
+  };
+});
+const layoutRef = ref(null); // 布局组件引用
+
+const lsjlList = ref([]);
+const detailCLick = (item) => {
+  KeFuApi.conversationId({
+    tenantId: item.id,
+    conversationId: item.id,
+  }).then((res) => {
+    messageListRef.value.messageList = res.data.reverse().map((item) => {
+      if (item.type == 'user') {
+        item.senderId = userInfo.value.id;
+      }
+      return item;
+    });
+    layoutRef.value.show = false;
+  });
+};
+
+const lsjlClick = () => {
+  KeFuApi.conversationMyList().then((res) => {
+    lsjlList.value = res.data;
+    layoutRef.value.show = true;
+    console.log(layoutRef.value, 222221111)
+  });
+};
+const triged = () => {
+  console.log(7777)
+  lsjlClick()
+
+}
+
+const answerArr = ref('');
+const loadingInput = ref(false); // 加载状态
+provide('loadingInput', loadingInput); // 依赖注入加载状态
+const EventSourceFun = async (data, is = true) => {
+  console.log('张耀文2', data)
+  loadingInput.value = true;
+  chatMsgData.value = data;
+  const avatarObj = messageListRef.value.getAvatar() || {};
+  const params = {
+    // id: 1,
+    conversationId: reateDifyParams.value.id,
+    senderId: userInfo.value.id,
+    senderAvatar: avatarObj.senderAvatar || '',
+    senderType: 1,
+    stateId: messageListRef.value.messageList.length + 2,
+    receiverId: route.value.relUserId,
+    receiverAvatar: avatarObj.receiverAvatar || '',
+    receiverType: null,
+    contentType: 1,
+    content: is ? JSON.stringify({ text: chat.msg }) : data.content,
+    readStatus: true,
+    createTime: 1745546275000,
+  };
+  if (data.content != 'ecologicalValueCalculate') {
+    await messageListRef.value.refreshMessageList(JSON.parse(JSON.stringify(params)));
+  }
+  const params1 = {
+    ids: 1,
+    isAi: true,
+    isLoading: true,
+    conversationId: reateDifyParams.value.id,
+    senderId: route.value.relUserId,
+    senderAvatar: avatarObj.receiverAvatar || '',
+    stateId: messageListRef.value.messageList.length + 2,
+    senderType: 1,
+    receiverId: userInfo.value.id,
+    receiverAvatar: avatarObj.senderAvatar || '',
+    receiverType: null,
+    contentType: 22,
+    content: '',
+    readStatus: true,
+    createTime: 1745546275000,
+  };
+  await messageListRef.value.refreshMessageList(JSON.parse(JSON.stringify(params1)));
+  await EventSourceRef.value.send(data);
+};
+provide('EventSourceFun', EventSourceFun); // 依赖注入加载状态
+
+const loadingId = ref(''); // 加载的id
+// ai客服发送消息接收回调
+const difyConversationId = ref('');
+
+const handleCallback = async (e) => {
+  const { type, msg, data } = e || {};
+  if (type == 'onmessage') {
+    const datas = JSON.parse(data);
+    console.log(datas, 66666);
+    difyConversationId.value = datas.data.receive.difyConversationId;
+    if (datas.data.receive.event === null) {
+      await messageListRef.value.updateMessage({ ...datas.data.receive, contentType: 1 });
+    } else if (datas.data.receive.event == 'message') {
+      await messageListRef.value.updateMessage(datas.data.receive);
+    }
+  }
+  if (type == 'onclose') {
+    loadingInput.value = false;
+    // await messageListRef.value.updateMessage({}, loadingId.value);
+    answerArr.value = '';
+  }
+};
+const sys_navBar = sheep.$platform.navbar;
+const options1 = [
+  {
+    label: 'ai',
+    value: 1,
+  },
+  {
+    label: '律师咨询',
+    value: 2,
+  },
+  {
+    label: '金融相关',
+    value: 3,
+  },
+];
+const value1 = ref(1);
+const chat = reactive({
+  msg: '',
+  scrollInto: '',
+  showTools: false,
+  toolsMode: '',
+  showSelect: false,
+  selectMode: '',
+});
+const route = ref({});
+const reateDifyParams = ref({});
+const init = async () => {
+  const res = await KeFuApi.sendCreateDify({
+    roleId: 666,
+    knowledgeId: 1204,
+  });
+  reateDifyParams.value = res.data;
+};
+
+onLoad((options) => {
+  route.value = options;
+});
+onMounted(async () => {
+  await init();
+  loadingInput.value = true;
+  const data = {
+    conversationId: reateDifyParams.value.id,
+    contentType: KeFuMessageContentTypeEnum.TEXT,
+    stateId: messageListRef.value ? messageListRef.value.messageList.length + 2 : 2,
+    content: 'ecologicalValueCalculate',
+    relUserId: route.value.relUserId,
+  };
+  await EventSourceFun(data, false);
+});
+const userInfo = computed(() => sheep.$store('user').userInfo);
+const addChat = async () => {
+  await init();
+  loadingInput.value = true;
+  messageListRef.value.messageList = [];
+  layoutRef.value.show = false;
+  difyConversationId.value = '';
+  const data = {
+    conversationId: reateDifyParams.value.id,
+    contentType: KeFuMessageContentTypeEnum.TEXT,
+    stateId: messageListRef.value ? messageListRef.value.messageList.length + 2 : 2,
+    content: 'ecologicalValueCalculate',
+    relUserId: route.value.relUserId,
+  };
+  await EventSourceFun(data, false);
+}
+// 发送消息
+async function onSendMessage() {
+  if (!chat.msg) return;
+  try {
+    loadingInput.value = true;
+    const data = {
+      conversationId: reateDifyParams.value.id,
+      contentType: KeFuMessageContentTypeEnum.TEXT,
+      stateId: messageListRef.value.messageList.length + 2,
+      content: JSON.stringify({ text: chat.msg }),
+      relUserId: route.value.relUserId,
+    };
+    await EventSourceFun(data);
+    chat.msg = '';
+  } finally {
+    chat.showTools = false;
+  }
+}
+
+//======================= 聊天工具相关 start =======================
+
+function handleToolsClose() {
+  chat.showTools = false;
+  chat.toolsMode = '';
+}
+
+function onEmoji(item) {
+  chat.msg += item.name;
+}
+
+// 点击工具栏开关
+function onTools(mode) {
+  if (!chat.toolsMode || chat.toolsMode === mode) {
+    chat.showTools = !chat.showTools;
+  }
+  chat.toolsMode = mode;
+  if (!chat.showTools) {
+    chat.toolsMode = '';
+  }
+}
+
+function onShowSelect(mode) {
+  chat.showTools = false;
+  chat.showSelect = true;
+  chat.selectMode = mode;
+}
+
+async function onSelect({ type, data }) {
+  console.log(data, 555222233);
+  let msg;
+  switch (type) {
+    case 'image':
+      const res = await FileApi.uploadFile(data.tempFiles[0].path);
+      msg = {
+        contentType: KeFuMessageContentTypeEnum.IMAGE,
+        content: JSON.stringify({ picUrl: res.data }),
+        conversationId: reateDifyParams.value.id,
+      };
+      break;
+    case 'goods':
+      msg = {
+        contentType: KeFuMessageContentTypeEnum.PRODUCT,
+        content: JSON.stringify(data),
+        conversationId: reateDifyParams.value.id,
+      };
+      break;
+    case 'order':
+      msg = {
+        contentType: KeFuMessageContentTypeEnum.ORDER,
+        content: JSON.stringify(data),
+        conversationId: reateDifyParams.value.id,
+      };
+      break;
+  }
+  if (msg) {
+    // 发送消息
+    // scrollBottom();
+    // await KeFuApi.sendKefuMessage(msg);
+    await KeFuApi.listSendNew(msg);
+    await messageListRef.value.refreshMessageList();
+    chat.showTools = false;
+    chat.showSelect = false;
+    chat.selectMode = '';
+  }
+}
+</script>
+
+<style scoped lang="scss">
+:deep(.z-paging-content-fixed) {
+  // background: red;
+  // top: 40px;
+}
+
+:deep(.zp-paging-container-content) {
+  padding: 0px 10px;
+}
+
+.dropdownClass {
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.chat-wrap {
+  .page-bg {
+    width: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    background-color: #3a74f2;
+    z-index: 1;
+  }
+
+  .status {
+    position: relative;
+    box-sizing: border-box;
+    z-index: 3;
+    height: 70rpx;
+    padding: 0 30rpx;
+    background: var(--ui-BG-Main-opacity-1);
+    display: flex;
+    align-items: center;
+    font-size: 30rpx;
+    font-weight: 400;
+    color: var(--ui-BG-Main);
+  }
+}
+
+.lsdhClass {
+  margin: 20px 0;
+  height: calc(100vh - 40px);
+  overflow: auto;
+
+  .addClass {
+    background: #C1D6F7;
+    border: #C1D6F7 2px solid;
+    color: #4076FE;
+    border-radius: 20px;
+    font-weight: 800;
+    padding: 16px;
+    margin: 20px;
+  }
+
+  .lsdhDetailClass {
+    background: #f5f7f9;
+    padding: 16px;
+    margin: 20px;
+    width: 60vw;
+  }
+}
+
+a {
+  color: red !important;
+}
+</style>

+ 8 - 7
pages/chat/components/components/enterpriseCard.vue

@@ -3,12 +3,12 @@
   <view class="log-card">
     <view class="log-card__content">
       <view @click="handleClick(item)" class="log-card__text-wrapper" v-for="item in productSpuPageData" :key="item.id">
-        <text class="log-card__text">
-          {{ item.name }}
-        </text>
         <scroll-view class="log-card__image-group" scroll-x>
           <image class="log-card__image" mode="aspectFill" :src="item.picUrl" />
         </scroll-view>
+        <text class="log-card__text">
+          {{ item.name }}
+        </text>
       </view>
     </view>
   </view>
@@ -46,7 +46,7 @@ const handleClick = async (item) => {
   const data = {
     conversationId: route.value.conversationId,
     contentType: '1',
-    content: JSON.stringify({ text: `components|<enterpriseDetailCard id="${item.id}"></enterpriseDetailCard>` }),
+    content: `components|<enterpriseDetailCard id='${item.id}'></enterpriseDetailCard>`,
     relUserId: route.value.relUserId
   };
   console.log(data, 33333222);
@@ -92,7 +92,7 @@ productSpuPageAxios();
   &__text-wrapper {
     width: 100%;
     display: flex;
-    flex-direction: column;
+    // flex-direction: column;
     justify-content: flex-start;
     align-items: flex-start;
     gap: 24rpx;
@@ -104,6 +104,7 @@ productSpuPageAxios();
     line-height: 46rpx;
     color: var(---1, #222222);
     position: relative;
+
   }
 
   &__text {
@@ -118,8 +119,8 @@ productSpuPageAxios();
   }
 
   &__image {
-    width: 186rpx;
-    height: 186rpx;
+    width: 100rpx;
+    height: 100rpx;
     flex-shrink: 0;
     border-radius: 8rpx;
     margin-right: 16rpx;

+ 16 - 19
pages/chat/components/components/enterpriseDetailCard.vue

@@ -1,14 +1,13 @@
 <template>
-  <view class="titleClass">您好,我是赢伟达智能客服小助手,很高兴为您服务</view>
   <view class="log-card">
     <view class="log-card__content">
-      <view @click="handleClick(item)" class="log-card__text-wrapper" v-for="item in productSpuPageData" :key="item.id">
-        <text class="log-card__text">
-          {{ item.name }}
-        </text>
+      <view class="log-card__text-wrapper">
         <scroll-view class="log-card__image-group" scroll-x>
-          <image class="log-card__image" mode="aspectFill" :src="item.picUrl" />
+          <image class="log-card__image" mode="aspectFill" :src="productSpuPageData.picUrl" />
         </scroll-view>
+        <text class="log-card__text">
+          {{ productSpuPageData.name }}
+        </text>
       </view>
     </view>
   </view>
@@ -17,7 +16,7 @@
 <script setup>
 import { ref, inject } from "vue";
 import AiApi from '@/sheep/api/aiApi/index.js'
-import KeFuApi from '@/sheep/api/promotion/kefu';
+import SpuApi from '@/sheep/api/product/spu';
 import { onLoad } from "@dcloudio/uni-app";
 const props = defineProps({
   item: {
@@ -25,16 +24,13 @@ const props = defineProps({
     default: () => ({}),
   },
 });
-console.log(props.item, 33333222);
 const productSpuPageData = ref([])
 const productSpuPageAxios = async () => {
-  const res = await AiApi.productSpuPage({
-    page: 1,
-    pageSize: 10,
-    categoryIds: props.item.id,
-  });
-  productSpuPageData.value = res.data.list
-  console.log(res, 33333222);
+  SpuApi.getSpuDetail(props.item.id).then((res) => {
+    console.log(res, 333332229999);
+    productSpuPageData.value = res.data;
+  })
+
 };
 const route = ref({})
 onLoad((options) => {
@@ -46,7 +42,7 @@ const handleClick = async (item) => {
   const data = {
     conversationId: route.value.conversationId,
     contentType: '1',
-    content: JSON.stringify({ text: `<enterpriseDetailCard id="${item.id}" #spu='${item.id}'></enterpriseDetailCard>` }),
+    content: JSON.stringify({ text: `components|<enterpriseDetailCard id='${item.id}'></enterpriseDetailCard>` }),
     relUserId: route.value.relUserId
   };
   console.log(data, 33333222);
@@ -92,7 +88,7 @@ productSpuPageAxios();
   &__text-wrapper {
     width: 100%;
     display: flex;
-    flex-direction: column;
+    // flex-direction: column;
     justify-content: flex-start;
     align-items: flex-start;
     gap: 24rpx;
@@ -104,6 +100,7 @@ productSpuPageAxios();
     line-height: 46rpx;
     color: var(---1, #222222);
     position: relative;
+
   }
 
   &__text {
@@ -118,8 +115,8 @@ productSpuPageAxios();
   }
 
   &__image {
-    width: 186rpx;
-    height: 186rpx;
+    width: 100rpx;
+    height: 100rpx;
     flex-shrink: 0;
     border-radius: 8rpx;
     margin-right: 16rpx;

+ 1 - 0
pages/chat/components/eventSource.vue

@@ -71,6 +71,7 @@ export default {
             // 看起来返回的res.data是一个字符串,需要先处理掉开头的"data:"
             const rawData = res.data.replace(/^data:/, '');
             // 然后解析JSON
+
             const parsedData = JSON.parse(rawData);
             this.$emit("callback", {
               type: "onmessage",

+ 33 - 27
pages/chat/components/messageList.vue

@@ -29,12 +29,18 @@
 
 <script setup>
 import MessageListItem from '@/pages/chat/components/messageListItem.vue';
-import { reactive, ref } from 'vue';
+import { reactive, ref, nextTick } from 'vue';
 import KeFuApi from '@/sheep/api/promotion/kefu';
 import { isEmpty } from '@/sheep/helper/utils';
 import sheep from '@/sheep';
 import { formatDate } from '@/sheep/util';
 import { onLoad } from '@dcloudio/uni-app';
+const props = defineProps({
+  queryShow: {
+    type: Boolean,
+    default: true,
+  },
+});
 const sys_navBar = sheep.$platform.navbar;
 const messageList = ref([]); // 消息列表
 const showNewMessageTip = ref(false); // 显示有新消息提示
@@ -55,21 +61,22 @@ const queryParams = reactive({
 });
 onLoad((options) => {
   if (options.conversationId) {
-    queryParams.conversationId = options.conversationId
+    queryParams.conversationId = options.conversationId;
   } else {
-    queryParams.conversationId = '1'
+    queryParams.conversationId = '1';
   }
-})
+});
 const pagingRef = ref(null); // 虚拟列表
 const queryList = async (no, limit) => {
   // 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
   queryParams.no = no;
   queryParams.limit = limit;
-  await getMessageList();
+  if (props.queryShow) {
+    await getMessageList();
+  }
 };
 // 获得消息分页列表
 const getMessageList = async () => {
-
   const { data } = await KeFuApi.getKefuMessageList(queryParams);
   if (isEmpty(data)) {
     pagingRef.value.completeByNoMore([], true);
@@ -98,8 +105,11 @@ const getMessageList = async () => {
 /** 刷新消息列表 */
 const refreshMessageList = async (message = undefined) => {
   if (typeof message !== 'undefined') {
-    // 追加数据
-    pagingRef.value.addChatRecordData([message], false);
+    // 小程序环境下使用 nextTick 更新数据
+    nextTick(() => {
+      messageList.value = [message, ...messageList.value];
+      pagingRef.value.updateCache();
+    });
   } else {
     queryParams.createTime = undefined;
     refreshMessage.value = true;
@@ -129,31 +139,27 @@ const onScrollToUpper = () => {
 };
 // 暴露方法 修改消息列表指定的消息
 const updateMessage = (items, messageId = '') => {
+  let isShow = true
   messageList.value = messageList.value.map((item) => {
-    if (item.ids == 1) {
-      delete item.ids
-      item.content = items.content
-      item.messageId = items.messageId
-      item.isAi = true
-      return item;
-    } else {
-      if (messageId == "") {
-        if (item.messageId == items.messageId) {
-          item.content += items.content
-        }
-      } else {
-        if (item.messageId == messageId) {
-          item.isAi = false
-        }
-      }
+    if (item.ids === 1) {
+      delete item.ids;
+      item.content = items.content;
+      item.messageId = items.messageId;
+      item.isAi = false
+      isShow = false;
       return item;
     }
+    return item;
   });
-}
+  console.log("updateMessage", items.event)
+  if (items.event !== null && items.event !== "message_end" && isShow) {
+    messageList.value[0].content += items.content;
+  }
+};
 // 通过id 获取对应的头像
 const getAvatar = (id) => {
-  return messageList.value[messageList.value.length - 1]
-}
+  return messageList.value[messageList.value.length - 1];
+};
 
 defineExpose({ getMessageList, refreshMessageList, updateMessage, messageList, getAvatar });
 </script>

+ 28 - 27
pages/chat/components/messageListItem.vue

@@ -16,29 +16,23 @@
         </view>
       </view>
 
-
       <!-- 消息体渲染管理员消息和用户消息并左右展示  -->
-      <view v-if="message.contentType !== KeFuMessageContentTypeEnum.SYSTEM" class="ss-flex ss-col-top" :class="[
-        message.senderId !== userInfo.id
-          ? `ss-row-left` : `ss-row-right`,
-      ]">
+      <view v-if="message.contentType !== KeFuMessageContentTypeEnum.SYSTEM" class="ss-flex ss-col-top"
+        :class="[message.senderId !== userInfo.id ? `ss-row-left` : `ss-row-right`]">
         <!-- 客服头像 -->
-        <image v-show="message.receiverId === userInfo.id" class="chat-avatar ss-m-r-24" :src="message.receiverAvatar ||
-          default1
-          " mode="aspectFill"></image>
+        <image v-show="message.receiverId === userInfo.id" class="chat-avatar ss-m-r-24"
+          :src="message.receiverAvatar || default1" mode="aspectFill"></image>
         <template v-if="message.content.indexOf('components|') != -1">
           <view class="message-box" :class="{ admin: message.senderId === UserTypeEnum.ADMIN }">
-            <template v-if="dynamicComponent(extractComponentName(message.content))">
-              <enterprise-card v-if="extractComponentName(message.content) === 'enterpriseCard'" 
-                :item="extractAttributes(message.content.split('|')[1])" />
-              <enterprise-detail-card v-if="extractComponentName(message.content) === 'enterpriseDetailCard'"
-                :item="extractAttributes(message.content.split('|')[1])" />
-            </template>
+            <enterpriseCard v-if="extractComponentName(message.content) === 'enterpriseCard'"
+              :item="extractAttributes(message.content.split('|')[1])" />
+            <enterprise-detail-card v-if="extractComponentName(message.content) === 'enterpriseDetailCard'"
+              :item="extractAttributes(message.content.split('|')[1])" />
           </view>
         </template>
         <template v-else>
-
           <!-- 内容 -->
+
           <template v-if="message.contentType === KeFuMessageContentTypeEnum.TEXT">
             <view class="message-box" :class="{ admin: message.senderId === UserTypeEnum.ADMIN }">
               <mp-html :content="replaceEmoji(getMessageContent(message).text || message.content)" />
@@ -47,16 +41,17 @@
           <template v-if="message.contentType === KeFuMessageContentTypeEnum.IMAGE">
             <view class="message-box" :class="{ admin: message.senderId === UserTypeEnum.ADMIN }"
               :style="{ width: '200rpx' }">
-              <su-image class="message-img" isPreview
-                :previewList="[sheep.$url.cdn(getMessageContent(message).picUrl || message.content)]" :current="0"
-                :src="sheep.$url.cdn(getMessageContent(message).picUrl || message.content)" :height="200" :width="200"
-                mode="aspectFill"></su-image>
+              <su-image class="message-img" isPreview :previewList="[
+                sheep.$url.cdn(getMessageContent(message).picUrl || message.content),
+              ]" :current="0" :src="sheep.$url.cdn(getMessageContent(message).picUrl || message.content)" :height="200"
+                :width="200" mode="aspectFill"></su-image>
             </view>
           </template>
           <template v-if="message.contentType === KeFuMessageContentTypeEnum.PRODUCT">
             <div class="ss-m-b-10">
-              <GoodsItem :goodsData="getMessageContent(message)"
-                @tap="sheep.$router.go('/pages/goods/index', { id: getMessageContent(message).spuId })" />
+              <GoodsItem :goodsData="getMessageContent(message)" @tap="
+                sheep.$router.go('/pages/goods/index', { id: getMessageContent(message).spuId })
+                " />
             </div>
           </template>
           <template v-if="message.contentType === KeFuMessageContentTypeEnum.ORDER">
@@ -65,19 +60,18 @@
           </template>
           <template v-if="message.contentType === KeFuMessageContentTypeEnum.AI">
             <view class="message-box" :class="{ admin: message.senderId === UserTypeEnum.ADMIN }">
-              <zero-markdown-view
+              <zero-markdown-view themeColor="red"
                 :markdown="replaceEmoji(getMessageContent(message).text || message.content)"></zero-markdown-view>
               <!-- <mp-html :content="replaceEmoji(getMessageContent(message).text || message.content)" /> -->
               <!-- {{ message.messageId }}----
               {{ loadingId }} -->
-              <text v-if="loadingInput && message.isAi">...</text>
+              <text v-if="loadingInput && message.isAi">思考中...</text>
             </view>
           </template>
         </template>
         <!-- user头像 -->
-        <image v-if="message.senderId === userInfo.id" class="chat-avatar ss-m-l-24" :src="message.receiverAvatar ||
-          default1
-          " mode="aspectFill">
+        <image v-if="message.senderId === userInfo.id" class="chat-avatar ss-m-l-24"
+          :src="message.receiverAvatar || default1" mode="aspectFill">
         </image>
       </view>
     </view>
@@ -157,7 +151,7 @@ const extractAttributes = (tagString) => {
     const [_, key, value] = match;
     try {
       // 尝试解析JSON格式的值
-      attributes[key] = value.startsWith("{") ? JSON.parse(value) : value;
+      attributes[key] = value.startsWith('{') ? JSON.parse(value) : value;
     } catch (e) {
       // 如果不是JSON格式,直接使用原始值
       attributes[key] = value;
@@ -194,6 +188,13 @@ const dynamicComponent = (conm) => {
   };
   return components[conm];
 };
+const handleItemClick = (event) => {
+  const { node } = event.detail;
+  const { name, attrs = {}, children } = node;
+  const dataValue = attrs['data-value'];
+  const dataText = attrs['data-text'];
+  console.log(dataValue, dataText, 444444);
+};
 </script>
 
 <style scoped lang="scss">

+ 100 - 109
pages/chat/components/select-popup.vue

@@ -4,19 +4,10 @@
       <view class="title">
         <span>{{ mode == 'goods' ? '我的浏览' : '我的订单' }}</span>
       </view>
-      <scroll-view
-        class="scroll-box"
-        scroll-y="true"
-        :scroll-with-animation="true"
-        :show-scrollbar="false"
-        @scrolltolower="loadmore"
-      >
-        <view
-          class="item"
-          v-for="item in state.pagination.data"
-          :key="item.id"
-          @tap="emits('select', { type: mode, data: item })"
-        >
+      <scroll-view class="scroll-box" scroll-y="true" :scroll-with-animation="true" :show-scrollbar="false"
+        @scrolltolower="loadmore">
+        <view class="item" v-for="item in state.pagination.data" :key="item.id"
+          @tap="emits('select', { type: mode, data: item })">
           <template v-if="mode == 'goods'">
             <GoodsItem :goodsData="item" />
           </template>
@@ -31,121 +22,121 @@
 </template>
 
 <script setup>
-  import { reactive, watch } from 'vue';
-  import _ from 'lodash-es';
-  import GoodsItem from './goods.vue';
-  import OrderItem from './order.vue';
-  import OrderApi from '@/sheep/api/trade/order';
-  import SpuHistoryApi from '@/sheep/api/product/history';
+import { reactive, watch } from 'vue';
+import _ from 'lodash-es';
+import GoodsItem from './goods.vue';
+import OrderItem from './order.vue';
+import OrderApi from '@/sheep/api/trade/order';
+import SpuHistoryApi from '@/sheep/api/product/history';
 
-  const emits = defineEmits(['select', 'close']);
-  const props = defineProps({
-    mode: {
-      type: String,
-      default: 'goods',
-    },
-    show: {
-      type: Boolean,
-      default: false,
-    },
-  });
+const emits = defineEmits(['select', 'close']);
+const props = defineProps({
+  mode: {
+    type: String,
+    default: 'goods',
+  },
+  show: {
+    type: Boolean,
+    default: false,
+  },
+});
 
-  watch(
-    () => props.mode,
-    () => {
-      state.pagination.data = [];
-      if (props.mode) {
-        getList(state.pagination.page);
-      }
-    },
-  );
+watch(
+  () => props.mode,
+  () => {
+    state.pagination.data = [];
+    if (props.mode) {
+      getList(state.pagination.page);
+    }
+  },
+);
 
-  const state = reactive({
-    loadStatus: '',
-    pagination: {
-      data: [],
-      current_page: 1,
-      total: 1,
-      last_page: 1,
-    },
-  });
+const state = reactive({
+  loadStatus: '',
+  pagination: {
+    data: [],
+    current_page: 1,
+    total: 1,
+    last_page: 1,
+  },
+});
 
-  async function getList(page, list_rows = 5) {
-    state.loadStatus = 'loading';
-    const res =
-      props.mode == 'goods'
-        ? await SpuHistoryApi.getBrowseHistoryPage({
-            page,
-            list_rows,
-          })
-        : await OrderApi.getOrderPage({
-            page,
-            list_rows,
-          });
-    let orderList = _.concat(state.pagination.data, res.data.list);
-    state.pagination = {
-      ...res.data,
-      data: orderList,
-    };
-    if (state.pagination.current_page < state.pagination.last_page) {
-      state.loadStatus = 'more';
-    } else {
-      state.loadStatus = 'noMore';
-    }
+async function getList(page, list_rows = 5) {
+  state.loadStatus = 'loading';
+  const res =
+    props.mode == 'goods'
+      ? await SpuHistoryApi.getBrowseHistoryPage({
+        page,
+        list_rows,
+      })
+      : await OrderApi.getOrderPage({
+        page,
+        list_rows,
+      });
+  let orderList = _.concat(state.pagination.data, res.data.list);
+  state.pagination = {
+    ...res.data,
+    data: orderList,
+  };
+  if (state.pagination.current_page < state.pagination.last_page) {
+    state.loadStatus = 'more';
+  } else {
+    state.loadStatus = 'noMore';
   }
+}
 
-  function loadmore() {
-    if (state.loadStatus !== 'noMore') {
-      getList(state.pagination.current_page + 1);
-    }
+function loadmore() {
+  if (state.loadStatus !== 'noMore') {
+    getList(state.pagination.current_page + 1);
   }
+}
 </script>
 
 <style lang="scss" scoped>
-  .select-popup {
-    max-height: 600rpx;
+.select-popup {
+  max-height: 600rpx;
 
-    .title {
-      height: 100rpx;
-      line-height: 100rpx;
-      padding: 0 26rpx;
-      background: #fff;
-      border-radius: 20rpx 20rpx 0 0;
+  .title {
+    height: 100rpx;
+    line-height: 100rpx;
+    padding: 0 26rpx;
+    background: #fff;
+    border-radius: 20rpx 20rpx 0 0;
 
-      span {
-        font-size: 32rpx;
-        position: relative;
+    span {
+      font-size: 32rpx;
+      position: relative;
 
-        &::after {
-          content: '';
-          display: block;
-          width: 100%;
-          height: 2px;
-          z-index: 1;
-          position: absolute;
-          left: 0;
-          bottom: -15px;
-          background: var(--ui-BG-Main);
-          pointer-events: none;
-        }
+      &::after {
+        content: '';
+        display: block;
+        width: 100%;
+        height: 2px;
+        z-index: 1;
+        position: absolute;
+        left: 0;
+        bottom: -15px;
+        background: var(--ui-BG-Main);
+        pointer-events: none;
       }
     }
+  }
 
-    .scroll-box {
-      height: 500rpx;
-    }
+  .scroll-box {
+    height: 500rpx;
+  }
 
-    .item {
-      background: #fff;
-      margin: 26rpx 26rpx 0;
-      border-radius: 20rpx;
+  .item {
+    background: #fff;
+    margin: 26rpx 26rpx 0;
+    border-radius: 20rpx;
 
-      :deep() {
-        .image {
-          width: 140rpx;
-          height: 140rpx;
-        }
+    :deep() {
+      .image {
+        width: 140rpx;
+        height: 140rpx;
       }
     }
   }
+}
 </style>

+ 1 - 1
pages/chat/index copy.vue

@@ -35,7 +35,7 @@ import MessageInput from '@/pages/chat/components/messageInput.vue';
 import SelectPopup from '@/pages/chat/components/select-popup.vue';
 import {
   KeFuMessageContentTypeEnum,
-  WebSocketMessageTypeConstants,
+  WebSocketMessageTypeConstants
 } from '@/pages/chat/util/constants';
 import FileApi from '@/sheep/api/infra/file';
 import KeFuApi from '@/sheep/api/promotion/kefu';

+ 17 - 2
pages/chat/index.vue

@@ -35,9 +35,12 @@ import FileApi from '@/sheep/api/infra/file';
 import KeFuApi from '@/sheep/api/promotion/kefu';
 import { useWebSocket } from '@/sheep/hooks/useWebSocket';
 import { jsonParse } from '@/sheep/util';
-
+import { onLoad } from '@dcloudio/uni-app';
 const sys_navBar = sheep.$platform.navbar;
-
+const route = ref({})
+onLoad((options) => {
+  route.value = options
+});
 const chat = reactive({
   msg: '',
   scrollInto: '',
@@ -54,8 +57,11 @@ async function onSendMessage() {
     const data = {
       contentType: KeFuMessageContentTypeEnum.TEXT,
       content: JSON.stringify({ text: chat.msg }),
+      conversationId: route.value.conversationId,
+
     };
     await KeFuApi.sendKefuMessage(data);
+    await messageListRef.value.refreshMessageList();
     chat.msg = '';
   } finally {
     chat.showTools = false;
@@ -113,12 +119,21 @@ async function onSelect({ type, data }) {
       msg = {
         contentType: KeFuMessageContentTypeEnum.IMAGE,
         content: JSON.stringify({ picUrl: res.data }),
+        conversationId: route.value.conversationId,
+      };
+      break;
+    case 'video':
+      msg = {
+        contentType: KeFuMessageContentTypeEnum.VIDEO,
+        content: JSON.stringify({ videoUrl: data }),
+        conversationId: route.value.conversationId,
       };
       break;
     case 'goods':
       msg = {
         contentType: KeFuMessageContentTypeEnum.PRODUCT,
         content: JSON.stringify(data),
+        conversationId: route.value.conversationId,
       };
       break;
     case 'order':

+ 57 - 1
sheep/api/promotion/kefu.js

@@ -3,7 +3,62 @@ import request from '@/sheep/request';
 const KeFuApi = {
   sendKefuMessage: (data) => {
     return request({
-      url: '/promotion/kefu-message/send',
+      url: '/promotion/kefu-message/sendNew',
+      // url: '/promotion/kefu-message/send',
+      method: 'POST',
+      data,
+      custom: {
+        auth: true,
+        showLoading: true,
+        loadingMsg: '发送中',
+        showSuccess: true,
+        successMsg: '发送成功',
+      },
+    });
+  },
+
+  conversationMyList: (data) => {
+    return request({
+      url: '/ai/chat/conversation/my-list',
+      method: 'GET',
+      data,
+      custom: {
+        auth: false,
+        showLoading: false,
+        showSuccess: false,
+      },
+    });
+  },
+  conversationId: (data) => {
+    return request({
+      url: '/ai/chat/message/list-by-conversation-id',
+      method: 'GET',
+      data,
+      custom: {
+        auth: false,
+        showLoading: false,
+        showSuccess: false,
+      },
+    });
+  },
+
+  // 创建会话id 
+  sendCreateDify: (data) => {
+    return request({
+      url: '/ai/chat/conversation/create-dify',
+      method: 'POST',
+      data,
+      custom: {
+        auth: true,
+        showLoading: true,
+        showSuccess: true,
+      },
+    });
+  },
+  // dify 发送
+  sendDifyStream: (data) => {
+    return request({
+      url: '/ai/chat/message/dify-stream',
       method: 'POST',
       data,
       custom: {
@@ -15,6 +70,7 @@ const KeFuApi = {
       },
     });
   },
+
   listSendNew: (data) => {
     return request({
       url: '/promotion/kefu-message/sendNew',

+ 37 - 5
sheep/components/s-layout/s-layout.vue

@@ -10,7 +10,14 @@
         :showLeftButton="showLeftButton" />
       <view class="page-body" :style="[bgBody]">
         <!-- 顶部导航栏-情况3:沉浸式头部 -->
-        <su-inner-navbar v-if="navbar === 'inner'" :title="title" />
+        <su-inner-navbar v-if="navbar === 'inner'" :title="title">
+          <template #right>
+            <!-- <text class="sicon-more" @click="lsjlClick" style="font-size:26px;color: #000;" /> -->
+          </template>
+        </su-inner-navbar>
+        <u-popup border-radius="14" v-model="show" mode="right">
+          <slot name="right"></slot>
+        </u-popup>
         <view v-if="navbar === 'inner'" :style="[{ paddingTop: sheep?.$platform?.navbar + 'px' }]"></view>
 
         <!-- 顶部导航栏-情况4:装修组件导航栏-沉浸式 -->
@@ -40,7 +47,7 @@
 /**
  * 模板组件 - 提供页面公共组件,属性,方法
  */
-import { computed, onMounted } from 'vue';
+import { computed, onMounted, ref } from 'vue';
 import sheep from '@/sheep';
 import { isEmpty } from 'lodash-es';
 // #ifdef MP-WEIXIN
@@ -116,8 +123,12 @@ const props = defineProps({
     default: false,
   },
 });
-const emits = defineEmits(['search']);
-
+const emits = defineEmits(['search', 'lsjlClick']);
+const show = ref(false);
+const lsjlClick = () => {
+  show.value = !show.value;
+  emits('lsjlClick', show.value)
+};
 const sysStore = sheep.$store('sys');
 const userStore = sheep.$store('user');
 const appStore = sheep.$store('app');
@@ -199,6 +210,7 @@ onMounted(() => {
     sheep.$platform.share.updateShareInfo(shareInfo.value);
   }
 })
+defineExpose({ show });
 </script>
 
 <style lang="scss" scoped>
@@ -236,4 +248,24 @@ onMounted(() => {
     }
   }
 }
-</style>
+
+// :deep(.u-drawer-content) {
+//   height: 90vh !important;
+//   margin-top: 10vh !important;
+// }
+
+// /* #ifdef H5 */
+// :deep(.u-drawer-content) {
+//   height: 90vh !important;
+//   margin-top: 10vh !important;
+// }
+
+// /* #endif */
+
+// /* #ifdef MP-WEIXIN */
+// :deep(.u-popup__content) {
+//   height: 90vh !important;
+//   margin-top: 10vh !important;
+// }
+
+// /* #endif */</style>

+ 298 - 297
sheep/ui/su-inner-navbar/su-inner-navbar.vue

@@ -1,26 +1,13 @@
 <template>
-  <su-fixed
-    :noFixed="props.noFixed"
-    :alway="props.alway"
-    :bgStyles="props.bgStyles"
-    :val="0"
-    :index="props.zIndex"
-    noNav
-    :bg="props.bg"
-    :ui="props.ui"
-    :opacity="props.opacity"
-    :placeholder="props.placeholder"
-  >
+  <su-fixed :noFixed="props.noFixed" :alway="props.alway" :bgStyles="props.bgStyles" :val="0" :index="props.zIndex"
+    noNav :bg="props.bg" :ui="props.ui" :opacity="props.opacity" :placeholder="props.placeholder">
     <su-status-bar />
     <!-- 
       :class="[{ 'border-bottom': !props.opacity && props.bg != 'bg-none' }]"
      -->
     <view class="ui-navbar-box">
-      <view
-        class="ui-bar ss-p-x-20"
-        :class="state.isDark ? 'text-white' : 'text-black'"
-        :style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
-      >
+      <view class="ui-bar ss-p-x-20" :class="state.isDark ? 'text-white' : 'text-black'"
+        :style="[{ height: sys_navBar - sys_statusBar + 'px' }]">
         <view class="icon-box ss-flex">
           <view class="icon-button icon-button-left ss-flex ss-row-center" @tap="onClickLeft">
             <text class="sicon-back" v-if="hasHistory" />
@@ -34,6 +21,9 @@
         <slot name="center">
           <view class="center navbar-title">{{ title }}</view>
         </slot>
+        <slot name="right">
+        </slot>
+
         <!-- #ifdef MP -->
         <view :style="[state.capsuleStyle]"></view>
         <!-- #endif -->
@@ -43,323 +33,334 @@
 </template>
 
 <script setup>
-  /**
-   * 标题栏 - 基础组件navbar
-   *
-   * @param {Number}  zIndex = 100  							- 层级
-   * @param {Boolean}  back = true 							- 是否返回上一页
-   * @param {String}  backtext = ''  							- 返回文本
-   * @param {String}  bg = 'bg-white'  						- 公共Class
-   * @param {String}  status = ''  							- 状态栏颜色
-   * @param {Boolean}  alway = true							- 是否常驻
-   * @param {Boolean}  opacity = false  						- 是否开启透明渐变
-   * @param {Boolean}  noFixed = false  						- 是否浮动
-   * @param {String}  ui = ''									- 公共Class
-   * @param {Boolean}  capsule = false  						- 是否开启胶囊返回
-   * @param {Boolean}  stopBack = false 					    - 是否禁用返回
-   * @param {Boolean}  placeholder = true 					- 是否开启占位
-   * @param {Object}   bgStyles = {} 					    	- 背景样式
-   *
-   */
-
-  import { computed, reactive, onBeforeMount, ref } from 'vue';
-  import sheep from '@/sheep';
-  import { onPageScroll } from '@dcloudio/uni-app';
-  import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
-
-  // 本地数据
-  const state = reactive({
-    statusCur: '',
-    capsuleStyle: {},
-    capsuleBack: {},
-    isDark: true,
-  });
-
-  const sys_statusBar = sheep.$platform.device.statusBarHeight;
-  const sys_navBar = sheep.$platform.navbar;
-
-  const props = defineProps({
-    zIndex: {
-      type: Number,
-      default: 100,
-    },
-
-    title: {
-      //返回文本
-      type: String,
-      default: '',
-    },
-    bg: {
-      type: String,
-      default: 'bg-white',
-    },
-    // 常驻
-    alway: {
-      type: Boolean,
-      default: true,
-    },
-    opacity: {
-      //是否开启滑动渐变
-      type: Boolean,
-      default: true,
-    },
-    noFixed: {
-      //是否浮动
-      type: Boolean,
-      default: true,
-    },
-    ui: {
-      type: String,
-      default: '',
-    },
-    capsule: {
-      //是否开启胶囊返回
-      type: Boolean,
-      default: false,
-    },
-    stopBack: {
-      type: Boolean,
-      default: false,
-    },
-    placeholder: {
-      type: [Boolean],
-      default: false,
-    },
-    bgStyles: {
-      type: Object,
-      default() {},
-    },
-  });
-
-  const emits = defineEmits(['navback', 'clickLeft']);
-  const hasHistory = sheep.$router.hasHistory();
-
-  onBeforeMount(() => {
-    init();
-  });
-
-  onPageScroll((e) => {
-    let top = e.scrollTop;
-    state.isDark = top < sheep.$platform.navbar;
-  });
-
-  function onClickLeft() {
-    if (hasHistory) {
-      sheep.$router.back();
-    } else {
-      sheep.$router.go('/pages/index/index');
-    }
-    emits('clickLeft');
-  }
-  function onClickRight() {
-    showMenuTools();
+/**
+ * 标题栏 - 基础组件navbar
+ *
+ * @param {Number}  zIndex = 100  							- 层级
+ * @param {Boolean}  back = true 							- 是否返回上一页
+ * @param {String}  backtext = ''  							- 返回文本
+ * @param {String}  bg = 'bg-white'  						- 公共Class
+ * @param {String}  status = ''  							- 状态栏颜色
+ * @param {Boolean}  alway = true							- 是否常驻
+ * @param {Boolean}  opacity = false  						- 是否开启透明渐变
+ * @param {Boolean}  noFixed = false  						- 是否浮动
+ * @param {String}  ui = ''									- 公共Class
+ * @param {Boolean}  capsule = false  						- 是否开启胶囊返回
+ * @param {Boolean}  stopBack = false 					    - 是否禁用返回
+ * @param {Boolean}  placeholder = true 					- 是否开启占位
+ * @param {Object}   bgStyles = {} 					    	- 背景样式
+ *
+ */
+
+import { computed, reactive, onBeforeMount, ref } from 'vue';
+import sheep from '@/sheep';
+import { onPageScroll } from '@dcloudio/uni-app';
+import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
+
+// 本地数据
+const state = reactive({
+  statusCur: '',
+  capsuleStyle: {},
+  capsuleBack: {},
+  isDark: true,
+});
+
+const sys_statusBar = sheep.$platform.device.statusBarHeight;
+const sys_navBar = sheep.$platform.navbar;
+
+const props = defineProps({
+  zIndex: {
+    type: Number,
+    default: 100,
+  },
+
+  title: {
+    //返回文本
+    type: String,
+    default: '',
+  },
+  bg: {
+    type: String,
+    default: 'bg-white',
+  },
+  // 常驻
+  alway: {
+    type: Boolean,
+    default: true,
+  },
+  opacity: {
+    //是否开启滑动渐变
+    type: Boolean,
+    default: true,
+  },
+  noFixed: {
+    //是否浮动
+    type: Boolean,
+    default: true,
+  },
+  ui: {
+    type: String,
+    default: '',
+  },
+  capsule: {
+    //是否开启胶囊返回
+    type: Boolean,
+    default: false,
+  },
+  stopBack: {
+    type: Boolean,
+    default: false,
+  },
+  placeholder: {
+    type: [Boolean],
+    default: false,
+  },
+  bgStyles: {
+    type: Object,
+    default() { },
+  },
+});
+
+const emits = defineEmits(['navback', 'clickLeft']);
+const hasHistory = sheep.$router.hasHistory();
+
+onBeforeMount(() => {
+  init();
+});
+
+onPageScroll((e) => {
+  let top = e.scrollTop;
+  state.isDark = top < sheep.$platform.navbar;
+});
+
+function onClickLeft() {
+  if (hasHistory) {
+    sheep.$router.back();
+  } else {
+    sheep.$router.go('/pages/index/index');
   }
-
-  // 初始化
-  const init = () => {
-    // #ifdef MP-ALIPAY
-    my.hideAllFavoriteMenu();
-    // #endif
-    state.capsuleStyle = {
-      width: sheep.$platform.capsule.width + 'px',
-      height: sheep.$platform.capsule.height + 'px',
-    };
-
-    state.capsuleBack = state.capsuleStyle;
+  emits('clickLeft');
+}
+function onClickRight() {
+  showMenuTools();
+}
+
+// 初始化
+const init = () => {
+  // #ifdef MP-ALIPAY
+  my.hideAllFavoriteMenu();
+  // #endif
+  state.capsuleStyle = {
+    width: sheep.$platform.capsule.width + 'px',
+    height: sheep.$platform.capsule.height + 'px',
   };
+
+  state.capsuleBack = state.capsuleStyle;
+};
 </script>
 
 <style lang="scss" scoped>
-  .icon-box {
-    box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
-    border-radius: 30rpx;
-    width: 134rpx;
-    height: 56rpx;
-    margin-left: 8rpx;
-    border: 1px solid rgba(#fff, 0.4);
-    .line {
-      width: 2rpx;
-      height: 24rpx;
-      background: #e5e5e7;
-    }
-    .sicon-back {
-      font-size: 32rpx;
-    }
-    .sicon-home {
-      font-size: 32rpx;
-    }
-    .sicon-more {
-      font-size: 32rpx;
-    }
-    .icon-button {
-      width: 67rpx;
-      height: 56rpx;
-      &-left:hover {
-        background: rgba(0, 0, 0, 0.16);
-        border-radius: 30rpx 0px 0px 30rpx;
-      }
-      &-right:hover {
-        background: rgba(0, 0, 0, 0.16);
-        border-radius: 0px 30rpx 30rpx 0px;
-      }
-    }
+.icon-box {
+  box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
+  border-radius: 30rpx;
+  width: 134rpx;
+  height: 56rpx;
+  margin-left: 8rpx;
+  border: 1px solid rgba(#fff, 0.4);
+
+  .line {
+    width: 2rpx;
+    height: 24rpx;
+    background: #e5e5e7;
   }
-  .navbar-title {
-    font-size: 36rpx;
+
+  .sicon-back {
+    font-size: 32rpx;
   }
-  .tools-icon {
-    font-size: 40rpx;
+
+  .sicon-home {
+    font-size: 32rpx;
   }
-  .ui-navbar-box {
-    background-color: transparent;
-    width: 100%;
 
-    .ui-bar {
-      position: relative;
-      z-index: 2;
-      white-space: nowrap;
-      display: flex;
-      position: relative;
-      align-items: center;
-      justify-content: space-between;
+  .sicon-more {
+    font-size: 32rpx;
+  }
 
-      .left {
-        @include flex-bar;
+  .icon-button {
+    width: 67rpx;
+    height: 56rpx;
 
-        .back {
-          @include flex-bar;
-
-          .back-icon {
-            @include flex-center;
-            width: 56rpx;
-            height: 56rpx;
-            margin: 0 10rpx;
-            font-size: 46rpx !important;
-
-            &.opacityIcon {
-              position: relative;
-              border-radius: 50%;
-              background-color: rgba(127, 127, 127, 0.5);
-
-              &::after {
-                content: '';
-                display: block;
-                position: absolute;
-                height: 200%;
-                width: 200%;
-                left: 0;
-                top: 0;
-                border-radius: inherit;
-                transform: scale(0.5);
-                transform-origin: 0 0;
-                opacity: 0.1;
-                border: 1px solid currentColor;
-                pointer-events: none;
-              }
-
-              &::before {
-                transform: scale(0.9);
-              }
-            }
-          }
+    &-left:hover {
+      background: rgba(0, 0, 0, 0.16);
+      border-radius: 30rpx 0px 0px 30rpx;
+    }
 
-          /* #ifdef  MP-ALIPAY */
-          ._icon-back {
-            opacity: 0;
-          }
+    &-right:hover {
+      background: rgba(0, 0, 0, 0.16);
+      border-radius: 0px 30rpx 30rpx 0px;
+    }
+  }
+}
 
-          /* #endif */
-        }
+.navbar-title {
+  font-size: 36rpx;
+}
 
-        .capsule {
-          @include flex-bar;
-          border-radius: 100px;
-          position: relative;
+.tools-icon {
+  font-size: 40rpx;
+}
 
-          &.dark {
-            background-color: rgba(255, 255, 255, 0.5);
-          }
+.ui-navbar-box {
+  background-color: transparent;
+  width: 100%;
 
-          &.light {
-            background-color: rgba(0, 0, 0, 0.15);
-          }
+  .ui-bar {
+    position: relative;
+    z-index: 2;
+    white-space: nowrap;
+    display: flex;
+    position: relative;
+    align-items: center;
+    justify-content: space-between;
 
-          &::after {
-            content: '';
-            display: block;
-            position: absolute;
-            height: 60%;
-            width: 1px;
-            left: 50%;
-            top: 20%;
-            background-color: currentColor;
-            opacity: 0.1;
-            pointer-events: none;
-          }
+    .left {
+      @include flex-bar;
 
-          &::before {
-            content: '';
-            display: block;
-            position: absolute;
-            height: 200%;
-            width: 200%;
-            left: 0;
-            top: 0;
-            border-radius: inherit;
-            transform: scale(0.5);
-            transform-origin: 0 0;
-            opacity: 0.1;
-            border: 1px solid currentColor;
-            pointer-events: none;
-          }
+      .back {
+        @include flex-bar;
 
-          .capsule-back,
-          .capsule-home {
-            @include flex-center;
-            flex: 1;
-          }
+        .back-icon {
+          @include flex-center;
+          width: 56rpx;
+          height: 56rpx;
+          margin: 0 10rpx;
+          font-size: 46rpx !important;
+
+          &.opacityIcon {
+            position: relative;
+            border-radius: 50%;
+            background-color: rgba(127, 127, 127, 0.5);
 
-          &.isFristPage {
-            .capsule-back,
             &::after {
-              display: none;
+              content: '';
+              display: block;
+              position: absolute;
+              height: 200%;
+              width: 200%;
+              left: 0;
+              top: 0;
+              border-radius: inherit;
+              transform: scale(0.5);
+              transform-origin: 0 0;
+              opacity: 0.1;
+              border: 1px solid currentColor;
+              pointer-events: none;
+            }
+
+            &::before {
+              transform: scale(0.9);
             }
           }
         }
+
+        /* #ifdef  MP-ALIPAY */
+        ._icon-back {
+          opacity: 0;
+        }
+
+        /* #endif */
       }
 
-      .right {
+      .capsule {
         @include flex-bar;
+        border-radius: 100px;
+        position: relative;
 
-        .right-content {
-          @include flex;
-          flex-direction: row-reverse;
+        &.dark {
+          background-color: rgba(255, 255, 255, 0.5);
+        }
+
+        &.light {
+          background-color: rgba(0, 0, 0, 0.15);
         }
-      }
 
-      .center {
-        @include flex-center;
-        text-overflow: ellipsis;
-        // text-align: center;
-        position: absolute;
-        left: 50%;
-        transform: translateX(-50%);
+        &::after {
+          content: '';
+          display: block;
+          position: absolute;
+          height: 60%;
+          width: 1px;
+          left: 50%;
+          top: 20%;
+          background-color: currentColor;
+          opacity: 0.1;
+          pointer-events: none;
+        }
 
-        .image {
+        &::before {
+          content: '';
           display: block;
-          height: 36px;
-          max-width: calc(100vw - 200px);
+          position: absolute;
+          height: 200%;
+          width: 200%;
+          left: 0;
+          top: 0;
+          border-radius: inherit;
+          transform: scale(0.5);
+          transform-origin: 0 0;
+          opacity: 0.1;
+          border: 1px solid currentColor;
+          pointer-events: none;
+        }
+
+        .capsule-back,
+        .capsule-home {
+          @include flex-center;
+          flex: 1;
+        }
+
+        &.isFristPage {
+
+          .capsule-back,
+          &::after {
+            display: none;
+          }
         }
       }
     }
 
-    .ui-bar-bg {
+    .right {
+      @include flex-bar;
+
+      .right-content {
+        @include flex;
+        flex-direction: row-reverse;
+      }
+    }
+
+    .center {
+      @include flex-center;
+      text-overflow: ellipsis;
+      // text-align: center;
       position: absolute;
-      width: 100%;
-      height: 100%;
-      top: 0;
-      z-index: 1;
-      pointer-events: none;
+      left: 50%;
+      transform: translateX(-50%);
+
+      .image {
+        display: block;
+        height: 36px;
+        max-width: calc(100vw - 200px);
+      }
     }
   }
+
+  .ui-bar-bg {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    z-index: 1;
+    pointer-events: none;
+  }
+}
 </style>

+ 1 - 1
sheep/util/index.js

@@ -147,7 +147,7 @@ export function jsonParse(str) {
   try {
     return JSON.parse(str);
   } catch (e) {
-    console.error(`str[${str}] 不是一个 JSON 字符串`);
+    // console.error(`str[${str}] 不是一个 JSON 字符串`);
     return '';
   }
 }

+ 1 - 1
uni_modules/mp-html/components/mp-html/mp-html.vue

@@ -38,7 +38,7 @@
  * @event {Function} error 媒体加载出错时触发
  */
 // #ifndef APP-PLUS-NVUE
-import node from './node/node'
+import node from './node/node'  
 // #endif
 import Parser from './parser'
 const plugins=[]

+ 2 - 2
uni_modules/mp-html/components/mp-html/node/node.vue

@@ -30,8 +30,8 @@
       <!-- #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 v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a':'_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 -->

+ 29 - 23
uni_modules/zero-markdown-view/components/mp-html/mp-html.vue

@@ -1,16 +1,19 @@
 <template>
-  <view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle">
+  <view id="_root" :class="(selectable ? '_select ' : '') + '_root'" :style="containerStyle">
     <slot v-if="!nodes[0]" />
     <!-- #ifndef APP-PLUS-NVUE -->
-    <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" />
+    <node v-else :childs="nodes" @linktap1="linktap" :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" />
+    <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.4.2
  * @description 富文本组件
@@ -44,13 +47,13 @@ import Parser from './parser'
 import markdown from './markdown/index.js'
 import highlight from './highlight/index.js'
 import style from './style/index.js'
-const plugins=[markdown,highlight,style,]
+const plugins = [markdown, highlight, style,]
 // #ifdef APP-PLUS-NVUE
 const dom = weex.requireModule('dom')
 // #endif
 export default {
   name: 'mp-html',
-  data () {
+  data() {
     return {
       nodes: [],
       // #ifdef APP-PLUS-NVUE
@@ -107,7 +110,7 @@ export default {
     useAnchor: [Boolean, Number]
   },
   // #ifdef VUE3
-  emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
+  emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error', 'linktap1'],
   // #endif
   // #ifndef APP-PLUS-NVUE
   components: {
@@ -115,22 +118,22 @@ export default {
   },
   // #endif
   watch: {
-    content (content) {
+    content(content) {
       this.setContent(content)
     }
   },
-  created () {
+  created() {
     this.plugins = []
     for (let i = plugins.length; i--;) {
       this.plugins.push(new plugins[i](this))
     }
   },
-  mounted () {
+  mounted() {
     if (this.content && !this.nodes.length) {
       this.setContent(this.content)
     }
   },
-  beforeDestroy () {
+  beforeDestroy() {
     this._hook('onDetached')
   },
   methods: {
@@ -140,7 +143,7 @@ export default {
      * @param {String} selector scroll-view 的选择器
      * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
      */
-    in (page, selector, scrollTop) {
+    in(page, selector, scrollTop) {
       // #ifndef APP-PLUS-NVUE
       if (page && selector && scrollTop) {
         this._in = {
@@ -158,7 +161,7 @@ export default {
      * @param {Number} offset 跳转位置的偏移量
      * @returns {Promise}
      */
-    navigateTo (id, offset) {
+    navigateTo(id, offset) {
       id = this._ids[decodeURI(id)] || id
       return new Promise((resolve, reject) => {
         if (!this.useAnchor) {
@@ -224,9 +227,9 @@ export default {
      * @description 获取文本内容
      * @return {String}
      */
-    getText (nodes) {
+    getText(nodes) {
       let text = '';
-      (function traversal (nodes) {
+      (function traversal(nodes) {
         for (let i = 0; i < nodes.length; i++) {
           const node = nodes[i]
           if (node.type === 'text') {
@@ -258,7 +261,7 @@ export default {
      * @description 获取内容大小和位置
      * @return {Promise}
      */
-    getRect () {
+    getRect() {
       return new Promise((resolve, reject) => {
         uni.createSelectorQuery()
           // #ifndef MP-ALIPAY
@@ -271,7 +274,7 @@ export default {
     /**
      * @description 暂停播放媒体
      */
-    pauseMedia () {
+    pauseMedia() {
       for (let i = (this._videos || []).length; i--;) {
         this._videos[i].pause()
       }
@@ -292,7 +295,7 @@ export default {
      * @description 设置媒体播放速率
      * @param {Number} rate 播放速率
      */
-    setPlaybackRate (rate) {
+    setPlaybackRate(rate) {
       this.playbackRate = rate
       for (let i = (this._videos || []).length; i--;) {
         this._videos[i].playbackRate(rate)
@@ -309,13 +312,15 @@ export default {
       // #endif
       // #endif
     },
-
+    linktap(event) {
+      this.$emit('linktap1', event)
+    },
     /**
      * @description 设置内容
      * @param {String} content html 内容
      * @param {Boolean} append 是否在尾部追加
      */
-    setContent (content, append) {
+    setContent(content, append) {
       if (!append || !this.imgList) {
         this.imgList = []
       }
@@ -366,7 +371,7 @@ export default {
     /**
      * @description 调用插件钩子函数
      */
-    _hook (name) {
+    _hook(name) {
       for (let i = plugins.length; i--;) {
         if (this.plugins[i][name]) {
           this.plugins[i][name]()
@@ -378,14 +383,14 @@ export default {
     /**
      * @description 设置内容
      */
-    _set (nodes, append) {
+    _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) {
+    _onMessage(e) {
       const message = e.detail.data[0]
       switch (message.action) {
         // web-view 初始化完毕
@@ -443,7 +448,7 @@ export default {
             } else {
               uni.navigateTo({
                 url: href,
-                fail () {
+                fail() {
                   uni.switchTab({
                     url: href
                   })
@@ -499,5 +504,6 @@ export default {
 ._select {
   user-select: text;
 }
+
 /* #endif */
 </style>

+ 188 - 32
uni_modules/zero-markdown-view/components/mp-html/node/node.vue

@@ -1,79 +1,109 @@
 <template>
-  <view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
+  <view :id="attrs.id" :class="'_block _' + name + ' ' + attrs.class" :style="attrs.style">
     <block v-for="(n, i) in childs" 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" />
+      <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" />
+      <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" />
+      <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 -->
       <!-- #ifndef H5 || APP-PLUS -->
-      <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':'')" :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" />
+      <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' : '')" :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 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':'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <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' : '')" :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>
+      <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>
+      <text v-else-if="n.text" decode>{{ n.text }}</text>
       <!-- #endif -->
-      <text v-else-if="n.name==='br'">\n</text>
+      <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">
+      <view v-else-if="n.name === 'a'" :id="n.attrs.id"
+        :class="[(n.attrs.href ? '_a ' : '') + n.attrs.class, (n.attrs['data-value'] ? '_ab ' : '')]"
+        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" @vplay.stop="play" />
+      <view v-else-if="n.html" :id="n.attrs.id" :class="'_video ' + n.attrs.class" :style="n.attrs.style"
+        v-html="n.html" @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="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
+      <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="n.src[ctrl[i] || 0]" :data-i="i" @play="play"
+        @error="mediaError" />
       <!-- #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" />
+      <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="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
+      <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="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" />
+      <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">
+            <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">
+            <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>
-      
+
       <!-- 富文本 -->
       <!-- #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]" @tap.stop="codeLongTap(n)"/>
+      <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]" @tap.stop="codeLongTap(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]" @tap.stop="codeLongTap(n)"/>
+      <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]" @tap.stop="codeLongTap(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 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"/>
+      <node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
     </block>
   </view>
 </template>
@@ -364,6 +394,10 @@ export default {
       const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
       const attrs = node.attrs || e
       const href = attrs.href
+      if (attrs['data-value']) {
+        this.root.$emit('linktap1', attrs);
+        return
+      }
       this.root.$emit('linktap', Object.assign({
         innerText: this.root.getText(node.children || []) // 链接内的文本内容
       }, attrs))
@@ -445,14 +479,122 @@ export default {
   }
 }
 </script>
-<style>/deep/ .hl-code,/deep/ .hl-pre{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}/deep/ .hl-pre{padding:1em;margin:.5em 0;overflow:auto}/deep/ .hl-pre{background:#2d2d2d}/deep/ .hl-block-comment,/deep/ .hl-cdata,/deep/ .hl-comment,/deep/ .hl-doctype,/deep/ .hl-prolog{color:#999}/deep/ .hl-punctuation{color:#ccc}/deep/ .hl-attr-name,/deep/ .hl-deleted,/deep/ .hl-namespace,/deep/ .hl-tag{color:#e2777a}/deep/ .hl-function-name{color:#6196cc}/deep/ .hl-boolean,/deep/ .hl-function,/deep/ .hl-number{color:#f08d49}/deep/ .hl-class-name,/deep/ .hl-constant,/deep/ .hl-property,/deep/ .hl-symbol{color:#f8c555}/deep/ .hl-atrule,/deep/ .hl-builtin,/deep/ .hl-important,/deep/ .hl-keyword,/deep/ .hl-selector{color:#cc99cd}/deep/ .hl-attr-value,/deep/ .hl-char,/deep/ .hl-regex,/deep/ .hl-string,/deep/ .hl-variable{color:#7ec699}/deep/ .hl-entity,/deep/ .hl-operator,/deep/ .hl-url{color:#67cdcc}/deep/ .hl-bold,/deep/ .hl-important{font-weight:700}/deep/ .hl-italic{font-style:italic}/deep/ .hl-entity{cursor:help}/deep/ .hl-inserted{color:green}/deep/ .md-p {
+<style>
+/deep/ .hl-code,
+/deep/ .hl-pre {
+  color: #ccc;
+  background: 0 0;
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+  font-size: 1em;
+  text-align: left;
+  white-space: pre;
+  word-spacing: normal;
+  word-break: normal;
+  word-wrap: normal;
+  line-height: 1.5;
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+  -webkit-hyphens: none;
+  -moz-hyphens: none;
+  -ms-hyphens: none;
+  hyphens: none
+}
+
+/deep/ .hl-pre {
+  padding: 1em;
+  margin: .5em 0;
+  overflow: auto
+}
+
+/deep/ .hl-pre {
+  background: #2d2d2d
+}
+
+/deep/ .hl-block-comment,
+/deep/ .hl-cdata,
+/deep/ .hl-comment,
+/deep/ .hl-doctype,
+/deep/ .hl-prolog {
+  color: #999
+}
+
+/deep/ .hl-punctuation {
+  color: #ccc
+}
+
+/deep/ .hl-attr-name,
+/deep/ .hl-deleted,
+/deep/ .hl-namespace,
+/deep/ .hl-tag {
+  color: #e2777a
+}
+
+/deep/ .hl-function-name {
+  color: #6196cc
+}
+
+/deep/ .hl-boolean,
+/deep/ .hl-function,
+/deep/ .hl-number {
+  color: #f08d49
+}
+
+/deep/ .hl-class-name,
+/deep/ .hl-constant,
+/deep/ .hl-property,
+/deep/ .hl-symbol {
+  color: #f8c555
+}
+
+/deep/ .hl-atrule,
+/deep/ .hl-builtin,
+/deep/ .hl-important,
+/deep/ .hl-keyword,
+/deep/ .hl-selector {
+  color: #cc99cd
+}
+
+/deep/ .hl-attr-value,
+/deep/ .hl-char,
+/deep/ .hl-regex,
+/deep/ .hl-string,
+/deep/ .hl-variable {
+  color: #7ec699
+}
+
+/deep/ .hl-entity,
+/deep/ .hl-operator,
+/deep/ .hl-url {
+  color: #67cdcc
+}
+
+/deep/ .hl-bold,
+/deep/ .hl-important {
+  font-weight: 700
+}
+
+/deep/ .hl-italic {
+  font-style: italic
+}
+
+/deep/ .hl-entity {
+  cursor: help
+}
+
+/deep/ .hl-inserted {
+  color: green
+}
+
+/deep/ .md-p {
   margin-block-start: 1em;
   margin-block-end: 1em;
 }
 
-/deep/.hl-copy{
-			color:#cccccc;
-		}
+/deep/.hl-copy {
+  color: #cccccc;
+}
+
 /deep/ .md-table,
 /deep/ .md-blockquote {
   margin-bottom: 16px;
@@ -505,6 +647,7 @@ export default {
   background: transparent;
   border: 0;
 }
+
 /* a 标签默认效果 */
 ._a {
   padding: 1.5px 0 1.5px 0;
@@ -512,6 +655,18 @@ export default {
   word-break: break-all;
 }
 
+._ab {
+  padding: 5px 7px;
+  color: #366092;
+  word-break: break-all;
+  background-color: #f1f8ff;
+  border: 1px solid #8bb8f3;
+  display: inline-block !important;
+  margin-top: 5px !important;
+  margin-right: 5px !important;
+  box-sizing: content-box !important;
+}
+
 /* a 标签点击态效果 */
 ._hover {
   text-decoration: underline;
@@ -674,5 +829,6 @@ export default {
   width: 300px;
   height: 225px;
 }
+
 /* #endif */
 </style>

+ 92 - 84
uni_modules/zero-markdown-view/components/zero-markdown-view/zero-markdown-view.vue

@@ -1,76 +1,84 @@
 <template>
 	<view class="zero-markdown-view">
-		<mp-html :key="mpkey" :selectable="selectable" :scroll-table='scrollTable' :tag-style="tagStyle"
-			:markdown="true" :content="html">
+		<mp-html @linktap1="handleClick" :key="mpkey" :selectable="selectable" :scroll-table='scrollTable'
+			:tag-style="tagStyle" :markdown="true" :content="html">
 		</mp-html>
 	</view>
 </template>
 
 <script>
-	import mpHtml from '../mp-html/mp-html';
-
-
-	export default {
-		name: 'zero-markdown-view',
-		components: {
-			mpHtml
+import mpHtml from '../mp-html/mp-html';
+export default {
+	name: 'zero-markdown-view',
+	inject: ['EventSourceFun'],
+	components: {
+		mpHtml
+	},
+	props: {
+		markdown: {
+			type: String,
+			default: ''
 		},
-		props: {
-			markdown: {
-				type: String,
-				default: ''
-			},
-			selectable: {
-				type: [Boolean, String],
-				default: true
-			},
-			scrollTable: {
-				type: Boolean,
-				default: true
-			},
-			themeColor: {
-				type: String,
-				default: '#007AFF'
-			},
-			codeBgColor: {
-				type: String,
-				default: '#2d2d2d'
-			},
+		selectable: {
+			type: [Boolean, String],
+			default: true
 		},
-		data() {
-			return {
-				html: '',
-				tagStyle: '',
-				mpkey: 'zero'
-			};
+		scrollTable: {
+			type: Boolean,
+			default: true
 		},
-		watch: {
-			markdown: function(val) {
-				this.html = this.markdown
-			}
+		themeColor: {
+			type: String,
+			default: '#007AFF'
 		},
-		created() {
-			this.initTagStyle();
+		codeBgColor: {
+			type: String,
+			default: '#2d2d2d'
 		},
-		mounted() {
-
+	},
+	data() {
+		return {
+			html: '',
+			tagStyle: '',
+			mpkey: 'zero'
+		};
+	},
+	watch: {
+		markdown: function (val) {
 			this.html = this.markdown
-		},
-		methods: {
+		}
+	},
+	created() {
+		this.initTagStyle();
+	},
+	mounted() {
 
-			initTagStyle() {
-				const themeColor = this.themeColor
-				const codeBgColor = this.codeBgColor
-				let zeroStyle = {
-					p: `
+		this.html = this.markdown
+	},
+	methods: {
+		handleClick(event) {
+			const data = {
+				contentType: 1,
+				content: event['data-text'],
+			};
+			console.log('张耀文', event)
+			this.EventSourceFun(data, false)
+			// 访问依赖注入的内容
+			// handleItemClick(event)
+		},
+		initTagStyle() {
+			const themeColor = this.themeColor
+			const codeBgColor = this.codeBgColor
+			let zeroStyle = {
+				p: `
 				margin:5px 5px;
 				font-size: 15px;
 				line-height:1.75;
 				letter-spacing:0.2em;
 				word-spacing:0.1em;
 				`,
-					// 一级标题
-					h1: `
+				// 一级标题
+				h1: `
 				margin:25px 0;
 				font-size: 24px;
 				text-align: center;
@@ -81,8 +89,8 @@
 				border-top-right-radius:3px;
 				border-top-left-radius:3px;
 				`,
-					// 二级标题
-					h2: `
+				// 二级标题
+				h2: `
 				margin:40px 0 20px 0;	
 				font-size: 20px;
 				text-align:center;
@@ -91,47 +99,47 @@
 				padding-left:10px;
 				// border:1px solid ${themeColor};
 				`,
-					// 三级标题
-					h3: `
+				// 三级标题
+				h3: `
 				margin:30px 0 10px 0;
 				font-size: 18px;
 				color: ${themeColor};
 				padding-left:10px;
 				border-left:3px solid ${themeColor};
 				`,
-					// 引用
-					blockquote: `
+				// 引用
+				blockquote: `
 				margin:15px 0;
 				font-size:15px;
 				color: #777777;
 				border-left: 4px solid #dddddd;
 				padding: 0 10px;
 				 `,
-					// 列表 
-					ul: `
+				// 列表 
+				ul: `
 				margin: 10px 0;
 				color: #555;
 				`,
-					li: `
+				li: `
 				margin: 5px 0;
 				color: #555;
 				`,
-					// 链接
-					a: `
+				// 链接
+				a: `
 				// color: ${themeColor};
 				`,
-					// 加粗
-					strong: `
+				// 加粗
+				strong: `
 				font-weight: border;
 				color: ${themeColor};
 				`,
-					// 斜体
-					em: `
+				// 斜体
+				em: `
 				color: ${themeColor};
 				letter-spacing:0.3em;
 				`,
-					// 分割线
-					hr: `
+				// 分割线
+				hr: `
 				height:1px;
 				padding:0;
 				border:none;
@@ -139,39 +147,39 @@
 				text-align:center;
 				background-image:linear-gradient(to right,rgba(248,57,41,0),${themeColor},rgba(248,57,41,0));
 				`,
-					// 表格
-					table: `
+				// 表格
+				table: `
 				border-spacing:0;
 				overflow:auto;
 				min-width:100%;
 				margin:10px 0;
 				border-collapse: collapse;
 				`,
-					th: `
+				th: `
 				border: 1px solid #202121;
 				color: #555;
 				`,
-					td: `
+				td: `
 				color:#555;
 				border: 1px solid #555555;
 				`,
-					pre: `
+				pre: `
 				border-radius: 5px;
 				white-space: pre;
 				background: ${codeBgColor};
 				font-size:12px;
 				position: relative;
 				`,
-				}
-				this.tagStyle = zeroStyle
-			},
-		}
-	};
+			}
+			this.tagStyle = zeroStyle
+		},
+	}
+};
 </script>
 
 <style lang="scss">
-	.zero-markdown-view {
-		padding: 15rpx;
-		position: relative;
-	}
+.zero-markdown-view {
+	padding: 15rpx;
+	position: relative;
+}
 </style>