Bläddra i källkod

jeecgboot3.4.2版本发布,新功能(表单右侧评论功能)

zhangdaiscott 2 år sedan
förälder
incheckning
df0441c8f5

+ 164 - 0
src/components/jeecg/comment/CommentFiles.vue

@@ -0,0 +1,164 @@
+<template>
+  <div>
+    <a-alert type="info" class="jeecg-comment-files">
+      <template #message>
+        <span class="j-icon">
+          <a-upload multiple v-model:file-list="selectFileList" :showUploadList="false" :before-upload="beforeUpload">
+            <span class="inner-button"><upload-outlined />上传</span>
+          </a-upload>
+        </span>
+        <span class="j-icon">
+          <span class="inner-button"><folder-outlined />从文件库选择?</span>
+        </span>
+      </template>
+    </a-alert>
+
+    <!-- 正在上传的文件 -->
+    <div class="selected-file-warp" v-if="selectFileList && selectFileList.length > 0">
+      <div class="selected-file-list">
+        <div class="item" v-for="item in selectFileList">
+          <div class="complex">
+            <div class="content" >
+              <!-- 图片 -->
+              <div v-if="isImage(item)" class="content-top" style="height: 100%">
+                <div class="content-image" :style="getImageAsBackground(item)">
+                  <!--  <img style="height: 100%;" :src="getImageSrc(item)">-->
+                </div>
+              </div>
+              <!-- 文件 -->
+              <template v-else>
+                <div class="content-top">
+                  <div class="content-icon" :style="{ background: 'url(' + getBackground(item) + ')  no-repeat' }"></div>
+                </div>
+                <div class="content-bottom" :title="item.name">
+                  <span>{{ item.name }}</span>
+                </div>
+              </template>
+            </div>
+            <div class="layer" :class="{'layer-image':isImage(item)}">
+              <div class="next" @click="viewImage(item)"><div class="text">{{ item.name }} </div></div>
+              <div class="buttons">
+                <div class="opt-icon">
+                  <Tooltip title="删除">
+                    <delete-outlined @click="handleRemove(item)" />
+                  </Tooltip>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="item empty"></div><div class="item empty"></div><div class="item empty"></div> <div class="item empty"></div><div class="item empty"></div><div class="item empty"></div>
+      </div>
+
+      <div style="margin-bottom: 24px; margin-top: 18px; text-align: right">
+        <a-button @click="quxiao">取消</a-button>
+        <a-button type="primary" style="margin-left: 10px" @click="queding" :loading="buttonLoading">确定</a-button>
+      </div>
+    </div>
+
+    <!-- 历史文件 -->
+    <history-file-list :dataList="dataList"></history-file-list>
+  </div>
+</template>
+
+<script>
+  import { UploadOutlined, FolderOutlined, DownloadOutlined, PaperClipOutlined, DeleteOutlined } from '@ant-design/icons-vue';
+  import JUpload from '/@/components/Form/src/jeecg/components/JUpload/JUpload.vue';
+  import { uploadFileUrl } from './useComment';
+  import { propTypes } from '/@/utils/propTypes';
+  import { computed, watchEffect, unref, ref } from 'vue';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { fileList } from './useComment';
+  import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
+  import { useUserStore } from '/@/store/modules/user';
+  import { saveOne, useCommentWithFile, useFileList } from './useComment';
+
+  import { Tooltip } from 'ant-design-vue';
+  import HistoryFileList from './HistoryFileList.vue';
+
+  export default {
+    name: 'CommentFiles',
+    components: {
+      UploadOutlined,
+      FolderOutlined,
+      JUpload,
+      DownloadOutlined,
+      PaperClipOutlined,
+      DeleteOutlined,
+      Tooltip,
+      HistoryFileList,
+    },
+    props: {
+      tableName: propTypes.string.def(''),
+      dataId: propTypes.string.def(''),
+      datetime:  propTypes.number.def(1)
+    },
+    setup(props) {
+      // const { createMessage } = useMessage();
+      const { userInfo } = useUserStore();
+      const dataList = ref([]);
+      const commentId = ref('');
+
+      async function loadFileList() {
+        const params = {
+          tableName: props.tableName,
+          tableDataId: props.dataId,
+        };
+        const data = await fileList(params);
+        console.log('1111', data)
+        if (!data || !data.records || data.records.length == 0) {
+          dataList.value = [];
+        } else {
+          let array = data.records;
+          console.log(123, array);
+          dataList.value = array;
+        }
+        commentId.value = '';
+      }
+
+      watchEffect(() => {
+        // 每次切换tab都会刷新文件列表--- VUEN-1884 评论里上传的图片未在文件中显示
+        if(props.datetime){
+          if (props.tableName && props.dataId) {
+            loadFileList();
+          }
+        }
+      });
+
+      const { saveCommentAndFiles, buttonLoading } = useCommentWithFile(props);
+      const { selectFileList, beforeUpload, handleRemove, getBackground, isImage, getImageAsBackground, viewImage } = useFileList();
+
+      function quxiao() {
+        selectFileList.value = [];
+      }
+      async function queding() {
+        let obj = {
+          fromUserId: userInfo.id,
+          commentContent: '上传了附件'
+        }
+        await saveCommentAndFiles(obj, selectFileList.value)
+        selectFileList.value = [];
+        await loadFileList();
+      }
+
+      return {
+        selectFileList,
+        beforeUpload,
+        handleRemove,
+        getBackground,
+        isImage,
+        dataList,
+        uploadFileUrl,
+        quxiao,
+        queding,
+        buttonLoading,
+        getImageAsBackground,
+        viewImage
+      };
+    },
+  };
+</script>
+
+<style lang="less" scoped>
+  @import 'comment.less';
+</style>

+ 328 - 0
src/components/jeecg/comment/CommentList.vue

@@ -0,0 +1,328 @@
+<template>
+  <div :style="{ position: 'relative', height: allHeight + 'px' }">
+    <a-list class="jeecg-comment-list" header="" item-layout="horizontal" :data-source="dataList" :style="{ height: commentHeight + 'px' }">
+      <template #renderItem="{ item }">
+        <a-list-item style="padding-left: 10px; flex-direction: column" @click="handleClickItem">
+          <a-comment>
+            <template #avatar>
+              <a-avatar class="tx" :src="getAvatar(item)" :alt="getAvatarText(item)">{{ getAvatarText(item) }}</a-avatar>
+            </template>
+
+            <template #author>
+              <div class="comment-author">
+                <span>{{ item.fromUserId_dictText }}</span>
+
+                <template v-if="item.toUserId">
+                  <span>回复</span>
+                  <span>{{ item.toUserId_dictText }}</span>
+                  <Tooltip class="comment-last-content" @visibleChange="(v)=>visibleChange(v, item)">
+                    <template #title>
+                      <div v-html="getHtml(item.commentId_dictText)"></div>
+                    </template>
+                    <message-outlined />
+                  </Tooltip>
+                </template>
+              </div>
+            </template>
+
+            <template #datetime>
+              <div>
+                <Tooltip :title="item.createTime">
+                  <span>{{ getDateDiff(item) }}</span>
+                </Tooltip>
+              </div>
+            </template>
+
+            <template #actions>
+              <span @click="showReply(item)">回复</span>
+
+              <Popconfirm title="确定删除吗?" @confirm="deleteComment(item)">
+                <span>删除</span>
+              </Popconfirm>
+            </template>
+
+            <template #content>
+              <div v-html="getHtml(item.commentContent)" style="font-size: 15px">
+              </div>
+
+              <div v-if="item.fileList && item.fileList.length > 0">
+                <!-- 历史文件 -->
+                <history-file-list :dataList="item.fileList" isComment></history-file-list>
+              </div>
+            </template>
+          </a-comment>
+          <div v-if="item.commentStatus" class="inner-comment">
+            <my-comment inner @cancel="item.commentStatus = false" @comment="(content, fileList) => replyComment(item, content, fileList)" :inputFocus="focusStatus"></my-comment>
+          </div>
+        </a-list-item>
+      </template>
+    </a-list>
+
+    <div style="position: absolute; bottom: 0; left: 0; width: 100%; background: #fff; border-top: 1px solid #eee">
+      <a-comment style="margin: 0 10px">
+        <template #avatar>
+          <a-avatar class="tx" :src="getMyAvatar()" :alt="getMyname()">{{ getMyname() }}</a-avatar>
+        </template>
+        <template #content>
+          <my-comment ref="bottomCommentRef" @comment="sendComment" :inputFocus="focusStatus"></my-comment>
+        </template>
+      </a-comment>
+    </div>
+  </div>
+</template>
+
+<script>
+  /**
+   * 评论列表
+   */
+  import { defineComponent, ref, onMounted, watch, watchEffect } from 'vue';
+  import { propTypes } from '/@/utils/propTypes';
+  import dayjs from 'dayjs';
+  import 'dayjs/locale/zh.js';
+  import relativeTime from 'dayjs/plugin/relativeTime';
+  import customParseFormat from 'dayjs/plugin/customParseFormat';
+  dayjs.locale('zh');
+  dayjs.extend(relativeTime);
+  dayjs.extend(customParseFormat);
+  import { MessageOutlined } from '@ant-design/icons-vue';
+  import { Comment, Tooltip } from 'ant-design-vue';
+  import { useUserStore } from '/@/store/modules/user';
+  import MyComment from './MyComment.vue';
+  import { list, saveOne, deleteOne, useCommentWithFile, useEmojiHtml, queryById } from './useComment';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import HistoryFileList from './HistoryFileList.vue';
+  import { Popconfirm } from 'ant-design-vue';
+  import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
+
+  export default defineComponent({
+    name: 'CommentList',
+    components: {
+      MessageOutlined,
+      AComment: Comment,
+      Tooltip,
+      MyComment,
+      Popconfirm,
+      HistoryFileList,
+    },
+    props: {
+      tableName: propTypes.string.def(''),
+      dataId: propTypes.string.def(''),
+      datetime:  propTypes.number.def(1)
+    },
+    setup(props) {
+      const { createMessage } = useMessage();
+      const dataList = ref([]);
+      const { userInfo } = useUserStore();
+      /**
+       * 获取当前用户名称
+       */
+      function getMyname() {
+        if (userInfo.realname) {
+          return userInfo.realname.substr(0, 2);
+        }
+        return '';
+      }
+      
+      function getMyAvatar(){
+        return userInfo.avatar;
+      }
+      
+      // 获取头像
+      function getAvatar(item) {
+        if (item.fromUserAvatar) {
+          return getFileAccessHttpUrl(item.fromUserAvatar)
+        }
+        return '';
+      }
+
+      // 头像没有获取 用户名前两位
+      function getAvatarText(item){
+        if (item.fromUserId_dictText) {
+          return item.fromUserId_dictText.substr(0, 2);
+        }
+        return '未知';
+      }
+
+      function getAuthor(item) {
+        if (item.toUser) {
+          return item.fromUserId_dictText + ' 回复 ' + item.fromUserId_dictText;
+        } else {
+          return item.fromUserId_dictText;
+        }
+      }
+
+      function getDateDiff(item) {
+        if (item.createTime) {
+          const temp = dayjs(item.createTime, 'YYYY-MM-DD hh:mm:ss');
+          return temp.fromNow();
+        }
+        return '';
+      }
+      const commentHeight = ref(300);
+      const allHeight = ref(300);
+      onMounted(() => {
+        commentHeight.value = window.innerHeight - 57 - 46 - 70 - 160;
+        allHeight.value = window.innerHeight - 57 - 46 - 53 -20;
+      });
+
+      /**
+       * 加载数据
+       * @returns {Promise<void>}
+       */
+      async function loadData() {
+        const params = {
+          tableName: props.tableName,
+          tableDataId: props.dataId,
+          column: 'createTime',
+          order: 'desc',
+        };
+        const data = await list(params);
+        if (!data || !data.records || data.records.length == 0) {
+          dataList.value = [];
+        } else {
+          let array = data.records;
+          console.log(123, array);
+          dataList.value = array;
+        }
+      }
+
+      const { saveCommentAndFiles } = useCommentWithFile(props);
+      // 回复
+      async function replyComment(item, content, fileList) {
+        console.log(content, item);
+        let obj = {
+          fromUserId: userInfo.id,
+          toUserId: item.fromUserId,
+          commentId: item.id,
+          commentContent: content
+        }
+        await saveCommentAndFiles(obj, fileList)
+        await loadData();
+      }
+      
+      //评论
+      async function sendComment(content, fileList) {
+        let obj = {
+          fromUserId: userInfo.id,
+          commentContent: content
+        }
+        await saveCommentAndFiles(obj, fileList)
+        await loadData();
+        focusStatus.value = false;
+        setTimeout(()=>{
+          focusStatus.value = true;
+        },100)
+      }
+
+      //删除
+      async function deleteComment(item) {
+        const params = { id: item.id };
+        await deleteOne(params);
+        await loadData();
+      }
+
+      /**
+       * 打开回复时触发
+       * @type {Ref<UnwrapRef<boolean>>}
+       */
+      const focusStatus = ref(false);
+      function showReply(item) {
+        let arr = dataList.value;
+        for (let temp of arr) {
+          temp.commentStatus = false;
+        }
+        item.commentStatus = true;
+        focusStatus.value = false;
+        focusStatus.value = true;
+      }
+
+      // 表单改变 -重新加载评论列表
+      watchEffect(() => {
+        if(props.datetime){
+          if (props.tableName && props.dataId) {
+            loadData();
+          }
+        }
+      });
+
+      const { getHtml } = useEmojiHtml();
+      const bottomCommentRef = ref()
+      function handleClickItem(){
+        bottomCommentRef.value.changeActive()
+      }
+
+
+      /**
+       * 根据id查询评论信息
+       */
+      async function visibleChange(v, item){
+        if(v==true){
+          if(!item.commentId_dictText){
+            const data = await queryById(item.commentId);
+            if(data.success == true){
+              item.commentId_dictText = data.result.commentContent
+            }else{
+              console.error(data.message)
+              item.commentId_dictText='该评论已被删除';
+            }
+          }
+        }
+      }
+
+      return {
+        dataList,
+        getAvatar,
+        getAvatarText,
+        getAuthor,
+        getDateDiff,
+        commentHeight,
+        allHeight,
+        replyComment,
+        sendComment,
+        getMyname,
+        getMyAvatar,
+
+        focusStatus,
+        showReply,
+        deleteComment,
+        getHtml,
+        handleClickItem,
+        bottomCommentRef,
+        visibleChange
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .jeecg-comment-list {
+    overflow: auto;
+    /* border-bottom: 1px solid #eee;*/
+    .inner-comment {
+      width: 100%;
+      padding: 0 10px;
+    }
+    .ant-comment {
+      width: 100%;
+    }
+  }
+  .comment-author {
+    span {
+      margin: 3px;
+    }
+    .comment-last-content {
+      margin-left: 5px;
+      &:hover{
+        color: #1890ff;
+      }
+    }
+  }
+  .ant-list-items{
+    .ant-list-item:last-child{
+      margin-bottom: 46px;
+    }
+  }
+  .tx{
+    margin-top: 4px;
+  }
+</style>

+ 93 - 0
src/components/jeecg/comment/CommentPanel.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="comment-tabs-warp" v-if="showStatus">
+    <a-tabs @change="handleChange" :animated="false">
+      <a-tab-pane tab="评论" key="comment" class="comment-list-tab">
+        <comment-list :tableName="tableName" :dataId="dataId" :datetime="datetime1"></comment-list>
+      </a-tab-pane>
+      <a-tab-pane tab="文件" key="file">
+        <comment-files :tableName="tableName" :dataId="dataId" :datetime="datetime2"></comment-files>
+      </a-tab-pane>
+      <a-tab-pane tab="日志" key="log">
+        <data-log-list :tableName="tableName" :dataId="dataId" :datetime="datetime3"></data-log-list>
+      </a-tab-pane>
+    </a-tabs>
+  </div>
+  <a-empty v-else description="新增页面不支持评论" />
+</template>
+
+<script>
+  /**
+   * 评论区域
+   */
+  import { propTypes } from '/@/utils/propTypes';
+  import { computed, ref } from 'vue';
+  import CommentList from './CommentList.vue';
+  import CommentFiles from './CommentFiles.vue';
+  import DataLogList from './DataLogList.vue';
+
+  export default {
+    name: 'CommentPanel',
+    components: {
+      CommentList,
+      CommentFiles,
+      DataLogList,
+    },
+    props: {
+      tableName: propTypes.string.def(''),
+      dataId: propTypes.string.def(''),
+    },
+    setup(props) {
+      const showStatus = computed(() => {
+        if (props.dataId && props.tableName) {
+          return true;
+        }
+        return false;
+      });
+
+      const datetime1 = ref(1);
+      const datetime2 = ref(1);
+      const datetime3 = ref(1);
+      function handleChange(e) {
+        let temp = new Date().getTime();
+        if (e == 'comment') {
+          datetime1.value = temp;
+        } else if (e == 'file') {
+          datetime2.value = temp;
+        } else {
+          datetime3.value = temp;
+        }
+      }
+
+      // VUEN-1978【bug】online关联记录和他表字段存在问题  20 修改完数据,再次打开不切换tab的时候,修改日志没有变化
+      function reload() {
+        let temp = new Date().getTime();
+        datetime1.value = temp;
+        datetime2.value = temp;
+        datetime3.value = temp;
+      }
+
+      return {
+        showStatus,
+        handleChange,
+        datetime1,
+        datetime2,
+        datetime3,
+        reload
+      };
+    },
+  };
+</script>
+
+<style lang="less" scoped>
+  .comment-tabs-warp {
+    height: 100%;
+    overflow: visible;
+    > .ant-tabs {
+      overflow: visible;
+    }
+  }
+  //antd3升级后,表单右侧讨论样式调整
+  ::v-deep(.ant-tabs-top  .ant-tabs-nav, .ant-tabs-bottom  .ant-tabs-nav, .ant-tabs-top  div  .ant-tabs-nav, .ant-tabs-bottom  div  .ant-tabs-nav) {
+    margin: 0 16px 0;
+  }
+</style>

+ 177 - 0
src/components/jeecg/comment/DataLogList.vue

@@ -0,0 +1,177 @@
+<template>
+    <div class="data-log-scroll" :style="{'height': height+'px'}">
+        <div class="data-log-content">
+            <div class="logbox">
+                
+                <div class="log-item" v-for="(item, index) in dataList">
+                    <span class="log-item-icon">
+                        <plus-outlined v-if="lastIndex == index" style="margin-top:3px"/>
+                        <edit-outlined v-else/>
+                    </span>
+                    <span class="log-item-content">
+                        <a @click="handleClickPerson">@{{item.createBy}}</a>
+                        {{ item.dataContent }}
+                    </span>
+                    <div class="log-item-date">
+                        <Tooltip :title="item.createTime">
+                            <span>{{ getDateDiff(item) }}</span>
+                        </Tooltip>
+                    </div>
+                </div>
+
+                
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+  import { PlusOutlined, EditOutlined } from '@ant-design/icons-vue';
+  import { getModalHeight, getLogList } from './useComment'
+  import {ref, watchEffect} from 'vue'
+  import { propTypes } from '/@/utils/propTypes';
+  import { Tooltip } from 'ant-design-vue';
+  import dayjs from 'dayjs';
+  import 'dayjs/locale/zh.js';
+  import relativeTime from 'dayjs/plugin/relativeTime';
+  import customParseFormat from 'dayjs/plugin/customParseFormat';
+  dayjs.locale('zh');
+  dayjs.extend(relativeTime);
+  dayjs.extend(customParseFormat);
+  
+  export default {
+    name: "DataLogList",
+    components:{
+      PlusOutlined,
+      EditOutlined,
+      Tooltip
+    },
+    props: {
+      tableName: propTypes.string.def(''),
+      dataId: propTypes.string.def(''),
+      datetime:  propTypes.number.def(1),
+    },
+    setup(props){
+      const winHeight = getModalHeight();
+      const height = ref(300);
+      height.value = winHeight - 46 - 57 -53 - 30;
+
+      const dataList = ref([]);
+      const lastIndex = ref(0);
+      /**
+       * 加载数据
+       * @returns {Promise<void>}
+       */
+      async function loadData() {
+        const params = {
+          dataTable: props.tableName,
+          dataId: props.dataId,
+          type: 'comment'
+        };
+        const res = await getLogList(params);
+        if (!res || !res.result || res.result.length == 0) {
+          dataList.value = [];
+          lastIndex.value = -1;
+        } else {
+          let arr = res.result;
+          lastIndex.value = arr.length-1;
+          console.log('log-list', arr);
+          dataList.value = arr;
+        }
+      }
+      
+      watchEffect(() => {
+        if(props.datetime){
+          if (props.tableName && props.dataId) {
+            console.log(props.tableName, props.dataId)
+            loadData();
+          }
+        }
+      });
+      
+      
+
+      function getDateDiff(item) {
+        if (item.createTime) {
+          const temp = dayjs(item.createTime, 'YYYY-MM-DD hh:mm:ss');
+          return temp.fromNow();
+        }
+        return '';
+      }
+      
+      function handleClickPerson() {
+        console.log('此功能未开放')
+      }
+      
+      return {
+        height,
+        lastIndex,
+        dataList,
+        getDateDiff,
+        handleClickPerson
+      }
+    }
+  }
+</script>
+
+<style lang="less" scoped>
+.data-log-scroll{
+    box-sizing: border-box;
+    height: 100%;
+    padding-bottom: 16px;
+    width: 100%;
+    overflow: hidden;
+    position: relative;
+    overflow-y: auto;
+    .data-log-content{
+ /*       right: -10px;
+        bottom: 0;
+        left: 0;
+        overflow: scroll;
+        overflow-x: hidden;
+        position: absolute;
+        top: 0;*/
+        -webkit-box-sizing: border-box;
+        box-sizing: border-box;
+        .logbox{
+            box-sizing: border-box;
+            padding-left: 16px;
+            .log-item{
+                box-sizing: border-box;
+                color: #9e9e9e;
+                margin-bottom: 20px;
+                padding-left: 20px;
+                padding-right: 25px;
+                position: relative;
+                .log-item-icon{
+                    left: 0;
+                    line-height: 16px;
+                    position: absolute;
+                    top: 3px;
+                    vertical-align: middle;
+                }
+                .log-item-content{
+                    word-wrap: break-word;
+                    display: inline-block;
+                    font-size: 13px;
+                    vertical-align: middle;
+                    width: 100%;
+                    word-break: break-word;
+                    box-sizing: border-box;
+                }
+                .log-item-date{
+                    word-wrap: break-word;
+                    display: inline-block;
+                    font-size: 13px;
+                    vertical-align: middle;
+                    width: 100%;
+                    word-break: break-word;
+                    box-sizing: border-box;
+                    margin-top: 5px;
+                }
+            }
+        }
+    }
+
+}
+</style>

+ 88 - 0
src/components/jeecg/comment/HistoryFileList.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="comment-file-his-list" :class="isComment === true ? 'in-comment' : ''">
+    <div class="selected-file-list">
+      <div class="item" v-for="item in dataList">
+        <div class="complex">
+          <div class="content">
+            <!-- 图片 -->
+            <div v-if="isImage(item)" class="content-top" style="height: 100%">
+              <div class="content-image" :style="getImageAsBackground(item)">
+                <!--<img style="height: 100%;" :src="getImageSrc(item)"/>-->
+              </div>
+            </div>
+            <!-- 文件 -->
+            <template v-else>
+              <div class="content-top">
+                <div class="content-icon" :style="{ background: 'url(' + getBackground(item) + ')  no-repeat' }"></div>
+              </div>
+              <div class="content-bottom" :title="item.name">
+                <span>{{ item.name }}</span>
+              </div>
+            </template>
+          </div>
+          <div class="layer" :class="{'layer-image':isImage(item)}">
+            <div class="next" @click="viewImage(item)">
+              <div class="text">
+                {{ item.name }}
+              </div>
+              <div class="text">
+                {{ getFileSize(item) }}
+              </div>
+            </div>
+            <div class="buttons">
+              <div class="opt-icon">
+                <Tooltip title="下载">
+                  <download-outlined @click="downLoad(item)" />
+                </Tooltip>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="item empty"></div><div class="item empty"></div><div class="item empty"></div> <div class="item empty"></div><div class="item empty"></div><div class="item empty"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+  import { Tooltip } from 'ant-design-vue';
+  import { UploadOutlined, FolderOutlined, DownloadOutlined, PaperClipOutlined, DeleteOutlined } from '@ant-design/icons-vue';
+  import { useFileList } from './useComment';
+
+  export default {
+    name: 'HistoryFileList',
+    props: {
+      dataList: {
+        type: Array,
+        default: () => [],
+      },
+      isComment: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    components: {
+      UploadOutlined,
+      FolderOutlined,
+      DownloadOutlined,
+      PaperClipOutlined,
+      DeleteOutlined,
+      Tooltip,
+    },
+    setup() {
+      const { getBackground, getFileSize, downLoad, isImage, getImageAsBackground, viewImage } = useFileList();
+      return {
+        getBackground,
+        downLoad,
+        getFileSize,
+        isImage,
+        getImageAsBackground,
+        viewImage
+      };
+    },
+  };
+</script>
+
+<style lang="less" scoped>
+  @import 'comment.less';
+</style>

+ 383 - 0
src/components/jeecg/comment/MyComment.vue

@@ -0,0 +1,383 @@
+<template>
+  <div :class="{'comment-active': commentActive}" style="border: 1px solid #eee; margin: 0; position: relative" @click="handleClickBlank">
+    <textarea ref="commentRef" v-model="myComment" @input="handleCommentChange" @blur="handleBlur" class="comment-content" :rows="3" placeholder="请输入你的评论,可以@成员" />
+    <div class="comment-content comment-html-shower" :class="{'no-content':noConent, 'top-div': showHtml, 'bottom-div': showHtml == false }" v-html="commentHtml" @click="handleClickHtmlShower"></div>
+    <div class="comment-buttons" v-if="commentActive">
+      <div style="cursor: pointer">
+        <Tooltip title="选择@用户">
+          <user-add-outlined @click="openSelectUser" />
+        </Tooltip>
+
+        <Tooltip title="上传附件">
+          <PaperClipOutlined @click="uploadVisible = !uploadVisible" />
+        </Tooltip>
+
+        <span title="表情" style="display: inline-block">
+          <SmileOutlined ref="emojiButton" @click="handleShowEmoji" />
+          <div style="position: relative" v-show=""> </div>
+        </span>
+      </div>
+      <div v-if="commentActive">
+        <a-button v-if="inner" @click="noComment" style="margin-right: 10px">取消</a-button>
+        <a-button type="primary" @click="sendComment" :loading="buttonLoading" :disabled="disabledButton">发 送</a-button>
+      </div>
+    </div>
+    <upload-chunk ref="uploadRef" :visible="uploadVisible" @select="selectFirstFile"></upload-chunk>
+  </div>
+  <UserSelectModal labelKey="realname" rowKey="username" @register="registerModal" @getSelectResult="setValue" isRadioSelection></UserSelectModal>
+  <a-modal v-model:visible="visibleEmoji" :footer="null" wrapClassName="emoji-modal" :closable="false" :width="490">
+    <template #title>
+      <span></span>
+    </template>
+    <Picker
+      :pickerStyles="pickerStyles" 
+      :i18n="optionsName" 
+      :data="emojiIndex"
+      emoji="grinning"
+      :showPreview="false" 
+      :infiniteScroll="false" 
+      :showSearch="false" 
+      :showSkinTones="false" 
+      set="apple" 
+      @select="showEmoji">
+    </Picker>
+  </a-modal>
+</template>
+
+<script lang="ts">
+  import { ref, watch, computed } from 'vue';
+  import { propTypes } from '/@/utils/propTypes';
+  import { UserAddOutlined, PaperClipOutlined, SmileOutlined } from '@ant-design/icons-vue';
+  import { Tooltip } from 'ant-design-vue';
+  import UserSelectModal from '/@/components/Form/src/jeecg/components/modal/UserSelectModal.vue';
+  import { useModal } from '/@/components/Modal';
+  import UploadChunk from './UploadChunk.vue';
+  import { Picker } from 'emoji-mart-vue-fast/src';
+  import 'emoji-mart-vue-fast/css/emoji-mart.css';
+  import { useEmojiHtml } from './useComment';
+
+  const optionsName = {
+    categories: {
+      recent: '最常用的',
+      smileys: '表情选择',
+      people: '人物&身体',
+      nature: '动物&自然',
+      foods: '食物&饮料',
+      activity: '活动',
+      places: '旅行&地点',
+      objects: '物品',
+      symbols: '符号',
+      flags: '旗帜',
+    },
+  };
+  export default {
+    name: 'MyComment',
+    components: {
+      UserAddOutlined,
+      Tooltip,
+      UserSelectModal,
+      PaperClipOutlined,
+      UploadChunk,
+      SmileOutlined,
+      Picker,
+    },
+    props: {
+      inner: propTypes.bool.def(false),
+      inputFocus: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    emits: ['cancel', 'comment'],
+    setup(props, { emit }) {
+      const uploadVisible = ref(false);
+      const uploadRef = ref();
+      //注册model
+      const [registerModal, { openModal }] = useModal();
+      const buttonLoading = ref(false);
+      const myComment = ref<string>('');
+      function sendComment() {
+        console.log(myComment.value);
+        let content = myComment.value;
+        if (!content && content !== '0') {
+          disabledButton.value = true;
+        } else {
+          buttonLoading.value = true;
+          let fileList = [];
+          if (uploadVisible.value == true) {
+            fileList = uploadRef.value.getUploadFileList();
+          }
+          emit('comment', content, fileList);
+          setTimeout(() => {
+            buttonLoading.value = false;
+          }, 350);
+        }
+      }
+      const disabledButton = ref(false);
+      watch(myComment, () => {
+        let content = myComment.value;
+        if (!content && content !== '0') {
+          disabledButton.value = true;
+        } else {
+          disabledButton.value = false;
+        }
+      });
+
+      function noComment() {
+        emit('cancel');
+      }
+
+      const commentRef = ref();
+      watch(
+        () => props.inputFocus,
+        (val) => {
+          if (val == true) {
+            // commentRef.value.focus()
+            myComment.value = '';
+            if (uploadVisible.value == true) {
+              uploadRef.value.clear();
+              uploadVisible.value = false;
+            }
+          }
+        },
+        { deep: true, immediate: true }
+      );
+
+      function openSelectUser() {
+        openModal(true, {
+          isUpdate: false,
+        });
+      }
+      function setValue(options) {
+        console.log('setValue', options);
+        if (options && options.length > 0) {
+          const { label, value } = options[0];
+          if (label && value) {
+            let str = `${label}[${value}]`;
+            let temp = myComment.value;
+            if (!temp) {
+              myComment.value = '@' + str;
+            } else {
+              if (temp.endsWith('@')) {
+                myComment.value = temp + str;
+              } else {
+                myComment.value = '@' + str + ' ' + temp;
+              }
+            }
+          }
+        }
+      }
+
+      function handleCommentChange() {
+        //console.log(1,e)
+      }
+      watch(
+        () => myComment.value,
+        (val) => {
+          if (val && val.endsWith('@')) {
+            openSelectUser();
+          }
+        }
+      );
+
+      const emojiButton = ref();
+      function onSelectEmoji(emoji) {
+        let temp = myComment.value || '';
+        temp += emoji;
+        myComment.value = temp;
+        emojiButton.value.click();
+      }
+      
+      const visibleEmoji = ref(false);
+      function showEmoji(e) {
+        let temp = myComment.value || '';
+        let str = e.colons;
+        if (str.indexOf('::') > 0) {
+          str = str.substring(0, str.indexOf(':') + 1);
+        }
+        myComment.value = temp + str;
+        visibleEmoji.value = false;
+        handleBlur();
+      }
+
+      const pickerStyles = {
+        width: '490px'
+        /* height: '350px',
+        top: '0px',
+        left: '-75px',
+        position: 'absolute',
+        'z-index': 9999*/
+      };
+      function handleClickBlank(e) {
+        console.log('handleClickBlank');
+        e.preventDefault();
+        e.stopPropagation();
+        visibleEmoji.value = false;
+        commentActive.value = true;
+      }
+      function handleShowEmoji(e) {
+        console.log('handleShowEmoji');
+        e.preventDefault();
+        e.stopPropagation();
+        visibleEmoji.value = !visibleEmoji.value;
+      }
+
+      const { emojiIndex, getHtml } = useEmojiHtml();
+
+      const commentHtml = computed(() => {
+        let temp = myComment.value;
+        if (!temp) {
+          return '请输入你的评论,可以@成员';
+        }
+        return getHtml(temp);
+      });
+
+      const showHtml = ref(false);
+      function handleClickHtmlShower(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        showHtml.value = false;
+        commentRef.value.focus();
+        console.log(234);
+        commentActive.value = true;
+      }
+      function handleBlur() {
+        showHtml.value = true;
+      }
+      
+      const commentActive = ref(false);
+      const noConent = computed(()=>{
+        if(myComment.value.length>0){
+          return false;
+        }
+        return true;
+      });
+      function changeActive(){
+        if(myComment.value.length==0){
+          commentActive.value = false
+          uploadVisible.value = false;
+        }
+      }
+      
+      function selectFirstFile(fileName){
+        if(myComment.value.length==0){
+          myComment.value = fileName;
+        }
+      }
+      
+      return {
+        myComment,
+        sendComment,
+        noComment,
+        disabledButton,
+        buttonLoading,
+        commentRef,
+        registerModal,
+        openSelectUser,
+        setValue,
+        handleCommentChange,
+        uploadRef,
+        uploadVisible,
+        onSelectEmoji,
+        optionsName,
+        emojiButton,
+        emojiIndex,
+        showEmoji,
+        pickerStyles,
+        visibleEmoji,
+        handleClickBlank,
+        handleShowEmoji,
+        commentHtml,
+        showHtml,
+        handleClickHtmlShower,
+        handleBlur,
+        commentActive,
+        noConent,
+        changeActive,
+        selectFirstFile
+      };
+    },
+  };
+</script>
+
+<style lang="less">
+  .comment-content {
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    font-variant: tabular-nums;
+    list-style: none;
+    font-feature-settings: tnum;
+    position: relative;
+    display: inline-block;
+    width: 100%;
+    padding: 4px 11px;
+    color: rgba(0, 0, 0, 0.85);
+    font-size: 15px;
+    line-height: 1.5715;
+    background-color: #fff;
+    background-image: none;
+    border: 1px solid #d9d9d9;
+    border-radius: 2px;
+    transition: all 0.3s;
+    width: 100%;
+    border: solid 0px;
+    outline: none;
+
+    .emoji-item {
+      display: inline-block !important;
+      width: 0 !important;
+    }
+  }
+  .comment-buttons {
+    padding: 10px;
+    display: flex;
+    justify-content: space-between;
+    border-top: 1px solid #d9d9d9;
+    .anticon {
+      margin: 5px;
+    }
+  }
+  .comment-html-shower {
+    position: absolute;
+    top: 0;
+    left: 0;
+    height: 70px;
+    &.bottom-div {
+      z-index: -99;
+    }
+    &.top-div {
+      z-index: 9;
+    }
+  }
+
+  .emoji-modal  {
+   > .ant-modal{
+      right: 25% !important;
+      margin-right: 16px !important;
+    }
+    .ant-modal-header{
+      padding: 0 !important;
+    }
+    .emoji-mart-bar{
+      display: none;
+    }
+    h3.emoji-mart-category-label{
+    /*  display: none;*/
+      border-bottom: 1px solid #eee;
+    }
+  }
+  
+  .comment-active{
+    border-color: #1e88e5 !important;
+    box-shadow: 0 1px 1px 0 #90caf9, 0 1px 6px 0 #90caf9;
+  }
+  .no-content{
+    color: #a1a1a1
+  }
+  
+  /**聊天表情本地化*/
+  .emoji-type-image.emoji-set-apple {
+    background-image: url("./image/emoji.png");
+  }
+</style>

+ 119 - 0
src/components/jeecg/comment/UploadChunk.vue

@@ -0,0 +1,119 @@
+<template>
+  <div v-if="visible">
+    <a-alert type="info" class="jeecg-comment-files" style="margin: 0">
+      <template #message>
+        <span class="j-icon">
+          <a-upload multiple v-model:file-list="selectFileList" :showUploadList="false" :before-upload="beforeUpload">
+            <span class="inner-button"><upload-outlined />上传</span>
+          </a-upload>
+        </span>
+        <span class="j-icon">
+          <span class="inner-button"><folder-outlined />从文件库选择?</span>
+        </span>
+      </template>
+    </a-alert>
+
+    <!-- 正在上传的文件 -->
+    <div class="selected-file-warp" v-if="selectFileList && selectFileList.length > 0">
+      <div class="selected-file-list">
+        <div class="item" v-for="item in selectFileList">
+          <div class="complex">
+            <div class="content">
+              <!-- 图片 -->
+              <div v-if="isImage(item)" class="content-top" style="height: 100%">
+                <div class="content-image" :style="{'height':'100%', 'backgroundImage': 'url('+getImageSrc(item)+')'}">
+                  <!--  <img style="height: 100%;" :src="getImageSrc(item)">-->
+                </div>
+              </div>
+              <!-- 文件 -->
+              <template v-else>
+                <div class="content-top">
+                  <div class="content-icon" :style="{ background: 'url(' + getBackground(item) + ')  no-repeat' }"></div>
+                </div>
+                <div class="content-bottom" :title="item.name">
+                  <span>{{ item.name }}</span>
+                </div>
+              </template>
+            </div>
+            <div class="layer" :class="{'layer-image':isImage(item)}">
+              <div class="next" @click="viewImage(item)">
+                <div class="text">{{ item.name }} </div>
+              </div>
+              <div class="buttons">
+                <div class="opt-icon">
+                  <Tooltip title="删除">
+                    <delete-outlined @click="handleRemove(item)" />
+                  </Tooltip>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="item empty"></div><div class="item empty"></div><div class="item empty"></div> <div class="item empty"></div><div class="item empty"></div><div class="item empty"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { toRaw, watch } from 'vue';
+  import { useFileList } from './useComment';
+  import { Tooltip } from 'ant-design-vue';
+  import { UploadOutlined, FolderOutlined, DownloadOutlined, PaperClipOutlined, DeleteOutlined } from '@ant-design/icons-vue';
+  export default {
+    name: 'UploadChunk',
+    components: {
+      Tooltip,
+      UploadOutlined,
+      FolderOutlined,
+      DownloadOutlined,
+      PaperClipOutlined,
+      DeleteOutlined,
+    },
+    props: {
+      visible: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    emits:['select'],
+    setup(_p, {emit}) {
+      const { selectFileList, beforeUpload, handleRemove, getBackground, isImage, getImageSrc, viewImage } = useFileList();
+
+      function getUploadFileList() {
+        let list = toRaw(selectFileList.value);
+        console.log(list);
+        return list;
+      }
+      
+      function clear(){
+        selectFileList.value = [];
+      }
+      
+      watch(()=>selectFileList.value, (arr)=>{
+        if(arr && arr.length>0){
+          let name = arr[0].name;
+          if(name){
+            emit('select', name)
+          }
+        }
+      });
+
+      return {
+        selectFileList,
+        beforeUpload,
+        handleRemove,
+        getBackground,
+        getUploadFileList,
+        clear,
+        isImage, 
+        getImageSrc, 
+        viewImage
+      };
+    },
+  };
+</script>
+
+<style lang="less" scoped>
+  @import 'comment.less';
+</style>

+ 234 - 0
src/components/jeecg/comment/comment.less

@@ -0,0 +1,234 @@
+/*文件上传列表-begin*/
+.selected-file-warp,
+.comment-file-his-list {
+  margin: 10px 20px;
+  &.in-comment{
+    margin: 10px 6px;
+  }
+}
+.selected-file-list {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  margin-right: -6px;
+  .item {
+    box-sizing: border-box;
+    display: inline-block;
+    flex: 1 1 0%;
+    height: 118px;
+    margin: 0 6px 6px 0;
+    min-width: 140px;
+    max-width: 200px;
+    width: 150px;
+    &.empty {
+      height: 0;
+      margin-bottom: 0;
+      margin-top: 0;
+    }
+    .complex {
+      border: 1px solid #e0e0e0;
+      box-sizing: border-box;
+      height: 100%;
+      position: relative;
+      .content {
+        display: flex;
+        flex-direction: column;
+        height: 100%;
+        box-sizing: border-box;
+        .content-top {
+          align-items: center;
+          background-color: #f5f5f5;
+          display: flex;
+          flex: 1 1 0%;
+          justify-content: center;
+          .content-icon {
+            background-position: 50%;
+            background-size: contain !important;
+            height: 55px;
+            width: 40px;
+            display: inline-block;
+            overflow: hidden;
+            text-align: left;
+            text-indent: -9999px;
+          }
+          .content-image{
+            background-position: 50%;
+            background-repeat: no-repeat;
+            background-size: cover;
+            height: 100%;
+            width: 100%;
+          }
+        }
+        .content-bottom {
+          align-items: center;
+          background-color: #fff;
+          display: flex;
+          flex-basis: 30px;
+          font-size: 13px;
+          justify-content: flex-start;
+          padding: 0 10px;
+          span {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+        }
+      }
+      .layer {
+        opacity: 0;
+        background-color: #f5f5f5;
+        cursor: pointer;
+        display: flex;
+        flex-direction: column;
+        height: 100%;
+        left: 0;
+        position: absolute;
+        top: 0;
+        transition: opacity 0.2s;
+        width: 100%;
+        &:hover {
+          opacity: 1;
+        }
+        .next {
+          height: 75px;
+          padding: 5px;
+          .text {
+            color: #1e88e5 !important;
+            align-items: center;
+            display: flex;
+            flex-basis: 30px;
+            font-size: 12px;
+            justify-content: flex-start;
+            padding: 3px 7px 4px;
+            word-break: break-all;
+            display: -webkit-box;
+            line-height: 14px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+        }
+        .buttons {
+          flex-basis: 32px;
+          text-align: right;
+          display: flex;
+          align-items: flex-end;
+          padding-right: 5px;
+          justify-content: flex-end;
+          .opt-icon {
+            background-color: #fff;
+            border-radius: 2px;
+            cursor: pointer;
+            height: 24px;
+            width: 32px;
+            margin: 5px;
+            text-align: center;
+            .anticon-delete:hover {
+              color: red;
+            }
+            .anticon-download:hover{
+              color: #1e88e5 !important
+            }
+          }
+        }
+      }
+      .layer-image{
+        background: #000;
+        &:hover {
+          opacity: 0.6;
+        }
+        .next{
+          .text{
+            color: #fff !important;
+          }
+        }
+        .opt-icon{
+          color: #000 !important;
+          .anticon-delete:hover {
+            color: red;
+          }
+        }
+      }
+      
+    }
+  }
+}
+
+.jeecg-comment-files {
+  margin: 0 20px;
+  padding-top: 3px;
+  padding-bottom: 3px;
+  &.ant-alert-info{
+    background-color: #f5f5f5;
+    border: 1px solid #f5f5f5;
+  }
+  .j-icon {
+    cursor: pointer;
+    display: inline-block;
+    border: 1px solid #e6f7ff;
+    padding: 2px 7px;
+    margin: 0 10px;
+    &:hover,
+    &:focus,
+    &:active {
+      border-color: #fff;
+      color: #096dd9;
+    }
+    .inner-button {
+      display: inline-block;
+      color:#9e9e9e;
+      &:hover,
+      &:focus,
+      &:active {
+        /*border-color: #fff;*/
+       /* color: #096dd9;*/
+        color: #000;
+      }
+      span{
+        margin-right: 3px;
+      }
+    }
+  }
+}
+
+.comment-file-list {
+  .detail-item {
+    display: flex;
+    flex-direction: row;
+    align-items: stretch;
+    line-height: 24px;
+    border-bottom: 1px solid #f0f0f0;
+    height: 100%;
+
+    .item-title {
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      flex-shrink: 0;
+      flex-grow: 0;
+      min-width: 100px;
+      width: 20%;
+      max-width: 220px;
+      background-color: #fafafa;
+      border-right: 1px solid #f0f0f0;
+      /* border-left: 1px solid #f0f0f0;*/
+      padding: 10px 0;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+    }
+
+    .item-content {
+      border-right: 1px solid #f0f0f0;
+      flex-grow: 1;
+      padding-left: 10px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-start;
+      .anticon {
+        &:hover {
+          color: #40a9ff;
+        }
+      }
+    }
+  }
+}

BIN
src/components/jeecg/comment/image/emoji.png


BIN
src/components/jeecg/comment/image/emoji_native.png


+ 416 - 0
src/components/jeecg/comment/useComment.ts

@@ -0,0 +1,416 @@
+import { useMessage } from '/@/hooks/web/useMessage';
+import { defHttp } from '/@/utils/http/axios';
+import { useGlobSetting } from '/@/hooks/setting';
+const globSetting = useGlobSetting();
+const baseUploadUrl = globSetting.uploadUrl;
+import { ref, toRaw, unref, reactive } from 'vue';
+import { uploadMyFile } from '/@/api/common/api';
+
+import excel from '/@/assets/svg/fileType/excel.svg';
+import other from '/@/assets/svg/fileType/other.svg';
+import pdf from '/@/assets/svg/fileType/pdf.svg';
+import txt from '/@/assets/svg/fileType/txt.svg';
+import word from '/@/assets/svg/fileType/word.svg';
+import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
+import { createImgPreview } from '/@/components/Preview';
+import {EmojiIndex} from "emoji-mart-vue-fast/src";
+import data from "emoji-mart-vue-fast/data/apple.json";
+
+enum Api {
+  list = '/sys/comment/listByForm',
+  addText = '/sys/comment/addText',
+  deleteOne = '/sys/comment/deleteOne',
+  fileList = '/sys/comment/fileList',
+  logList = '/sys/dataLog/queryDataVerList',
+  queryById = '/sys/comment/queryById',
+  getFileViewDomain = '/sys/comment/getFileViewDomain',
+}
+
+// 文件预览地址的domain 在后台配置的
+let onlinePreviewDomain = '';
+
+/**
+ * 获取文件预览的domain
+ */
+const getViewFileDomain = () => defHttp.get({ url: Api.getFileViewDomain });
+
+/**
+ * 列表接口
+ * @param params
+ */
+export const list = (params) => defHttp.get({ url: Api.list, params });
+
+/**
+ * 查询单条记录
+ * @param params
+ */
+export const queryById = (id) => {
+  let params = { id: id };
+  return defHttp.get({ url: Api.queryById, params },{ isTransformResponse: false });
+};
+
+/**
+ * 文件列表接口
+ * @param params
+ */
+export const fileList = (params) => defHttp.get({ url: Api.fileList, params });
+
+/**
+ * 删除单个
+ */
+export const deleteOne = (params) => {
+  return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true });
+};
+
+/**
+ * 保存
+ * @param params
+ */
+export const saveOne = (params) => {
+  let url = Api.addText;
+  return defHttp.post({ url: url, params }, { isTransformResponse: false });
+};
+
+/**
+ * 数据日志列表接口
+ * @param params
+ */
+export const getLogList = (params) => defHttp.get({ url: Api.logList, params }, {isTransformResponse: false});
+
+
+/**
+ * 文件上传接口
+ */
+export const uploadFileUrl = `${baseUploadUrl}/sys/comment/addFile`;
+
+export function useCommentWithFile(props) {
+  let uploadData = {
+    biz: 'comment',
+    commentId: '',
+  };
+  const { createMessage } = useMessage();
+  const buttonLoading = ref(false);
+
+  //确定按钮触发
+  async function saveCommentAndFiles(obj, fileList) {
+    buttonLoading.value = true;
+    setTimeout(() => {
+      buttonLoading.value = false;
+    }, 500);
+    await saveComment(obj);
+    await uploadFiles(fileList);
+  }
+
+  /**
+   * 保存评论
+   */
+  async function saveComment(obj) {
+    const {fromUserId, toUserId, commentId, commentContent} = obj;
+    let commentData = {
+      tableName: props.tableName,
+      tableDataId: props.dataId,
+      fromUserId,
+      commentContent,
+      toUserId: '',
+      commentId: ''
+    };
+    if(toUserId){
+      commentData.toUserId = toUserId;
+    }
+    if(commentId){
+      commentData.commentId = commentId;
+    }
+    uploadData.commentId = '';
+    const res = await saveOne(commentData);
+    if (res.success) {
+      uploadData.commentId = res.result;
+    } else {
+      createMessage.warning(res.message);
+      return Promise.reject('保存评论失败');
+    }
+  }
+
+  async function uploadOne(file) {
+    let url = uploadFileUrl;
+    const formData = new FormData();
+    formData.append('file', file);
+    formData.append('tableName', props.tableName);
+    formData.append('tableDataId', props.dataId);
+    Object.keys(uploadData).map((k) => {
+      formData.append(k, uploadData[k]);
+    });
+    return new Promise((resolve, reject) => {
+      uploadMyFile(url, formData).then((res: any) => {
+        console.log('uploadMyFile', res);
+        if (res && res.data) {
+          if (res.data.result == 'success') {
+            resolve(1);
+          } else {
+            createMessage.warning(res.data.message);
+            reject();
+          }
+        } else {
+          reject();
+        }
+      });
+    });
+  }
+
+  async function uploadFiles(fileList) {
+    if (fileList && fileList.length > 0) {
+      for (let i = 0; i < fileList.length; i++) {
+        let file = toRaw(fileList[i]);
+        await uploadOne(file.originFileObj);
+      }
+    }
+  }
+
+  return {
+    saveCommentAndFiles,
+    buttonLoading,
+  };
+}
+
+export function uploadMu(fileList) {
+  const formData = new FormData();
+  // let arr = []
+  for(let file of fileList){
+    formData.append('files[]', file.originFileObj);
+  }
+  console.log(formData)
+  let url = `${baseUploadUrl}/sys/comment/addFile2`;
+  uploadMyFile(url, formData).then((res: any) => {
+    console.log('uploadMyFile', res);
+  });
+}
+
+/**
+ * 显示文件列表
+ */
+export function useFileList() {
+  const imageSrcMap = reactive({});
+  const typeMap = {
+    xls: excel,
+    xlsx: excel,
+    pdf: pdf,
+    txt: txt,
+    docx: word,
+    doc: word,
+  };
+   function getBackground(item) {
+    console.log('获取文件背景图', item);
+    if (isImage(item)) {
+      return 'none'
+    } else {
+      const name = item.name;
+      if(!name){
+        return 'none';
+      }
+      const suffix = name.substring(name.lastIndexOf('.') + 1);
+      console.log('suffix', suffix)
+      let bg = typeMap[suffix];
+      if (!bg) {
+        bg = other;
+      }
+      return bg;
+    }
+  }
+
+  function getBase64(file, id){
+    return new Promise((resolve, reject) => {
+      //声明js的文件流
+      let reader = new FileReader();
+      if(file){
+        //通过文件流将文件转换成Base64字符串
+        reader.readAsDataURL(file);
+        //转换成功后
+        reader.onload = function () {
+          let base = reader.result;
+          console.log('base', base)
+          imageSrcMap[id] = base;
+          console.log('imageSrcMap', imageSrcMap)
+          resolve(base)
+        }
+      }else{
+        reject();
+      }
+    })
+  }
+  function handleImageSrc(file){
+    if(isImage(file)){
+      let id = file.uid;
+      getBase64(file, id);
+    }
+  }
+
+  function downLoad(file) {
+    let url = getFileAccessHttpUrl(file.url);
+    if (url) {
+      window.open(url);
+    }
+  }
+
+  function getFileSize(item) {
+    let size = item.fileSize;
+    if (!size) {
+      return '0B';
+    }
+    let temp = Math.round(size / 1024);
+    return temp + ' KB';
+  }
+
+  const selectFileList = ref<any[]>([]);
+  function beforeUpload(file) {
+    handleImageSrc(file);
+    selectFileList.value = [...selectFileList.value, file];
+    console.log('selectFileList', unref(selectFileList));
+    return false
+  }
+
+  function handleRemove(file) {
+    const index = selectFileList.value.indexOf(file);
+    const newFileList = selectFileList.value.slice();
+    newFileList.splice(index, 1);
+    selectFileList.value = newFileList;
+  }
+
+  function isImage(item){
+    const type = item.type||'';
+    if (type.indexOf('image') >= 0) {
+      return true;
+    }
+    return false;
+  }
+
+  function getImageSrc(file){
+    if(isImage(file)){
+      let id = file.uid;
+      if(id){
+        if(imageSrcMap[id]){
+          return imageSrcMap[id];
+        }
+      }else if(file.url){
+        //数据库中地址
+        let url = getFileAccessHttpUrl(file.url);
+        return url;
+      }
+    }
+    return ''
+  }
+
+  /**
+   * 显示图片
+   * @param item
+   */
+  function getImageAsBackground(item){
+    let url = getImageSrc(item);
+    if(url){
+      return {
+        "backgroundImage": "url('"+url+"')"
+      }
+    }
+    return {}
+  }
+
+  /**
+   * 预览列表 cell 图片
+   * @param text
+   */
+  async function viewImage(file) {
+    if(isImage(file)){
+      let text = getImageSrc(file)
+      if (text) {
+        let imgList = [text];
+        createImgPreview({ imageList: imgList });
+      }
+    }else{
+      if(file.url){
+        //数据库中地址
+        let url = getFileAccessHttpUrl(file.url);
+        await initViewDomain();
+        //本地测试需要将文件地址的localhost/127.0.0.1替换成IP, 或是直接修改全局domain
+        //url = url.replace('localhost', '192.168.1.100')
+        //如果集成的KkFileview-v3.3.0+ 需要对url再做一层base64编码 encodeURIComponent(encryptByBase64(url))
+        window.open(onlinePreviewDomain+'?officePreviewType=pdf&url='+encodeURIComponent(url));
+      }
+    }
+  }
+
+  /**
+   * 初始化domain
+   */
+  async function initViewDomain(){
+    if(!onlinePreviewDomain){
+      onlinePreviewDomain = await getViewFileDomain();
+    }
+    if(!onlinePreviewDomain.startsWith('http')){
+      onlinePreviewDomain = 'http://'+ onlinePreviewDomain;
+    }
+  }
+
+  return {
+    selectFileList,
+    getBackground,
+    getFileSize,
+    downLoad,
+    beforeUpload,
+    handleRemove,
+    isImage,
+    getImageSrc,
+    getImageAsBackground,
+    viewImage
+  };
+}
+
+/**
+ * 用于emoji渲染
+ */
+export function useEmojiHtml(){
+  const COLONS_REGEX = new RegExp('([^:]+)?(:[a-zA-Z0-9-_+]+:(:skin-tone-[2-6]:)?)','g');
+  let emojisToShowFilter = function() {
+    return true;
+  }
+  let emojiIndex = new EmojiIndex(data, {
+    emojisToShowFilter,
+    exclude:['recent','people','nature','foods','activity','places','objects','symbols','flags']
+  });
+  
+  function getHtml(text) {
+    if(!text){
+      return ''
+    }
+    return text.replace(COLONS_REGEX, function (match, p1, p2) {
+      const before = p1 || ''
+      if (endsWith(before, 'alt="') || endsWith(before, 'data-text="')) {
+        return match
+      }
+      let emoji = emojiIndex.findEmoji(p2)
+      if (!emoji) {
+        return match
+      }
+      return before + emoji2Html(emoji)
+    })
+    return text;
+  }
+
+  function endsWith(str, temp){
+    return str.endsWith(temp)
+  }
+
+  function emoji2Html(emoji) {
+    let style = `position: absolute;top: -3px;left: 3px;width: 18px; height: 18px;background-position: ${emoji.getPosition()}`
+    return `<span style="width: 24px" class="emoji-mart-emoji"><span class="my-emoji-icon emoji-set-apple emoji-type-image" style="${style}"> </span> </span>`
+  }
+  
+  return {
+    emojiIndex,
+    getHtml
+  }
+}
+
+/**
+ * 获取modal窗体高度
+ */
+export function getModalHeight(){
+  return window.innerHeight;
+}