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

pull/170/head^2
zhangdaiscott 2022-09-22 14:06:13 +08:00
parent 78d182ba0c
commit df0441c8f5
11 changed files with 2002 additions and 0 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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-1978bugonline 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

View File

@ -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;
}