mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-02-02 16:45:24 +08:00
前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题
This commit is contained in:
38
jeecgboot-vue3/src/components/jeecg/AIcon.vue
Normal file
38
jeecgboot-vue3/src/components/jeecg/AIcon.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<Icon :icon="icon" :size="size"></Icon>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { isEmpty } from '/@/utils/is';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AIcon',
|
||||
components: { Icon },
|
||||
props: {
|
||||
icon: String,
|
||||
type: String,
|
||||
// 图标大小,默认 16
|
||||
size: propTypes.any,
|
||||
// 样式
|
||||
theme: propTypes.any,
|
||||
},
|
||||
setup(props) {
|
||||
const icon = computed(() => {
|
||||
if (props.icon && !isEmpty(props.icon)) {
|
||||
return props.icon;
|
||||
}
|
||||
let iconTheme = props.theme ? `-${props.theme}` : '';
|
||||
return `ant-design:${props.type}${iconTheme}`;
|
||||
});
|
||||
|
||||
return {
|
||||
icon,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
BIN
jeecgboot-vue3/src/components/jeecg/AiChat/assets/avatar.jpg
Normal file
BIN
jeecgboot-vue3/src/components/jeecg/AiChat/assets/avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
463
jeecgboot-vue3/src/components/jeecg/AiChat/components/chat.vue
Normal file
463
jeecgboot-vue3/src/components/jeecg/AiChat/components/chat.vue
Normal file
@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<div class="chatWrap">
|
||||
<div class="content">
|
||||
<div class="main">
|
||||
<div id="scrollRef" ref="scrollRef" class="scrollArea">
|
||||
<template v-if="chatData.length">
|
||||
<div class="chatContentArea">
|
||||
<chatMessage
|
||||
v-for="(item, index) of chatData"
|
||||
:key="index"
|
||||
:date-time="item.dateTime"
|
||||
:text="item.text"
|
||||
:inversion="item.inversion"
|
||||
:error="item.error"
|
||||
:loading="item.loading"
|
||||
></chatMessage>
|
||||
</div>
|
||||
<div v-if="loading" class="stopArea">
|
||||
<a-button type="primary" danger @click="handleStop" class="stopBtn">
|
||||
<svg
|
||||
t="1706148514627"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="5214"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path
|
||||
d="M512 967.111111c-250.311111 0-455.111111-204.8-455.111111-455.111111s204.8-455.111111 455.111111-455.111111 455.111111 204.8 455.111111 455.111111-204.8 455.111111-455.111111 455.111111z m0-56.888889c221.866667 0 398.222222-176.355556 398.222222-398.222222s-176.355556-398.222222-398.222222-398.222222-398.222222 176.355556-398.222222 398.222222 176.355556 398.222222 398.222222 398.222222z"
|
||||
fill="currentColor"
|
||||
p-id="5215"
|
||||
></path>
|
||||
<path d="M341.333333 341.333333h341.333334v341.333334H341.333333z" fill="currentColor" p-id="5216"></path>
|
||||
</svg>
|
||||
<span>停止响应</span>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="emptyArea">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="mr-2 text-3xl iconify iconify--ri"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16 16a3 3 0 1 1 0 6a3 3 0 0 1 0-6M6 12a4 4 0 1 1 0 8a4 4 0 0 1 0-8m8.5-10a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11"
|
||||
></path>
|
||||
</svg>
|
||||
<span>新建聊天</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="topArea">
|
||||
<presetQuestion @outQuestion="handleOutQuestion"></presetQuestion>
|
||||
</div>
|
||||
<div class="bottomArea">
|
||||
<a-button type="text" class="delBtn" @click="handleDelSession">
|
||||
<svg
|
||||
t="1706504908534"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="1584"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path
|
||||
d="M816.872727 158.254545h-181.527272V139.636364c0-39.563636-30.254545-69.818182-69.818182-69.818182h-107.054546c-39.563636 0-69.818182 30.254545-69.818182 69.818182v18.618181H207.127273c-48.872727 0-90.763636 41.890909-90.763637 93.09091s41.890909 90.763636 90.763637 90.763636h609.745454c51.2 0 90.763636-41.890909 90.763637-90.763636 0-51.2-41.890909-93.090909-90.763637-93.09091zM435.2 139.636364c0-13.963636 9.309091-23.272727 23.272727-23.272728h107.054546c13.963636 0 23.272727 9.309091 23.272727 23.272728v18.618181h-153.6V139.636364z m381.672727 155.927272H207.127273c-25.6 0-44.218182-20.945455-44.218182-44.218181 0-25.6 20.945455-44.218182 44.218182-44.218182h609.745454c25.6 0 44.218182 20.945455 44.218182 44.218182 0 23.272727-20.945455 44.218182-44.218182 44.218181zM835.490909 407.272727h-121.018182c-13.963636 0-23.272727 9.309091-23.272727 23.272728s9.309091 23.272727 23.272727 23.272727h97.745455V837.818182c0 39.563636-30.254545 69.818182-69.818182 69.818182h-37.236364V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-118.690909V602.763636c0-13.963636-9.309091-23.272727-23.272728-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364H372.363636V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-34.909091c-39.563636 0-69.818182-30.254545-69.818182-69.818182V453.818182H558.545455c13.963636 0 23.272727-9.309091 23.272727-23.272727s-9.309091-23.272727-23.272727-23.272728H197.818182c-13.963636 0-23.272727 9.309091-23.272727 23.272728V837.818182c0 65.163636 51.2 116.363636 116.363636 116.363636h451.490909c65.163636 0 116.363636-51.2 116.363636-116.363636V430.545455c0-13.963636-11.636364-23.272727-23.272727-23.272728z"
|
||||
fill="currentColor"
|
||||
p-id="1585"
|
||||
></path>
|
||||
</svg>
|
||||
</a-button>
|
||||
<a-button type="text" class="contextBtn" :class="[usingContext && 'enabled']" @click="handleUsingContext">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--ri"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.956 9.956 0 0 1-4.708-1.175L2 22l1.176-5.29A9.956 9.956 0 0 1 2 12C2 6.477 6.477 2 12 2m0 2a8 8 0 0 0-8 8c0 1.335.326 2.618.94 3.766l.35.654l-.656 2.946l2.948-.654l.653.349A7.955 7.955 0 0 0 12 20a8 8 0 1 0 0-16m1 3v5h4v2h-6V7z"
|
||||
></path>
|
||||
</svg>
|
||||
</a-button>
|
||||
<a-textarea
|
||||
ref="inputRef"
|
||||
v-model:value="prompt"
|
||||
:autoSize="{ minRows: 1, maxRows: 6 }"
|
||||
:placeholder="placeholder"
|
||||
@pressEnter="handleEnter"
|
||||
autofocus
|
||||
></a-textarea>
|
||||
<a-button
|
||||
@click="
|
||||
() => {
|
||||
handleSubmit();
|
||||
}
|
||||
"
|
||||
:disabled="loading"
|
||||
type="primary"
|
||||
class="sendBtn"
|
||||
>
|
||||
<svg
|
||||
t="1706147858151"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="4237"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<path
|
||||
d="M865.28 202.5472c-17.1008-15.2576-41.0624-19.6608-62.5664-11.5712L177.7664 427.1104c-23.2448 8.8064-38.5024 29.696-39.6288 54.5792-1.1264 24.8832 11.9808 47.104 34.4064 58.0608l97.5872 47.7184c4.5056 2.2528 8.0896 6.0416 9.9328 10.6496l65.4336 161.1776c7.7824 19.1488 24.4736 32.9728 44.7488 37.0688 20.2752 4.096 41.0624-2.1504 55.6032-16.7936l36.352-36.352c6.4512-6.4512 16.5888-7.8848 24.576-3.3792l156.5696 88.8832c9.4208 5.3248 19.8656 8.0896 30.3104 8.0896 8.192 0 16.4864-1.6384 24.2688-5.0176 17.8176-7.68 30.72-22.8352 35.4304-41.6768l130.7648-527.1552c5.5296-22.016-1.7408-45.2608-18.8416-60.416z m-20.8896 50.7904L713.5232 780.4928c-1.536 6.2464-5.8368 11.3664-11.776 13.9264s-12.5952 2.1504-18.2272-1.024L526.9504 704.512c-9.4208-5.3248-19.8656-7.9872-30.208-7.9872-15.9744 0-31.744 6.144-43.52 17.92l-36.352 36.352c-3.8912 3.8912-8.9088 5.9392-14.2336 6.0416l55.6032-152.1664c0.512-1.3312 1.2288-2.56 2.2528-3.6864l240.3328-246.1696c8.2944-8.4992-2.048-21.9136-12.3904-16.0768L301.6704 559.8208c-4.096-3.584-8.704-6.656-13.6192-9.1136L190.464 502.9888c-11.264-5.5296-11.5712-16.1792-11.4688-19.3536 0.1024-3.1744 1.536-13.824 13.2096-18.2272L817.152 229.2736c10.4448-3.9936 18.0224 1.3312 20.8896 3.8912 2.8672 2.4576 9.0112 9.3184 6.3488 20.1728z"
|
||||
p-id="4238"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
import { computed, ref, createVNode, onUnmounted, onMounted } from 'vue';
|
||||
import { useScroll } from '../hooks/useScroll';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { ConfigEnum } from '/@/enums/httpEnum';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
import { getAppEnvConfig } from '/@/utils/env';
|
||||
import chatMessage from './chatMessage.vue';
|
||||
import presetQuestion from './presetQuestion.vue';
|
||||
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal, Tabs } from 'ant-design-vue';
|
||||
import '../style/github-markdown.less';
|
||||
import '../style/highlight.less';
|
||||
import '../style/style.less';
|
||||
|
||||
const props = defineProps(['chatData', 'uuid', 'dataSource']);
|
||||
const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
|
||||
const prompt = ref<string>('');
|
||||
const loading = ref<boolean>(false);
|
||||
const inputRef = ref<Ref | null>(null);
|
||||
// const chatData = computed(() => {
|
||||
// return props.chatData;
|
||||
// });
|
||||
// 当前模式下, 发送消息会携带之前的聊天记录
|
||||
const usingContext = ref<any>(true);
|
||||
const uuid = computed(() => {
|
||||
return props.uuid;
|
||||
});
|
||||
let evtSource: any = null;
|
||||
// const presetQuestion = ref(['小红书文案', '朋友圈文案', '演讲稿生成']);
|
||||
const { VITE_GLOB_API_URL } = getAppEnvConfig();
|
||||
|
||||
const conversationList = computed(() => props.chatData.filter((item) => !item.inversion && !!item.conversationOptions));
|
||||
const placeholder = computed(() => {
|
||||
return '来说点什么吧...(Shift + Enter = 换行)';
|
||||
});
|
||||
|
||||
function handleEnter(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
function handleSubmit() {
|
||||
let message = prompt.value;
|
||||
if (!message || message.trim() === '') return;
|
||||
prompt.value = '';
|
||||
onConversation(message);
|
||||
}
|
||||
const handleOutQuestion = (message) => {
|
||||
onConversation(message);
|
||||
};
|
||||
async function onConversation(message) {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
|
||||
if (props.chatData.length == 0) {
|
||||
const findItem = props.dataSource.history.find((item) => item.uuid === uuid.value);
|
||||
if (findItem && findItem.title == '新建聊天') {
|
||||
findItem.title = message;
|
||||
}
|
||||
}
|
||||
addChat(uuid.value, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
text: message,
|
||||
inversion: true,
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: null },
|
||||
});
|
||||
scrollToBottom();
|
||||
|
||||
let options: any = {};
|
||||
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions;
|
||||
if (lastContext && usingContext.value) options = { ...lastContext };
|
||||
|
||||
addChat(uuid.value, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
text: '思考中...',
|
||||
loading: true,
|
||||
inversion: false,
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
});
|
||||
scrollToBottom();
|
||||
|
||||
const initEventSource = () => {
|
||||
let lastText = '';
|
||||
if (typeof EventSource !== 'undefined') {
|
||||
const token = getToken();
|
||||
evtSource = new EventSourcePolyfill(
|
||||
`${VITE_GLOB_API_URL}/test/ai/chat/send?message=${message}${options.parentMessageId ? '&topicId=' + options.parentMessageId : ''}`,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
[ConfigEnum.TOKEN]: token,
|
||||
},
|
||||
}
|
||||
); // 后端接口,要配置允许跨域属性
|
||||
// 与事件源的连接刚打开时触发
|
||||
evtSource.onopen = function (e) {
|
||||
console.log(e);
|
||||
};
|
||||
// 当从事件源接收到数据时触发
|
||||
evtSource.onmessage = function (e) {
|
||||
const data = e.data;
|
||||
// console.log(e);
|
||||
if (data === '[DONE]') {
|
||||
updateChatSome(uuid, props.chatData.length - 1, { loading: false });
|
||||
scrollToBottom();
|
||||
handleStop();
|
||||
evtSource.close(); // 关闭连接
|
||||
} else {
|
||||
try {
|
||||
const _data = JSON.parse(data);
|
||||
const content = _data.content;
|
||||
if (content != undefined) {
|
||||
lastText += content;
|
||||
updateChat(uuid.value, props.chatData.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
text: lastText,
|
||||
inversion: false,
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: e.lastEventId == '[ERR]' ? null : { conversationId: data.conversationId, parentMessageId: e.lastEventId },
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
});
|
||||
scrollToBottom();
|
||||
} else {
|
||||
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
|
||||
scrollToBottom();
|
||||
handleStop();
|
||||
}
|
||||
} catch (error) {
|
||||
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
|
||||
scrollToBottom();
|
||||
handleStop();
|
||||
evtSource.close(); // 关闭连接
|
||||
}
|
||||
}
|
||||
};
|
||||
// 与事件源的连接无法打开时触发
|
||||
evtSource.onerror = function (e) {
|
||||
// console.log(e);
|
||||
if (e.error?.message || e.statusText) {
|
||||
updateChat(uuid.value, props.chatData.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
text: e.error?.message ?? e.statusText,
|
||||
inversion: false,
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
});
|
||||
scrollToBottom();
|
||||
}
|
||||
evtSource.close(); // 关闭连接
|
||||
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
|
||||
handleStop();
|
||||
};
|
||||
} else {
|
||||
console.log('当前浏览器不支持使用EventSource接收服务器推送事件!');
|
||||
}
|
||||
};
|
||||
initEventSource();
|
||||
}
|
||||
onUnmounted(() => {
|
||||
evtSource?.close();
|
||||
updateChatSome(uuid.value, props.chatData.length - 1, { loading: false });
|
||||
});
|
||||
const addChat = (uuid, data) => {
|
||||
props.chatData.push({ ...data });
|
||||
};
|
||||
const updateChat = (uuid, index, data) => {
|
||||
props.chatData.splice(index, 1, data);
|
||||
};
|
||||
const updateChatSome = (uuid, index, data) => {
|
||||
props.chatData[index] = { ...props.chatData[index], ...data };
|
||||
};
|
||||
// 清空会话
|
||||
const handleDelSession = () => {
|
||||
Modal.confirm({
|
||||
title: '清空会话',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: '是否清空会话?',
|
||||
closable: true,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
return await new Promise<void>((resolve) => {
|
||||
props.chatData.length = 0;
|
||||
resolve();
|
||||
});
|
||||
} catch {
|
||||
return console.log('Oops errors!');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
// 停止响应
|
||||
const handleStop = () => {
|
||||
if (loading.value) {
|
||||
loading.value = false;
|
||||
}
|
||||
if (evtSource) {
|
||||
evtSource?.close();
|
||||
updateChatSome(uuid, props.chatData.length - 1, { loading: false });
|
||||
}
|
||||
};
|
||||
// 是否使用上下文
|
||||
const handleUsingContext = () => {
|
||||
usingContext.value = !usingContext.value;
|
||||
if (usingContext.value) {
|
||||
message.success('当前模式下, 发送消息会携带之前的聊天记录');
|
||||
} else {
|
||||
message.warning('当前模式下, 发送消息不会携带之前的聊天记录');
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chatWrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
.content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
.scrollArea {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
.chatContentArea {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
.emptyArea {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
.stopArea {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
.stopBtn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 6px 16px;
|
||||
.topArea {
|
||||
padding-left: 94px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.bottomArea {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ant-input {
|
||||
margin: 0 16px;
|
||||
}
|
||||
.ant-input,
|
||||
.ant-btn {
|
||||
height: 36px;
|
||||
}
|
||||
textarea.ant-input {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.contextBtn,
|
||||
.delBtn {
|
||||
padding: 0;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.delBtn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.contextBtn {
|
||||
color: #a8071a;
|
||||
&.enabled {
|
||||
color: @primary-color;
|
||||
}
|
||||
font-size: 18px;
|
||||
}
|
||||
.sendBtn {
|
||||
padding: 0 10px;
|
||||
font-size: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="chat" :class="[inversion ? 'self' : 'chatgpt']">
|
||||
<div class="avatar">
|
||||
<img v-if="inversion" :src="avatar()" />
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" width="1em" height="1em">
|
||||
<path
|
||||
d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="date">{{ dateTime }}</p>
|
||||
<div class="msgArea">
|
||||
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading"></chatText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import chatText from './chatText.vue';
|
||||
import defaultAvatar from '../assets/avatar.jpg';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
const { userInfo } = useUserStore();
|
||||
const avatar = () => {
|
||||
return getFileAccessHttpUrl(userInfo?.avatar)|| defaultAvatar;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chat {
|
||||
display: flex;
|
||||
margin-bottom: 1.5rem;
|
||||
&.self {
|
||||
flex-direction: row-reverse;
|
||||
.avatar {
|
||||
margin-right: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.msgArea {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.date {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
flex: none;
|
||||
margin-right: 10px;
|
||||
img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
svg {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
.date {
|
||||
color: #b4bbc4;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.msgArea {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="textWrap" :class="[inversion ? 'self' : 'chatgpt']" ref="textRef">
|
||||
<div v-if="!inversion">
|
||||
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" />
|
||||
</div>
|
||||
<div v-else class="msg" v-text="text" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import mdKatex from '@traptitech/markdown-it-katex';
|
||||
import mila from 'markdown-it-link-attributes';
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
|
||||
const textRef = ref();
|
||||
const mdi = new MarkdownIt({
|
||||
html: false,
|
||||
linkify: true,
|
||||
highlight(code, language) {
|
||||
const validLang = !!(language && hljs.getLanguage(language));
|
||||
if (validLang) {
|
||||
const lang = language ?? '';
|
||||
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang);
|
||||
}
|
||||
return highlightBlock(hljs.highlightAuto(code).value, '');
|
||||
},
|
||||
});
|
||||
|
||||
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } });
|
||||
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
|
||||
|
||||
const text = computed(() => {
|
||||
const value = props.text ?? '';
|
||||
if (!props.inversion) return mdi.render(value);
|
||||
return value;
|
||||
});
|
||||
|
||||
function highlightBlock(str: string, lang?: string) {
|
||||
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制代码</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`;
|
||||
}
|
||||
function addCopyEvents() {
|
||||
if (textRef.value) {
|
||||
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
|
||||
copyBtn.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const code = btn.parentElement?.nextElementSibling?.textContent;
|
||||
if (code) {
|
||||
copyToClip(code).then(() => {
|
||||
btn.textContent = '复制成功';
|
||||
setTimeout(() => {
|
||||
btn.textContent = '复制代码';
|
||||
}, 1e3);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeCopyEvents() {
|
||||
if (textRef.value) {
|
||||
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
|
||||
copyBtn.forEach((btn) => {
|
||||
btn.removeEventListener('click', () => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addCopyEvents();
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
addCopyEvents();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
removeCopyEvents();
|
||||
});
|
||||
|
||||
function copyToClip(text: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const input: HTMLTextAreaElement = document.createElement('textarea');
|
||||
input.setAttribute('readonly', 'readonly');
|
||||
input.value = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
if (document.execCommand('copy')) document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
resolve(text);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.textWrap {
|
||||
border-radius: 0.375rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.self {
|
||||
// background-color: #d2f9d1;
|
||||
background-color: @primary-color;
|
||||
color: #fff;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.625;
|
||||
min-width: 20px;
|
||||
}
|
||||
.chatgpt {
|
||||
background-color: #f4f6f8;
|
||||
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="presetQuestion-wrap">
|
||||
<svg
|
||||
v-if="btnShow"
|
||||
class="leftBtn"
|
||||
:class="leftBtnStatus"
|
||||
t="1710296339017"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="5070"
|
||||
@click="onScroll('prev')"
|
||||
>
|
||||
<path
|
||||
d="M970.496 543.829333l30.165333-30.165333-415.829333-415.914667a42.837333 42.837333 0 0 0-60.288 0 42.538667 42.538667 0 0 0 0 60.330667l355.413333 355.498667-355.413333 355.285333a42.496 42.496 0 0 0 0 60.288c16.64 16.64 43.861333 16.469333 60.288 0.042667l383.914667-383.701334 1.749333-1.664z"
|
||||
fill="currentColor"
|
||||
p-id="5071"
|
||||
></path>
|
||||
</svg>
|
||||
<div class="content">
|
||||
<ul ref="ulElemRef">
|
||||
<li v-for="(item, index) in data" :key="index" class="item" @click="handleQuestion(item)">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<svg
|
||||
v-if="btnShow"
|
||||
class="rightBtn"
|
||||
:class="rightBtnStatus"
|
||||
t="1710296339017"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="5070"
|
||||
@click="onScroll('next')"
|
||||
>
|
||||
<path
|
||||
d="M970.496 543.829333l30.165333-30.165333-415.829333-415.914667a42.837333 42.837333 0 0 0-60.288 0 42.538667 42.538667 0 0 0 0 60.330667l355.413333 355.498667-355.413333 355.285333a42.496 42.496 0 0 0 0 60.288c16.64 16.64 43.861333 16.469333 60.288 0.042667l383.914667-383.701334 1.749333-1.664z"
|
||||
fill="currentColor"
|
||||
p-id="5071"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script name="presetQuestion" setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
const emit = defineEmits(['outQuestion']);
|
||||
const data = ref(['请介绍一下JeecgBoot', 'JEECG有哪些优势?', 'JEECG可以做哪些事情?']);
|
||||
const leftBtnStatus = ref('');
|
||||
const rightBtnStatus = ref('');
|
||||
const rightBtn = ref('');
|
||||
const ulElemRef = ref(null);
|
||||
const btnShow = ref(false);
|
||||
let timer = null;
|
||||
const handleScroll = (e) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
const scrollLeft = e.target.scrollLeft;
|
||||
const offsetWidth = e.target.offsetWidth;
|
||||
const scrollWidth = e.target.scrollWidth;
|
||||
if (scrollWidth > offsetWidth) {
|
||||
btnShow.value = true;
|
||||
} else {
|
||||
btnShow.value = false;
|
||||
}
|
||||
if (scrollLeft <= 0) {
|
||||
leftBtnStatus.value = 'disabled';
|
||||
} else if (scrollWidth - offsetWidth == scrollLeft) {
|
||||
rightBtnStatus.value = 'disabled';
|
||||
} else {
|
||||
leftBtnStatus.value = '';
|
||||
rightBtnStatus.value = '';
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
const onScroll = (flag) => {
|
||||
const offsetWidth = ulElemRef.value.offsetWidth;
|
||||
if (flag == 'prev') {
|
||||
ulElemRef.value.scrollLeft = ulElemRef.value.scrollLeft - offsetWidth;
|
||||
} else if (flag == 'next') {
|
||||
ulElemRef.value.scrollLeft = ulElemRef.value.scrollLeft + offsetWidth;
|
||||
}
|
||||
};
|
||||
const handleQuestion = (item) => {
|
||||
emit('outQuestion', item);
|
||||
};
|
||||
onMounted(() => {
|
||||
ulElemRef.value.addEventListener('scroll', handleScroll, false);
|
||||
handleScroll({ target: ulElemRef.value });
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
ulElemRef.value.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.presetQuestion-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
&.leftBtn {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
/* 隐藏所有滚动条 */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
.item {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 2px 10px;
|
||||
width: max-content;
|
||||
margin-right: 6px;
|
||||
white-space: nowrap;
|
||||
transition: all 300ms ease;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
border-color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
288
jeecgboot-vue3/src/components/jeecg/AiChat/components/slide.vue
Normal file
288
jeecgboot-vue3/src/components/jeecg/AiChat/components/slide.vue
Normal file
@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div class="slide-wrap">
|
||||
<div class="createArea">
|
||||
<a-button type="dashed" @click="handleCreate">新建聊天</a-button>
|
||||
</div>
|
||||
<div class="historyArea">
|
||||
<ul>
|
||||
<li
|
||||
v-for="item in dataSource.history"
|
||||
:key="item.uuid"
|
||||
class="list"
|
||||
:class="[item.uuid == dataSource.active ? 'active' : 'normal', dataSource.history.length == 1 ? 'last' : '']"
|
||||
@click="handleToggleChat(item)"
|
||||
>
|
||||
<i class="icon message">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--ri"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2 8.994A5.99 5.99 0 0 1 8 3h8c3.313 0 6 2.695 6 5.994V21H8c-3.313 0-6-2.695-6-5.994zM20 19V8.994A4.004 4.004 0 0 0 16 5H8a3.99 3.99 0 0 0-4 3.994v6.012A4.004 4.004 0 0 0 8 19zm-6-8h2v2h-2zm-6 0h2v2H8z"
|
||||
></path>
|
||||
</svg>
|
||||
</i>
|
||||
<a-input
|
||||
class="title"
|
||||
ref="inputRef"
|
||||
v-if="item.isEdit"
|
||||
:defaultValue="item.title"
|
||||
placeholder="请输入标题"
|
||||
@change="handleInputChange"
|
||||
@blur="inputBlur(item)"
|
||||
/>
|
||||
<span class="title" v-else>{{ item.title }}</span>
|
||||
<span class="icon edit" @click="handleEdit(item)" v-if="!item.isEdit">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--ri"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6.414 15.89L16.556 5.748l-1.414-1.414L5 14.476v1.414zm.829 2H3v-4.243L14.435 2.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414zM3 19.89h18v2H3z"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="icon del" v-if="!item.isEdit">
|
||||
<a-popconfirm title="确定删除此记录?" placement="bottom" ok-text="确定" cancel-text="取消" @confirm="handleDel(item)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--ri"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@click.stop=""
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zm1 2H6v12h12zm-9 3h2v6H9zm4 0h2v6h-2zM9 4v2h6V4z"
|
||||
></path>
|
||||
</svg>
|
||||
</a-popconfirm>
|
||||
</span>
|
||||
<span class="icon save" v-if="item.isEdit" @click="handleSave(item)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--ri"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 19v-6h10v6h2V7.828L16.172 5H5v14zM4 3h13l4 4v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m5 12v4h6v-4z"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
const props = defineProps(['dataSource']);
|
||||
const inputRef = ref(null);
|
||||
let inputValue = '';
|
||||
//新建聊天
|
||||
const handleCreate = () => {
|
||||
const uuid = getUuid();
|
||||
props.dataSource.history.unshift({ title: '新建聊天', uuid, isEdit: false });
|
||||
props.dataSource.chat.unshift({ uuid, data: [] });
|
||||
// 新建第一个(需要高亮选中)
|
||||
if (props.dataSource.history.length == 1) {
|
||||
props.dataSource.active = uuid;
|
||||
}
|
||||
};
|
||||
// 切换聊天
|
||||
const handleToggleChat = (item) => {
|
||||
if (item.uuid != props.dataSource.active) {
|
||||
props.dataSource.active = item.uuid;
|
||||
const findItem = props.dataSource.history.find((item) => item.isEdit);
|
||||
if (findItem) {
|
||||
handleSave(findItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleInputChange = (e) => {
|
||||
inputValue = e.target.value.trim();
|
||||
};
|
||||
// 失去焦点
|
||||
const inputBlur = (item) => {
|
||||
item.isEdit = false;
|
||||
item.title = inputValue;
|
||||
};
|
||||
// 编辑
|
||||
const handleEdit = (item) => {
|
||||
item.isEdit = true;
|
||||
inputValue = item.title;
|
||||
};
|
||||
// 保存
|
||||
const handleSave = (item) => {
|
||||
item.isEdit = false;
|
||||
item.title = inputValue;
|
||||
};
|
||||
// 删除
|
||||
const handleDel = (data) => {
|
||||
const findIndex = props.dataSource.history.findIndex((item) => item.uuid == data.uuid);
|
||||
if (findIndex != -1) {
|
||||
props.dataSource.history.splice(findIndex, 1);
|
||||
props.dataSource.chat.splice(findIndex, 1);
|
||||
// 删除的是当前active的,active往前移,前面没了往后移。
|
||||
if (props.dataSource.history.length) {
|
||||
if (props.dataSource.active == data.uuid) {
|
||||
if (findIndex > 0) {
|
||||
props.dataSource.active = props.dataSource.history[findIndex - 1].uuid;
|
||||
} else {
|
||||
props.dataSource.active = props.dataSource.history[0].uuid;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 删没了(删除了最后一个)
|
||||
props.dataSource.active = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
watch(
|
||||
() => inputRef.value,
|
||||
(newVal: any) => {
|
||||
if (newVal?.length) {
|
||||
newVal[0].focus();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
// 指定长度和基数
|
||||
const getUuid = (len = 10, radix = 16) => {
|
||||
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
|
||||
var uuid: any = [],
|
||||
i;
|
||||
radix = radix || chars.length;
|
||||
|
||||
if (len) {
|
||||
for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
|
||||
} else {
|
||||
var r;
|
||||
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
|
||||
uuid[14] = '4';
|
||||
for (i = 0; i < 36; i++) {
|
||||
if (!uuid[i]) {
|
||||
r = 0 | (Math.random() * 16);
|
||||
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
|
||||
}
|
||||
}
|
||||
}
|
||||
return uuid.join('');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.slide-wrap {
|
||||
border-right: 1px solid #e5e7eb;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.historyArea {
|
||||
padding: 20px;
|
||||
padding-top: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
.historyArea ul li:hover{
|
||||
.del{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.createArea {
|
||||
padding: 20px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.ant-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.list {
|
||||
width: 100%;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
border-width: 1px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
&:hover,
|
||||
&.active {
|
||||
border-color: @primary-color;
|
||||
color: @primary-color;
|
||||
}
|
||||
.edit,
|
||||
.save,
|
||||
.del {
|
||||
display: none;
|
||||
}
|
||||
&.active {
|
||||
.edit,
|
||||
.save,
|
||||
.del {
|
||||
display: block;
|
||||
}
|
||||
&.last {
|
||||
.del {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.message {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.edit {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
&.ant-input {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
186
jeecgboot-vue3/src/components/jeecg/AiChat/data.js
Normal file
186
jeecgboot-vue3/src/components/jeecg/AiChat/data.js
Normal file
@ -0,0 +1,186 @@
|
||||
export const localData = {
|
||||
active: 1002,
|
||||
usingContext: true,
|
||||
history: [
|
||||
{
|
||||
title: '标题02',
|
||||
uuid: 1706083575869,
|
||||
isEdit: false,
|
||||
},
|
||||
{
|
||||
uuid: 1002,
|
||||
title: '标题01',
|
||||
isEdit: false,
|
||||
},
|
||||
],
|
||||
chat: [
|
||||
{
|
||||
uuid: 1706083575869,
|
||||
data: [
|
||||
{
|
||||
dateTime: '2024/1/24 16:06:27',
|
||||
text: '?',
|
||||
inversion: true,
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: {
|
||||
prompt: '?',
|
||||
options: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 16:06:29',
|
||||
text: 'Hello! How can I assist you today?',
|
||||
inversion: false,
|
||||
error: false,
|
||||
loading: false,
|
||||
conversationOptions: {
|
||||
parentMessageId: 'chatcmpl-8kSZA0wju7X8sOdJIyxtpDj0RQVu1',
|
||||
},
|
||||
requestOptions: {
|
||||
prompt: '?',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
uuid: 1002,
|
||||
data: [
|
||||
{
|
||||
dateTime: '2024/1/24 14:01:52',
|
||||
text: '1',
|
||||
inversion: true,
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: {
|
||||
prompt: '1',
|
||||
options: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 14:01:54',
|
||||
text: 'Yes, how can I assist you?',
|
||||
inversion: false,
|
||||
error: false,
|
||||
loading: false,
|
||||
conversationOptions: {
|
||||
parentMessageId: 'chatcmpl-8kQcb6mbF04o5hpule4SdHk2jFvNQ',
|
||||
},
|
||||
requestOptions: {
|
||||
prompt: '1',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 14:03:45',
|
||||
text: '?',
|
||||
inversion: true,
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: {
|
||||
prompt: '?',
|
||||
options: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 14:03:47',
|
||||
text: "I'm sorry if my previous response was not clear. Please let me know how I can help you or what you would like to discuss.",
|
||||
inversion: false,
|
||||
error: false,
|
||||
loading: false,
|
||||
conversationOptions: {
|
||||
parentMessageId: 'chatcmpl-8kQeQ2t8YCXmLeF0ECGkkuOJlk4Pi',
|
||||
},
|
||||
requestOptions: {
|
||||
prompt: '?',
|
||||
options: {
|
||||
parentMessageId: 'chatcmpl-8kQcb6mbF04o5hpule4SdHk2jFvNQ',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 14:10:19',
|
||||
text: 'js 递归',
|
||||
inversion: true,
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: {
|
||||
prompt: 'js 递归',
|
||||
options: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 14:10:33',
|
||||
text: 'JavaScript supports recursion, which is the process of a function calling itself. Recursion can be useful for solving problems that can be broken down into smaller, similar sub-problems.\n\nHere\'s an example of a simple recursive function in JavaScript:\n\n```javascript\nfunction countdown(n) {\n if (n <= 0) {\n console.log("Done!");\n } else {\n console.log(n);\n countdown(n - 1); // recursive call\n }\n}\n\ncountdown(5);\n```\n\nIn this example, the `countdown` function takes an argument `n` and logs the value of `n` to the console. If `n` is greater than zero, it then calls itself with `n - 1`. This process continues until `n` becomes less than or equal to zero, at which point it logs "Done!".\n\nRecursion can be helpful in solving problems that involve tree structures, factorial calculations, searching algorithms, and more. However, it\'s important to use recursion properly to avoid infinite loops or excessive stack usage.',
|
||||
inversion: false,
|
||||
error: false,
|
||||
loading: false,
|
||||
conversationOptions: {
|
||||
parentMessageId: 'chatcmpl-8kQkmCbnRe4fG1FhWTlY0EyHTpqau',
|
||||
},
|
||||
requestOptions: {
|
||||
prompt: 'js 递归',
|
||||
options: {
|
||||
parentMessageId: 'chatcmpl-8kQeQ2t8YCXmLeF0ECGkkuOJlk4Pi',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 14:17:15',
|
||||
text: 'js 递归',
|
||||
inversion: true,
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: {
|
||||
prompt: 'js 递归',
|
||||
options: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 14:23:50',
|
||||
text: "Certainly! Here's an example of how you can use recursion in JavaScript:\n\n```javascript\nfunction factorial(n) {\n if (n === 0) {\n return 1;\n } else {\n return n * factorial(n - 1);\n }\n}\n\nconsole.log(factorial(5)); // Output: 120\n```\n\nIn this example, the `factorial` function calculates the factorial of a given number `n` using recursion. If `n` is equal to 0, it returns 1, which is the base case. Otherwise, it recursively calls itself with `n - 1`, multiplying the current value of `n` with the result of the recursive call.\n\nWhen calling `factorial(5)`, the function will execute as follows:\n\n- `factorial(5)` calls `factorial(4)`\n- `factorial(4)` calls `factorial(3)`\n- `factorial(3)` calls `factorial(2)`\n- `factorial(2)` calls `factorial(1)`\n- `factorial(1)` calls `factorial(0)`\n- `factorial(0)` returns 1\n- `factorial(1)` returns 1 * 1 = 1\n- `factorial(2)` returns 2 * 1 = 2\n- `factorial(3)` returns 3 * 2 = 6\n- `factorial(4)` returns 4 * 6 = 24\n- `factorial(5)` returns 5 * 24 = 120\n\nThe final result is then printed to the console using `console.log`.",
|
||||
inversion: false,
|
||||
error: false,
|
||||
loading: false,
|
||||
conversationOptions: {
|
||||
parentMessageId: 'chatcmpl-8kQwWVoZoWyqjbWuwMJmu6w3hBvXj',
|
||||
},
|
||||
requestOptions: {
|
||||
prompt: 'js 递归',
|
||||
options: {
|
||||
parentMessageId: 'chatcmpl-8kQkmCbnRe4fG1FhWTlY0EyHTpqau',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 15:05:30',
|
||||
text: '///',
|
||||
inversion: true,
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: {
|
||||
prompt: '///',
|
||||
options: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
dateTime: '2024/1/24 15:05:33',
|
||||
text: "I apologize if my previous response was not what you were expecting. If you have any specific questions or need further assistance, please let me know and I'll be happy to help.",
|
||||
inversion: false,
|
||||
error: false,
|
||||
loading: false,
|
||||
conversationOptions: {
|
||||
parentMessageId: 'chatcmpl-8kRcAggkC4u47d34UcQW3cI0htw0w',
|
||||
},
|
||||
requestOptions: {
|
||||
prompt: '///',
|
||||
options: {
|
||||
parentMessageId: 'chatcmpl-8kQwWVoZoWyqjbWuwMJmu6w3hBvXj',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
28
jeecgboot-vue3/src/components/jeecg/AiChat/hooks/useChat.ts
Normal file
28
jeecgboot-vue3/src/components/jeecg/AiChat/hooks/useChat.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { useChatStore } from '@/store'
|
||||
|
||||
export function useChat() {
|
||||
const chatStore = useChatStore()
|
||||
|
||||
const getChatByUuidAndIndex = (uuid: number, index: number) => {
|
||||
return chatStore.getChatByUuidAndIndex(uuid, index)
|
||||
}
|
||||
|
||||
const addChat = (uuid: number, chat: Chat.Chat) => {
|
||||
chatStore.addChatByUuid(uuid, chat)
|
||||
}
|
||||
|
||||
const updateChat = (uuid: number, index: number, chat: Chat.Chat) => {
|
||||
chatStore.updateChatByUuid(uuid, index, chat)
|
||||
}
|
||||
|
||||
const updateChatSome = (uuid: number, index: number, chat: Partial<Chat.Chat>) => {
|
||||
chatStore.updateChatSomeByUuid(uuid, index, chat)
|
||||
}
|
||||
|
||||
return {
|
||||
addChat,
|
||||
updateChat,
|
||||
updateChatSome,
|
||||
getChatByUuidAndIndex,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
type ScrollElement = HTMLDivElement | null
|
||||
|
||||
interface ScrollReturn {
|
||||
scrollRef: Ref<ScrollElement>
|
||||
scrollToBottom: () => Promise<void>
|
||||
scrollToTop: () => Promise<void>
|
||||
scrollToBottomIfAtBottom: () => Promise<void>
|
||||
}
|
||||
|
||||
export function useScroll(): ScrollReturn {
|
||||
const scrollRef = ref<ScrollElement>(null)
|
||||
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick()
|
||||
if (scrollRef.value)
|
||||
scrollRef.value.scrollTop = scrollRef.value.scrollHeight
|
||||
}
|
||||
|
||||
const scrollToTop = async () => {
|
||||
await nextTick()
|
||||
if (scrollRef.value)
|
||||
scrollRef.value.scrollTop = 0
|
||||
}
|
||||
|
||||
const scrollToBottomIfAtBottom = async () => {
|
||||
await nextTick()
|
||||
if (scrollRef.value) {
|
||||
const threshold = 100 // Threshold, indicating the distance threshold to the bottom of the scroll bar.
|
||||
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight
|
||||
if (distanceToBottom <= threshold)
|
||||
scrollRef.value.scrollTop = scrollRef.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
scrollToBottom,
|
||||
scrollToTop,
|
||||
scrollToBottomIfAtBottom,
|
||||
}
|
||||
}
|
||||
214
jeecgboot-vue3/src/components/jeecg/AiChat/index.vue
Normal file
214
jeecgboot-vue3/src/components/jeecg/AiChat/index.vue
Normal file
@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div ref="chatContainerRef" class="chat-container" :style="chatContainerStyle">
|
||||
<template v-if="dataSource">
|
||||
<div class="leftArea" :class="[expand ? 'expand' : 'shrink']">
|
||||
<div class="content">
|
||||
<slide v-if="uuid" :dataSource="dataSource"></slide>
|
||||
</div>
|
||||
<div class="toggle-btn" @click="handleToggle">
|
||||
<span class="icon">
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rightArea" :class="[expand ? 'expand' : 'shrink']">
|
||||
<chat v-if="uuid && chatVisible" :uuid="uuid" :chatData="chatData" :dataSource="dataSource"></chat>
|
||||
</div>
|
||||
</template>
|
||||
<Spin v-else :spinning="true"></Spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import slide from './components/slide.vue';
|
||||
import chat from './components/chat.vue';
|
||||
import { Spin } from 'ant-design-vue';
|
||||
import { ref, watch, nextTick, onUnmounted } from 'vue';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
const configUrl = {
|
||||
get: '/test/ai/chat/history/get',
|
||||
save: '/test/ai/chat/history/save',
|
||||
};
|
||||
const userId = useUserStore().getUserInfo?.id;
|
||||
const localKey = JEECG_CHAT_KEY + userId;
|
||||
let timer: any = null;
|
||||
let unwatch01: any = null;
|
||||
let unwatch02: any = null;
|
||||
const dataSource = ref<any>(null);
|
||||
const uuid = ref(null);
|
||||
const chatData = ref([]);
|
||||
const expand = ref<any>(true);
|
||||
const chatVisible = ref(true);
|
||||
const chatContainerRef = ref<any>(null);
|
||||
const chatContainerStyle = ref({});
|
||||
const handleToggle = () => {
|
||||
expand.value = !expand.value;
|
||||
};
|
||||
// 初始查询历史
|
||||
const init = () => {
|
||||
const priming = () => {
|
||||
dataSource.value = {
|
||||
active: 1002,
|
||||
usingContext: true,
|
||||
history: [{ uuid: 1002, title: '新建聊天', isEdit: false }],
|
||||
chat: [{ uuid: 1002, data: [] }],
|
||||
};
|
||||
};
|
||||
defHttp
|
||||
.get({ url: configUrl.get })
|
||||
.then((res) => {
|
||||
const { content } = res;
|
||||
if (content) {
|
||||
const json = JSON.parse(content);
|
||||
if (json.history?.length) {
|
||||
dataSource.value = json;
|
||||
} else {
|
||||
priming();
|
||||
}
|
||||
} else {
|
||||
priming();
|
||||
}
|
||||
!unwatch01 && execute();
|
||||
})
|
||||
.catch(() => {
|
||||
priming();
|
||||
});
|
||||
};
|
||||
const save = (content) => {
|
||||
defHttp.post({ url: configUrl.save, params: { content: JSON.stringify(content) } }, { isTransformResponse: false });
|
||||
};
|
||||
// 监听dataSource变化执行操作
|
||||
const execute = () => {
|
||||
unwatch01 = watch(
|
||||
() => dataSource.value.active,
|
||||
(value) => {
|
||||
if (value) {
|
||||
const findItem = dataSource.value.chat.find((item) => item.uuid === value);
|
||||
if (findItem) {
|
||||
uuid.value = findItem.uuid;
|
||||
chatData.value = findItem.data;
|
||||
}
|
||||
chatVisible.value = false;
|
||||
nextTick(() => {
|
||||
chatVisible.value = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
unwatch02 = watch(dataSource.value, () => {
|
||||
clearInterval(timer);
|
||||
timer = setTimeout(() => {
|
||||
save(dataSource.value);
|
||||
}, 2e3);
|
||||
});
|
||||
};
|
||||
onUnmounted(() => {
|
||||
unwatch01 && unwatch01();
|
||||
unwatch02 && unwatch02();
|
||||
});
|
||||
watch(
|
||||
() => chatContainerRef.value,
|
||||
() => {
|
||||
chatContainerStyle.value = { height: `${chatContainerRef.value.offsetHeight}px` };
|
||||
}
|
||||
);
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@width: 260px;
|
||||
.chat-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
box-shadow:
|
||||
0 0 #0000,
|
||||
0 0 #0000,
|
||||
0 0 #0000,
|
||||
0 0 #0000,
|
||||
0 4px 6px -1px rgb(0 0 0 / 0.1),
|
||||
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
border-width: 1px;
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
:deep(.ant-spin) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
.leftArea {
|
||||
width: @width;
|
||||
transition: 0.3s left;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
&.shrink {
|
||||
left: -@width;
|
||||
.toggle-btn {
|
||||
.icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.toggle-btn {
|
||||
transition:
|
||||
color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
right 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: rgb(51, 54, 57);
|
||||
border: 1px solid rgb(239, 239, 245);
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.06);
|
||||
transform: translateX(50%) translateY(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
.icon {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform: rotate(180deg);
|
||||
font-size: 18px;
|
||||
height: 18px;
|
||||
svg {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
.rightArea {
|
||||
margin-left: @width;
|
||||
transition: 0.3s margin-left;
|
||||
&.shrink {
|
||||
margin-left: 0;
|
||||
}
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
206
jeecgboot-vue3/src/components/jeecg/AiChat/style/highlight.less
Normal file
206
jeecgboot-vue3/src/components/jeecg/AiChat/style/highlight.less
Normal file
@ -0,0 +1,206 @@
|
||||
html.dark {
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
padding: 3px 5px
|
||||
}
|
||||
|
||||
.hljs {
|
||||
color: #abb2bf;
|
||||
background: #282c34
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-operator,
|
||||
.hljs-pattern-match {
|
||||
color: #f92672
|
||||
}
|
||||
|
||||
.hljs-function,
|
||||
.hljs-pattern-match .hljs-constructor {
|
||||
color: #61aeee
|
||||
}
|
||||
|
||||
.hljs-function .hljs-params {
|
||||
color: #a6e22e
|
||||
}
|
||||
|
||||
.hljs-function .hljs-params .hljs-typing {
|
||||
color: #fd971f
|
||||
}
|
||||
|
||||
.hljs-module-access .hljs-module {
|
||||
color: #7e57c2
|
||||
}
|
||||
|
||||
.hljs-constructor {
|
||||
color: #e2b93d
|
||||
}
|
||||
|
||||
.hljs-constructor .hljs-string {
|
||||
color: #9ccc65
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #b18eb1;
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-formula {
|
||||
color: #c678dd
|
||||
}
|
||||
|
||||
.hljs-deletion,
|
||||
.hljs-name,
|
||||
.hljs-section,
|
||||
.hljs-selector-tag,
|
||||
.hljs-subst {
|
||||
color: #e06c75
|
||||
}
|
||||
|
||||
.hljs-literal {
|
||||
color: #56b6c2
|
||||
}
|
||||
|
||||
.hljs-addition,
|
||||
.hljs-attribute,
|
||||
.hljs-meta .hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-string {
|
||||
color: #98c379
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-title.class_ {
|
||||
color: #e6c07b
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-number,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable {
|
||||
color: #d19a66
|
||||
}
|
||||
|
||||
.hljs-bullet,
|
||||
.hljs-link,
|
||||
.hljs-meta,
|
||||
.hljs-selector-id,
|
||||
.hljs-symbol,
|
||||
.hljs-title {
|
||||
color: #61aeee
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
padding: 3px 5px;
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.hljs {
|
||||
color: #383a42;
|
||||
background: #fafafa
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #a0a1a7;
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-formula,
|
||||
.hljs-keyword {
|
||||
color: #a626a4
|
||||
}
|
||||
|
||||
.hljs-deletion,
|
||||
.hljs-name,
|
||||
.hljs-section,
|
||||
.hljs-selector-tag,
|
||||
.hljs-subst {
|
||||
color: #e45649
|
||||
}
|
||||
|
||||
.hljs-literal {
|
||||
color: #0184bb
|
||||
}
|
||||
|
||||
.hljs-addition,
|
||||
.hljs-attribute,
|
||||
.hljs-meta .hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-string {
|
||||
color: #50a14f
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-number,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable {
|
||||
color: #986801
|
||||
}
|
||||
|
||||
.hljs-bullet,
|
||||
.hljs-link,
|
||||
.hljs-meta,
|
||||
.hljs-selector-id,
|
||||
.hljs-symbol,
|
||||
.hljs-title {
|
||||
color: #4078f2
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-title.class_ {
|
||||
color: #c18401
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline
|
||||
}
|
||||
}
|
||||
135
jeecgboot-vue3/src/components/jeecg/AiChat/style/style.less
Normal file
135
jeecgboot-vue3/src/components/jeecg/AiChat/style/style.less
Normal file
@ -0,0 +1,135 @@
|
||||
.markdown-body {
|
||||
background-color: transparent;
|
||||
font-size: 14px;
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
pre code,
|
||||
pre tt {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.highlight pre,
|
||||
pre {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: #b3b3b3;
|
||||
|
||||
&__copy {
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: #65a665;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.markdown-body-generate>dd:last-child:after,
|
||||
&.markdown-body-generate>dl:last-child:after,
|
||||
&.markdown-body-generate>dt:last-child:after,
|
||||
&.markdown-body-generate>h1:last-child:after,
|
||||
&.markdown-body-generate>h2:last-child:after,
|
||||
&.markdown-body-generate>h3:last-child:after,
|
||||
&.markdown-body-generate>h4:last-child:after,
|
||||
&.markdown-body-generate>h5:last-child:after,
|
||||
&.markdown-body-generate>h6:last-child:after,
|
||||
&.markdown-body-generate>li:last-child:after,
|
||||
&.markdown-body-generate>ol:last-child li:last-child:after,
|
||||
&.markdown-body-generate>p:last-child:after,
|
||||
&.markdown-body-generate>pre:last-child code:after,
|
||||
&.markdown-body-generate>td:last-child:after,
|
||||
&.markdown-body-generate>ul:last-child li:last-child:after {
|
||||
animation: blink 1s steps(5, start) infinite;
|
||||
color: #000;
|
||||
content: '_';
|
||||
font-weight: 700;
|
||||
margin-left: 3px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
|
||||
.markdown-body {
|
||||
|
||||
&.markdown-body-generate>dd:last-child:after,
|
||||
&.markdown-body-generate>dl:last-child:after,
|
||||
&.markdown-body-generate>dt:last-child:after,
|
||||
&.markdown-body-generate>h1:last-child:after,
|
||||
&.markdown-body-generate>h2:last-child:after,
|
||||
&.markdown-body-generate>h3:last-child:after,
|
||||
&.markdown-body-generate>h4:last-child:after,
|
||||
&.markdown-body-generate>h5:last-child:after,
|
||||
&.markdown-body-generate>h6:last-child:after,
|
||||
&.markdown-body-generate>li:last-child:after,
|
||||
&.markdown-body-generate>ol:last-child li:last-child:after,
|
||||
&.markdown-body-generate>p:last-child:after,
|
||||
&.markdown-body-generate>pre:last-child code:after,
|
||||
&.markdown-body-generate>td:last-child:after,
|
||||
&.markdown-body-generate>ul:last-child li:last-child:after {
|
||||
color: #65a665;
|
||||
}
|
||||
}
|
||||
|
||||
.message-reply {
|
||||
.whitespace-pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
color: var(--n-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.highlight pre,
|
||||
pre {
|
||||
background-color: #282c34;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 533px) {
|
||||
.markdown-body .code-block-wrapper {
|
||||
padding: unset;
|
||||
|
||||
code {
|
||||
padding: 24px 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
84
jeecgboot-vue3/src/components/jeecg/ExcelButton.vue
Normal file
84
jeecgboot-vue3/src/components/jeecg/ExcelButton.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<a-button type="primary" v-if="hasExportAuth() && config.export" preIcon="ant-design:export-outlined" @click="onExportXls()"> 导出</a-button>
|
||||
<a-upload name="file" :showUploadList="false" v-if="hasImportAuth() && config.import" :customRequest="(file) => onImportXls(file)">
|
||||
<a-button type="primary" preIcon="ant-design:import-outlined">导入</a-button>
|
||||
</a-upload>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="ExcelButton">
|
||||
import { PropType } from 'vue';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import { useMethods } from '/@/hooks/system/useMethods';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
// 定义 excel 方法所需参数
|
||||
interface ExcelConfig {
|
||||
// 导出配置
|
||||
exportConfig: {
|
||||
url: string;
|
||||
// 导出文件名
|
||||
name?: string | (() => string);
|
||||
//按钮权限
|
||||
auth?: string | string[];
|
||||
};
|
||||
// 导入配置
|
||||
importConfig: {
|
||||
url: string;
|
||||
// 导出成功后的回调
|
||||
success?: (fileInfo?: any) => void;
|
||||
//按钮权限
|
||||
auth?: string | string[];
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 定义组件参数
|
||||
*/
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object as PropType<ExcelConfig>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
//按钮权限问题
|
||||
const { hasPermission } = usePermission();
|
||||
//导入导出方法
|
||||
const { handleExportXls, handleImportXls } = useMethods();
|
||||
|
||||
const $message = useMessage();
|
||||
// 导出 excel
|
||||
function onExportXls() {
|
||||
let { url, name } = props.config?.export ?? {};
|
||||
if (url) {
|
||||
let title = typeof name === 'function' ? name() : name;
|
||||
return handleExportXls(title as string, url);
|
||||
} else {
|
||||
$message.createMessage.warn('没有传递 export.url 参数');
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
// 导入 excel
|
||||
function onImportXls(file) {
|
||||
let { url, success } = props.config?.import ?? {};
|
||||
if (url) {
|
||||
return handleImportXls(file, url, success);
|
||||
} else {
|
||||
$message.createMessage.warn('没有传递 import.url 参数');
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
// 导入按钮权限
|
||||
function hasImportAuth() {
|
||||
let auth = props.config?.import?.auth;
|
||||
return auth && auth.length > 0 ? hasPermission(auth) : true;
|
||||
}
|
||||
|
||||
// 导出按钮权限
|
||||
function hasExportAuth() {
|
||||
let auth = props.config?.export?.auth;
|
||||
return auth && auth.length > 0 ? hasPermission(auth) : true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
160
jeecgboot-vue3/src/components/jeecg/JPrompt/JPrompt.vue
Normal file
160
jeecgboot-vue3/src/components/jeecg/JPrompt/JPrompt.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<ConfigProvider :locale="getAntdLocale">
|
||||
<Modal v-bind="getProps">
|
||||
<Spin :spinning="loading">
|
||||
<div style="padding: 20px;">
|
||||
<div v-html="options.content" style="margin-bottom: 8px"></div>
|
||||
<BasicForm @register="registerForm">
|
||||
<template #customInput="{ model, field }">
|
||||
<Input ref="inputRef" v-model:value="model[field]" :placeholder="placeholder" @pressEnter="onSubmit" @input="onChange" />
|
||||
</template>
|
||||
</BasicForm>
|
||||
</div>
|
||||
</Spin>
|
||||
</Modal>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { JPromptProps } from './typing';
|
||||
import type { ModalProps } from '/@/components/Modal';
|
||||
import { ref, defineComponent, computed, unref, onMounted, nextTick } from 'vue';
|
||||
import { BasicForm, useForm } from '/@/components/Form';
|
||||
import { Modal, Spin, Input, ConfigProvider } from 'ant-design-vue';
|
||||
import { useLocale } from '/@/locales/useLocale';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JPrompt',
|
||||
components: {
|
||||
Modal,
|
||||
Spin,
|
||||
Input,
|
||||
BasicForm,
|
||||
ConfigProvider,
|
||||
},
|
||||
emits: ['register'],
|
||||
setup(props, { emit }) {
|
||||
const inputRef = ref();
|
||||
const { getAntdLocale } = useLocale();
|
||||
const visible = ref(false);
|
||||
// 当前是否正在加载中
|
||||
const loading = ref(false);
|
||||
const options = ref<JPromptProps>({});
|
||||
const placeholder = computed(() => options.value.placeholder ?? '请输入内容');
|
||||
// 注册表单
|
||||
const [registerForm, { clearValidate, setFieldsValue, validate, updateSchema }] = useForm({
|
||||
compact: true,
|
||||
wrapperCol: { span: 24 },
|
||||
schemas: [
|
||||
{
|
||||
label: '',
|
||||
field: 'input',
|
||||
component: 'Input',
|
||||
slot: 'customInput',
|
||||
},
|
||||
],
|
||||
showActionButtonGroup: false,
|
||||
});
|
||||
|
||||
// 弹窗最终props
|
||||
const getProps = computed(() => {
|
||||
let opt = options.value;
|
||||
let modalProps: Partial<ModalProps> = {
|
||||
width: (opt.width ?? 500) as number,
|
||||
title: (opt.title ?? 'prompt') as string,
|
||||
open: unref(visible),
|
||||
confirmLoading: unref(loading),
|
||||
};
|
||||
let finalProps: Recordable = {
|
||||
...modalProps,
|
||||
...props,
|
||||
...opt,
|
||||
onOk: onSubmit,
|
||||
onCancel() {
|
||||
if (typeof options.value.onCancel === 'function') {
|
||||
options.value.onCancel();
|
||||
}
|
||||
close();
|
||||
},
|
||||
};
|
||||
return finalProps;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
emit('register', {
|
||||
openModal,
|
||||
setLoading,
|
||||
getVisible: visible,
|
||||
});
|
||||
});
|
||||
|
||||
/** 弹窗开启 */
|
||||
async function openModal(opt: any) {
|
||||
document.body.focus();
|
||||
|
||||
options.value = opt;
|
||||
visible.value = true;
|
||||
await nextTick();
|
||||
await updateSchema({
|
||||
field: 'input',
|
||||
required: options.value.required,
|
||||
rules: options.value.rules,
|
||||
dynamicRules: options.value.dynamicRules,
|
||||
} as any);
|
||||
await setFieldsValue({
|
||||
input: options.value.defaultValue ?? '',
|
||||
});
|
||||
await clearValidate();
|
||||
inputRef.value?.focus();
|
||||
}
|
||||
|
||||
/** 弹窗关闭 */
|
||||
function close() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function onChange() {
|
||||
validate()
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function onSubmit() {
|
||||
try {
|
||||
const { onOk } = options.value;
|
||||
// 表单验证
|
||||
let values = await validate();
|
||||
setLoading(true);
|
||||
if (typeof onOk === 'function') {
|
||||
let flag = await onOk(values.input);
|
||||
// 只有返回 false 才阻止关闭弹窗
|
||||
if (!(flag === false)) {
|
||||
close();
|
||||
}
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置加载状态*/
|
||||
function setLoading(flag) {
|
||||
loading.value = flag;
|
||||
}
|
||||
|
||||
return {
|
||||
inputRef,
|
||||
getProps,
|
||||
loading,
|
||||
options,
|
||||
placeholder,
|
||||
getAntdLocale,
|
||||
onChange,
|
||||
onSubmit,
|
||||
|
||||
registerForm,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,57 @@
|
||||
import type { JPromptProps } from '../typing';
|
||||
import { render, createVNode, nextTick } from 'vue';
|
||||
import { error } from '/@/utils/log';
|
||||
import JPrompt from '../JPrompt.vue';
|
||||
|
||||
export function useJPrompt() {
|
||||
|
||||
function createJPrompt(options: JPromptProps) {
|
||||
let instance = null;
|
||||
const box = document.createElement('div');
|
||||
const vm = createVNode(JPrompt, {
|
||||
// 注册
|
||||
async onRegister(ins) {
|
||||
instance = ins;
|
||||
await nextTick();
|
||||
ins.openModal(options);
|
||||
},
|
||||
// 销毁
|
||||
afterClose() {
|
||||
render(null, box);
|
||||
document.body.removeChild(box);
|
||||
},
|
||||
});
|
||||
// 挂载到 body
|
||||
render(vm, box);
|
||||
document.body.appendChild(box);
|
||||
|
||||
function getInstance(): any {
|
||||
if (instance == null) {
|
||||
error('useJPrompt instance is undefined!');
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
function updateModal(options: JPromptProps) {
|
||||
getInstance()?.updateModal(options);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
getInstance()?.closeModal();
|
||||
}
|
||||
|
||||
function setLoading(loading) {
|
||||
getInstance()?.setLoading(loading);
|
||||
}
|
||||
|
||||
return {
|
||||
closeModal,
|
||||
updateModal,
|
||||
setLoading,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createJPrompt,
|
||||
};
|
||||
}
|
||||
2
jeecgboot-vue3/src/components/jeecg/JPrompt/index.ts
Normal file
2
jeecgboot-vue3/src/components/jeecg/JPrompt/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { useJPrompt } from './hooks/useJPrompt';
|
||||
export { default as JPrompt } from './JPrompt.vue';
|
||||
15
jeecgboot-vue3/src/components/jeecg/JPrompt/typing.ts
Normal file
15
jeecgboot-vue3/src/components/jeecg/JPrompt/typing.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ModalOptionsPartial } from '/@/hooks/web/useMessage';
|
||||
import { RenderCallbackParams, Rule } from '/@/components/Form';
|
||||
|
||||
export interface JPromptProps extends ModalOptionsPartial {
|
||||
// 输入框是否必填
|
||||
required?: boolean;
|
||||
// 校验
|
||||
rules?: Rule[];
|
||||
// 动态校验
|
||||
dynamicRules?: (renderCallbackParams: RenderCallbackParams) => Rule[];
|
||||
// 占位字符
|
||||
placeholder?: string;
|
||||
// 输入框默认值
|
||||
defaultValue?: string;
|
||||
}
|
||||
2
jeecgboot-vue3/src/components/jeecg/JVxeTable/hooks.ts
Normal file
2
jeecgboot-vue3/src/components/jeecg/JVxeTable/hooks.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { useJVxeCompProps, useJVxeComponent } from './src/hooks/useJVxeComponent';
|
||||
export { useResolveComponent } from './src/hooks/useData';
|
||||
4
jeecgboot-vue3/src/components/jeecg/JVxeTable/index.ts
Normal file
4
jeecgboot-vue3/src/components/jeecg/JVxeTable/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as JVxeTable } from './src/JVxeTable';
|
||||
export { registerJVxeTable } from './src/install';
|
||||
export { deleteComponent } from './src/componentMap';
|
||||
export { registerComponent, registerAsyncComponent, registerASyncComponentReal } from './src/utils/registerUtils';
|
||||
@ -0,0 +1,82 @@
|
||||
import { defineComponent, h, nextTick, ref, useSlots } from 'vue';
|
||||
import { vxeEmits, vxeProps } from './vxe.data';
|
||||
import { useData, useRefs, useResolveComponent as rc } from './hooks/useData';
|
||||
import { useColumns } from './hooks/useColumns';
|
||||
import { useColumnsCache } from './hooks/useColumnsCache';
|
||||
import { useMethods } from './hooks/useMethods';
|
||||
import { useDataSource } from './hooks/useDataSource';
|
||||
import { useDragSort } from './hooks/useDragSort';
|
||||
import { useRenderComponents } from './hooks/useRenderComponents';
|
||||
import { useFinallyProps } from './hooks/useFinallyProps';
|
||||
import { JVxeTableProps } from './types';
|
||||
import './style/index.less';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeTable',
|
||||
inheritAttrs: false,
|
||||
props: vxeProps(),
|
||||
emits: [...vxeEmits],
|
||||
setup(props: JVxeTableProps, context) {
|
||||
const instanceRef = ref();
|
||||
const refs = useRefs();
|
||||
const slots = useSlots();
|
||||
const data = useData(props);
|
||||
const { methods, publicMethods, created } = useMethods(props, context, data, refs, instanceRef);
|
||||
created();
|
||||
useColumns(props, data, methods, slots);
|
||||
useDataSource(props, data, methods, refs);
|
||||
useDragSort(props, methods);
|
||||
// update-begin--author:liaozhiyang---date:20240321---for:【QQYUN-8566】JVXETable无法记住列设置
|
||||
const { initSetting } = useColumnsCache({ cacheColumnsKey: props.cacheColumnsKey });
|
||||
initSetting(props);
|
||||
// update-end--author:liaozhiyang---date:20240321---for:【QQYUN-8566】JVXETable无法记住列设置
|
||||
// 最终传入到 template 里的 props
|
||||
const finallyProps = useFinallyProps(props, data, methods);
|
||||
// 渲染子组件
|
||||
const renderComponents = useRenderComponents(props, data, methods, slots);
|
||||
return {
|
||||
instanceRef,
|
||||
...refs,
|
||||
...publicMethods,
|
||||
...finallyProps,
|
||||
...renderComponents,
|
||||
vxeDataSource: data.vxeDataSource,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: this.$attrs.class,
|
||||
style: this.$attrs.style,
|
||||
},
|
||||
h(
|
||||
rc('a-spin'),
|
||||
{
|
||||
spinning: this.loading,
|
||||
wrapperClassName: this.prefixCls,
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
this.renderSubPopover(),
|
||||
this.renderToolbar(),
|
||||
this.renderToolbarAfterSlot(),
|
||||
h(
|
||||
rc('vxe-grid'),
|
||||
{
|
||||
...this.vxeProps,
|
||||
data: this.vxeDataSource,
|
||||
},
|
||||
this.$slots
|
||||
),
|
||||
this.renderPagination(),
|
||||
this.renderDetailsModal(),
|
||||
],
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
created() {
|
||||
this.instanceRef = this;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,97 @@
|
||||
import type { JVxeVueComponent } from './types';
|
||||
import { JVxeTypes } from './types/JVxeTypes';
|
||||
|
||||
import JVxeSlotCell from './components/cells/JVxeSlotCell';
|
||||
import JVxeNormalCell from './components/cells/JVxeNormalCell.vue';
|
||||
import JVxeDragSortCell from './components/cells/JVxeDragSortCell.vue';
|
||||
|
||||
import JVxeInputCell from './components/cells/JVxeInputCell.vue';
|
||||
import JVxeDateCell from './components/cells/JVxeDateCell.vue';
|
||||
import JVxeTimeCell from './components/cells/JVxeTimeCell.vue';
|
||||
import JVxeSelectCell from './components/cells/JVxeSelectCell.vue';
|
||||
import JVxeRadioCell from './components/cells/JVxeRadioCell.vue';
|
||||
import JVxeCheckboxCell from './components/cells/JVxeCheckboxCell.vue';
|
||||
import JVxeUploadCell from './components/cells/JVxeUploadCell.vue';
|
||||
// import { TagsInputCell, TagsSpanCell } from './components/cells/JVxeTagsCell.vue'
|
||||
import JVxeProgressCell from './components/cells/JVxeProgressCell.vue';
|
||||
import JVxeTextareaCell from './components/cells/JVxeTextareaCell.vue';
|
||||
// import JVxeDepartSelectCell from './components/cells/JVxeDepartSelectCell.vue'
|
||||
// import JVxeUserSelectCell from './components/cells/JVxeUserSelectCell.vue'
|
||||
|
||||
let componentMap = new Map<JVxeTypes | string, JVxeVueComponent>();
|
||||
// update-begin--author:liaozhiyang---date:20231208---for:【issues/860】生成的一对多代码,热更新之后点击新增卡死[暂时先解决]
|
||||
const JVxeComponents = 'JVxeComponents__';
|
||||
if (import.meta.env.DEV && componentMap.size === 0 && window[JVxeComponents] && window[JVxeComponents].size > 0) {
|
||||
componentMap = window[JVxeComponents];
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231027---for:【issues/860】生成的一对多代码,热更新之后点击新增卡死[暂时先解决]
|
||||
/** span 组件结尾 */
|
||||
export const spanEnds: string = ':span';
|
||||
|
||||
/** 定义不能用于注册的关键字 */
|
||||
export const excludeKeywords: Array<JVxeTypes> = [
|
||||
JVxeTypes.hidden,
|
||||
JVxeTypes.rowNumber,
|
||||
JVxeTypes.rowCheckbox,
|
||||
JVxeTypes.rowRadio,
|
||||
JVxeTypes.rowExpand,
|
||||
];
|
||||
|
||||
/**
|
||||
* 注册组件
|
||||
*
|
||||
* @param type 组件 type
|
||||
* @param component Vue组件
|
||||
* @param spanComponent 显示组件,可空,默认为 JVxeNormalCell 组件
|
||||
*/
|
||||
export function addComponent(type: JVxeTypes, component: JVxeVueComponent, spanComponent?: JVxeVueComponent) {
|
||||
if (excludeKeywords.includes(type)) {
|
||||
throw new Error(`【addComponent】不能使用"${type}"作为组件的name,因为这是关键字。`);
|
||||
}
|
||||
if (componentMap.has(type)) {
|
||||
throw new Error(`【addComponent】组件"${type}"已存在`);
|
||||
}
|
||||
componentMap.set(type, component);
|
||||
if (spanComponent) {
|
||||
componentMap.set(type + spanEnds, spanComponent);
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20231208---for:【issues/860】生成的一对多代码,热更新之后点击新增卡死[暂时先解决]
|
||||
import.meta.env.DEV && (window[JVxeComponents] = componentMap);
|
||||
// update-end--author:liaozhiyang---date:20231208---for:【issues/860】生成的一对多代码,热更新之后点击新增卡死[暂时先解决]
|
||||
}
|
||||
|
||||
export function deleteComponent(type: JVxeTypes) {
|
||||
componentMap.delete(type);
|
||||
componentMap.delete(type + spanEnds);
|
||||
// update-begin--author:liaozhiyang---date:20231208---for:【issues/860】生成的一对多代码,热更新之后点击新增卡死[暂时先解决]
|
||||
import.meta.env.DEV && (window[JVxeComponents] = componentMap);
|
||||
// update-end--author:liaozhiyang---date:20231208---for:【issues/860】生成的一对多代码,热更新之后点击新增卡死[暂时先解决]
|
||||
}
|
||||
|
||||
/** 定义内置自定义组件 */
|
||||
export function definedComponent() {
|
||||
addComponent(JVxeTypes.slot, JVxeSlotCell);
|
||||
addComponent(JVxeTypes.normal, JVxeNormalCell);
|
||||
addComponent(JVxeTypes.rowDragSort, JVxeDragSortCell);
|
||||
|
||||
addComponent(JVxeTypes.input, JVxeInputCell);
|
||||
addComponent(JVxeTypes.inputNumber, JVxeInputCell);
|
||||
addComponent(JVxeTypes.radio, JVxeRadioCell);
|
||||
addComponent(JVxeTypes.checkbox, JVxeCheckboxCell);
|
||||
addComponent(JVxeTypes.select, JVxeSelectCell);
|
||||
addComponent(JVxeTypes.selectSearch, JVxeSelectCell); // 下拉搜索
|
||||
addComponent(JVxeTypes.selectMultiple, JVxeSelectCell); // 下拉多选
|
||||
addComponent(JVxeTypes.date, JVxeDateCell);
|
||||
addComponent(JVxeTypes.datetime, JVxeDateCell);
|
||||
addComponent(JVxeTypes.time, JVxeTimeCell);
|
||||
addComponent(JVxeTypes.upload, JVxeUploadCell);
|
||||
addComponent(JVxeTypes.textarea, JVxeTextareaCell);
|
||||
|
||||
// addComponent(JVxeTypes.tags, TagsInputCell, TagsSpanCell)
|
||||
addComponent(JVxeTypes.progress, JVxeProgressCell);
|
||||
|
||||
// addComponent(JVxeTypes.departSelect, JVxeDepartSelectCell)
|
||||
// addComponent(JVxeTypes.userSelect, JVxeUserSelectCell)
|
||||
}
|
||||
|
||||
export { componentMap };
|
||||
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<BasicModal @register="registerModel" title="详细信息" :width="1200" :keyboard="true" @ok="handleOk" @cancel="close">
|
||||
<transition name="fade">
|
||||
<div v-if="getVisible">
|
||||
<slot name="mainForm" :row="row" :column="column" />
|
||||
</div>
|
||||
</transition>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { ref, defineComponent } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useModal } from '/@/components/Modal/src/hooks/useModal';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasicModal: createAsyncComponent(() => import('/@/components/Modal/src/BasicModal.vue'), {
|
||||
loading: true,
|
||||
}),
|
||||
},
|
||||
props: {
|
||||
trigger: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const row = ref(null);
|
||||
const column = ref(null);
|
||||
|
||||
const [registerModel, { openModal, closeModal, getVisible }] = useModal();
|
||||
|
||||
function open(event) {
|
||||
let { row: $row, column: $column } = event;
|
||||
row.value = cloneDeep($row);
|
||||
column.value = $column;
|
||||
openModal();
|
||||
}
|
||||
|
||||
function close() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function handleOk() {
|
||||
props.trigger('detailsConfirm', {
|
||||
row: row.value,
|
||||
column: column.value,
|
||||
callback: (success) => {
|
||||
success ? closeModal() : openModal();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
getVisible,
|
||||
row,
|
||||
column,
|
||||
open,
|
||||
close,
|
||||
handleOk,
|
||||
registerModel,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,93 @@
|
||||
import { defineComponent, h, ref, watch } from 'vue';
|
||||
import { randomString } from '/@/utils/common/compUtils';
|
||||
import '../style/reload-effect.less';
|
||||
|
||||
// 修改数据特效
|
||||
export default defineComponent({
|
||||
props: {
|
||||
vNode: null,
|
||||
// 是否启用特效
|
||||
effect: Boolean,
|
||||
},
|
||||
emits: ['effectBegin', 'effectEnd'],
|
||||
setup(props, { emit }) {
|
||||
// vNode: null,
|
||||
const innerEffect = ref(props.effect);
|
||||
// 应付同时多个特效
|
||||
const effectIdx = ref(0);
|
||||
const effectList = ref<any[]>([]);
|
||||
|
||||
watch(
|
||||
() => props.effect,
|
||||
() => (innerEffect.value = props.effect)
|
||||
);
|
||||
watch(
|
||||
() => props.vNode,
|
||||
(_vNode, old) => {
|
||||
if (props.effect && old != null) {
|
||||
let topLayer = renderSpan(old, 'top');
|
||||
effectList.value.push(topLayer);
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 条件渲染内容 span
|
||||
function renderVNode() {
|
||||
if (props.vNode == null) {
|
||||
return null;
|
||||
}
|
||||
let bottom = renderSpan(props.vNode, 'bottom');
|
||||
// 启用了特效,并且有旧数据,就渲染特效顶层
|
||||
if (innerEffect.value && effectList.value.length > 0) {
|
||||
emit('effectBegin');
|
||||
// 1.4s 以后关闭特效
|
||||
window.setTimeout(() => {
|
||||
let item = effectList.value[effectIdx.value];
|
||||
if (item && item.elm) {
|
||||
// 特效结束后,展示先把 display 设为 none,而不是直接删掉该元素,
|
||||
// 目的是为了防止页面重新渲染,导致动画重置
|
||||
item.elm.style.display = 'none';
|
||||
}
|
||||
// 当所有的层级动画都结束时,再删掉所有元素
|
||||
if (++effectIdx.value === effectList.value.length) {
|
||||
innerEffect.value = false;
|
||||
effectIdx.value = 0;
|
||||
effectList.value = [];
|
||||
emit('effectEnd');
|
||||
}
|
||||
}, 1400);
|
||||
return [effectList.value, bottom];
|
||||
} else {
|
||||
return bottom;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染内容 span
|
||||
function renderSpan(vNode, layer) {
|
||||
let options = {
|
||||
key: layer + effectIdx.value + randomString(6),
|
||||
class: ['j-vxe-reload-effect-span', `layer-${layer}`],
|
||||
style: {},
|
||||
// update-begin--author:liaozhiyang---date:20240424---for:【issues/1175】解决vxetable鼠标hover之后title显示不对的问题
|
||||
title: vNode,
|
||||
// update-end--author:liaozhiyang---date:20240424---for:【issues/1175】解决vxetable鼠标hover之后title显示不对的问题
|
||||
|
||||
};
|
||||
if (layer === 'top') {
|
||||
// 最新渲染的在下面
|
||||
options.style['z-index'] = 9999 - effectIdx.value;
|
||||
}
|
||||
return h('span', options, [vNode]);
|
||||
}
|
||||
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: ['j-vxe-reload-effect-box'],
|
||||
},
|
||||
[renderVNode()]
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<a-popover :open="visible" :placement="placement" overlayClassName="j-vxe-popover-overlay" :overlayStyle="overlayStyle">
|
||||
<template #title>
|
||||
<div class="j-vxe-popover-title">
|
||||
<div>子表</div>
|
||||
<div class="j-vxe-popover-title-close" @click="close">
|
||||
<a-icon type="close" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<transition name="fade">
|
||||
<slot v-if="visible" name="subForm" :row="row" :column="column" />
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<div ref="divRef" class="j-vxe-popover-div"></div>
|
||||
</a-popover>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { ref, reactive, nextTick, defineComponent } from 'vue';
|
||||
import domAlign from 'dom-align';
|
||||
import { getParentNodeByTagName } from '../utils/vxeUtils';
|
||||
import { triggerWindowResizeEvent } from '/@/utils/common/compUtils';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { isString } from '/@/utils/is';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeSubPopover',
|
||||
setup() {
|
||||
const visible = ref(false);
|
||||
const row = ref<any>(null);
|
||||
const column = ref<any>(null);
|
||||
const overlayStyle = reactive<{
|
||||
width?: number | string;
|
||||
maxWidth?: number | string;
|
||||
zIndex: number;
|
||||
}>({
|
||||
zIndex: 100,
|
||||
});
|
||||
const placement = ref('bottom');
|
||||
const divRef = ref<HTMLElement>();
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
function toggle(event) {
|
||||
if (document.body.clientHeight - event.$event.clientY > 350) {
|
||||
placement.value = 'bottom';
|
||||
} else {
|
||||
placement.value = 'top';
|
||||
}
|
||||
if (row.value == null) {
|
||||
open(event);
|
||||
} else {
|
||||
row.value.id === event.row.id ? close() : reopen(event);
|
||||
}
|
||||
}
|
||||
|
||||
function open(event, level = 0) {
|
||||
if (level > 3) {
|
||||
createMessage.error('打开子表失败');
|
||||
console.warn('【JVxeSubPopover】打开子表失败');
|
||||
return;
|
||||
}
|
||||
|
||||
let {
|
||||
row: $row,
|
||||
column: $column,
|
||||
$table,
|
||||
$event: { target },
|
||||
} = event;
|
||||
row.value = cloneDeep($row);
|
||||
column.value = $column;
|
||||
|
||||
let className = target.className || '';
|
||||
className = isString(className) ? className : className.toString();
|
||||
|
||||
// 获取 td 父级
|
||||
let td = getParentNodeByTagName(target, 'td');
|
||||
// 点击的是拖拽排序列,不做处理
|
||||
if (td && td.querySelector('.j-vxe-drag-box')) {
|
||||
return;
|
||||
}
|
||||
// 点击的是expand,不做处理
|
||||
if (className.includes('vxe-table--expand-btn')) {
|
||||
return;
|
||||
}
|
||||
// 点击的是checkbox,不做处理
|
||||
if (className.includes('vxe-checkbox--icon') || className.includes('vxe-cell--checkbox')) {
|
||||
return;
|
||||
}
|
||||
// 点击的是radio,不做处理
|
||||
if (className.includes('vxe-radio--icon') || className.includes('vxe-cell--radio')) {
|
||||
return;
|
||||
}
|
||||
let parentElem = $table.getParentElem();
|
||||
let tr = getParentNodeByTagName(target, 'tr');
|
||||
if (parentElem && tr) {
|
||||
let clientWidth = parentElem.clientWidth;
|
||||
let clientHeight = tr.clientHeight;
|
||||
divRef.value!.style.width = clientWidth + 'px';
|
||||
divRef.value!.style.height = clientHeight + 'px';
|
||||
overlayStyle.width = Number.parseInt(`${clientWidth - clientWidth * 0.04}`) + 'px';
|
||||
overlayStyle.maxWidth = overlayStyle.width;
|
||||
//let realTable = getParentNodeByTagName(tr, 'table')
|
||||
//let left = realTable.parentNode.scrollLeft
|
||||
let h = event.$event.clientY;
|
||||
if (h) {
|
||||
h = h - 140;
|
||||
}
|
||||
let toolbar = divRef.value!.nextElementSibling;
|
||||
domAlign(divRef.value, toolbar, {
|
||||
points: ['tl', 'tl'],
|
||||
offset: [0, h],
|
||||
overflow: {
|
||||
alwaysByViewport: true,
|
||||
},
|
||||
});
|
||||
nextTick(() => {
|
||||
visible.value = true;
|
||||
nextTick(() => {
|
||||
triggerWindowResizeEvent();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let num = ++level;
|
||||
console.warn('【JVxeSubPopover】table or tr 获取失败,正在进行第 ' + num + '次重试', {
|
||||
event,
|
||||
table: parentElem,
|
||||
tr,
|
||||
});
|
||||
window.setTimeout(() => open(event, num), 100);
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (visible.value) {
|
||||
row.value = null;
|
||||
visible.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reopen(event) {
|
||||
open(event);
|
||||
}
|
||||
|
||||
return {
|
||||
divRef,
|
||||
row,
|
||||
column,
|
||||
visible,
|
||||
placement,
|
||||
overlayStyle,
|
||||
close,
|
||||
toggle,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.j-vxe-popover-title {
|
||||
.j-vxe-popover-title-close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
text-align: center;
|
||||
line-height: 31px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
cursor: pointer;
|
||||
transition: color 300ms;
|
||||
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.j-vxe-popover-div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 31px;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.j-vxe-popover-overlay.ant-popover {
|
||||
.ant-popover-title {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div :class="boxClass">
|
||||
<vxe-toolbar ref="xToolbarRef" :custom="custom">
|
||||
<!-- 工具按钮 -->
|
||||
<template #buttons>
|
||||
<div :class="`${prefixCls}-button div`" :size="btnSize">
|
||||
<slot v-if="showPrefix" name="toolbarPrefix" :size="btnSize" />
|
||||
<a-button v-if="showAdd" type="primary" preIcon="ant-design:plus-outlined" :disabled="disabled" @click="trigger('add')">
|
||||
<span>新增</span>
|
||||
</a-button>
|
||||
<a-button v-if="showSave" preIcon="ant-design:save-outlined" :disabled="disabled" @click="trigger('save')">
|
||||
<span>保存</span>
|
||||
</a-button>
|
||||
<template v-if="selectedRowIds.length > 0">
|
||||
<Popconfirm v-if="showRemove" :title="`确定要删除这 ${selectedRowIds.length} 项吗?`" @confirm="trigger('remove')">
|
||||
<a-button preIcon="ant-design:minus-outlined" :disabled="disabled">删除</a-button>
|
||||
</Popconfirm>
|
||||
<template v-if="showClearSelection">
|
||||
<a-button preIcon="ant-design:delete-outlined" @click="trigger('clearSelection')">清空选择</a-button>
|
||||
</template>
|
||||
</template>
|
||||
<slot v-if="showSuffix" name="toolbarSuffix" :size="btnSize" />
|
||||
<a v-if="showCollapse" style="margin-left: 4px" @click="toggleCollapse">
|
||||
<span>{{ collapsed ? '展开' : '收起' }}</span>
|
||||
<Icon :icon="collapsed ? 'ant-design:down-outlined' : 'ant-design:up-outlined'" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref, onMounted } from 'vue';
|
||||
// noinspection ES6UnusedImports
|
||||
import { Popconfirm } from 'ant-design-vue';
|
||||
import { VxeToolbarInstance } from 'vxe-table';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
const props = defineProps({
|
||||
size: propTypes.string,
|
||||
disabled: propTypes.bool.def(false),
|
||||
custom: propTypes.bool.def(false),
|
||||
toolbarConfig: propTypes.object,
|
||||
disabledRows: propTypes.object,
|
||||
hasBtnAuth: propTypes.func,
|
||||
selectedRowIds: propTypes.array.def(() => []),
|
||||
});
|
||||
const emit = defineEmits(['save', 'add', 'remove', 'clearSelection', 'register']);
|
||||
const xToolbarRef = ref({} as VxeToolbarInstance);
|
||||
const prefixCls = `${inject('prefixCls')}-toolbar`;
|
||||
const boxClass = computed(() => [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}-collapsed`]: collapsed.value,
|
||||
},
|
||||
]);
|
||||
// 是否收起
|
||||
const collapsed = ref(true);
|
||||
// 配置的按钮
|
||||
const btns = computed(() => {
|
||||
let { btn, btns } = props.toolbarConfig || {};
|
||||
btns = btn || btns || ['add', 'remove', 'clearSelection'];
|
||||
// 排除掉没有授权的按钮
|
||||
return btns.filter((btn) => {
|
||||
// 系统默认的批量删除编码配置为 batch_delete 此处需要兼容一下
|
||||
if (btn === 'remove') {
|
||||
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1162 子表按钮没控制
|
||||
return hasBtnAuth(btn) && hasBtnAuth('batch_delete');
|
||||
//update-end-author:taoyan date:2022-6-1 for: VUEN-1162 子表按钮没控制
|
||||
}
|
||||
return hasBtnAuth(btn);
|
||||
});
|
||||
});
|
||||
const showAdd = computed(() => btns.value.includes('add'));
|
||||
const showSave = computed(() => btns.value.includes('save'));
|
||||
const showRemove = computed(() => btns.value.includes('remove'));
|
||||
// 配置的插槽
|
||||
const slots = computed(() => props.toolbarConfig?.slot || ['prefix', 'suffix']);
|
||||
const showPrefix = computed(() => slots.value.includes('prefix'));
|
||||
const showSuffix = computed(() => slots.value.includes('suffix'));
|
||||
// 是否显示清除选择按钮
|
||||
const showClearSelection = computed(() => {
|
||||
if (btns.value.includes('clearSelection')) {
|
||||
// 有禁用行时才显示清空选择按钮
|
||||
// 因为禁用行会阻止选择行,导致无法取消全选
|
||||
// return Object.keys(props.disabledRows).length > 0
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// 是否显示展开收起按钮
|
||||
const showCollapse = computed(() => btns.value.includes('collapse'));
|
||||
// 按钮 size
|
||||
const btnSize = computed(() => (props.size === 'tiny' ? 'small' : null));
|
||||
|
||||
onMounted(() => {
|
||||
// 注册 vxe-toolbar
|
||||
emit('register', {
|
||||
xToolbarRef,
|
||||
});
|
||||
});
|
||||
|
||||
// 判断按钮是否已授权
|
||||
function hasBtnAuth(key: string) {
|
||||
return props.hasBtnAuth ? props.hasBtnAuth(key) : true;
|
||||
}
|
||||
|
||||
/** 触发事件 */
|
||||
function trigger(name) {
|
||||
emit(name);
|
||||
}
|
||||
|
||||
// 切换展开收起
|
||||
function toggleCollapse() {
|
||||
collapsed.value = !collapsed.value;
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div :class="boxClass" :style="boxStyle" title="">
|
||||
<a-checkbox :checked="innerValue" v-bind="cellProps" @change="handleChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
import { isArray, isBoolean } from '/@/utils/is';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeCheckboxCell',
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, originColumn, scrolling, handleChangeCommon } = useJVxeComponent(props);
|
||||
// 是否启用边框
|
||||
const bordered = computed(() => !!props.renderOptions.bordered);
|
||||
// box 类名
|
||||
const boxClass = computed(() => {
|
||||
return {
|
||||
'j-vxe-checkbox': true,
|
||||
'no-animation': scrolling.value,
|
||||
};
|
||||
});
|
||||
// box 行内样式
|
||||
const boxStyle = computed(() => {
|
||||
const style = {};
|
||||
// 如果有边框且未设置align属性,就强制居中
|
||||
if (bordered.value && !originColumn.value.align) {
|
||||
style['text-align'] = 'center';
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
// onChange 事件
|
||||
function handleChange(event) {
|
||||
handleChangeCommon(event.target.checked);
|
||||
}
|
||||
|
||||
return {
|
||||
cellProps,
|
||||
innerValue,
|
||||
boxClass,
|
||||
boxStyle,
|
||||
handleChange,
|
||||
};
|
||||
},
|
||||
|
||||
// 【组件增强】注释详见:JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
switches: {
|
||||
visible: true,
|
||||
},
|
||||
getValue(value, ctx) {
|
||||
let { context } = ctx!;
|
||||
let { originColumn } = context;
|
||||
// 处理 customValue
|
||||
if (isArray(originColumn.value.customValue)) {
|
||||
let customValue = getCustomValue(originColumn.value);
|
||||
if (isBoolean(value)) {
|
||||
return value ? customValue[0] : customValue[1];
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
setValue(value, ctx) {
|
||||
let { context } = ctx!;
|
||||
let { originColumn } = context;
|
||||
// 判断是否设定了customValue(自定义值)
|
||||
if (isArray(originColumn.value.customValue)) {
|
||||
let customValue = getCustomValue(originColumn.value);
|
||||
return neverNull(value).toString() === customValue[0].toString();
|
||||
} else {
|
||||
return !!value;
|
||||
}
|
||||
},
|
||||
createValue(_defaultValue, ctx) {
|
||||
let { context } = ctx!;
|
||||
let {
|
||||
column: { params: col },
|
||||
} = context;
|
||||
if (isArray(col.customValue)) {
|
||||
let customValue = getCustomValue(col);
|
||||
return col.defaultChecked ? customValue[0] : customValue[1];
|
||||
} else {
|
||||
return !!col.defaultChecked;
|
||||
}
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
|
||||
function neverNull(value, def?) {
|
||||
return value == null ? neverNull(def, '') : value;
|
||||
}
|
||||
|
||||
function getCustomValue(col) {
|
||||
let customTrue = neverNull(col.customValue[0], true);
|
||||
let customFalse = neverNull(col.customValue[1], false);
|
||||
return [customTrue, customFalse];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
// 关闭动画,防止滚动时动态赋值出现问题
|
||||
.j-vxe-checkbox.no-animation {
|
||||
.ant-checkbox-inner,
|
||||
.ant-checkbox-inner::after {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<a-date-picker
|
||||
:value="innerDateValue"
|
||||
allowClear
|
||||
:format="picker ? null : dateFormat"
|
||||
:showTime="isDatetime"
|
||||
:valueFormat="picker ? dateFormat : null"
|
||||
popupClassName="j-vxe-date-picker"
|
||||
style="min-width: 0"
|
||||
v-model:open="openPicker"
|
||||
v-bind="cellProps"
|
||||
:picker="picker"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, watch, defineComponent } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { JVxeComponent, JVxeTypes } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
import { isEmpty } from '/@/utils/is';
|
||||
import { getWeekMonthQuarterYear } from '/@/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeDateCell',
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, originColumn, handleChangeCommon } = useJVxeComponent(props);
|
||||
const innerDateValue = ref<any>(null);
|
||||
const isDatetime = computed(() => props.type === JVxeTypes.datetime);
|
||||
const dateFormat = computed(() => {
|
||||
let format = originColumn.value.format;
|
||||
return format ? format : isDatetime.value ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
||||
});
|
||||
const openPicker = ref(true);
|
||||
// update-begin--author:liaozhiyang---date:20240509---for:【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
|
||||
const picker = computed(() => {
|
||||
const picker = originColumn.value.picker;
|
||||
return picker ? picker : null;
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240509---for:【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
|
||||
watch(
|
||||
innerValue,
|
||||
(val) => {
|
||||
if (val == null || isEmpty(val)) {
|
||||
innerDateValue.value = null;
|
||||
} else {
|
||||
innerDateValue.value = dayjs(val, dateFormat.value);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function handleChange(_mom, dateStr) {
|
||||
// update-begin--author:liaozhiyang---date:20240509---for:【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
|
||||
if (picker.value) {
|
||||
handleChangeCommon(_mom);
|
||||
} else {
|
||||
handleChangeCommon(dateStr);
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240509---for:【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
|
||||
}
|
||||
|
||||
return {
|
||||
cellProps,
|
||||
isDatetime,
|
||||
dateFormat,
|
||||
innerDateValue,
|
||||
openPicker,
|
||||
handleChange,
|
||||
picker,
|
||||
};
|
||||
},
|
||||
// 【组件增强】注释详见:JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
aopEvents: {
|
||||
},
|
||||
// update-begin--author:liaozhiyang---date:20240509---for:【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
|
||||
translate: {
|
||||
enabled: true,
|
||||
handler(value, ctx) {
|
||||
let { props, context } = ctx!;
|
||||
let { row, originColumn } = context;
|
||||
if (originColumn.value.picker && value) {
|
||||
return getWeekMonthQuarterYear(value)[originColumn.value.picker];
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
// update-end--author:liaozhiyang---date:20240509---for:【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="j-vxe-drag-box">
|
||||
<span v-if="!isAllowDrag"><span class="not-drag-btn"> <Icon icon="mi:drag" /> </span
|
||||
></span>
|
||||
<a-dropdown v-else :trigger="['click']" >
|
||||
<span
|
||||
><span class="drag-btn"> <Icon icon="mi:drag" /> </span
|
||||
></span>
|
||||
<template #overlay >
|
||||
<a-menu>
|
||||
<a-menu-item key="0" :disabled="disabledMoveUp" @click="handleRowMoveUp">向上移</a-menu-item>
|
||||
<a-menu-item key="1" :disabled="disabledMoveDown" @click="handleRowMoveDown">向下移</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="3" @click="handleRowInsertDown">插入一行</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeDragSortCell',
|
||||
components: { Icon },
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { rowIndex, originColumn, fullDataLength, trigger } = useJVxeComponent(props);
|
||||
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
const isAllowDrag = computed(() => {
|
||||
const notAllowDrag = originColumn.value.notAllowDrag;
|
||||
if (notAllowDrag.length) {
|
||||
const row = props.params.row;
|
||||
const find = notAllowDrag.find((item: any) => {
|
||||
const { key, value } = item;
|
||||
return row[key] == value;
|
||||
});
|
||||
return !find;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
const disabledMoveUp = computed(() => rowIndex.value === 0);
|
||||
const disabledMoveDown = computed(() => rowIndex.value === fullDataLength.value - 1);
|
||||
|
||||
/** 向上移 */
|
||||
function handleRowMoveUp() {
|
||||
if (!disabledMoveUp.value) {
|
||||
trigger('rowResort', {
|
||||
oldIndex: rowIndex.value,
|
||||
newIndex: rowIndex.value - 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 向下移 */
|
||||
function handleRowMoveDown() {
|
||||
if (!disabledMoveDown.value) {
|
||||
trigger('rowResort', {
|
||||
oldIndex: rowIndex.value,
|
||||
newIndex: rowIndex.value + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 插入一行 */
|
||||
function handleRowInsertDown() {
|
||||
trigger('rowInsertDown', rowIndex.value);
|
||||
}
|
||||
|
||||
return {
|
||||
disabledMoveUp,
|
||||
disabledMoveDown,
|
||||
handleRowMoveUp,
|
||||
handleRowMoveDown,
|
||||
handleRowInsertDown,
|
||||
isAllowDrag
|
||||
};
|
||||
},
|
||||
// 【组件增强】注释详见:JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
// 【功能开关】
|
||||
switches: {
|
||||
editRender: false,
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.j-vxe-drag-box {
|
||||
.app-iconify {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.vxe-table--fixed-wrapper {
|
||||
.j-vxe-drag-box {
|
||||
.app-iconify {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.not-drag-btn {
|
||||
opacity: 0.5;
|
||||
.app-iconify {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<a-input ref="input" :value="innerValue" v-bind="cellProps" @blur="handleBlur" @change="handleChange" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { isString } from '/@/utils/is';
|
||||
import { JVxeComponent, JVxeTypes } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
|
||||
const NumberRegExp = /^-?\d+\.?\d*$/;
|
||||
export default defineComponent({
|
||||
name: 'JVxeInputCell',
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, handleChangeCommon, handleBlurCommon } = useJVxeComponent(props);
|
||||
|
||||
/** 处理change事件 */
|
||||
function handleChange(event) {
|
||||
let { target } = event;
|
||||
let { value, selectionStart } = target;
|
||||
let change = true;
|
||||
if (props.type === JVxeTypes.inputNumber) {
|
||||
// 判断输入的值是否匹配数字正则表达式,不匹配就还原
|
||||
if (!NumberRegExp.test(value) && value !== '' && value !== '-') {
|
||||
change = false;
|
||||
value = innerValue.value;
|
||||
target.value = value || '';
|
||||
if (typeof selectionStart === 'number') {
|
||||
target.selectionStart = selectionStart - 1;
|
||||
target.selectionEnd = selectionStart - 1;
|
||||
}
|
||||
} else {
|
||||
// update-begin--author:liaozhiyang---date:20240227---for:【QQYUN-8347】小数点后大于两位且最后一位是0,输入框不可输入了
|
||||
// 例如:41.1 -> 41.10, 100.1 -> 100.10 不执行handleChangeCommon 函数。
|
||||
if (value.indexOf('.') != -1) {
|
||||
const result = value.split('.').pop();
|
||||
if (result && result.length >= 2 && result.substr(-1) === '0') {
|
||||
change = false;
|
||||
innerValue.value = value;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240227---for:【QQYUN-8347】小数点后大于两位且最后一位是0,输入框不可输入了
|
||||
}
|
||||
}
|
||||
// 触发事件,存储输入的值
|
||||
if (change) {
|
||||
handleChangeCommon(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理blur失去焦点事件 */
|
||||
function handleBlur(event) {
|
||||
let { target } = event;
|
||||
// 判断输入的值是否匹配数字正则表达式,不匹配就置空
|
||||
if (props.type === JVxeTypes.inputNumber) {
|
||||
if (!NumberRegExp.test(target.value)) {
|
||||
target.value = '';
|
||||
} else {
|
||||
target.value = Number.parseFloat(target.value);
|
||||
}
|
||||
}
|
||||
handleChangeCommon(target.value, true);
|
||||
handleBlurCommon(target.value);
|
||||
}
|
||||
|
||||
return {
|
||||
innerValue,
|
||||
cellProps,
|
||||
handleChange,
|
||||
handleBlur,
|
||||
};
|
||||
},
|
||||
enhanced: {
|
||||
installOptions: {
|
||||
autofocus: '.ant-input',
|
||||
},
|
||||
getValue(value, ctx) {
|
||||
if (ctx?.props?.type === JVxeTypes.inputNumber && isString(value)) {
|
||||
if (NumberRegExp.test(value)) {
|
||||
// 【issues/I5IHN7】修复无法输入小数点的bug
|
||||
if (/\.0*$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return Number.parseFloat(value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<JVxeReloadEffect :vNode="innerValue" :effect="isEffect" @effectEnd="handleEffectEnd" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, defineComponent } from 'vue';
|
||||
import JVxeReloadEffect from '../JVxeReloadEffect';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
import { watch } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeNormalCell',
|
||||
components: { JVxeReloadEffect },
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const setup = useJVxeComponent(props);
|
||||
const { innerValue, row } = setup;
|
||||
|
||||
const reloadEffect = props.renderOptions.reloadEffect;
|
||||
const isEffect = ref<boolean>(false);
|
||||
|
||||
watch(
|
||||
innerValue,
|
||||
() => {
|
||||
if (reloadEffect.enabled) {
|
||||
if (reloadEffect.isEffect(row.value.id)) {
|
||||
isEffect.value = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 特效结束
|
||||
function handleEffectEnd() {
|
||||
isEffect.value = false;
|
||||
reloadEffect.removeEffect(row.value.id);
|
||||
}
|
||||
|
||||
return {
|
||||
innerValue,
|
||||
isEffect,
|
||||
handleEffectEnd,
|
||||
};
|
||||
},
|
||||
enhanced: {
|
||||
switches: { editRender: false },
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<Progress :class="clazz" :percent="innerValue" size="small" v-bind="cellProps" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { Progress } from 'ant-design-vue';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeCheckboxCell',
|
||||
components: { Progress },
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, scrolling } = useJVxeComponent(props);
|
||||
const clazz = computed(() => {
|
||||
return {
|
||||
'j-vxe-progress': true,
|
||||
'no-animation': scrolling.value,
|
||||
};
|
||||
});
|
||||
return { innerValue, cellProps, clazz };
|
||||
},
|
||||
// 【组件增强】注释详见::JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
switches: {
|
||||
editRender: false,
|
||||
},
|
||||
setValue(value) {
|
||||
try {
|
||||
if (typeof value !== 'number') {
|
||||
return Number.parseFloat(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 关闭进度条的动画,防止滚动时动态赋值出现问题
|
||||
.j-vxe-progress.no-animation {
|
||||
:deep(.ant-progress-bg) {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<a-radio-group :class="clazz" :value="innerValue" v-bind="cellProps" @change="(e) => handleChangeCommon(e.target.value)">
|
||||
<a-radio v-for="item of originColumn.options" :key="item.value" :value="item.value" @click="(e) => handleRadioClick(item, e)"
|
||||
>{{ item.text || item.label || item.title || item.value }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeRadioCell',
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, originColumn, handleChangeCommon } = useJVxeComponent(props);
|
||||
const scrolling = computed(() => !!props.renderOptions.scrolling);
|
||||
const clazz = computed(() => {
|
||||
return {
|
||||
'j-vxe-radio': true,
|
||||
'no-animation': scrolling.value,
|
||||
};
|
||||
});
|
||||
|
||||
function handleRadioClick(item) {
|
||||
if (originColumn.value.allowClear === true) {
|
||||
// 取消选择
|
||||
if (item.value === innerValue.value) {
|
||||
handleChangeCommon(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
clazz,
|
||||
innerValue,
|
||||
originColumn,
|
||||
cellProps,
|
||||
handleRadioClick,
|
||||
handleChangeCommon,
|
||||
};
|
||||
},
|
||||
// 【组件增强】注释详见:JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
switches: { visible: true },
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
// 关闭动画,防止滚动时动态赋值出现问题
|
||||
.j-vxe-radio.no-animation {
|
||||
.ant-radio-inner,
|
||||
.ant-radio-inner::after {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<a-select :value="innerValue" v-bind="selectProps">
|
||||
<template v-if="loading" #notFoundContent>
|
||||
<LoadingOutlined />
|
||||
<span> 加载中…</span>
|
||||
</template>
|
||||
<template v-for="option of selectOptions" :key="option.value">
|
||||
<a-select-option :value="option.value" :title="option.text || option.label || option.title" :disabled="option.disabled">
|
||||
<span>{{ option.text || option.label || option.title || option.value }}</span>
|
||||
</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, defineComponent } from 'vue';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import { filterDictText } from '/@/utils/dict/JDictSelectUtil';
|
||||
import { JVxeComponent, JVxeTypes } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
import { dispatchEvent } from '/@/components/jeecg/JVxeTable/utils';
|
||||
import { isPromise } from '/@/utils/is';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeSelectCell',
|
||||
components: { LoadingOutlined },
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, row, originColumn, scrolling, handleChangeCommon, handleBlurCommon } = useJVxeComponent(props);
|
||||
const loading = ref(false);
|
||||
// 异步加载的options(用于多级联动)
|
||||
const asyncOptions = ref<any[] | null>(null);
|
||||
// 下拉框 props
|
||||
const selectProps = computed(() => {
|
||||
let selProps = {
|
||||
...cellProps.value,
|
||||
allowClear: true,
|
||||
autofocus: true,
|
||||
defaultOpen: !scrolling.value,
|
||||
style: { width: '100%' },
|
||||
filterOption: handleSelectFilterOption,
|
||||
onBlur: handleBlur,
|
||||
onChange: handleChange,
|
||||
};
|
||||
// 判断select是否允许输入
|
||||
let { allowSearch, allowInput } = originColumn.value;
|
||||
if (allowInput === true || allowSearch === true) {
|
||||
selProps['showSearch'] = true;
|
||||
selProps['onSearch'] = handleSearchSelect;
|
||||
}
|
||||
return selProps;
|
||||
});
|
||||
// 下拉选项
|
||||
const selectOptions = computed(() => {
|
||||
if (asyncOptions.value) {
|
||||
return asyncOptions.value;
|
||||
}
|
||||
let { linkage } = props.renderOptions;
|
||||
if (linkage) {
|
||||
let { getLinkageOptionsSibling, config } = linkage;
|
||||
let res = getLinkageOptionsSibling(row.value, originColumn.value, config, true);
|
||||
// 当返回Promise时,说明是多级联动
|
||||
if (res instanceof Promise) {
|
||||
loading.value = true;
|
||||
res
|
||||
.then((opt) => {
|
||||
asyncOptions.value = opt;
|
||||
loading.value = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
loading.value = false;
|
||||
});
|
||||
} else {
|
||||
asyncOptions.value = null;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return originColumn.value.options;
|
||||
});
|
||||
|
||||
// --------- created ---------
|
||||
|
||||
// 多选、搜索type
|
||||
let multipleTypes = [JVxeTypes.selectMultiple, 'list_multi'];
|
||||
let searchTypes = [JVxeTypes.selectSearch, 'sel_search'];
|
||||
if (multipleTypes.includes(props.type)) {
|
||||
// 处理多选
|
||||
let props = originColumn.value.props || {};
|
||||
props['mode'] = 'multiple';
|
||||
props['maxTagCount'] = 1;
|
||||
//update-begin-author:taoyan date:2022-12-5 for: issues/271 Online表单主子表单下拉多选无法搜索
|
||||
originColumn.value.allowSearch = true;
|
||||
//update-end-author:taoyan date:2022-12-5 for: issues/271 Online表单主子表单下拉多选无法搜索
|
||||
originColumn.value.props = props;
|
||||
} else if (searchTypes.includes(props.type)) {
|
||||
// 处理搜索
|
||||
originColumn.value.allowSearch = true;
|
||||
}
|
||||
|
||||
/** 处理 change 事件 */
|
||||
function handleChange(value) {
|
||||
// 处理下级联动
|
||||
let linkage = props.renderOptions.linkage;
|
||||
if (linkage) {
|
||||
linkage.handleLinkageSelectChange(row.value, originColumn.value, linkage.config, value);
|
||||
}
|
||||
handleChangeCommon(value);
|
||||
}
|
||||
|
||||
/** 处理blur失去焦点事件 */
|
||||
function handleBlur(value) {
|
||||
let { allowInput, options } = originColumn.value;
|
||||
if (allowInput === true) {
|
||||
// 删除无用的因搜索(用户输入)而创建的项
|
||||
if (typeof value === 'string') {
|
||||
let indexes: number[] = [];
|
||||
options.forEach((option, index) => {
|
||||
if (option.value.toLocaleString() === value.toLocaleString()) {
|
||||
delete option.searchAdd;
|
||||
} else if (option.searchAdd === true) {
|
||||
indexes.push(index);
|
||||
}
|
||||
});
|
||||
// 翻转删除数组中的项
|
||||
for (let index of indexes.reverse()) {
|
||||
options.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
handleBlurCommon(value);
|
||||
}
|
||||
|
||||
/** 用于搜索下拉框中的内容 */
|
||||
function handleSelectFilterOption(input, option) {
|
||||
|
||||
let { allowSearch, allowInput } = originColumn.value;
|
||||
if (allowSearch === true || allowInput === true) {
|
||||
// update-begin--author:liaozhiyang---date:20240321---for:【QQYUN-5806】js增强改变下拉搜索options (防止option.title为null报错)
|
||||
if (option.title == null) return false;
|
||||
// update-begin--author:liaozhiyang---date:20240321---for:【QQYUN-5806】js增强改变下拉搜索options (防止option.title为null报错)
|
||||
// update-begin--author:liaozhiyang---date:20230904---for:【issues/5305】JVxeTypes.select 无法按照预期进行搜索
|
||||
return option.title.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
// update-begin--author:liaozhiyang---date:20230904---for:【issues/5305】JVxeTypes.select 无法按照预期进行搜索
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** select 搜索时的事件,用于动态添加options */
|
||||
function handleSearchSelect(value) {
|
||||
let { allowSearch, allowInput, options } = originColumn.value;
|
||||
|
||||
if (allowSearch !== true && allowInput === true) {
|
||||
// 是否找到了对应的项,找不到则添加这一项
|
||||
let flag = false;
|
||||
for (let option of options) {
|
||||
if (option.value.toLocaleString() === value.toLocaleString()) {
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// !!value :不添加空值
|
||||
if (!flag && !!value) {
|
||||
// searchAdd 是否是通过搜索添加的
|
||||
options.push({ title: value, value: value, searchAdd: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
innerValue,
|
||||
selectProps,
|
||||
selectOptions,
|
||||
handleChange,
|
||||
handleBlur,
|
||||
};
|
||||
},
|
||||
// 【组件增强】注释详见:JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
aopEvents: {
|
||||
editActived({ $event, row, column }) {
|
||||
dispatchEvent({
|
||||
$event,
|
||||
row,
|
||||
column,
|
||||
props: this.props,
|
||||
instance: this,
|
||||
className: '.ant-select .ant-select-selection-search-input',
|
||||
isClick: false,
|
||||
handler: (el) => el.focus(),
|
||||
});
|
||||
},
|
||||
},
|
||||
translate: {
|
||||
enabled: true,
|
||||
async handler(value, ctx) {
|
||||
let { props, context } = ctx!;
|
||||
let { row, originColumn } = context;
|
||||
let options;
|
||||
let linkage = props?.renderOptions.linkage;
|
||||
// 判断是否是多级联动,如果是就通过接口异步翻译
|
||||
if (linkage) {
|
||||
let { getLinkageOptionsSibling, config } = linkage;
|
||||
let linkageOptions = getLinkageOptionsSibling(row.value, originColumn.value, config, true);
|
||||
options = isPromise(linkageOptions) ? await linkageOptions : linkageOptions;
|
||||
} else if (isPromise(originColumn.value.optionsPromise)) {
|
||||
options = await originColumn.value.optionsPromise;
|
||||
} else {
|
||||
options = originColumn.value.options;
|
||||
}
|
||||
return filterDictText(options, value);
|
||||
},
|
||||
},
|
||||
getValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(',');
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
setValue(value, ctx) {
|
||||
let { context } = ctx!;
|
||||
let { originColumn } = context;
|
||||
// 判断是否是多选
|
||||
if ((originColumn.value.props || {})['mode'] === 'multiple') {
|
||||
originColumn.value.props['maxTagCount'] = 1;
|
||||
}
|
||||
if (value != null && value !== '') {
|
||||
if (typeof value === 'string') {
|
||||
return value === '' ? [] : value.split(',');
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,41 @@
|
||||
import { computed, defineComponent, h } from 'vue';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/src/hooks/useJVxeComponent';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/src/types/JVxeComponent';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeSlotCell',
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const data = useJVxeComponent(props);
|
||||
const slotProps = computed(() => {
|
||||
return {
|
||||
value: data.innerValue.value,
|
||||
row: data.row.value,
|
||||
column: data.originColumn.value,
|
||||
params: props.params,
|
||||
$table: props.params.$table,
|
||||
rowId: props.params.rowid,
|
||||
index: props.params.rowIndex,
|
||||
rowIndex: props.params.rowIndex,
|
||||
columnIndex: props.params.columnIndex,
|
||||
scrolling: props.renderOptions.scrolling,
|
||||
reloadEffect: props.renderOptions.reloadEffect.enabled,
|
||||
triggerChange: (v) => data.handleChangeCommon(v),
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
let { slot } = props.renderOptions;
|
||||
if (slot) {
|
||||
return h('div', {}, slot(slotProps.value));
|
||||
} else {
|
||||
return h('div');
|
||||
}
|
||||
};
|
||||
},
|
||||
// 【组件增强】注释详见:JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
switches: {
|
||||
editRender: false,
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<JInputPop
|
||||
:value="innerValue"
|
||||
:width="300"
|
||||
:height="210"
|
||||
:pop-container="getPopupContainer"
|
||||
v-bind="cellProps"
|
||||
style="width: 100%"
|
||||
@blur="handleBlurCommon"
|
||||
@change="handleChangeCommon"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import JInputPop from '/@/components/Form/src/jeecg/components/JInputPop.vue';
|
||||
import { dispatchEvent } from '/@/components/jeecg/JVxeTable/utils';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeTextareaCell',
|
||||
components: { JInputPop },
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, handleChangeCommon, handleBlurCommon } = useJVxeComponent(props);
|
||||
|
||||
function getPopupContainer() {
|
||||
return document.body;
|
||||
}
|
||||
|
||||
return { innerValue, cellProps, handleChangeCommon, handleBlurCommon, getPopupContainer };
|
||||
},
|
||||
// 【组件增强】注释详见:JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
installOptions: {
|
||||
autofocus: '.ant-input',
|
||||
},
|
||||
aopEvents: {
|
||||
editActived({ $event, row, column }) {
|
||||
// 是否默认打开右侧弹窗
|
||||
if (column.params.defaultOpen ?? false) {
|
||||
dispatchEvent({
|
||||
$event,
|
||||
row,
|
||||
column,
|
||||
props: this.props,
|
||||
instance: this,
|
||||
className: '.ant-input-suffix .app-iconify',
|
||||
isClick: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<TimePicker
|
||||
:value="innerTimeValue"
|
||||
allowClear
|
||||
:format="format"
|
||||
popupClassName="j-vxe-time-picker"
|
||||
style="min-width: 0"
|
||||
v-model:open="openPicker"
|
||||
v-bind="cellProps"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, watch, defineComponent } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { TimePicker } from 'ant-design-vue';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
import { isEmpty } from '/@/utils/is';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeTimeCell',
|
||||
components: { TimePicker },
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, originColumn, handleChangeCommon } = useJVxeComponent(props);
|
||||
const innerTimeValue = ref<any>(null);
|
||||
const format = computed(() => {
|
||||
let format = originColumn.value.format;
|
||||
return format ? format : 'HH:mm:ss';
|
||||
});
|
||||
const openPicker = ref(true);
|
||||
watch(
|
||||
innerValue,
|
||||
(val) => {
|
||||
if (val == null || isEmpty(val)) {
|
||||
innerTimeValue.value = null;
|
||||
} else {
|
||||
innerTimeValue.value = dayjs(val, format.value);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function handleChange(_mom, dateStr) {
|
||||
handleChangeCommon(dateStr);
|
||||
}
|
||||
|
||||
return {
|
||||
cellProps,
|
||||
format,
|
||||
innerTimeValue,
|
||||
openPicker,
|
||||
handleChange,
|
||||
};
|
||||
},
|
||||
// 【组件增强】注释详见:JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
aopEvents: {
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="hasFile" v-for="(file, fileKey) of [innerFile || {}]" :key="fileKey">
|
||||
<a-input :readOnly="true" :value="file.name">
|
||||
<template #addonBefore style="width: 30px">
|
||||
<a-tooltip v-if="file.status === 'uploading'" :title="`上传中(${Math.floor(file.percent)}%)`">
|
||||
<LoadingOutlined />
|
||||
</a-tooltip>
|
||||
<a-tooltip v-else-if="file.status === 'done'" title="上传完成">
|
||||
<Icon icon="ant-design:check-circle-outlined" style="color: #00db00" />
|
||||
</a-tooltip>
|
||||
<a-tooltip v-else :title="file.message || '上传失败'">
|
||||
<Icon icon="ant-design:exclamation-circle-outlined" style="color: red" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<span v-if="file.status === 'uploading'" slot="addonAfter">{{ Math.floor(file.percent) }}%</span>
|
||||
<template v-if="originColumn.allowDownload !== false || originColumn.allowRemove !== false" #addonAfter>
|
||||
<Dropdown :trigger="['click']" placement="bottomRight">
|
||||
<a-tooltip title="操作">
|
||||
<Icon icon="ant-design:setting-outlined" style="cursor: pointer" />
|
||||
</a-tooltip>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-if="originColumn.allowDownload !== false" @click="handleClickDownloadFile">
|
||||
<span><Icon icon="ant-design:download-outlined" /> 下载</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="originColumn.allowRemove !== false" @click="handleClickDeleteFile">
|
||||
<span><Icon icon="ant-design:delete-outlined" /> 删除</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
</a-input>
|
||||
</template>
|
||||
<a-upload
|
||||
v-if="!cellProps.disabledTable"
|
||||
v-show="!hasFile"
|
||||
name="file"
|
||||
:data="{ isup: 1 }"
|
||||
:multiple="false"
|
||||
:action="originColumn.action"
|
||||
:headers="uploadHeaders"
|
||||
:showUploadList="false"
|
||||
v-bind="cellProps"
|
||||
@change="handleChangeUpload"
|
||||
>
|
||||
<a-button preIcon="ant-design:upload-outlined">{{ originColumn.btnText || '点击上传' }}</a-button>
|
||||
</a-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { Dropdown } from 'ant-design-vue';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
import { useJVxeUploadCell, fileGetValue, fileSetValue } from '../../hooks/cells/useJVxeUploadCell';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JVxeUploadCell',
|
||||
components: { Icon, Dropdown, LoadingOutlined },
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const setup = useJVxeUploadCell(props);
|
||||
return { ...setup };
|
||||
},
|
||||
// 【组件增强】注释详见::JVxeComponent.Enhanced
|
||||
enhanced: {
|
||||
switches: { visible: true },
|
||||
getValue: (value) => fileGetValue(value),
|
||||
setValue: (value) => fileSetValue(value),
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,139 @@
|
||||
import { ref, computed, watch } from 'vue';
|
||||
|
||||
import {getTenantId, getToken} from '/@/utils/auth';
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { JVxeComponent } from '../../types/JVxeComponent';
|
||||
import { useJVxeComponent } from '../useJVxeComponent';
|
||||
|
||||
/**
|
||||
* use 公共上传组件
|
||||
* @param props
|
||||
* @param options 组件选项,token:默认是否传递token,action:默认上传路径,multiple:是否允许多文件
|
||||
*/
|
||||
export function useJVxeUploadCell(props: JVxeComponent.Props, options?) {
|
||||
const setup = useJVxeComponent(props);
|
||||
const { innerValue, originColumn, handleChangeCommon } = setup;
|
||||
|
||||
const innerFile = ref<any>(null);
|
||||
|
||||
/** upload headers */
|
||||
const uploadHeaders = computed(() => {
|
||||
let headers = {};
|
||||
if ((originColumn.value.token ?? options?.token ?? false) === true) {
|
||||
headers['X-Access-Token'] = getToken();
|
||||
}
|
||||
let tenantId = getTenantId();
|
||||
headers['X-Tenant-Id'] = tenantId ? tenantId : '0';
|
||||
return headers;
|
||||
});
|
||||
|
||||
/** 上传请求地址 */
|
||||
const uploadAction = computed(() => {
|
||||
if (!originColumn.value.action) {
|
||||
return options?.action ?? '';
|
||||
} else {
|
||||
return originColumn.value.action;
|
||||
}
|
||||
});
|
||||
const hasFile = computed(() => innerFile.value != null);
|
||||
const responseName = computed(() => originColumn.value.responseName ?? 'message');
|
||||
|
||||
watch(
|
||||
innerValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
innerFile.value = val;
|
||||
} else {
|
||||
innerFile.value = null;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function handleChangeUpload(info) {
|
||||
let { file } = info;
|
||||
let value = {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
status: file.status,
|
||||
percent: file.percent,
|
||||
path: innerFile.value?.path ?? '',
|
||||
};
|
||||
if (file.response) {
|
||||
value['responseName'] = file.response[responseName.value];
|
||||
}
|
||||
let paths: string[] = [];
|
||||
if (options?.multiple && innerFile.value && innerFile.value.path) {
|
||||
paths = innerFile.value.path.split(',');
|
||||
}
|
||||
if (file.status === 'done') {
|
||||
if (typeof file.response.success === 'boolean') {
|
||||
if (file.response.success) {
|
||||
paths.push(file.response[responseName.value]);
|
||||
value['path'] = paths.join(',');
|
||||
handleChangeCommon(value);
|
||||
} else {
|
||||
value['status'] = 'error';
|
||||
value['message'] = file.response.message || '未知错误';
|
||||
}
|
||||
} else {
|
||||
// 考虑到如果设置action上传路径为非jeecg-boot后台,可能不会返回 success 属性的情况,就默认为成功
|
||||
paths.push(file.response[responseName.value]);
|
||||
value['path'] = paths.join(',');
|
||||
handleChangeCommon(value);
|
||||
}
|
||||
} else if (file.status === 'error') {
|
||||
value['message'] = file.response.message || '未知错误';
|
||||
}
|
||||
innerFile.value = value;
|
||||
}
|
||||
|
||||
function handleClickDownloadFile() {
|
||||
let { url, path } = innerFile.value || {};
|
||||
if (!url || url.length === 0) {
|
||||
if (path && path.length > 0) {
|
||||
url = getFileAccessHttpUrl(path.split(',')[0]);
|
||||
}
|
||||
}
|
||||
if (url) {
|
||||
window.open(url);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickDeleteFile() {
|
||||
handleChangeCommon(null);
|
||||
}
|
||||
|
||||
return {
|
||||
...setup,
|
||||
innerFile,
|
||||
uploadAction,
|
||||
uploadHeaders,
|
||||
hasFile,
|
||||
responseName,
|
||||
handleChangeUpload,
|
||||
handleClickDownloadFile,
|
||||
handleClickDeleteFile,
|
||||
};
|
||||
}
|
||||
|
||||
export function fileGetValue(value) {
|
||||
if (value && value.path) {
|
||||
return value.path;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function fileSetValue(value) {
|
||||
if (value) {
|
||||
let first = value.split(',')[0];
|
||||
let name = first.substring(first.lastIndexOf('/') + 1);
|
||||
return {
|
||||
name: name,
|
||||
path: value,
|
||||
status: 'done',
|
||||
};
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@ -0,0 +1,420 @@
|
||||
import type { JVxeColumn, JVxeDataProps, JVxeTableProps } from '../types';
|
||||
import { computed, nextTick } from 'vue';
|
||||
import { isArray, isEmpty, isPromise } from '/@/utils/is';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { JVxeTypePrefix, JVxeTypes } from '../types/JVxeTypes';
|
||||
import { initDictOptions } from '/@/utils/dict';
|
||||
import { pushIfNotExist } from '/@/utils/common/compUtils';
|
||||
import { getEnhanced } from '../utils/enhancedUtils';
|
||||
import { isRegistered } from '../utils/registerUtils';
|
||||
import { JVxeComponent } from '../types/JVxeComponent';
|
||||
import { useValidateRules } from './useValidateRules';
|
||||
import { JVxeTableMethods } from '../types';
|
||||
|
||||
// handle 方法参数
|
||||
export interface HandleArgs {
|
||||
props: JVxeTableProps;
|
||||
slots: any;
|
||||
data: JVxeDataProps;
|
||||
methods: JVxeTableMethods;
|
||||
col?: JVxeColumn;
|
||||
columns: JVxeColumn[];
|
||||
renderOptions?: any;
|
||||
enhanced?: JVxeComponent.Enhanced;
|
||||
}
|
||||
|
||||
export function useColumns(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods, slots) {
|
||||
data.vxeColumns = computed(() => {
|
||||
let columns: JVxeColumn[] = [];
|
||||
if (isArray(props.columns)) {
|
||||
// handle 方法参数
|
||||
const args: HandleArgs = { props, slots, data, methods, columns };
|
||||
let seqColumn, selectionColumn, expandColumn, dragSortColumn;
|
||||
props.columns.forEach((column: JVxeColumn) => {
|
||||
// 排除未授权的列 1 = 显示/隐藏; 2 = 禁用
|
||||
let auth = methods.getColAuth(column.key);
|
||||
if (auth?.type == '1' && !auth.isAuth) {
|
||||
return;
|
||||
} else if (auth?.type == '2' && !auth.isAuth) {
|
||||
column.disabled = true;
|
||||
}
|
||||
// type 不填,默认为 normal
|
||||
if (column.type == null || isEmpty(column.type)) {
|
||||
column.type = JVxeTypes.normal;
|
||||
}
|
||||
let col: JVxeColumn = cloneDeep(column);
|
||||
// 处理隐藏列
|
||||
if (col.type === JVxeTypes.hidden) {
|
||||
return handleInnerColumn(args, col, handleHiddenColumn);
|
||||
}
|
||||
// 组件未注册,自动设置为 normal
|
||||
if (!isRegistered(col.type)) {
|
||||
col.type = JVxeTypes.normal;
|
||||
}
|
||||
args.enhanced = getEnhanced(col.type);
|
||||
args.col = col;
|
||||
args.renderOptions = {
|
||||
bordered: props.bordered,
|
||||
disabled: props.disabled,
|
||||
scrolling: data.scrolling,
|
||||
isDisabledRow: methods.isDisabledRow,
|
||||
listeners: {
|
||||
trigger: (name, event) => methods.trigger(name, event),
|
||||
valueChange: (event) => methods.trigger('valueChange', event),
|
||||
/** 重新排序行 */
|
||||
rowResort: (event) => {
|
||||
methods.doSort(event.oldIndex, event.newIndex);
|
||||
methods.trigger('dragged', event);
|
||||
},
|
||||
/** 在当前行下面插入一行 */
|
||||
rowInsertDown: (rowIndex) => methods.insertRows({}, rowIndex + 1),
|
||||
},
|
||||
};
|
||||
if (col.type === JVxeTypes.rowNumber) {
|
||||
seqColumn = col;
|
||||
columns.push(col);
|
||||
} else if (col.type === JVxeTypes.rowRadio || col.type === JVxeTypes.rowCheckbox) {
|
||||
selectionColumn = col;
|
||||
columns.push(col);
|
||||
} else if (col.type === JVxeTypes.rowExpand) {
|
||||
expandColumn = col;
|
||||
columns.push(col);
|
||||
} else if (col.type === JVxeTypes.rowDragSort) {
|
||||
dragSortColumn = col;
|
||||
columns.push(col);
|
||||
} else {
|
||||
col.params = column;
|
||||
handlerCol(args);
|
||||
}
|
||||
});
|
||||
handleInnerColumn(args, seqColumn, handleSeqColumn);
|
||||
handleInnerColumn(args, selectionColumn, handleSelectionColumn);
|
||||
handleInnerColumn(args, expandColumn, handleExpandColumn);
|
||||
handleInnerColumn(args, dragSortColumn, handleDragSortColumn, true);
|
||||
// update-begin--author:liaozhiyang---date:2024-05-30---for【TV360X-371】不可编辑组件必填缺少*号
|
||||
customComponentAddStar(columns);
|
||||
// update-end--author:liaozhiyang---date:2024-05-30---for:【TV360X-371】不可编辑组件必填缺少*号
|
||||
}
|
||||
return columns;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 2024-05-30
|
||||
* liaozhiyang
|
||||
* 不可编辑组件必填通过title人为加*号
|
||||
*/
|
||||
function customComponentAddStar(columns) {
|
||||
columns.forEach((column) => {
|
||||
const { params } = column;
|
||||
if (params) {
|
||||
const { validateRules, type } = params;
|
||||
if (
|
||||
validateRules?.length &&
|
||||
[
|
||||
JVxeTypes.checkbox,
|
||||
JVxeTypes.radio,
|
||||
JVxeTypes.upload,
|
||||
JVxeTypes.progress,
|
||||
JVxeTypes.departSelect,
|
||||
JVxeTypes.userSelect,
|
||||
JVxeTypes.image,
|
||||
JVxeTypes.file,
|
||||
].includes(type)
|
||||
) {
|
||||
if (validateRules.find((item) => item.required)) {
|
||||
column.title = ` * ${column.title}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理内置列 */
|
||||
function handleInnerColumn(args: HandleArgs, col: JVxeColumn, handler: (args: HandleArgs) => void, assign?: boolean) {
|
||||
let renderOptions = col?.editRender || col?.cellRender;
|
||||
return handler({
|
||||
...args,
|
||||
col: col,
|
||||
renderOptions: assign ? Object.assign({}, args.renderOptions, renderOptions) : renderOptions,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理隐藏列
|
||||
*/
|
||||
function handleHiddenColumn({ col, columns }: HandleArgs) {
|
||||
col!.params = cloneDeep(col);
|
||||
delete col!.type;
|
||||
col!.field = col!.key;
|
||||
col!.visible = false;
|
||||
columns.push(col!);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理行号列
|
||||
*/
|
||||
function handleSeqColumn({ props, col, columns }: HandleArgs) {
|
||||
// 判断是否开启了行号列
|
||||
if (props.rowNumber) {
|
||||
let column = {
|
||||
type: 'seq',
|
||||
title: '#',
|
||||
width: 60,
|
||||
// 【QQYUN-8405】
|
||||
fixed: props.rowNumberFixed,
|
||||
align: 'center',
|
||||
};
|
||||
// update-begin--author:liaozhiyang---date:20240306---for:【QQYUN-8405】vxetable支持序号是否固定(移动端需要)
|
||||
if (props.rowNumberFixed === 'none') {
|
||||
delete column.fixed;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240306---for:QQYUN-8405】vxetable支持序号是否固定(移动端需要)
|
||||
if (col) {
|
||||
Object.assign(col, column);
|
||||
} else {
|
||||
columns.unshift(column as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理可选择列
|
||||
*/
|
||||
function handleSelectionColumn({ props, data, col, columns }: HandleArgs) {
|
||||
// 判断是否开启了可选择行
|
||||
if (props.rowSelection) {
|
||||
let width = 45;
|
||||
if (data.statistics.has && !props.rowExpand && !props.dragSort) {
|
||||
width = 60;
|
||||
}
|
||||
let column: any = {
|
||||
type: props.rowSelectionType,
|
||||
width: width,
|
||||
fixed: 'left',
|
||||
align: 'center',
|
||||
};
|
||||
// update-begin--author:liaozhiyang---date:20240509---for:【issues/1162】JVxeTable列过长(出现横向滚动条)时无法拖拽排序
|
||||
if (props.rowSelectionFixed === 'none') {
|
||||
delete column.fixed;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240509---for:【issues/1162】JVxeTable列过长(出现横向滚动条)时无法拖拽排序
|
||||
if (col) {
|
||||
Object.assign(col, column);
|
||||
} else {
|
||||
columns.unshift(column as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理可展开行
|
||||
*/
|
||||
function handleExpandColumn({ props, data, col, columns }: HandleArgs) {
|
||||
// 是否可展开行
|
||||
if (props.rowExpand) {
|
||||
let width = 40;
|
||||
if (data.statistics.has && !props.dragSort) {
|
||||
width = 60;
|
||||
}
|
||||
let column = {
|
||||
type: 'expand',
|
||||
title: '',
|
||||
width: width,
|
||||
fixed: 'left',
|
||||
align: 'center',
|
||||
slots: { content: 'expandContent' },
|
||||
};
|
||||
if (col) {
|
||||
Object.assign(col, column);
|
||||
} else {
|
||||
columns.unshift(column as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理可排序列 */
|
||||
function handleDragSortColumn({ props, data, col, columns, renderOptions }: HandleArgs) {
|
||||
// 是否可拖动排序
|
||||
if (props.dragSort) {
|
||||
let width = 40;
|
||||
if (data.statistics.has) {
|
||||
width = 60;
|
||||
}
|
||||
let column: any = {
|
||||
title: '',
|
||||
width: width,
|
||||
fixed: 'left',
|
||||
align: 'center',
|
||||
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
params: {
|
||||
notAllowDrag: props.notAllowDrag,
|
||||
...col?.params,
|
||||
},
|
||||
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
};
|
||||
// update-begin--author:liaozhiyang---date:20240506---for:【issues/1162】JVxeTable列过长(出现横向滚动条)时无法拖拽排序
|
||||
if (props.dragSortFixed === 'none') {
|
||||
delete column.fixed;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240506---for:【issues/1162】JVxeTable列过长(出现横向滚动条)时无法拖拽排序
|
||||
let cellRender = {
|
||||
name: JVxeTypePrefix + JVxeTypes.rowDragSort,
|
||||
sortKey: props.sortKey,
|
||||
};
|
||||
if (renderOptions) {
|
||||
column.cellRender = Object.assign(renderOptions, cellRender);
|
||||
} else {
|
||||
column.cellRender = cellRender;
|
||||
}
|
||||
if (col) {
|
||||
Object.assign(col, column);
|
||||
} else {
|
||||
columns.unshift(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理自定义组件列 */
|
||||
function handlerCol(args: HandleArgs) {
|
||||
const { props, col, columns, enhanced } = args;
|
||||
if (!col) return;
|
||||
let { type } = col;
|
||||
col.field = col.key;
|
||||
delete col.type;
|
||||
let renderName = 'cellRender';
|
||||
// 渲染选项
|
||||
let $renderOptions: any = { name: JVxeTypePrefix + type };
|
||||
if (enhanced?.switches.editRender) {
|
||||
if (!(enhanced.switches.visible || props.alwaysEdit)) {
|
||||
renderName = 'editRender';
|
||||
}
|
||||
// $renderOptions.type = (enhanced.switches.visible || props.alwaysEdit) ? 'visible' : 'default'
|
||||
}
|
||||
col[renderName] = $renderOptions;
|
||||
// update-begin--author:liaozhiyang---date:20240321---for:【QQYUN-5806】js增强改变下拉搜索options(添加customOptions为true不读字典,走自己的options)
|
||||
!col.params.customOptions && handleDict(args);
|
||||
// update-end--author:liaozhiyang---date:20240321---for:【QQYUN-5806】js增强改变下拉搜索options(添加customOptions为true不读字典,走自己的options)
|
||||
handleRules(args);
|
||||
handleStatistics(args);
|
||||
handleSlots(args);
|
||||
handleLinkage(args);
|
||||
handleReloadEffect(args);
|
||||
|
||||
if (col.editRender) {
|
||||
Object.assign(col.editRender, args.renderOptions);
|
||||
}
|
||||
if (col.cellRender) {
|
||||
Object.assign(col.cellRender, args.renderOptions);
|
||||
}
|
||||
|
||||
columns.push(col);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理字典
|
||||
*/
|
||||
async function handleDict({ col, methods }: HandleArgs) {
|
||||
if (col && col.params.dictCode) {
|
||||
/** 加载数据字典并合并到 options */
|
||||
try {
|
||||
// 查询字典
|
||||
if (!isPromise(col.params.optionsPromise)) {
|
||||
col.params.optionsPromise = new Promise(async (resolve) => {
|
||||
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1180 【代码生成】子表不支持带条件?
|
||||
let dictCodeString = col.params.dictCode;
|
||||
if (dictCodeString) {
|
||||
dictCodeString = encodeURI(dictCodeString);
|
||||
}
|
||||
const dictOptions: any = await initDictOptions(dictCodeString);
|
||||
//update-end-author:taoyan date:2022-6-1 for: VUEN-1180 【代码生成】子表不支持带条件?
|
||||
let options = col.params.options ?? [];
|
||||
dictOptions.forEach((dict) => {
|
||||
// 过滤重复数据
|
||||
if (options.findIndex((o) => o.value === dict.value) === -1) {
|
||||
options.push(dict);
|
||||
}
|
||||
});
|
||||
resolve(options);
|
||||
});
|
||||
}
|
||||
col.params.options = await col.params.optionsPromise;
|
||||
await nextTick();
|
||||
await methods.getXTable().updateData();
|
||||
} catch (e) {
|
||||
console.group(`[JVxeTable] 查询字典 "${col.params.dictCode}" 时发生异常!`);
|
||||
console.warn(e);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理校验
|
||||
*/
|
||||
function handleRules(args: HandleArgs) {
|
||||
if (isArray(args.col?.validateRules)) {
|
||||
useValidateRules(args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理统计列
|
||||
*/
|
||||
function handleStatistics({ col, data }: HandleArgs) {
|
||||
// sum = 求和、average = 平均值
|
||||
if (col && isArray(col.statistics)) {
|
||||
data.statistics.has = true;
|
||||
col.statistics.forEach((item) => {
|
||||
if (!isEmpty(item)) {
|
||||
let arr = data.statistics[(item as string).toLowerCase()];
|
||||
if (isArray(arr)) {
|
||||
pushIfNotExist(arr, col.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理插槽
|
||||
*/
|
||||
function handleSlots({ slots, col, renderOptions }: HandleArgs) {
|
||||
// slot 组件特殊处理
|
||||
if (col && col.params.type === JVxeTypes.slot) {
|
||||
if (!isEmpty(col.slotName) && slots.hasOwnProperty(col.slotName)) {
|
||||
renderOptions.slot = slots[col.slotName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理联动列 */
|
||||
function handleLinkage({ data, col, renderOptions, methods }: HandleArgs) {
|
||||
// 处理联动列,联动列只能作用于 select 组件
|
||||
if (col && col.params.type === JVxeTypes.select && data.innerLinkageConfig != null) {
|
||||
// 判断当前列是否是联动列
|
||||
if (data.innerLinkageConfig.has(col.key)) {
|
||||
renderOptions.linkage = {
|
||||
config: data.innerLinkageConfig.get(col.key),
|
||||
getLinkageOptionsAsync: methods.getLinkageOptionsAsync,
|
||||
getLinkageOptionsSibling: methods.getLinkageOptionsSibling,
|
||||
handleLinkageSelectChange: methods.handleLinkageSelectChange,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleReloadEffect({ props, data, renderOptions }: HandleArgs) {
|
||||
renderOptions.reloadEffect = {
|
||||
enabled: props.reloadEffect,
|
||||
getMap() {
|
||||
return data.reloadEffectRowKeysMap;
|
||||
},
|
||||
isEffect(rowId) {
|
||||
return data.reloadEffectRowKeysMap[rowId] === true;
|
||||
},
|
||||
removeEffect(rowId) {
|
||||
return (data.reloadEffectRowKeysMap[rowId] = false);
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
import { computed } from 'vue';
|
||||
import { router } from '/@/router';
|
||||
import { createLocalStorage } from '/@/utils/cache';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
export function useColumnsCache({ cacheColumnsKey, refs }: any) {
|
||||
const $ls = createLocalStorage();
|
||||
const { createMessage: $message } = useMessage();
|
||||
const cacheKey = computed(() => {
|
||||
const path = router.currentRoute.value.fullPath;
|
||||
let key = path.replace(/[\/\\]/g, '_');
|
||||
if (cacheColumnsKey) {
|
||||
key += ':' + cacheColumnsKey;
|
||||
}
|
||||
return 'vxe-columnCache:' + key;
|
||||
});
|
||||
const initSetting = (props) => {
|
||||
const columnCache = $ls.get(cacheKey.value);
|
||||
if (columnCache) {
|
||||
columnCache.forEach((key) => {
|
||||
const column = props.columns.find((item) => item.key === key);
|
||||
if (column) {
|
||||
column.visible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
// const initSetting = (refs) => {
|
||||
// let columnCache = $ls.get(cacheKey.value);
|
||||
// if (columnCache) {
|
||||
// const $grid = refs.gridRef.value!.getRefMaps().refTable.value;
|
||||
// console.log('refs.gridRef', $grid);
|
||||
// const { fullColumn } = $grid.getTableColumn();
|
||||
// const hideColumns = getHideColumn(fullColumn, columnCache);
|
||||
// if (hideColumns?.length) {
|
||||
// hideColumns.forEach((column) => {
|
||||
// $grid.hideColumn(column);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// console.log(columnCache);
|
||||
// };
|
||||
function saveSetting($grid: any) {
|
||||
console.log($grid);
|
||||
const { fullColumn, visibleColumn } = $grid.getTableColumn();
|
||||
const hideColumnKey = getHideColumnKey(fullColumn, visibleColumn);
|
||||
if (hideColumnKey.length) {
|
||||
$ls.set(cacheKey.value, hideColumnKey);
|
||||
$message.success('保存成功');
|
||||
}
|
||||
}
|
||||
const resetSetting = ($grid) => {
|
||||
const columnCache = $ls.get(cacheKey.value);
|
||||
if (columnCache) {
|
||||
const { fullColumn } = $grid.getTableColumn();
|
||||
const hideColumns = getHideColumn(fullColumn, columnCache);
|
||||
if (hideColumns?.length) {
|
||||
hideColumns.forEach((column) => {
|
||||
if (columnCache.includes(column?.params?.key)) {
|
||||
$grid.showColumn(column);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
$ls.remove(cacheKey.value);
|
||||
$message.success('重置成功');
|
||||
};
|
||||
const getHideColumn = (fullColumn, columnCache) => {
|
||||
const result: any = [];
|
||||
if (columnCache?.length) {
|
||||
console.log('--fullColumn:',fullColumn);
|
||||
columnCache.forEach((key) => {
|
||||
const column = fullColumn.find((item) => item?.params?.key === key);
|
||||
if (column) {
|
||||
result.push(column);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const getHideColumnKey = (fullColumn, visibleColumn) => {
|
||||
const reuslt: any = [];
|
||||
if (fullColumn.length === visibleColumn.length) {
|
||||
return reuslt;
|
||||
} else {
|
||||
fullColumn.forEach((item) => {
|
||||
const fKey = item?.params?.key;
|
||||
if (fKey) {
|
||||
const vItem = visibleColumn.find((item) => {
|
||||
return item?.params?.key === fKey;
|
||||
});
|
||||
if (!vItem) {
|
||||
reuslt.push(fKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
return reuslt;
|
||||
}
|
||||
};
|
||||
return {
|
||||
initSetting,
|
||||
resetSetting,
|
||||
saveSetting,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
import { ref, reactive, provide, resolveComponent } from 'vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { JVxeDataProps, JVxeRefs, JVxeTableProps } from '../types';
|
||||
import { VxeGridInstance } from 'vxe-table';
|
||||
import { randomString } from '/@/utils/common/compUtils';
|
||||
|
||||
export function useData(props: JVxeTableProps): JVxeDataProps {
|
||||
const { prefixCls } = useDesign('j-vxe-table');
|
||||
provide('prefixCls', prefixCls);
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
caseId: `j-vxe-${randomString(8)}`,
|
||||
vxeDataSource: ref([]),
|
||||
scroll: reactive({ top: 0, left: 0 }),
|
||||
scrolling: ref(false),
|
||||
defaultVxeProps: reactive({
|
||||
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-327】vxetable警告
|
||||
// rowId: props.rowKey,
|
||||
rowConfig: {
|
||||
keyField: props.rowKey,
|
||||
},
|
||||
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-327】vxetable警告
|
||||
// 高亮hover的行
|
||||
highlightHoverRow: true,
|
||||
|
||||
// --- 【issues/209】自带的tooltip会错位,所以替换成原生的title ---
|
||||
// 溢出隐藏并显示tooltip
|
||||
showOverflow: "title",
|
||||
// 表头溢出隐藏并显示tooltip
|
||||
showHeaderOverflow: "title",
|
||||
// --- 【issues/209】自带的tooltip会错位,所以替换成原生的title ---
|
||||
|
||||
showFooterOverflow: true,
|
||||
// 可编辑配置
|
||||
editConfig: {
|
||||
trigger: 'click',
|
||||
mode: 'cell',
|
||||
// update-begin--author:liaozhiyang---date:20231013---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
//activeMethod: () => !props.disabled,
|
||||
beforeEditMethod: () => !props.disabled,
|
||||
// update-end--author:liaozhiyang---date:20231013---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
},
|
||||
expandConfig: {
|
||||
iconClose: 'ant-table-row-expand-icon ant-table-row-expand-icon-collapsed',
|
||||
iconOpen: 'ant-table-row-expand-icon ant-table-row-expand-icon-expanded',
|
||||
},
|
||||
// 虚拟滚动配置,y轴大于xx条数据时启用虚拟滚动
|
||||
scrollY: {
|
||||
gt: 30,
|
||||
},
|
||||
scrollX: {
|
||||
gt: 20,
|
||||
// 暂时关闭左右虚拟滚动
|
||||
enabled: false,
|
||||
},
|
||||
radioConfig: { highlight: true },
|
||||
checkboxConfig: { highlight: true },
|
||||
mouseConfig: { selected: false },
|
||||
keyboardConfig: {
|
||||
// 删除键功能
|
||||
isDel: false,
|
||||
// Esc键关闭编辑功能
|
||||
isEsc: true,
|
||||
// Tab 键功能
|
||||
isTab: true,
|
||||
// 任意键进入编辑(功能键除外)
|
||||
isEdit: true,
|
||||
// 方向键功能
|
||||
isArrow: true,
|
||||
// 回车键功能
|
||||
isEnter: true,
|
||||
// 如果功能被支持,用于 column.type=checkbox|radio,开启空格键切换复选框或单选框状态功能
|
||||
isChecked: true,
|
||||
},
|
||||
}),
|
||||
selectedRows: ref<any[]>([]),
|
||||
selectedRowIds: ref<string[]>([]),
|
||||
disabledRowIds: [],
|
||||
statistics: reactive({
|
||||
has: false,
|
||||
sum: [],
|
||||
average: [],
|
||||
}),
|
||||
authsMap: ref(null),
|
||||
innerEditRules: {},
|
||||
innerLinkageConfig: new Map<string, any>(),
|
||||
reloadEffectRowKeysMap: reactive({}),
|
||||
};
|
||||
}
|
||||
|
||||
export function useRefs(): JVxeRefs {
|
||||
return {
|
||||
gridRef: ref<VxeGridInstance>(),
|
||||
subPopoverRef: ref<any>(),
|
||||
detailsModalRef: ref<any>(),
|
||||
};
|
||||
}
|
||||
|
||||
export function useResolveComponent(...t: any[]): any {
|
||||
// @ts-ignore
|
||||
return resolveComponent(...t);
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { nextTick, watch } from 'vue';
|
||||
import { JVxeDataProps, JVxeRefs, JVxeTableMethods } from '../types';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
export function useDataSource(props, data: JVxeDataProps, methods: JVxeTableMethods, refs: JVxeRefs) {
|
||||
watch(
|
||||
() => props.dataSource,
|
||||
async () => {
|
||||
data.disabledRowIds = [];
|
||||
data.vxeDataSource.value = cloneDeep(props.dataSource);
|
||||
data.vxeDataSource.value.forEach((row, rowIndex) => {
|
||||
// 判断是否是禁用行
|
||||
if (methods.isDisabledRow(row, rowIndex)) {
|
||||
data.disabledRowIds.push(row.id);
|
||||
}
|
||||
// 处理联动回显数据
|
||||
methods.handleLinkageBackData(row);
|
||||
});
|
||||
await waitRef(refs.gridRef);
|
||||
methods.recalcSortNumber();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
}
|
||||
|
||||
function waitRef($ref) {
|
||||
return new Promise<any>((resolve) => {
|
||||
(function next() {
|
||||
if ($ref.value) {
|
||||
resolve($ref);
|
||||
} else {
|
||||
nextTick(() => next());
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
import { onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { JVxeTableMethods, JVxeTableProps } from '/@/components/jeecg/JVxeTable/src/types';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
export function useDragSort(props: JVxeTableProps, methods: JVxeTableMethods) {
|
||||
if (props.dragSort) {
|
||||
let sortable2: Sortable;
|
||||
let initTime: any;
|
||||
|
||||
onMounted(() => {
|
||||
// 加载完成之后再绑定拖动事件
|
||||
initTime = setTimeout(createSortable, 300);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(initTime);
|
||||
if (sortable2) {
|
||||
sortable2.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
function createSortable() {
|
||||
let xTable = methods.getXTable();
|
||||
// let dom = xTable.$el.querySelector('.vxe-table--fixed-wrapper .vxe-table--body tbody')
|
||||
let dom = xTable.$el.querySelector('.body--wrapper>.vxe-table--body tbody');
|
||||
let startChildren = [];
|
||||
sortable2 = Sortable.create(dom as HTMLElement, {
|
||||
handle: '.drag-btn',
|
||||
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
filter: '.not-allow-drag',
|
||||
draggable: ".allow-drag",
|
||||
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
direction: 'vertical',
|
||||
animation: 300,
|
||||
onStart(e) {
|
||||
let from = e.from;
|
||||
// @ts-ignore
|
||||
startChildren = [...from.children];
|
||||
},
|
||||
onEnd(e) {
|
||||
let oldIndex = e.oldIndex as number;
|
||||
let newIndex = e.newIndex as number;
|
||||
if (oldIndex === newIndex) {
|
||||
return;
|
||||
}
|
||||
// 【VUEN-2505】获取当前行数据
|
||||
let rowNode = xTable.getRowNode(e.item);
|
||||
if (!rowNode) {
|
||||
return;
|
||||
}
|
||||
let from = e.from;
|
||||
let element = startChildren[oldIndex];
|
||||
let target = null;
|
||||
if (oldIndex > newIndex) {
|
||||
// 向上移动
|
||||
if (oldIndex + 1 < startChildren.length) {
|
||||
target = startChildren[oldIndex + 1];
|
||||
}
|
||||
} else {
|
||||
// 向下移动
|
||||
target = startChildren[oldIndex + 1];
|
||||
}
|
||||
from.removeChild(element);
|
||||
from.insertBefore(element, target);
|
||||
nextTick(() => {
|
||||
// 【VUEN-2505】算出因虚拟滚动导致的偏移量
|
||||
let diffIndex = rowNode!.index - oldIndex;
|
||||
if (diffIndex > 0) {
|
||||
oldIndex = oldIndex + diffIndex;
|
||||
newIndex = newIndex + diffIndex;
|
||||
}
|
||||
methods.doSort(oldIndex, newIndex);
|
||||
methods.trigger('dragged', { oldIndex, newIndex });
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
import { unref, computed } from 'vue';
|
||||
import { merge } from 'lodash-es';
|
||||
import { isArray } from '/@/utils/is';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { useKeyboardEdit } from '../hooks/useKeyboardEdit';
|
||||
import { JVxeDataProps, JVxeTableMethods, JVxeTableProps } from '../types';
|
||||
|
||||
export function useFinallyProps(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods) {
|
||||
const attrs = useAttrs();
|
||||
// vxe 键盘操作配置
|
||||
const { keyboardEditConfig } = useKeyboardEdit(props);
|
||||
// vxe 最终 editRules
|
||||
const vxeEditRules = computed(() => merge({}, props.editRules, data.innerEditRules));
|
||||
// vxe 最终 events
|
||||
const vxeEvents = computed(() => {
|
||||
let listeners = { ...unref(attrs) };
|
||||
let events = {
|
||||
onScroll: methods.handleVxeScroll,
|
||||
onCellClick: methods.handleCellClick,
|
||||
onEditClosed: methods.handleEditClosed,
|
||||
onEditActived: methods.handleEditActived,
|
||||
onRadioChange: methods.handleVxeRadioChange,
|
||||
onCheckboxAll: methods.handleVxeCheckboxAll,
|
||||
onCheckboxChange: methods.handleVxeCheckboxChange,
|
||||
// update-begin--author:liaozhiyang---date:20240321---for:【QQYUN-8566】JVXETable无法记住列设置
|
||||
onCustom: methods.handleCustom,
|
||||
// update-begin--author:liaozhiyang---date:20240321---for:【QQYUN-8566】JVXETable无法记住列设置
|
||||
};
|
||||
// 用户传递的事件,进行合并操作
|
||||
Object.keys(listeners).forEach((key) => {
|
||||
let listen = listeners[key];
|
||||
if (events.hasOwnProperty(key)) {
|
||||
if (isArray(listen)) {
|
||||
listen.push(events[key]);
|
||||
} else {
|
||||
listen = [events[key], listen];
|
||||
}
|
||||
}
|
||||
events[key] = listen;
|
||||
});
|
||||
return events;
|
||||
});
|
||||
// vxe 最终 props
|
||||
const vxeProps = computed(() => {
|
||||
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
let rowClass = {};
|
||||
if (props.dragSort) {
|
||||
rowClass = {
|
||||
rowClassName: (params) => {
|
||||
let { row } = params;
|
||||
const find = props.notAllowDrag?.find((item:any) => {
|
||||
const {key, value} = item;
|
||||
return row[key] == value;
|
||||
});
|
||||
// 业务传进的来的rowClassName
|
||||
const popsRowClassName = props.rowClassName ?? '';
|
||||
let outClass = '';
|
||||
if(typeof popsRowClassName==='string'){
|
||||
popsRowClassName && (outClass = popsRowClassName);
|
||||
}else if(typeof popsRowClassName==='function'){
|
||||
outClass = popsRowClassName(params)
|
||||
}
|
||||
return find ? `not-allow-drag ${outClass}` : `allow-drag ${outClass}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
return merge(
|
||||
{},
|
||||
data.defaultVxeProps,
|
||||
{
|
||||
showFooter: data.statistics.has,
|
||||
},
|
||||
unref(attrs),
|
||||
{
|
||||
ref: 'gridRef',
|
||||
size: props.size,
|
||||
loading: false,
|
||||
disabled: props.disabled,
|
||||
columns: unref(data.vxeColumns),
|
||||
editRules: unref(vxeEditRules),
|
||||
height: props.height === 'auto' ? null : props.height,
|
||||
maxHeight: props.maxHeight,
|
||||
// update-begin--author:liaozhiyang---date:20231013---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
scrollY: props.scrollY,
|
||||
scrollX: props.scrollX,
|
||||
// update-end--author:liaozhiyang---date:20231013---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
border: props.bordered,
|
||||
footerMethod: methods.handleFooterMethod,
|
||||
// 展开行配置
|
||||
expandConfig: {
|
||||
toggleMethod: methods.handleExpandToggleMethod,
|
||||
},
|
||||
// 可编辑配置
|
||||
editConfig: {
|
||||
// update-begin--author:liaozhiyang---date:20231013---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
//activeMethod: methods.handleActiveMethod,
|
||||
beforeEditMethod: methods.handleActiveMethod,
|
||||
// update-end--author:liaozhiyang---date:20231013---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
},
|
||||
radioConfig: {
|
||||
checkMethod: methods.handleCheckMethod,
|
||||
},
|
||||
checkboxConfig: {
|
||||
checkMethod: methods.handleCheckMethod,
|
||||
},
|
||||
...rowClass
|
||||
// rowClassName:(params)=>{
|
||||
// const { row } = params;
|
||||
// return row.dbFieldName=='id'?"not-allow-drag":"allow-drag"
|
||||
// }
|
||||
},
|
||||
unref(vxeEvents),
|
||||
unref(keyboardEditConfig)
|
||||
);
|
||||
});
|
||||
return {
|
||||
vxeProps,
|
||||
prefixCls: data.prefixCls,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,298 @@
|
||||
import { computed, nextTick, ref, unref, watch } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { getEnhanced, replaceProps } from '../utils/enhancedUtils';
|
||||
import { vModel } from '/@/components/jeecg/JVxeTable/utils';
|
||||
import { JVxeRenderType } from '../types/JVxeTypes';
|
||||
import { isBoolean, isFunction, isObject, isPromise } from '/@/utils/is';
|
||||
import { JVxeComponent } from '../types/JVxeComponent';
|
||||
import { filterDictText } from '/@/utils/dict/JDictSelectUtil';
|
||||
|
||||
export function useJVxeCompProps() {
|
||||
return {
|
||||
// 组件类型
|
||||
type: propTypes.string,
|
||||
// 渲染类型
|
||||
renderType: propTypes.string.def('default'),
|
||||
// 渲染参数
|
||||
params: propTypes.object,
|
||||
// 渲染自定义选项
|
||||
renderOptions: propTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
export function useJVxeComponent(props: JVxeComponent.Props) {
|
||||
const value = computed(() => {
|
||||
// update-begin--author:liaozhiyang---date:20240430---for:【QQYUN-9125】oracle数据库日期类型字段会默认带上时分秒
|
||||
const val = props.params.row[props.params.column.property];
|
||||
if (props.type === 'date' && typeof val === 'string') {
|
||||
return val.split(' ').shift();
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240430---for:【QQYUN-9125】oracle数据库日期类型字段会默认带上时分秒
|
||||
});
|
||||
const innerValue = ref(value.value);
|
||||
const row = computed(() => props.params.row);
|
||||
const rows = computed(() => props.params.data);
|
||||
const column = computed(() => props.params.column);
|
||||
// 用户配置的原始 column
|
||||
const originColumn = computed(() => column.value.params);
|
||||
const rowIndex = computed(() => props.params._rowIndex);
|
||||
const columnIndex = computed(() => props.params._columnIndex);
|
||||
// 表格数据长度
|
||||
const fullDataLength = computed(() => props.params.$table.internalData.tableFullData.length);
|
||||
// 是否正在滚动中
|
||||
const scrolling = computed(() => !!props.renderOptions.scrolling);
|
||||
const cellProps = computed(() => {
|
||||
let renderOptions = props.renderOptions;
|
||||
let col = originColumn.value;
|
||||
|
||||
let cellProps = {};
|
||||
|
||||
// 输入占位符
|
||||
cellProps['placeholder'] = replaceProps(col, col.placeholder);
|
||||
|
||||
// 解析props
|
||||
if (isObject(col.props)) {
|
||||
Object.keys(col.props).forEach((key) => {
|
||||
cellProps[key] = replaceProps(col, col.props[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// 判断是否是禁用的列
|
||||
cellProps['disabled'] = isBoolean(col['disabled']) ? col['disabled'] : cellProps['disabled'];
|
||||
// 判断是否禁用行
|
||||
if (renderOptions.isDisabledRow(row.value, rowIndex.value)) {
|
||||
cellProps['disabled'] = true;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240528---for:【TV360X-291】没勾选同步数据库禁用排序功能
|
||||
if (col.props && col.props.isDisabledCell) {
|
||||
if (col.props.isDisabledCell({ row: row.value, rowIndex: rowIndex.value, column: col, columnIndex: columnIndex.value })) {
|
||||
cellProps['disabled'] = true;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240528---for:【TV360X-291】没勾选同步数据库禁用排序功能
|
||||
// 判断是否禁用所有组件
|
||||
if (renderOptions.disabled === true) {
|
||||
cellProps['disabled'] = true;
|
||||
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-1068】行编辑整体禁用时上传按钮不显示
|
||||
cellProps['disabledTable'] = true;
|
||||
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-1068】行编辑整体禁用时上传按钮不显示
|
||||
}
|
||||
//update-begin-author:taoyan date:2022-5-25 for: VUEN-1111 一对多子表 部门选择 不应该级联
|
||||
if (col.checkStrictly === true) {
|
||||
cellProps['checkStrictly'] = true;
|
||||
}
|
||||
//update-end-author:taoyan date:2022-5-25 for: VUEN-1111 一对多子表 部门选择 不应该级联
|
||||
|
||||
//update-begin-author:taoyan date:2022-5-27 for: 用户组件 控制单选多选新的参数配置
|
||||
if (col.isRadioSelection === true) {
|
||||
cellProps['isRadioSelection'] = true;
|
||||
} else if (col.isRadioSelection === false) {
|
||||
cellProps['isRadioSelection'] = false;
|
||||
}
|
||||
//update-end-author:taoyan date:2022-5-27 for: 用户组件 控制单选多选新的参数配置
|
||||
|
||||
return cellProps;
|
||||
});
|
||||
|
||||
const listeners = computed(() => {
|
||||
let listeners = Object.assign({}, props.renderOptions.listeners || {});
|
||||
// 默认change事件
|
||||
if (!listeners.change) {
|
||||
listeners.change = async (event) => {
|
||||
vModel(event.value, row, column);
|
||||
await nextTick();
|
||||
// 处理 change 事件相关逻辑(例如校验)
|
||||
props.params.$table.updateStatus(props.params);
|
||||
};
|
||||
}
|
||||
return listeners;
|
||||
});
|
||||
const context = {
|
||||
innerValue,
|
||||
row,
|
||||
rows,
|
||||
rowIndex,
|
||||
column,
|
||||
columnIndex,
|
||||
originColumn,
|
||||
fullDataLength,
|
||||
cellProps,
|
||||
scrolling,
|
||||
handleChangeCommon,
|
||||
handleBlurCommon,
|
||||
};
|
||||
const ctx = { props, context };
|
||||
|
||||
// 获取组件增强
|
||||
const enhanced = getEnhanced(props.type);
|
||||
|
||||
watch(
|
||||
value,
|
||||
(newValue) => {
|
||||
// 验证值格式
|
||||
let getValue = enhanced.getValue(newValue, ctx);
|
||||
if (newValue !== getValue) {
|
||||
// 值格式不正确,重新赋值
|
||||
newValue = getValue;
|
||||
vModel(newValue, row, column);
|
||||
}
|
||||
innerValue.value = enhanced.setValue(newValue, ctx);
|
||||
// update-begin--author:liaozhiyang---date:20240509---for:【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
|
||||
if (props.type === 'date' && props.renderType === JVxeRenderType.spaner && enhanced.translate.enabled === true) {
|
||||
if (isFunction(enhanced.translate.handler)) {
|
||||
innerValue.value = enhanced.translate.handler(newValue, ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240509---for:【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
|
||||
// 判断是否启用翻译
|
||||
if (props.renderType === JVxeRenderType.spaner && enhanced.translate.enabled === true) {
|
||||
if (isFunction(enhanced.translate.handler)) {
|
||||
let res = enhanced.translate.handler(newValue, ctx);
|
||||
// 异步翻译,可解决字典查询慢的问题
|
||||
if (isPromise(res)) {
|
||||
res.then((v) => (innerValue.value = v));
|
||||
} else {
|
||||
innerValue.value = res;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/** 通用处理 change 事件 */
|
||||
function handleChangeCommon($value, force = false) {
|
||||
const newValue = enhanced.getValue($value, ctx);
|
||||
const oldValue = value.value;
|
||||
// update-begin--author:liaozhiyang---date:20230718---for:【issues-5025】JVueTable的事件 @valueChange重复触发问题
|
||||
const execute = force ? true : newValue !== oldValue;
|
||||
if (execute) {
|
||||
trigger('change', { value: newValue });
|
||||
// 触发valueChange事件
|
||||
parentTrigger('valueChange', {
|
||||
type: props.type,
|
||||
value: newValue,
|
||||
oldValue: oldValue,
|
||||
col: originColumn.value,
|
||||
rowIndex: rowIndex.value,
|
||||
columnIndex: columnIndex.value,
|
||||
});
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230718---for:【issues-5025】JVueTable的事件 @valueChange重复触发问题
|
||||
}
|
||||
|
||||
/** 通用处理 blur 事件 */
|
||||
function handleBlurCommon($value) {
|
||||
// update-begin--author:liaozhiyang---date:20230817---for:【issues/636】JVxeTable加上blur事件
|
||||
const newValue = enhanced.getValue($value, ctx);
|
||||
const oldValue = value.value;
|
||||
//trigger('blur', { value });
|
||||
// 触发blur事件
|
||||
parentTrigger('blur', {
|
||||
type: props.type,
|
||||
value: newValue,
|
||||
oldValue: oldValue,
|
||||
col: originColumn.value,
|
||||
rowIndex: rowIndex.value,
|
||||
columnIndex: columnIndex.value,
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20230817---for:【issues/636】JVxeTable加上blur事件
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果事件存在的话,就触发
|
||||
* @param name 事件名
|
||||
* @param event 事件参数
|
||||
* @param args 其他附带参数
|
||||
*/
|
||||
function trigger(name, event?, args: any[] = []) {
|
||||
let listener = listeners.value[name];
|
||||
if (isFunction(listener)) {
|
||||
if (isObject(event)) {
|
||||
event = packageEvent(name, event);
|
||||
}
|
||||
listener(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
function parentTrigger(name, event, args: any[] = []) {
|
||||
args.unshift(packageEvent(name, event));
|
||||
trigger('trigger', name, args);
|
||||
}
|
||||
|
||||
function packageEvent(name, event: any = {}) {
|
||||
event.row = row.value;
|
||||
event.column = column.value;
|
||||
// online增强参数兼容
|
||||
event.column['key'] = column.value['property'];
|
||||
// event.cellTarget = this
|
||||
if (!event.type) {
|
||||
event.type = name;
|
||||
}
|
||||
if (!event.cellType) {
|
||||
event.cellType = props.type;
|
||||
}
|
||||
// 是否校验表单,默认为true
|
||||
if (isBoolean(event.validate)) {
|
||||
event.validate = true;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 防样式冲突类名生成器
|
||||
* @param scope
|
||||
*/
|
||||
function useCellDesign(scope: string) {
|
||||
return useDesign(`vxe-cell-${scope}`);
|
||||
}
|
||||
|
||||
return {
|
||||
...context,
|
||||
enhanced,
|
||||
trigger,
|
||||
useCellDesign,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件默认增强
|
||||
*/
|
||||
export function useDefaultEnhanced(): JVxeComponent.EnhancedPartial {
|
||||
return {
|
||||
installOptions: {
|
||||
autofocus: '',
|
||||
},
|
||||
interceptor: {
|
||||
'event.clearActived': () => true,
|
||||
'event.clearActived.className': () => true,
|
||||
},
|
||||
switches: {
|
||||
editRender: true,
|
||||
visible: false,
|
||||
},
|
||||
aopEvents: {
|
||||
editActived() {},
|
||||
editClosed() {},
|
||||
activeMethod: () => true,
|
||||
},
|
||||
translate: {
|
||||
enabled: false,
|
||||
handler(value, ctx) {
|
||||
// 默认翻译方法
|
||||
if (ctx) {
|
||||
return filterDictText(unref(ctx.context.column).params.options, value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
},
|
||||
getValue: (value) => value,
|
||||
setValue: (value) => value,
|
||||
createValue: (defaultValue) => defaultValue,
|
||||
} as JVxeComponent.Enhanced;
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* JVxeTable 键盘操作
|
||||
*/
|
||||
import type { VxeTablePropTypes } from 'vxe-table';
|
||||
import type { JVxeTableProps } from '../types';
|
||||
import { computed } from 'vue';
|
||||
|
||||
/**
|
||||
* JVxeTable 键盘操作
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export function useKeyboardEdit(props: JVxeTableProps) {
|
||||
// 是否开启了键盘操作
|
||||
const enabledKeyboard = computed(() => props.keyboardEdit ?? false);
|
||||
// 重写 keyboardConfig
|
||||
const keyboardConfig: VxeTablePropTypes.KeyboardConfig = {
|
||||
editMethod({ row, column, $table }) {
|
||||
// 重写默认的覆盖式,改为追加式
|
||||
$table.setActiveCell(row, column);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
// 键盘操作配置
|
||||
const keyboardEditConfig = computed(() => {
|
||||
return {
|
||||
mouseConfig: {
|
||||
selected: enabledKeyboard.value,
|
||||
},
|
||||
keyboardConfig,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
keyboardEditConfig,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,145 @@
|
||||
import { watch } from 'vue';
|
||||
import { isFunction, isPromise, isArray } from '/@/utils/is';
|
||||
import { JVxeColumn, JVxeDataProps, JVxeTableProps, JVxeLinkageConfig } from '../types';
|
||||
|
||||
/**
|
||||
* 多级联动
|
||||
*/
|
||||
export function useLinkage(props: JVxeTableProps, data: JVxeDataProps, methods) {
|
||||
// 整理多级联动配置
|
||||
watch(
|
||||
() => props.linkageConfig,
|
||||
(linkageConfig: JVxeLinkageConfig[]) => {
|
||||
data.innerLinkageConfig.clear();
|
||||
if (isArray(linkageConfig) && linkageConfig.length > 0) {
|
||||
linkageConfig.forEach((config) => {
|
||||
let keys = getLinkageKeys(config.key, []);
|
||||
// 多个key共享一个,引用地址
|
||||
let configItem = {
|
||||
...config,
|
||||
keys,
|
||||
optionsMap: new Map(),
|
||||
};
|
||||
keys.forEach((k) => data.innerLinkageConfig.set(k, configItem));
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 获取联动的key顺序
|
||||
function getLinkageKeys(key: string, keys: string[]): string[] {
|
||||
let col = props.columns?.find((col: JVxeColumn) => col.key === key) as JVxeColumn;
|
||||
if (col) {
|
||||
keys.push(col.key);
|
||||
// 寻找下级
|
||||
if (col.linkageKey) {
|
||||
return getLinkageKeys(col.linkageKey, keys);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// 处理联动回显数据
|
||||
function handleLinkageBackData(row) {
|
||||
if (data.innerLinkageConfig.size > 0) {
|
||||
for (let configItem of data.innerLinkageConfig.values()) {
|
||||
autoSetLinkageOptionsByData(row, '', configItem, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 【多级联动】获取同级联动下拉选项 */
|
||||
function getLinkageOptionsSibling(row, col, config, request) {
|
||||
// 如果当前列不是顶级列
|
||||
let key = '';
|
||||
if (col.key !== config.key) {
|
||||
// 就找出联动上级列
|
||||
let idx = config.keys.findIndex((k) => col.key === k);
|
||||
let parentKey = config.keys[idx - 1];
|
||||
key = row[parentKey];
|
||||
// 如果联动上级列没有选择数据,就直接返回空数组
|
||||
if (key === '' || key == null) {
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
key = 'root';
|
||||
}
|
||||
let options = config.optionsMap.get(key);
|
||||
if (!Array.isArray(options)) {
|
||||
if (request) {
|
||||
let parent = key === 'root' ? '' : key;
|
||||
return getLinkageOptionsAsync(config, parent);
|
||||
} else {
|
||||
options = [];
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/** 【多级联动】获取联动下拉选项(异步) */
|
||||
function getLinkageOptionsAsync(config, parent) {
|
||||
return new Promise((resolve) => {
|
||||
let key = parent ? parent : 'root';
|
||||
let options;
|
||||
if (config.optionsMap.has(key)) {
|
||||
options = config.optionsMap.get(key);
|
||||
if (isPromise(options)) {
|
||||
options.then((opt) => {
|
||||
config.optionsMap.set(key, opt);
|
||||
resolve(opt);
|
||||
});
|
||||
} else {
|
||||
resolve(options);
|
||||
}
|
||||
} else if (isFunction(config.requestData)) {
|
||||
// 调用requestData方法,通过传入parent来获取子级
|
||||
// noinspection JSVoidFunctionReturnValueUsed,TypeScriptValidateJSTypes
|
||||
let promise = config.requestData(parent);
|
||||
config.optionsMap.set(key, promise);
|
||||
promise.then((opt) => {
|
||||
config.optionsMap.set(key, opt);
|
||||
resolve(opt);
|
||||
});
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 【多级联动】 用于回显数据,自动填充 optionsMap
|
||||
function autoSetLinkageOptionsByData(data, parent, config, level) {
|
||||
if (level === 0) {
|
||||
getLinkageOptionsAsync(config, '');
|
||||
} else {
|
||||
getLinkageOptionsAsync(config, parent);
|
||||
}
|
||||
if (config.keys.length - 1 > level) {
|
||||
let value = data[config.keys[level]];
|
||||
if (value) {
|
||||
autoSetLinkageOptionsByData(data, value, config, level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 【多级联动】联动组件change时,清空下级组件
|
||||
function handleLinkageSelectChange(row, col, config, value) {
|
||||
if (col.linkageKey) {
|
||||
getLinkageOptionsAsync(config, value);
|
||||
let idx = config.keys.findIndex((k) => k === col.key);
|
||||
let values = {};
|
||||
for (let i = idx; i < config.keys.length; i++) {
|
||||
values[config.keys[i]] = '';
|
||||
}
|
||||
// 清空后几列的数据
|
||||
methods.setValues([{ rowKey: row.id, values }]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getLinkageOptionsAsync,
|
||||
getLinkageOptionsSibling,
|
||||
handleLinkageSelectChange,
|
||||
handleLinkageBackData,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,886 @@
|
||||
import { Ref, watch } from 'vue';
|
||||
import XEUtils from 'xe-utils';
|
||||
import { simpleDebounce } from '/@/utils/common/compUtils';
|
||||
import { JVxeDataProps, JVxeRefs, JVxeTableProps, JVxeTypes } from '../types';
|
||||
import { getEnhanced } from '../utils/enhancedUtils';
|
||||
import { VxeTableInstance, VxeTablePrivateMethods } from 'vxe-table';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { isArray, isEmpty, isNull, isString } from '/@/utils/is';
|
||||
import { useLinkage } from './useLinkage';
|
||||
import { useWebSocket } from './useWebSocket';
|
||||
import { getPrefix, getJVxeAuths } from '../utils/authUtils';
|
||||
import { excludeKeywords } from '../componentMap';
|
||||
import { useColumnsCache } from './useColumnsCache';
|
||||
|
||||
export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps, refs: JVxeRefs, instanceRef: Ref) {
|
||||
let xTableTemp: VxeTableInstance & VxeTablePrivateMethods;
|
||||
|
||||
function getXTable() {
|
||||
if (!xTableTemp) {
|
||||
// !. 为 typescript 的非空断言
|
||||
xTableTemp = refs.gridRef.value!.getRefMaps().refTable.value;
|
||||
}
|
||||
return xTableTemp;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
const hookMethods = {
|
||||
getXTable,
|
||||
addRows,
|
||||
pushRows,
|
||||
insertRows,
|
||||
addOrInsert,
|
||||
setValues,
|
||||
getValues,
|
||||
getTableData,
|
||||
getNewData,
|
||||
getNewDataWithId,
|
||||
getIfRowById,
|
||||
getNewRowById,
|
||||
getDeleteData,
|
||||
getSelectionData,
|
||||
getSelectedData,
|
||||
removeRows,
|
||||
removeRowsById,
|
||||
removeSelection,
|
||||
resetScrollTop,
|
||||
validateTable,
|
||||
fullValidateTable,
|
||||
clearSelection,
|
||||
filterNewRows,
|
||||
isDisabledRow,
|
||||
recalcDisableRows,
|
||||
rowResort,
|
||||
};
|
||||
|
||||
// 多级联动
|
||||
const linkageMethods = useLinkage(props, data, hookMethods);
|
||||
// WebSocket 无痕刷新
|
||||
const socketMethods = useWebSocket(props, data, hookMethods);
|
||||
|
||||
// 可显式供外部调用的方法
|
||||
const publicMethods = {
|
||||
...hookMethods,
|
||||
...linkageMethods,
|
||||
...socketMethods,
|
||||
};
|
||||
|
||||
/** 监听vxe滚动条位置 */
|
||||
function handleVxeScroll(event) {
|
||||
let { scroll } = data;
|
||||
|
||||
// 记录滚动条的位置
|
||||
scroll.top = event.scrollTop;
|
||||
scroll.left = event.scrollLeft;
|
||||
|
||||
refs.subPopoverRef.value?.close();
|
||||
data.scrolling.value = true;
|
||||
closeScrolling();
|
||||
}
|
||||
|
||||
// 当手动勾选单选时触发的事件
|
||||
function handleVxeRadioChange(event) {
|
||||
let row = event.$table.getRadioRecord();
|
||||
data.selectedRows.value = row ? [row] : [];
|
||||
handleSelectChange('radio', data.selectedRows.value, event);
|
||||
}
|
||||
|
||||
// 当手动勾选全选时触发的事件
|
||||
function handleVxeCheckboxAll(event) {
|
||||
data.selectedRows.value = event.$table.getCheckboxRecords();
|
||||
handleSelectChange('checkbox-all', data.selectedRows.value, event);
|
||||
}
|
||||
|
||||
// 当手动勾选并且值发生改变时触发的事件
|
||||
function handleVxeCheckboxChange(event) {
|
||||
data.selectedRows.value = event.$table.getCheckboxRecords();
|
||||
handleSelectChange('checkbox', data.selectedRows.value, event);
|
||||
}
|
||||
|
||||
// 行选择change事件
|
||||
function handleSelectChange(type, selectedRows, $event) {
|
||||
let action;
|
||||
if (type === 'radio') {
|
||||
action = 'selected';
|
||||
} else if (type === 'checkbox') {
|
||||
action = selectedRows.includes($event.row) ? 'selected' : 'unselected';
|
||||
} else {
|
||||
action = 'selected-all';
|
||||
}
|
||||
|
||||
data.selectedRowIds.value = selectedRows.map((row) => row.id);
|
||||
trigger('selectRowChange', {
|
||||
type: type,
|
||||
action: action,
|
||||
$event: $event,
|
||||
row: $event.row,
|
||||
selectedRows: data.selectedRows.value,
|
||||
selectedRowIds: data.selectedRowIds.value,
|
||||
});
|
||||
}
|
||||
|
||||
// 点击单元格时触发的事件
|
||||
function handleCellClick(event) {
|
||||
let { row, column, $event, $table } = event;
|
||||
|
||||
// 点击了可编辑的
|
||||
if (column.editRender) {
|
||||
refs.subPopoverRef.value?.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示详细信息
|
||||
if (column.params?.showDetails) {
|
||||
refs.detailsModalRef.value?.open(event);
|
||||
} else if (refs.subPopoverRef.value) {
|
||||
refs.subPopoverRef.value.toggle(event);
|
||||
} else if (props.clickSelectRow) {
|
||||
let className = $event.target.className || '';
|
||||
className = isString(className) ? className : className.toString();
|
||||
// 点击的是expand,不做处理
|
||||
if (className.includes('vxe-table--expand-btn')) {
|
||||
return;
|
||||
}
|
||||
// 点击的是checkbox,不做处理
|
||||
if (className.includes('vxe-checkbox--icon') || className.includes('vxe-cell--checkbox')) {
|
||||
return;
|
||||
}
|
||||
// 点击的是radio,不做处理
|
||||
if (className.includes('vxe-radio--icon') || className.includes('vxe-cell--radio')) {
|
||||
return;
|
||||
}
|
||||
if (props.rowSelectionType === 'radio') {
|
||||
$table.setRadioRow(row);
|
||||
handleVxeRadioChange(event);
|
||||
} else {
|
||||
$table.toggleCheckboxRow(row);
|
||||
handleVxeCheckboxChange(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单元格被激活编辑时会触发该事件
|
||||
function handleEditActived({ column }) {
|
||||
// 执行增强
|
||||
getEnhanced(column.params.type).aopEvents.editActived!.apply(instanceRef.value, arguments as any);
|
||||
}
|
||||
|
||||
// 单元格编辑状态下被关闭时会触发该事件
|
||||
function handleEditClosed({ column }) {
|
||||
// 执行增强
|
||||
getEnhanced(column.params.type).aopEvents.editClosed!.apply(instanceRef.value, arguments as any);
|
||||
}
|
||||
|
||||
// 返回值决定行是否可选中
|
||||
function handleCheckMethod({ row }) {
|
||||
if (props.disabled) {
|
||||
return false;
|
||||
}
|
||||
return !data.disabledRowIds.includes(row.id);
|
||||
}
|
||||
|
||||
// 返回值决定单元格是否可以编辑
|
||||
function handleActiveMethod({ row, column }) {
|
||||
let flag = (() => {
|
||||
if (props.disabled) {
|
||||
return false;
|
||||
}
|
||||
if (data.disabledRowIds.includes(row.id)) {
|
||||
return false;
|
||||
}
|
||||
if (column.params?.disabled) {
|
||||
return false;
|
||||
}
|
||||
// 执行增强
|
||||
return getEnhanced(column.params.type).aopEvents.activeMethod!.apply(instanceRef.value, arguments as any) ?? true;
|
||||
})();
|
||||
if (!flag) {
|
||||
getXTable().clearActived();
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是禁用行
|
||||
* @param row 行数据
|
||||
* @param rowIndex 行号
|
||||
* @param force 是否强制判断
|
||||
*/
|
||||
function isDisabledRow(row, rowIndex: number | boolean = -1, force = true) {
|
||||
if(typeof rowIndex === 'boolean'){
|
||||
force = rowIndex;
|
||||
rowIndex = -1;
|
||||
}
|
||||
if (!force) {
|
||||
return !data.disabledRowIds.includes(row.id);
|
||||
}
|
||||
if (props.disabledRows == null || isEmpty(props.disabledRows)) {
|
||||
return false;
|
||||
}
|
||||
let disabled: boolean = false;
|
||||
let keys: string[] = Object.keys(props.disabledRows);
|
||||
for (const key of keys) {
|
||||
// 判断是否有该属性
|
||||
if (row.hasOwnProperty(key)) {
|
||||
let value = row[key];
|
||||
let temp: any = props.disabledRows![key];
|
||||
// 禁用规则可以是一个函数
|
||||
if (typeof temp === 'function') {
|
||||
disabled = temp(value, row, rowIndex);
|
||||
} else if (isArray(temp)) {
|
||||
// 禁用规则可以是一个数组
|
||||
disabled = temp.includes(value);
|
||||
} else {
|
||||
// 禁用规则可以是一个具体值
|
||||
disabled = temp === value;
|
||||
}
|
||||
if (disabled) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return disabled;
|
||||
}
|
||||
|
||||
// 重新计算禁用行
|
||||
function recalcDisableRows() {
|
||||
let xTable = getXTable();
|
||||
data.disabledRowIds = [];
|
||||
const { tableFullData } = xTable.internalData;
|
||||
tableFullData.forEach((row, rowIndex) => {
|
||||
// 判断是否是禁用行
|
||||
if (isDisabledRow(row, rowIndex)) {
|
||||
data.disabledRowIds.push(row.id);
|
||||
}
|
||||
});
|
||||
xTable.updateData();
|
||||
}
|
||||
|
||||
// 监听 disabledRows,更改时重新计算禁用行
|
||||
watch(
|
||||
() => props.disabledRows,
|
||||
() => recalcDisableRows()
|
||||
);
|
||||
|
||||
// 返回值决定是否允许展开、收起行
|
||||
function handleExpandToggleMethod({ expanded }) {
|
||||
return !(expanded && props.disabled);
|
||||
}
|
||||
|
||||
// 设置 data.scrolling 防抖模式
|
||||
const closeScrolling = simpleDebounce(function () {
|
||||
data.scrolling.value = false;
|
||||
}, 100);
|
||||
|
||||
/** 表尾数据处理方法,用于显示统计信息 */
|
||||
function handleFooterMethod({ columns, data: $data }) {
|
||||
const { statistics } = data;
|
||||
let footers: any[] = [];
|
||||
if (statistics.has) {
|
||||
if (statistics.sum.length > 0) {
|
||||
footers.push(
|
||||
getFooterStatisticsMap({
|
||||
columns: columns,
|
||||
title: '合计',
|
||||
checks: statistics.sum,
|
||||
method: (column) => XEUtils.sum($data, column.property),
|
||||
})
|
||||
);
|
||||
}
|
||||
if (statistics.average.length > 0) {
|
||||
footers.push(
|
||||
getFooterStatisticsMap({
|
||||
columns: columns,
|
||||
title: '平均',
|
||||
checks: statistics.average,
|
||||
method: (column) => XEUtils.mean($data, column.property),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return footers;
|
||||
}
|
||||
|
||||
/** 获取底部统计Map */
|
||||
function getFooterStatisticsMap({ columns, title, checks, method }) {
|
||||
return columns.map((column, columnIndex) => {
|
||||
if (columnIndex === 0) {
|
||||
return title;
|
||||
}
|
||||
if (checks.includes(column.property)) {
|
||||
return method(column, columnIndex);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// 创建新行,自动添加默认值
|
||||
function createRow(record: Recordable = {}) {
|
||||
let xTable = getXTable();
|
||||
// 添加默认值
|
||||
xTable.internalData.tableFullColumn.forEach((column) => {
|
||||
let col = column.params;
|
||||
// 不能被注册的列不获取增强
|
||||
if (col && !excludeKeywords.includes(col.type)) {
|
||||
if (col.key && (record[col.key] == null || record[col.key] === '')) {
|
||||
// 设置默认值
|
||||
let createValue = getEnhanced(col.type).createValue;
|
||||
let defaultValue = col.defaultValue ?? '';
|
||||
let ctx = { context: { row: record, column, $table: xTable } };
|
||||
record[col.key] = createValue(defaultValue, ctx);
|
||||
}
|
||||
// 处理联动列
|
||||
if (col.type === JVxeTypes.select && data.innerLinkageConfig.size > 0) {
|
||||
// 判断当前列是否是联动列
|
||||
if (data.innerLinkageConfig.has(col.key)) {
|
||||
let configItem = data.innerLinkageConfig.get(col.key);
|
||||
linkageMethods.getLinkageOptionsAsync(configItem, '');
|
||||
}
|
||||
}
|
||||
} else if (col?.type === JVxeTypes.hidden) {
|
||||
record[col.key] = col.defaultValue ?? '';
|
||||
}
|
||||
});
|
||||
return record;
|
||||
}
|
||||
|
||||
async function addOrInsert(rows: Recordable | Recordable[] = {}, index, triggerName, options?: IAddRowsOptions) {
|
||||
let xTable = getXTable();
|
||||
let records;
|
||||
if (isArray(rows)) {
|
||||
records = rows;
|
||||
} else {
|
||||
records = [rows];
|
||||
}
|
||||
// 遍历添加默认值
|
||||
records.forEach((record) => createRow(record));
|
||||
let setActive = options?.setActive ?? props.addSetActive ?? true;
|
||||
let result = await pushRows(records, { index: index, setActive });
|
||||
// 遍历插入的行
|
||||
// online js增强时以传过来值为准,不再赋默认值
|
||||
if (!(options?.isOnlineJS ?? false)) {
|
||||
if (triggerName != null) {
|
||||
for (let i = 0; i < result.rows.length; i++) {
|
||||
let row = result.rows[i];
|
||||
trigger(triggerName, {
|
||||
row: row,
|
||||
rows: result.rows,
|
||||
insertIndex: index,
|
||||
$table: xTable,
|
||||
target: instanceRef.value,
|
||||
isModalData: options?.isModalData
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 新增、插入一行时的可选参数
|
||||
interface IAddRowsOptions {
|
||||
// 是否是 onlineJS增强 触发的
|
||||
isOnlineJS?: boolean;
|
||||
// 是否激活编辑状态
|
||||
setActive?: boolean;
|
||||
//是否需要触发change事件
|
||||
emitChange?:boolean
|
||||
// 是否是modal弹窗添加的数据
|
||||
isModalData?:boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一行或多行
|
||||
*
|
||||
* @param rows
|
||||
* @param options 参数
|
||||
* @return
|
||||
*/
|
||||
async function addRows(rows: Recordable | Recordable[] = {}, options?: IAddRowsOptions) {
|
||||
//update-begin-author:taoyan date:2022-8-12 for: VUEN-1892【online子表弹框】有主从关联js时,子表弹框修改了数据,主表字段未修改
|
||||
let result = await addOrInsert(rows, -1, 'added', options);
|
||||
if(options && options!.emitChange==true){
|
||||
trigger('valueChange', {column: 'all', row: result.row})
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-279】行编辑添加新字段滚动对应位置
|
||||
let xTable = getXTable();
|
||||
setTimeout(() => {
|
||||
xTable.scrollToRow(result.row);
|
||||
}, 0);
|
||||
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-279】行编辑添加新字段滚动对应位置
|
||||
return result;
|
||||
//update-end-author:taoyan date:2022-8-12 for: VUEN-1892【online子表弹框】有主从关联js时,子表弹框修改了数据,主表字段未修改
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一行或多行临时数据,不会填充默认值,传什么就添加进去什么
|
||||
* @param rows
|
||||
* @param options 选项
|
||||
* @param options.setActive 是否激活最后一行的编辑模式
|
||||
*/
|
||||
async function pushRows(rows: Recordable | Recordable[] = {}, options = { setActive: false, index: -1 }) {
|
||||
let xTable = getXTable();
|
||||
let { setActive, index } = options;
|
||||
index = index === -1 ? index : xTable.internalData.tableFullData[index];
|
||||
// 插入行
|
||||
let result = await xTable.insertAt(rows, index);
|
||||
if (setActive) {
|
||||
// 激活最后一行的编辑模式
|
||||
xTable.setActiveRow(result.rows[result.rows.length - 1]);
|
||||
}
|
||||
await recalcSortNumber();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入一行或多行临时数据
|
||||
*
|
||||
* @param rows
|
||||
* @param index 添加下标,数字,必填
|
||||
* @param options 参数
|
||||
* @return
|
||||
*/
|
||||
function insertRows(rows: Recordable | Recordable[] = {}, index: number, options?: IAddRowsOptions) {
|
||||
if (index < 0) {
|
||||
console.warn(`【JVxeTable】insertRows:index必须传递数字,且大于-1`);
|
||||
return;
|
||||
}
|
||||
return addOrInsert(rows, index, 'inserted', options);
|
||||
}
|
||||
|
||||
/** 获取表格表单里的值 */
|
||||
function getValues(callback, rowIds) {
|
||||
let tableData = getTableData({ rowIds: rowIds });
|
||||
callback('', tableData);
|
||||
}
|
||||
|
||||
/** 获取表格数据 */
|
||||
function getTableData(options: any = {}) {
|
||||
let { rowIds } = options;
|
||||
let tableData;
|
||||
// 仅查询指定id的行
|
||||
if (isArray(rowIds) && rowIds.length > 0) {
|
||||
tableData = [];
|
||||
rowIds.forEach((rowId) => {
|
||||
let { row } = getIfRowById(rowId);
|
||||
if (row) {
|
||||
tableData.push(row);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 查询所有行
|
||||
tableData = getXTable().getTableData().fullData;
|
||||
}
|
||||
return filterNewRows(tableData, false);
|
||||
}
|
||||
|
||||
/** 仅获取新增的数据 */
|
||||
function getNewData() {
|
||||
let newData = getNewDataWithId();
|
||||
newData.forEach((row) => delete row.id);
|
||||
return newData;
|
||||
}
|
||||
|
||||
/** 仅获取新增的数据,带有id */
|
||||
function getNewDataWithId() {
|
||||
let xTable = getXTable();
|
||||
return cloneDeep(xTable.getInsertRecords());
|
||||
}
|
||||
|
||||
/** 根据ID获取行,新增的行也能查出来 */
|
||||
function getIfRowById(id) {
|
||||
let xTable = getXTable();
|
||||
let row = xTable.getRowById(id),
|
||||
isNew = false;
|
||||
if (!row) {
|
||||
row = getNewRowById(id);
|
||||
if (!row) {
|
||||
console.warn(`JVxeTable.getIfRowById:没有找到id为"${id}"的行`);
|
||||
return { row: null };
|
||||
}
|
||||
isNew = true;
|
||||
}
|
||||
return { row, isNew };
|
||||
}
|
||||
|
||||
/** 通过临时ID获取新增的行 */
|
||||
function getNewRowById(id) {
|
||||
let records = getXTable().getInsertRecords();
|
||||
for (let record of records) {
|
||||
if (record.id === id) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤添加的行
|
||||
* @param rows 要筛选的行数据
|
||||
* @param remove true = 删除新增,false=只删除id
|
||||
* @param handler function
|
||||
*/
|
||||
function filterNewRows(rows, remove = true, handler?: Fn) {
|
||||
let insertRecords = getXTable().getInsertRecords();
|
||||
let records: Recordable[] = [];
|
||||
for (let row of rows) {
|
||||
let item = cloneDeep(row);
|
||||
if (insertRecords.includes(row)) {
|
||||
handler ? handler({ item, row, insertRecords }) : null;
|
||||
if (remove) {
|
||||
continue;
|
||||
}
|
||||
delete item.id;
|
||||
}
|
||||
records.push(item);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置滚动条Top位置
|
||||
* @param top 新top位置,留空则滚动到上次记录的位置,用于解决切换tab选项卡时导致白屏以及自动将滚动条滚动到顶部的问题
|
||||
*/
|
||||
function resetScrollTop(top?) {
|
||||
let xTable = getXTable();
|
||||
xTable.scrollTo(null, top == null || top === '' ? data.scroll.top : top);
|
||||
}
|
||||
|
||||
/** 校验table,失败返回errMap,成功返回null */
|
||||
async function validateTable(rows?) {
|
||||
let xTable = getXTable();
|
||||
const errMap = await xTable.validate(rows ?? true).catch((errMap) => errMap);
|
||||
return errMap ? errMap : null;
|
||||
}
|
||||
|
||||
/** 完整校验 */
|
||||
async function fullValidateTable(rows?) {
|
||||
let xTable = getXTable();
|
||||
const errMap = await xTable.fullValidate(rows ?? true).catch((errMap) => errMap);
|
||||
return errMap ? errMap : null;
|
||||
}
|
||||
|
||||
type setValuesParam = { rowKey: string; values: Recordable };
|
||||
|
||||
/**
|
||||
* 设置某行某列的值
|
||||
*
|
||||
* @param values
|
||||
* @return 返回受影响的单元格数量
|
||||
*/
|
||||
function setValues(values: setValuesParam[]): number {
|
||||
if (!isArray(values)) {
|
||||
console.warn(`[JVxeTable] setValues 必须传递数组`);
|
||||
return 0;
|
||||
}
|
||||
let xTable = getXTable();
|
||||
let count = 0;
|
||||
values.forEach((item) => {
|
||||
let { rowKey, values: record } = item;
|
||||
let { row } = getIfRowById(rowKey);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
Object.keys(record).forEach((colKey) => {
|
||||
let column = xTable.getColumnByField(colKey);
|
||||
if (column) {
|
||||
let oldValue = row[colKey];
|
||||
let newValue = record[colKey];
|
||||
if (newValue !== oldValue) {
|
||||
row[colKey] = newValue;
|
||||
// 触发 valueChange 事件
|
||||
trigger('valueChange', {
|
||||
type: column.params.type,
|
||||
value: newValue,
|
||||
oldValue: oldValue,
|
||||
col: column.params,
|
||||
column: column,
|
||||
isSetValues: true,
|
||||
row: {...row}
|
||||
});
|
||||
count++;
|
||||
}
|
||||
} else {
|
||||
console.warn(`[JVxeTable] setValues 没有找到key为"${colKey}"的列`);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (count > 0) {
|
||||
xTable.updateData();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** 清空选择行 */
|
||||
async function clearSelection() {
|
||||
const xTable = getXTable();
|
||||
let event = { $table: xTable, target: instanceRef.value };
|
||||
if (props.rowSelectionType === JVxeTypes.rowRadio) {
|
||||
await xTable.clearRadioRow();
|
||||
handleVxeRadioChange(event);
|
||||
} else {
|
||||
await xTable.clearCheckboxRow();
|
||||
handleVxeCheckboxChange(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中数据
|
||||
* @param isFull 如果 isFull=true 则获取全表已选中的数据
|
||||
*/
|
||||
function getSelectionData(isFull?: boolean) {
|
||||
const xTable = getXTable();
|
||||
if (props.rowSelectionType === JVxeTypes.rowRadio) {
|
||||
let row = xTable.getRadioRecord(isFull);
|
||||
if (isNull(row)) {
|
||||
return [];
|
||||
}
|
||||
return filterNewRows([row], false);
|
||||
} else {
|
||||
return filterNewRows(xTable.getCheckboxRecords(isFull), false);
|
||||
}
|
||||
}
|
||||
|
||||
/** 仅获取被删除的数据(新增又被删除的数据不会被获取到) */
|
||||
function getDeleteData() {
|
||||
return filterNewRows(getXTable().getRemoveRecords(), false);
|
||||
}
|
||||
|
||||
/** 删除一行或多行数据 */
|
||||
async function removeRows(rows, asyncRemove = false) {
|
||||
// update-begin--author:liaozhiyang---date:20231123---for:vxe-table removeRows方法加上异步删除
|
||||
const xTable = getXTable();
|
||||
const removeEvent: any = { deleteRows: rows, $table: xTable };
|
||||
if (asyncRemove) {
|
||||
const selectedRows = Array.isArray(rows) ? rows : [rows];
|
||||
const deleteOldRows = filterNewRows(selectedRows);
|
||||
if (deleteOldRows.length) {
|
||||
return new Promise((resolve) => {
|
||||
// 确认删除,只有调用这个方法才会真删除
|
||||
removeEvent.confirmRemove = async () => {
|
||||
const insertRecords = xTable.getInsertRecords();
|
||||
selectedRows.forEach((item) => {
|
||||
// 删除新添加的数据id
|
||||
if (insertRecords.includes(item)) {
|
||||
delete item.id;
|
||||
}
|
||||
});
|
||||
const res = await xTable.remove(rows);
|
||||
await recalcSortNumber();
|
||||
resolve(res);
|
||||
};
|
||||
trigger('removed', removeEvent);
|
||||
});
|
||||
} else {
|
||||
// 全新的行立马删除,不等待。
|
||||
const res = await xTable.remove(rows);
|
||||
removeEvent.confirmRemove = () => {};
|
||||
trigger('removed', removeEvent);
|
||||
await recalcSortNumber();
|
||||
return res;
|
||||
}
|
||||
} else {
|
||||
const res = await xTable.remove(rows);
|
||||
trigger('removed', removeEvent);
|
||||
await recalcSortNumber();
|
||||
return res;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231123---for:vxe-table removeRows方法加上异步删除
|
||||
}
|
||||
|
||||
/** 根据id删除一行或多行 */
|
||||
function removeRowsById(rowId) {
|
||||
let rowIds;
|
||||
if (isArray(rowId)) {
|
||||
rowIds = rowId;
|
||||
} else {
|
||||
rowIds = [rowId];
|
||||
}
|
||||
let rows = rowIds
|
||||
.map((id) => {
|
||||
let { row } = getIfRowById(id);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
if (row) {
|
||||
return row;
|
||||
} else {
|
||||
console.warn(`【JVxeTable】removeRowsById:${id}不存在`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((row) => row != null);
|
||||
return removeRows(rows);
|
||||
}
|
||||
|
||||
// 删除选中的数据
|
||||
async function removeSelection() {
|
||||
let xTable = getXTable();
|
||||
let res;
|
||||
if (props.rowSelectionType === JVxeTypes.rowRadio) {
|
||||
res = await xTable.removeRadioRow();
|
||||
} else {
|
||||
res = await xTable.removeCheckboxRow();
|
||||
}
|
||||
await clearSelection();
|
||||
await recalcSortNumber();
|
||||
return res;
|
||||
}
|
||||
|
||||
/** 重新计算排序字段的数值 */
|
||||
async function recalcSortNumber(force = false) {
|
||||
if (props.dragSort || force) {
|
||||
let xTable = getXTable();
|
||||
let sortKey = props.sortKey ?? 'orderNum';
|
||||
let sortBegin = props.sortBegin ?? 0;
|
||||
xTable.internalData.tableFullData.forEach((data) => (data[sortKey] = sortBegin++));
|
||||
// update-begin--author:liaozhiyang---date:20231011---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
// 4.1.0
|
||||
//await xTable.updateCache();
|
||||
// 4.1.1
|
||||
await xTable.cacheRowMap()
|
||||
// update-end--author:liaozhiyang---date:20231011---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
return await xTable.updateData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序表格
|
||||
* @param oldIndex
|
||||
* @param newIndex
|
||||
* @param force 强制排序
|
||||
*/
|
||||
async function doSort(oldIndex: number, newIndex: number, force = false) {
|
||||
if (props.dragSort || force) {
|
||||
let xTable = getXTable();
|
||||
let sort = (array) => {
|
||||
// 存储old数据,并删除该项
|
||||
let row = array.splice(oldIndex, 1)[0];
|
||||
// 向newIndex处添加old数据
|
||||
array.splice(newIndex, 0, row);
|
||||
};
|
||||
sort(xTable.internalData.tableFullData);
|
||||
if (xTable.keepSource) {
|
||||
sort(xTable.internalData.tableSourceData);
|
||||
}
|
||||
return await recalcSortNumber(force);
|
||||
}
|
||||
}
|
||||
|
||||
/** 行重新排序 */
|
||||
function rowResort(oldIndex: number, newIndex: number) {
|
||||
return doSort(oldIndex, newIndex, true);
|
||||
}
|
||||
|
||||
// ---------------- begin 权限控制 ----------------
|
||||
// 加载权限
|
||||
function loadAuthsMap() {
|
||||
if (!props.authPre || props.authPre.length == 0) {
|
||||
data.authsMap.value = null;
|
||||
} else {
|
||||
data.authsMap.value = getJVxeAuths(props.authPre);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 权限code 获取权限
|
||||
* @param authCode
|
||||
*/
|
||||
function getAuth(authCode) {
|
||||
if (data.authsMap.value != null && props.authPre) {
|
||||
let prefix = getPrefix(props.authPre);
|
||||
return data.authsMap.value.get(prefix + authCode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取列权限
|
||||
function getColAuth(key: string) {
|
||||
return getAuth(key);
|
||||
}
|
||||
|
||||
// 判断按钮权限
|
||||
function hasBtnAuth(key: string) {
|
||||
return getAuth('btn:' + key)?.isAuth ?? true;
|
||||
}
|
||||
|
||||
// ---------------- end 权限控制 ----------------
|
||||
|
||||
/* --- 辅助方法 ---*/
|
||||
|
||||
function created() {
|
||||
loadAuthsMap();
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
function trigger(name, event: any = {}) {
|
||||
event.$target = instanceRef.value;
|
||||
event.$table = getXTable();
|
||||
//online增强参数兼容
|
||||
event.target = instanceRef.value;
|
||||
emit(name, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中的行-和 getSelectionData 区别在于对于新增的行也会返回ID
|
||||
* 用于onlinePopForm
|
||||
* @param isFull
|
||||
*/
|
||||
function getSelectedData(isFull?: boolean) {
|
||||
const xTable = getXTable();
|
||||
let rows:any[] = []
|
||||
if (props.rowSelectionType === JVxeTypes.rowRadio) {
|
||||
let row = xTable.getRadioRecord(isFull);
|
||||
if (isNull(row)) {
|
||||
return [];
|
||||
}
|
||||
rows = [row]
|
||||
} else {
|
||||
rows = xTable.getCheckboxRecords(isFull)
|
||||
}
|
||||
let records: Recordable[] = [];
|
||||
for (let row of rows) {
|
||||
let item = cloneDeep(row);
|
||||
records.push(item);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
/**
|
||||
* 2024-03-21
|
||||
* liaozhiyang
|
||||
* VXETable列设置保存缓存字段名
|
||||
* */
|
||||
function handleCustom({ type, $grid }) {
|
||||
const { saveSetting, resetSetting } = useColumnsCache({ cacheColumnsKey: props.cacheColumnsKey });
|
||||
if (type === 'confirm') {
|
||||
saveSetting($grid);
|
||||
} else if (type == 'reset') {
|
||||
resetSetting($grid);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
methods: {
|
||||
trigger,
|
||||
...publicMethods,
|
||||
closeScrolling,
|
||||
doSort,
|
||||
recalcSortNumber,
|
||||
handleVxeScroll,
|
||||
handleVxeRadioChange,
|
||||
handleVxeCheckboxAll,
|
||||
handleVxeCheckboxChange,
|
||||
handleFooterMethod,
|
||||
handleCellClick,
|
||||
handleEditActived,
|
||||
handleEditClosed,
|
||||
handleCheckMethod,
|
||||
handleActiveMethod,
|
||||
handleExpandToggleMethod,
|
||||
getColAuth,
|
||||
hasBtnAuth,
|
||||
handleCustom,
|
||||
},
|
||||
publicMethods,
|
||||
created,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import { computed, reactive, h } from 'vue';
|
||||
import { JVxeTableMethods, JVxeTableProps } from '/@/components/jeecg/JVxeTable/src/types';
|
||||
import { isEmpty } from '/@/utils/is';
|
||||
import { Pagination } from 'ant-design-vue';
|
||||
|
||||
export function usePagination(props: JVxeTableProps, methods: JVxeTableMethods) {
|
||||
const innerPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: ['10', '20', '30'],
|
||||
showTotal: (total, range) => {
|
||||
return range[0] + '-' + range[1] + ' 共 ' + total + ' 条';
|
||||
},
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
total: 100,
|
||||
});
|
||||
|
||||
const bindProps = computed(() => {
|
||||
return {
|
||||
...innerPagination,
|
||||
...props.pagination,
|
||||
size: props.size === 'tiny' ? 'small' : '',
|
||||
};
|
||||
});
|
||||
|
||||
const boxClass = computed(() => {
|
||||
return {
|
||||
'j-vxe-pagination': true,
|
||||
'show-quick-jumper': !!bindProps.value.showQuickJumper,
|
||||
};
|
||||
});
|
||||
|
||||
function handleChange(current, pageSize) {
|
||||
innerPagination.current = current;
|
||||
methods.trigger('pageChange', { current, pageSize });
|
||||
}
|
||||
|
||||
function handleShowSizeChange(current, pageSize) {
|
||||
innerPagination.pageSize = pageSize;
|
||||
methods.trigger('pageChange', { current, pageSize });
|
||||
}
|
||||
|
||||
/** 渲染分页器 */
|
||||
function renderPagination() {
|
||||
if (props.pagination && !isEmpty(props.pagination)) {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: boxClass.value,
|
||||
},
|
||||
[
|
||||
h(Pagination, {
|
||||
...bindProps.value,
|
||||
disabled: props.disabled,
|
||||
onChange: handleChange,
|
||||
onShowSizeChange: handleShowSizeChange,
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return { renderPagination };
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
import { h } from 'vue';
|
||||
import { JVxeDataProps, JVxeTableMethods, JVxeTableProps } from '../types';
|
||||
import JVxeSubPopover from '../components/JVxeSubPopover.vue';
|
||||
import JVxeDetailsModal from '../components/JVxeDetailsModal.vue';
|
||||
import { useToolbar } from '/@/components/jeecg/JVxeTable/src/hooks/useToolbar';
|
||||
import { usePagination } from '/@/components/jeecg/JVxeTable/src/hooks/usePagination';
|
||||
|
||||
export function useRenderComponents(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods, slots) {
|
||||
// 渲染 toolbar
|
||||
const { renderToolbar } = useToolbar(props, data, methods, slots);
|
||||
// 渲染分页器
|
||||
const { renderPagination } = usePagination(props, methods);
|
||||
|
||||
// 渲染 toolbarAfter 插槽
|
||||
function renderToolbarAfterSlot() {
|
||||
if (slots['toolbarAfter']) {
|
||||
return slots['toolbarAfter']();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 渲染点击时弹出的子表
|
||||
function renderSubPopover() {
|
||||
if (props.clickRowShowSubForm && slots.subForm) {
|
||||
return h(
|
||||
JVxeSubPopover,
|
||||
{
|
||||
ref: 'subPopoverRef',
|
||||
},
|
||||
{
|
||||
subForm: slots.subForm,
|
||||
}
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 渲染点击时弹出的详细信息
|
||||
function renderDetailsModal() {
|
||||
if (props.clickRowShowMainForm && slots.mainForm) {
|
||||
return h(
|
||||
JVxeDetailsModal,
|
||||
{
|
||||
ref: 'detailsModalRef',
|
||||
trigger: methods.trigger,
|
||||
},
|
||||
{
|
||||
mainForm: slots.mainForm,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
renderToolbar,
|
||||
renderPagination,
|
||||
renderSubPopover,
|
||||
renderDetailsModal,
|
||||
renderToolbarAfterSlot,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import { h } from 'vue';
|
||||
import JVxeToolbar from '../components/JVxeToolbar.vue';
|
||||
import { JVxeDataProps, JVxeTableMethods, JVxeTableProps } from '../types';
|
||||
|
||||
export function useToolbar(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods, $slots) {
|
||||
/** 渲染工具栏 */
|
||||
function renderToolbar() {
|
||||
if (props.toolbar) {
|
||||
return h(
|
||||
JVxeToolbar,
|
||||
{
|
||||
size: props.size,
|
||||
disabled: props.disabled,
|
||||
toolbarConfig: props.toolbarConfig,
|
||||
disabledRows: props.disabledRows,
|
||||
hasBtnAuth: methods.hasBtnAuth,
|
||||
selectedRowIds: data.selectedRowIds.value,
|
||||
custom: props.custom,
|
||||
// 新增事件
|
||||
onAdd: () => {
|
||||
// update-begin--author:liaozhiyang---date:20240521---for:【TV360X-212】online新增字段就出校验提示
|
||||
setTimeout(() => {
|
||||
methods.addRows();
|
||||
}, 0);
|
||||
// update-end--author:liaozhiyang---date:20240521---for:【TV360X-212】online新增字段就出校验提示
|
||||
},
|
||||
// 保存事件
|
||||
onSave: () => methods.trigger('save'),
|
||||
onRemove() {
|
||||
const $table = methods.getXTable();
|
||||
// update-begin--author:liaozhiyang---date:20231018---for:【QQYUN-6805】修复asyncRemove字段不生效
|
||||
// 触发删除事件
|
||||
if (data.selectedRows.value.length > 0) {
|
||||
const deleteOldRows = methods.filterNewRows(data.selectedRows.value);
|
||||
const removeEvent: any = { deleteRows: data.selectedRows.value, $table };
|
||||
const insertRecords = $table.getInsertRecords();
|
||||
if (props.asyncRemove && deleteOldRows.length) {
|
||||
data.selectedRows.value.forEach((item) => {
|
||||
// 删除新添加的数据id
|
||||
if (insertRecords.includes(item)) {
|
||||
delete item.id;
|
||||
}
|
||||
});
|
||||
// 确认删除,只有调用这个方法才会真删除
|
||||
removeEvent.confirmRemove = () => methods.removeSelection();
|
||||
} else {
|
||||
if (props.asyncRemove) {
|
||||
// asyncRemove删除的只有新增的数据时,防止调用confirmRemove报错
|
||||
removeEvent.confirmRemove = () => {};
|
||||
}
|
||||
methods.removeSelection();
|
||||
}
|
||||
methods.trigger('removed', removeEvent);
|
||||
} else {
|
||||
methods.removeSelection();
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231018---for:【QQYUN-6805】修复asyncRemove字段不生效
|
||||
},
|
||||
// 清除选择事件
|
||||
onClearSelection: () => methods.clearSelection(),
|
||||
onRegister: ({ xToolbarRef }) => methods.getXTable().connect(xToolbarRef.value),
|
||||
},
|
||||
{
|
||||
toolbarPrefix: $slots.toolbarPrefix,
|
||||
toolbarSuffix: $slots.toolbarSuffix,
|
||||
}
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return { renderToolbar };
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
import { VxeTablePropTypes } from 'vxe-table';
|
||||
import { isArray } from '/@/utils/is';
|
||||
import { HandleArgs } from './useColumns';
|
||||
import { replaceProps } from '../utils/enhancedUtils';
|
||||
|
||||
export function useValidateRules(args: HandleArgs) {
|
||||
const { data } = args;
|
||||
const col = args.col!;
|
||||
let rules: VxeTablePropTypes.EditRules[] = [];
|
||||
if (isArray(col.validateRules)) {
|
||||
for (let rule of col.validateRules) {
|
||||
let replace = {
|
||||
message: replaceProps(col, rule.message),
|
||||
};
|
||||
if (rule.unique || rule.pattern === 'only') {
|
||||
// 唯一校验器
|
||||
rule.validator = uniqueValidator(args);
|
||||
} else if (rule.pattern) {
|
||||
// 非空
|
||||
if (rule.pattern === fooPatterns[0].value) {
|
||||
rule.required = true;
|
||||
delete rule.pattern;
|
||||
} else {
|
||||
// 兼容Online表单的特殊规则
|
||||
for (let foo of fooPatterns) {
|
||||
if (foo.value === rule.pattern) {
|
||||
rule.pattern = foo.pattern;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof rule.handler === 'function') {
|
||||
// 自定义函数校验
|
||||
rule.validator = handlerConvertToValidator;
|
||||
}
|
||||
rules.push(Object.assign({}, rule, replace));
|
||||
}
|
||||
}
|
||||
data.innerEditRules[col.key] = rules;
|
||||
}
|
||||
|
||||
/** 唯一校验器 */
|
||||
function uniqueValidator({ methods }: HandleArgs) {
|
||||
return function (event) {
|
||||
const { cellValue, column, rule } = event;
|
||||
// update-begin--author:liaozhiyang---date:20240522---for:【TV360X-299】JVxetable组件中唯一校验过滤掉空字符串
|
||||
if (cellValue == '') return Promise.resolve();
|
||||
// update-end--author:liaozhiyang---date:20240522---for:【TV360X-299】JVxetable组件中唯一校验过滤掉空字符串
|
||||
let tableData = methods.getTableData();
|
||||
let findCount = 0;
|
||||
for (let rowData of tableData) {
|
||||
if (rowData[column.params.key] === cellValue) {
|
||||
if (++findCount >= 2) {
|
||||
return Promise.reject(new Error(rule.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
/** 旧版handler转为新版Validator */
|
||||
function handlerConvertToValidator(event) {
|
||||
const { column, rule } = event;
|
||||
return new Promise((resolve, reject) => {
|
||||
rule.handler(event, (flag, msg) => {
|
||||
let message = rule.message;
|
||||
if (typeof msg === 'string') {
|
||||
message = replaceProps(column.params, msg);
|
||||
}
|
||||
if (flag == null) {
|
||||
resolve(message);
|
||||
} else if (!!flag) {
|
||||
resolve(message);
|
||||
} else {
|
||||
reject(new Error(message));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 兼容 online 的规则
|
||||
const fooPatterns = [
|
||||
{ title: '非空', value: '*', pattern: /^.+$/ },
|
||||
{ title: '6到16位数字', value: 'n6-16', pattern: /^\d{6,16}$/ },
|
||||
{ title: '6到16位任意字符', value: '*6-16', pattern: /^.{6,16}$/ },
|
||||
{ title: '6到18位字母', value: 's6-18', pattern: /^[a-z|A-Z]{6,18}$/ },
|
||||
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1160 对多子表,网址校验不正确
|
||||
{
|
||||
title: '网址',
|
||||
value: 'url',
|
||||
pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/,
|
||||
},
|
||||
//update-end-author:taoyan date:2022-6-1 for: VUEN-1160 对多子表,网址校验不正确
|
||||
// update-begin--author:liaozhiyang---date:20240527---for:【TV360X-466】邮箱跟一对第一校验规则一致
|
||||
{ title: '电子邮件', value: 'e', pattern: /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ },
|
||||
// update-end--author:liaozhiyang---date:20240527---for:【TV360X-466】邮箱跟一对第一校验规则一致
|
||||
{ title: '手机号码', value: 'm', pattern: /^1[3456789]\d{9}$/ },
|
||||
{ title: '邮政编码', value: 'p', pattern: /^\d{6}$/ },
|
||||
{ title: '字母', value: 's', pattern: /^[A-Z|a-z]+$/ },
|
||||
{ title: '数字', value: 'n', pattern: /^-?\d+(\.?\d+|\d?)$/ },
|
||||
{ title: '整数', value: 'z', pattern: /^-?\d+$/ },
|
||||
{
|
||||
title: '金额',
|
||||
value: 'money',
|
||||
pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,5}))$/,
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,236 @@
|
||||
import { watch, onUnmounted } from 'vue';
|
||||
import { buildUUID } from '/@/utils/uuid';
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { JVxeDataProps, JVxeTableMethods, JVxeTableProps } from '../types';
|
||||
import { isArray } from '/@/utils/is';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
|
||||
// vxe socket
|
||||
const vs = {
|
||||
// 页面唯一 id,用于标识同一用户,不同页面的websocket
|
||||
pageId: buildUUID(),
|
||||
// webSocket 对象
|
||||
ws: null,
|
||||
// 一些常量
|
||||
constants: {
|
||||
// 消息类型
|
||||
TYPE: 'type',
|
||||
// 消息数据
|
||||
DATA: 'data',
|
||||
// 消息类型:心跳检测
|
||||
TYPE_HB: 'heart_beat',
|
||||
// 消息类型:更新vxe table数据
|
||||
TYPE_UVT: 'update_vxe_table',
|
||||
},
|
||||
// 心跳检测
|
||||
heartCheck: {
|
||||
// 间隔时间,间隔多久发送一次心跳消息
|
||||
interval: 10000,
|
||||
// 心跳消息超时时间,心跳消息多久没有回复后重连
|
||||
timeout: 6000,
|
||||
timeoutTimer: -1,
|
||||
clear() {
|
||||
clearTimeout(this.timeoutTimer);
|
||||
return this;
|
||||
},
|
||||
start() {
|
||||
vs.sendMessage(vs.constants.TYPE_HB, '');
|
||||
// 如果超过一定时间还没重置,说明后端主动断开了
|
||||
this.timeoutTimer = window.setTimeout(() => {
|
||||
vs.reconnect();
|
||||
}, this.timeout);
|
||||
return this;
|
||||
},
|
||||
// 心跳消息返回
|
||||
back() {
|
||||
this.clear();
|
||||
window.setTimeout(() => this.start(), this.interval);
|
||||
},
|
||||
},
|
||||
|
||||
/** 初始化 WebSocket */
|
||||
initialWebSocket() {
|
||||
if (this.ws === null) {
|
||||
const userId = useUserStore().getUserInfo?.id;
|
||||
const domainURL = useGlobSetting().uploadUrl!;
|
||||
const domain = domainURL.replace('https://', 'wss://').replace('http://', 'ws://');
|
||||
const url = `${domain}/vxeSocket/${userId}/${this.pageId}`;
|
||||
//update-begin-author:taoyan date:2022-4-24 for: v2.4.6 的 websocket 服务端,存在性能和安全问题。 #3278
|
||||
let token = (getToken() || '') as string;
|
||||
this.ws = new WebSocket(url, [token]);
|
||||
//update-end-author:taoyan date:2022-4-24 for: v2.4.6 的 websocket 服务端,存在性能和安全问题。 #3278
|
||||
this.ws.onopen = this.on.open.bind(this);
|
||||
this.ws.onerror = this.on.error.bind(this);
|
||||
this.ws.onmessage = this.on.message.bind(this);
|
||||
this.ws.onclose = this.on.close.bind(this);
|
||||
}
|
||||
},
|
||||
|
||||
// 发送消息
|
||||
sendMessage(type, message) {
|
||||
try {
|
||||
let ws = this.ws;
|
||||
if (ws != null && ws.readyState === ws.OPEN) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: type,
|
||||
data: message,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn('【JVxeWebSocket】发送消息失败:(' + err.code + ')');
|
||||
}
|
||||
},
|
||||
|
||||
/** 绑定全局VXE表格 */
|
||||
tableMap: new Map(),
|
||||
/** 添加绑定 */
|
||||
addBind(map, key, value: VmArgs) {
|
||||
let binds = map.get(key);
|
||||
if (isArray(binds)) {
|
||||
binds.push(value);
|
||||
} else {
|
||||
map.set(key, [value]);
|
||||
}
|
||||
},
|
||||
/** 移除绑定 */
|
||||
removeBind(map, key, value: VmArgs) {
|
||||
let binds = map.get(key);
|
||||
if (isArray(binds)) {
|
||||
for (let i = 0; i < binds.length; i++) {
|
||||
let bind = binds[i];
|
||||
if (bind === value) {
|
||||
binds.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (binds.length === 0) {
|
||||
map.delete(key);
|
||||
}
|
||||
} else {
|
||||
map.delete(key);
|
||||
}
|
||||
},
|
||||
// 呼叫绑定的表单
|
||||
callBind(map, key, callback) {
|
||||
let binds = map.get(key);
|
||||
if (isArray(binds)) {
|
||||
binds.forEach(callback);
|
||||
}
|
||||
},
|
||||
|
||||
lockReconnect: false,
|
||||
/** 尝试重连 */
|
||||
reconnect() {
|
||||
if (this.lockReconnect) return;
|
||||
this.lockReconnect = true;
|
||||
setTimeout(() => {
|
||||
if (this.ws && this.ws.close) {
|
||||
this.ws.close();
|
||||
}
|
||||
this.ws = null;
|
||||
console.info('【JVxeWebSocket】尝试重连...');
|
||||
this.initialWebSocket();
|
||||
this.lockReconnect = false;
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
on: {
|
||||
open() {
|
||||
console.info('【JVxeWebSocket】连接成功');
|
||||
this.heartCheck.start();
|
||||
},
|
||||
error(e) {
|
||||
console.warn('【JVxeWebSocket】连接发生错误:', e);
|
||||
this.reconnect();
|
||||
},
|
||||
message(e) {
|
||||
// 解析消息
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(e.data);
|
||||
} catch (e: any) {
|
||||
console.warn('【JVxeWebSocket】收到无法解析的消息:', e.data);
|
||||
return;
|
||||
}
|
||||
let type = json[this.constants.TYPE];
|
||||
let data = json[this.constants.DATA];
|
||||
switch (type) {
|
||||
// 心跳检测
|
||||
case this.constants.TYPE_HB:
|
||||
this.heartCheck.back();
|
||||
break;
|
||||
// 更新form数据
|
||||
case this.constants.TYPE_UVT:
|
||||
this.callBind(this.tableMap, data.socketKey, (args) => this.onVM.onUpdateTable(args, ...data.args));
|
||||
break;
|
||||
default:
|
||||
console.warn('【JVxeWebSocket】收到不识别的消息类型:' + type);
|
||||
break;
|
||||
}
|
||||
},
|
||||
close(e) {
|
||||
console.info('【JVxeWebSocket】连接被关闭:', e);
|
||||
this.reconnect();
|
||||
},
|
||||
},
|
||||
|
||||
onVM: {
|
||||
/** 收到更新表格的消息 */
|
||||
onUpdateTable({ props, data, methods }: VmArgs, row, caseId) {
|
||||
if (data.caseId !== caseId) {
|
||||
const tableRow = methods.getIfRowById(row.id).row;
|
||||
// 局部保更新数据
|
||||
if (tableRow) {
|
||||
if (props.reloadEffect) {
|
||||
data.reloadEffectRowKeysMap[row.id] = true;
|
||||
}
|
||||
Object.assign(tableRow, row, { id: tableRow.id });
|
||||
methods.getXTable().reloadRow(tableRow);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
} as {
|
||||
ws: Nullable<WebSocket>;
|
||||
} & Recordable;
|
||||
|
||||
type VmArgs = {
|
||||
props: JVxeTableProps;
|
||||
data: JVxeDataProps;
|
||||
methods: JVxeTableMethods;
|
||||
};
|
||||
|
||||
export function useWebSocket(props: JVxeTableProps, data: JVxeDataProps, methods) {
|
||||
const args: VmArgs = { props, data, methods };
|
||||
watch(
|
||||
() => props.socketReload,
|
||||
(socketReload: boolean) => {
|
||||
if (socketReload) {
|
||||
vs.initialWebSocket();
|
||||
vs.addBind(vs.tableMap, props.socketKey, args);
|
||||
} else {
|
||||
vs.removeBind(vs.tableMap, props.socketKey, args);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/** 发送socket消息更新行 */
|
||||
function socketSendUpdateRow(row) {
|
||||
vs.sendMessage(vs.constants.TYPE_UVT, {
|
||||
socketKey: props.socketKey,
|
||||
args: [row, data.caseId],
|
||||
});
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
vs.removeBind(vs.tableMap, props.socketKey, args);
|
||||
});
|
||||
|
||||
return {
|
||||
socketSendUpdateRow,
|
||||
};
|
||||
}
|
||||
75
jeecgboot-vue3/src/components/jeecg/JVxeTable/src/install.ts
Normal file
75
jeecgboot-vue3/src/components/jeecg/JVxeTable/src/install.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import type { App } from 'vue';
|
||||
// 引入 vxe-table
|
||||
import 'xe-utils';
|
||||
import VXETable /*Grid*/ from 'vxe-table';
|
||||
import VXETablePluginAntd from 'vxe-table-plugin-antd';
|
||||
import 'vxe-table/lib/style.css';
|
||||
|
||||
import JVxeTable from './JVxeTable';
|
||||
import { getEventPath } from '/@/utils/common/compUtils';
|
||||
import { registerAllComponent } from './utils/registerUtils';
|
||||
import { getEnhanced } from './utils/enhancedUtils';
|
||||
|
||||
export function registerJVxeTable(app: App) {
|
||||
// VXETable 全局配置
|
||||
const VXETableSettings = {
|
||||
// z-index 起始值
|
||||
zIndex: 1000,
|
||||
table: {},
|
||||
};
|
||||
|
||||
// 添加事件拦截器 event.clearActived
|
||||
// 比如点击了某个组件的弹出层面板之后,此时被激活单元格不应该被自动关闭,通过返回 false 可以阻止默认的行为。
|
||||
VXETable.interceptor.add('event.clearActived', preventClosingPopUp);
|
||||
VXETable.interceptor.add('event.clearEdit', preventClosingPopUp);
|
||||
// 注册插件
|
||||
VXETable.use(VXETablePluginAntd);
|
||||
// 注册自定义组件
|
||||
registerAllComponent();
|
||||
// 执行注册方法
|
||||
app.use(VXETable, VXETableSettings);
|
||||
app.component('JVxeTable', JVxeTable);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 阻止行编辑中关闭弹窗
|
||||
* @param params
|
||||
*/
|
||||
function preventClosingPopUp(this: any, params) {
|
||||
// 获取组件增强
|
||||
let col = params.column.params;
|
||||
let { $event } = params;
|
||||
const interceptor = getEnhanced(col.type).interceptor;
|
||||
// 执行增强
|
||||
let flag = interceptor['event.clearActived']?.call(this, ...arguments);
|
||||
if (flag === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let path = getEventPath($event);
|
||||
for (let p of path) {
|
||||
let className: any = p.className || '';
|
||||
className = typeof className === 'string' ? className : className.toString();
|
||||
|
||||
/* --- 特殊处理以下组件,点击以下标签时不清空编辑状态 --- */
|
||||
|
||||
// 点击的标签是JInputPop
|
||||
if (className.includes('j-input-pop')) {
|
||||
return false;
|
||||
}
|
||||
// 点击的标签是JPopup的弹出层、部门选择、用户选择
|
||||
if (className.includes('j-popup-modal') || className.includes('j-depart-select-modal') || className.includes('j-user-select-modal')) {
|
||||
return false;
|
||||
}
|
||||
// 点击的是日期选择器
|
||||
if (className.includes('j-vxe-date-picker')) {
|
||||
return false;
|
||||
}
|
||||
// 执行增强
|
||||
let flag = interceptor['event.clearActived.className']?.call(this, className, ...arguments);
|
||||
if (flag === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
@import 'vxe.const';
|
||||
@import 'vxe.dark';
|
||||
|
||||
.@{prefix-cls} {
|
||||
// 编辑按钮样式
|
||||
.vxe-cell--edit-icon {
|
||||
border-color: #606266;
|
||||
}
|
||||
|
||||
.sort--active {
|
||||
border-color: @primary-color;
|
||||
}
|
||||
|
||||
// toolbar 样式
|
||||
&-toolbar {
|
||||
&-collapsed {
|
||||
[data-collapse] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-button.div .ant-btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 分页器
|
||||
.j-vxe-pagination {
|
||||
margin-top: 8px;
|
||||
text-align: right;
|
||||
|
||||
.ant-pagination-options-size-changer.ant-select {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.show-quick-jumper {
|
||||
.ant-pagination-options-size-changer.ant-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更改 header 底色
|
||||
.vxe-table.border--default .vxe-table--header-wrapper,
|
||||
.vxe-table.border--full .vxe-table--header-wrapper,
|
||||
.vxe-table.border--outer .vxe-table--header-wrapper {
|
||||
//background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
// 更改 tooltip 校验失败的颜色
|
||||
.vxe-table--tooltip-wrapper.vxe-table--valid-error {
|
||||
background-color: #f5222d !important;
|
||||
}
|
||||
|
||||
// 更改 输入框 校验失败的颜色
|
||||
.col--valid-error > .vxe-cell > .ant-input,
|
||||
.col--valid-error > .vxe-cell > .ant-select .ant-input,
|
||||
.col--valid-error > .vxe-cell > .ant-select .ant-select-selection,
|
||||
.col--valid-error > .vxe-cell > .ant-input-number,
|
||||
.col--valid-error > .vxe-cell > .ant-cascader-picker .ant-cascader-input,
|
||||
.col--valid-error > .vxe-cell > .ant-calendar-picker .ant-calendar-picker-input,
|
||||
.col--valid-error > .vxe-tree-cell > .ant-input,
|
||||
.col--valid-error > .vxe-tree-cell > .ant-select .ant-input,
|
||||
.col--valid-error > .vxe-tree-cell > .ant-select .ant-select-selection,
|
||||
.col--valid-error > .vxe-tree-cell > .ant-input-number,
|
||||
.col--valid-error > .vxe-tree-cell > .ant-cascader-picker .ant-cascader-input,
|
||||
.col--valid-error > .vxe-tree-cell > .ant-calendar-picker .ant-calendar-picker-input {
|
||||
border-color: #f5222d !important;
|
||||
}
|
||||
|
||||
.vxe-body--row.sortable-ghost,
|
||||
.vxe-body--row.sortable-chosen {
|
||||
background-color: #dfecfb;
|
||||
}
|
||||
|
||||
// ----------- 【VUEN-1691】默认隐藏滚动条,鼠标放上去才显示 -------------------------------------------
|
||||
.vxe-table {
|
||||
//.vxe-table--footer-wrapper.body--wrapper,
|
||||
.vxe-table--body-wrapper.body--wrapper {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//.vxe-table--footer-wrapper.body--wrapper,
|
||||
.vxe-table--body-wrapper.body--wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ----------- 【VUEN-1691】默认隐藏滚动条,鼠标放上去才显示 -------------------------------------------
|
||||
|
||||
// 调整展开/收起图标样式
|
||||
.vxe-table--render-default .vxe-table--expanded .vxe-table--expand-btn {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
/*【美化表单】行编辑table的title字体改小一号*/
|
||||
.vxe-header--column.col--ellipsis>.vxe-cell .vxe-cell--title{
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
.j-vxe-reload-effect-box {
|
||||
&,
|
||||
.j-vxe-reload-effect-span {
|
||||
display: inline;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.j-vxe-reload-effect-span {
|
||||
&.layer-top {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
background-color: white;
|
||||
|
||||
transform-origin: 0 0;
|
||||
animation: reload-effect 1.5s forwards;
|
||||
}
|
||||
|
||||
&.layer-bottom {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 定义动画
|
||||
@keyframes reload-effect {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: rotateX(0);
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-j-vxe-table';
|
||||
@ -0,0 +1,124 @@
|
||||
@import 'vxe.const';
|
||||
// update-begin--author:liaozhiyang---date:20240313---for:【QQYUN-8493】修正暗黑模式online表单Erp和编辑页面显示不正确
|
||||
html[data-theme='dark'] {
|
||||
--vxe-table-body-background-color: #151515;
|
||||
--vxe-table-footer-background-color: #151515;
|
||||
--vxe-table-border-color: #606060;
|
||||
--vxe-table-popup-border-color:#606060;
|
||||
--vxe-table-row-hover-background-color:#1e1e1e;
|
||||
--vxe-input-border-color: #606266;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240313---for:【QQYUN-8493】修正暗黑模式online表单Erp和编辑页面显示不正确
|
||||
[data-theme='dark'] .@{prefix-cls} {
|
||||
@fontColor: #c9d1d9;
|
||||
@bgColor: #151515;
|
||||
@borderColor: #606060;
|
||||
|
||||
.vxe-cell--item,
|
||||
.vxe-cell--title,
|
||||
.vxe-cell,
|
||||
.vxe-body--expanded-cell {
|
||||
color: @fontColor;
|
||||
}
|
||||
|
||||
.vxe-toolbar {
|
||||
// update-begin--author:liaozhiyang---date:20240313---for:【QQYUN-8493】修正暗黑模式online表单Erp和编辑页面显示不正确
|
||||
background-color: #1f1f1f;
|
||||
// update-end--author:liaozhiyang---date:20240313---for:【QQYUN-8493】修正暗黑模式online表单Erp和编辑页面显示不正确
|
||||
}
|
||||
|
||||
.vxe-table--render-default .vxe-table--body-wrapper,
|
||||
.vxe-table--render-default .vxe-table--footer-wrapper {
|
||||
background-color: @bgColor;
|
||||
}
|
||||
|
||||
// 外边框
|
||||
.vxe-table--render-default .vxe-table--border-line {
|
||||
border-color: @borderColor;
|
||||
}
|
||||
|
||||
// header 下边框
|
||||
.vxe-table .vxe-table--header-wrapper .vxe-table--header-border-line {
|
||||
border-bottom-color: @borderColor;
|
||||
}
|
||||
|
||||
// footer 上边框
|
||||
.vxe-table--render-default .vxe-table--footer-wrapper {
|
||||
border-top-color: @borderColor;
|
||||
}
|
||||
|
||||
// 展开行 边框
|
||||
.vxe-table--render-default .vxe-body--expanded-column {
|
||||
border-bottom-color: @borderColor;
|
||||
}
|
||||
|
||||
// 行斑马纹
|
||||
.vxe-table--render-default .vxe-body--row.row--stripe {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
// 行hover
|
||||
.vxe-table--render-default .vxe-body--row.row--hover {
|
||||
background-color: #262626;
|
||||
}
|
||||
|
||||
// 选中行
|
||||
.vxe-table--render-default .vxe-body--row.row--checked {
|
||||
background-color: #44403a;
|
||||
|
||||
&.row--hover {
|
||||
background-color: #59524b;
|
||||
}
|
||||
}
|
||||
|
||||
.vxe-table--render-default.border--default .vxe-table--header-wrapper,
|
||||
.vxe-table--render-default.border--full .vxe-table--header-wrapper,
|
||||
.vxe-table--render-default.border--outer .vxe-table--header-wrapper {
|
||||
background-color: #1d1d1d;
|
||||
}
|
||||
|
||||
.vxe-table--render-default.border--default .vxe-body--column,
|
||||
.vxe-table--render-default.border--default .vxe-footer--column,
|
||||
.vxe-table--render-default.border--default .vxe-header--column,
|
||||
.vxe-table--render-default.border--inner .vxe-body--column,
|
||||
.vxe-table--render-default.border--inner .vxe-footer--column,
|
||||
.vxe-table--render-default.border--inner .vxe-header--column {
|
||||
background-image: linear-gradient(#1d1d1d, #1d1d1d);
|
||||
}
|
||||
|
||||
// 列宽拖动
|
||||
.vxe-header--column .vxe-resizable.is--line:before {
|
||||
background-color: #505050;
|
||||
}
|
||||
|
||||
// checkbox
|
||||
.vxe-custom--option .vxe-checkbox--icon:before,
|
||||
.vxe-export--panel-column-option .vxe-checkbox--icon:before,
|
||||
.vxe-table--filter-option .vxe-checkbox--icon:before,
|
||||
.vxe-table--render-default .vxe-cell--checkbox .vxe-checkbox--icon:before {
|
||||
background-color: @bgColor;
|
||||
border-color: @borderColor;
|
||||
}
|
||||
|
||||
.vxe-toolbar .vxe-custom--option-wrapper {
|
||||
background-color: @bgColor;
|
||||
}
|
||||
|
||||
.vxe-button {
|
||||
background-color: @bgColor;
|
||||
border-color: @borderColor;
|
||||
}
|
||||
|
||||
.vxe-button.type--button:not(.is--disabled):active {
|
||||
background-color: @bgColor;
|
||||
}
|
||||
|
||||
.vxe-toolbar .vxe-custom--wrapper.is--active > .vxe-button {
|
||||
background-color: @bgColor;
|
||||
}
|
||||
|
||||
.vxe-toolbar .vxe-custom--option-wrapper .vxe-custom--footer button {
|
||||
color: @fontColor;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
import { ComponentInternalInstance, ExtractPropTypes } from 'vue';
|
||||
import { useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
|
||||
export namespace JVxeComponent {
|
||||
export type Props = ExtractPropTypes<ReturnType<typeof useJVxeCompProps>>;
|
||||
|
||||
interface EnhancedCtx {
|
||||
props?: JVxeComponent.Props;
|
||||
context?: any;
|
||||
}
|
||||
|
||||
/** 组件增强类型 */
|
||||
export interface Enhanced {
|
||||
// 注册参数(详见:https://xuliangzhan_admin.gitee.io/vxe-table/v4/table/renderer/edit)
|
||||
installOptions: {
|
||||
// 自动聚焦的 class 类名
|
||||
autofocus?: string;
|
||||
} & Recordable;
|
||||
// 事件拦截器(用于兼容)
|
||||
interceptor: {
|
||||
// 已实现:event.clearActived
|
||||
// 说明:比如点击了某个组件的弹出层面板之后,此时被激活单元格不应该被自动关闭,通过返回 false 可以阻止默认的行为。
|
||||
'event.clearActived'?: (params, event, target, ctx?: EnhancedCtx) => boolean;
|
||||
// 自定义:event.clearActived.className
|
||||
// 说明:比原生的多了一个参数:className,用于判断点击的元素的样式名(递归到顶层)
|
||||
'event.clearActived.className'?: (params, event, target, ctx?: EnhancedCtx) => boolean;
|
||||
};
|
||||
// 【功能开关】
|
||||
switches: {
|
||||
// 是否使用 editRender 模式(仅当前组件,并非全局)
|
||||
// 如果设为true,则表头上方会出现一个可编辑的图标
|
||||
editRender?: boolean;
|
||||
// false = 组件触发后可视);true = 组件一直可视
|
||||
visible?: boolean;
|
||||
};
|
||||
// 【切面增强】切面事件处理,一般在某些方法执行后同步执行
|
||||
aopEvents: {
|
||||
// 单元格被激活编辑时会触发该事件
|
||||
editActived?: (this: ComponentInternalInstance, ...args) => any;
|
||||
// 单元格编辑状态下被关闭时会触发该事件
|
||||
editClosed?: (this: ComponentInternalInstance, ...args) => any;
|
||||
// 返回值决定单元格是否可以编辑
|
||||
activeMethod?: (this: ComponentInternalInstance, ...args) => boolean;
|
||||
};
|
||||
// 【翻译增强】可以实现例如select组件保存的value,但是span模式下需要显示成text
|
||||
translate: {
|
||||
// 是否启用翻译
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* 【翻译处理方法】如果handler留空,则使用默认的翻译方法
|
||||
*
|
||||
* @param value 需要翻译的值
|
||||
* @returns{*} 返回翻译后的数据
|
||||
*/
|
||||
handler?: (value, ctx?: EnhancedCtx) => any;
|
||||
};
|
||||
/**
|
||||
* 【获取值增强】组件抛出的值
|
||||
*
|
||||
* @param value 保存到数据库里的值
|
||||
* @returns{*} 返回处理后的值
|
||||
*/
|
||||
getValue: (value, ctx?: EnhancedCtx) => any;
|
||||
/**
|
||||
* 【设置值增强】设置给组件的值
|
||||
*
|
||||
* @param value 组件触发的值
|
||||
* @returns{*} 返回处理后的值
|
||||
*/
|
||||
setValue: (value, ctx?: EnhancedCtx) => any;
|
||||
/**
|
||||
* 【新增行增强】在用户点击新增时触发的事件,返回新行的默认值
|
||||
*
|
||||
* @param defaultValue 默认值
|
||||
* @param row 行数据
|
||||
* @param column 列配置,.params 是用户配置的参数
|
||||
* @param $table vxe 实例
|
||||
* @param renderOptions 渲染选项
|
||||
* @param params 可以在这里获取 $table
|
||||
*
|
||||
* @returns 返回新值
|
||||
*/
|
||||
createValue: (defaultValue: any, ctx?: EnhancedCtx) => any;
|
||||
}
|
||||
|
||||
export type EnhancedPartial = Partial<Enhanced>;
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
/** 组件类型 */
|
||||
export enum JVxeTypes {
|
||||
// 行号列
|
||||
rowNumber = 'row-number',
|
||||
// 选择列
|
||||
rowCheckbox = 'row-checkbox',
|
||||
// 单选列
|
||||
rowRadio = 'row-radio',
|
||||
// 展开列
|
||||
rowExpand = 'row-expand',
|
||||
// 上下排序
|
||||
rowDragSort = 'row-drag-sort',
|
||||
|
||||
input = 'input',
|
||||
inputNumber = 'input-number',
|
||||
textarea = 'textarea',
|
||||
select = 'select',
|
||||
date = 'date',
|
||||
datetime = 'datetime',
|
||||
time = 'time',
|
||||
checkbox = 'checkbox',
|
||||
upload = 'upload',
|
||||
// 下拉搜索
|
||||
selectSearch = 'select-search',
|
||||
// 下拉多选
|
||||
selectMultiple = 'select-multiple',
|
||||
// 进度条
|
||||
progress = 'progress',
|
||||
//部门选择
|
||||
departSelect = 'depart-select',
|
||||
//用户选择
|
||||
userSelect = 'user-select',
|
||||
|
||||
// 拖轮Tags(暂无用)
|
||||
tags = 'tags', // TODO 待实现
|
||||
|
||||
slot = 'slot',
|
||||
normal = 'normal',
|
||||
hidden = 'hidden',
|
||||
|
||||
// 以下为自定义组件
|
||||
popup = 'popup',
|
||||
selectDictSearch = 'selectDictSearch',
|
||||
radio = 'radio',
|
||||
image = 'image',
|
||||
file = 'file',
|
||||
// 省市区
|
||||
pca = 'pca',
|
||||
}
|
||||
|
||||
// 为了防止和 vxe 内置的类型冲突,所以加上一个前缀
|
||||
// 前缀是自动加的,代码中直接用就行(JVxeTypes.input)
|
||||
export const JVxeTypePrefix = 'j-';
|
||||
|
||||
/** VxeTable 渲染类型 */
|
||||
export enum JVxeRenderType {
|
||||
editer = 'editer',
|
||||
spaner = 'spaner',
|
||||
default = 'default',
|
||||
}
|
||||
120
jeecgboot-vue3/src/components/jeecg/JVxeTable/src/types/index.ts
Normal file
120
jeecgboot-vue3/src/components/jeecg/JVxeTable/src/types/index.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import type { Component, Ref, ComputedRef, ExtractPropTypes } from 'vue';
|
||||
import type { VxeColumnProps } from 'vxe-table/types/column';
|
||||
import type { JVxeComponent } from './JVxeComponent';
|
||||
import type { VxeGridInstance, VxeTablePropTypes } from 'vxe-table';
|
||||
import { JVxeTypes } from './JVxeTypes';
|
||||
import { vxeProps } from '../vxe.data';
|
||||
import { useMethods } from '../hooks/useMethods';
|
||||
import { getJVxeAuths } from '../utils/authUtils';
|
||||
|
||||
export type JVxeTableProps = Partial<ExtractPropTypes<ReturnType<typeof vxeProps>>>;
|
||||
export type JVxeTableMethods = ReturnType<typeof useMethods>['methods'];
|
||||
|
||||
export type JVxeVueComponent = {
|
||||
enhanced?: JVxeComponent.EnhancedPartial;
|
||||
} & Component;
|
||||
|
||||
type statisticsTypes = 'sum' | 'average';
|
||||
|
||||
export type JVxeColumn = IJVxeColumn & Recordable;
|
||||
|
||||
/**
|
||||
* JVxe 列配置项
|
||||
*/
|
||||
export interface IJVxeColumn extends VxeColumnProps {
|
||||
type?: any;
|
||||
// 行唯一标识
|
||||
key: string;
|
||||
// 表单预期值的提示信息,可以使用${...}变量替换文本
|
||||
placeholder?: string;
|
||||
// 默认值
|
||||
defaultValue?: any;
|
||||
// 是否禁用当前列,默认false
|
||||
disabled?: boolean;
|
||||
// 校验规则 TODO 类型待定义
|
||||
validateRules?: any;
|
||||
// 联动下一级的字段key
|
||||
linkageKey?: string;
|
||||
// 自定义传入组件的其他属性
|
||||
props?: Recordable;
|
||||
allowClear?: boolean; // 允许清除
|
||||
// 【inputNumber】是否是统计列,只有 inputNumber 才能设置统计列。统计列:sum 求和;average 平均值
|
||||
statistics?: boolean | [statisticsTypes, statisticsTypes?];
|
||||
// 【select】
|
||||
dictCode?: string; // 字典 code
|
||||
options?: { title?: string; label?: string; text?: string; value: any; disabled?: boolean }[]; // 下拉选项列表
|
||||
allowInput?: boolean; // 允许输入
|
||||
allowSearch?: boolean; // 允许搜索
|
||||
// 【slot】
|
||||
slotName?: string; // 插槽名
|
||||
// 【checkbox】
|
||||
customValue?: [any, any]; // 自定义值
|
||||
defaultChecked?: boolean; // 默认选中
|
||||
// 【upload】 upload
|
||||
btnText?: string; // 上传按钮文字
|
||||
token?: boolean; // 是否传递 token
|
||||
responseName?: string; // 返回取值名称
|
||||
action?: string; // 上传地址
|
||||
allowRemove?: boolean; // 是否允许删除
|
||||
allowDownload?: boolean; // 是否允许下载
|
||||
// 【下拉字典搜索】
|
||||
dict?: string; // 字典表配置信息:数据库表名,显示字段名,存储字段名
|
||||
async?: boolean; // 是否同步模式
|
||||
tipsContent?: string;
|
||||
// 【popup】
|
||||
popupCode?: string;
|
||||
field?: string;
|
||||
orgFields?: string;
|
||||
destFields?: string;
|
||||
}
|
||||
|
||||
export interface JVxeRefs {
|
||||
gridRef: Ref<VxeGridInstance | undefined>;
|
||||
subPopoverRef: Ref<any>;
|
||||
detailsModalRef: Ref<any>;
|
||||
}
|
||||
|
||||
export interface JVxeDataProps {
|
||||
prefixCls: string;
|
||||
// vxe 实例ID
|
||||
caseId: string;
|
||||
// vxe 最终 columns
|
||||
vxeColumns?: ComputedRef;
|
||||
// vxe 最终 dataSource
|
||||
vxeDataSource: Ref<Recordable[]>;
|
||||
// 记录滚动条位置
|
||||
scroll: { top: number; left: number };
|
||||
// 当前是否正在滚动
|
||||
scrolling: Ref<boolean>;
|
||||
// vxe 默认配置
|
||||
defaultVxeProps: object;
|
||||
// 绑定左侧选择框
|
||||
selectedRows: Ref<any[]>;
|
||||
// 绑定左侧选择框已选择的id
|
||||
selectedRowIds: Ref<string[]>;
|
||||
disabledRowIds: string[];
|
||||
// 统计列配置
|
||||
statistics: {
|
||||
has: boolean;
|
||||
sum: string[];
|
||||
average: string[];
|
||||
};
|
||||
// 所有和当前表格相关的授权信息
|
||||
authsMap: Ref<Nullable<ReturnType<typeof getJVxeAuths>>>;
|
||||
// 内置 EditRules
|
||||
innerEditRules: Recordable<VxeTablePropTypes.EditRules[]>;
|
||||
// 联动下拉选项(用于隔离不同的下拉选项)
|
||||
// 内部联动配置,map
|
||||
innerLinkageConfig: Map<string, any>;
|
||||
// 开启了数据刷新效果的行
|
||||
reloadEffectRowKeysMap: Recordable;
|
||||
}
|
||||
|
||||
export interface JVxeLinkageConfig {
|
||||
// 联动第一级的 key
|
||||
key: string;
|
||||
// 获取数据的方法
|
||||
requestData: (parent: string) => Promise<any>;
|
||||
}
|
||||
|
||||
export { JVxeTypes };
|
||||
@ -0,0 +1,50 @@
|
||||
/* JVxeTable 行编辑 权限 */
|
||||
import { usePermissionStoreWithOut } from '/@/store/modules/permission';
|
||||
|
||||
const permissionStore = usePermissionStoreWithOut();
|
||||
|
||||
/**
|
||||
* JVxe 专用,获取权限
|
||||
* @param prefix
|
||||
*/
|
||||
export function getJVxeAuths(prefix) {
|
||||
prefix = getPrefix(prefix);
|
||||
let { authList, allAuthList } = permissionStore;
|
||||
let authsMap = new Map<string, typeof allAuthList[0]>();
|
||||
if (!prefix || prefix.length == 0) {
|
||||
return authsMap;
|
||||
}
|
||||
// 将所有vxe用到的权限取出来
|
||||
for (let auth of allAuthList) {
|
||||
if (auth.status == '1' && (auth.action || '').startsWith(prefix)) {
|
||||
authsMap.set(auth.action, { ...auth, isAuth: false });
|
||||
}
|
||||
}
|
||||
// 设置是否已授权
|
||||
for (let auth of authList) {
|
||||
let getAuth = authsMap.get(auth.action);
|
||||
if (getAuth != null) {
|
||||
getAuth.isAuth = true;
|
||||
}
|
||||
}
|
||||
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1162 子表按钮没控制
|
||||
let onlineButtonAuths = permissionStore.getOnlineSubTableAuth(prefix);
|
||||
if (onlineButtonAuths && onlineButtonAuths.length > 0) {
|
||||
for (let auth of onlineButtonAuths) {
|
||||
authsMap.set(prefix + 'btn:' + auth, { action: auth, type: 1, status: 1, isAuth: false });
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:2022-6-1 for: VUEN-1162 子表按钮没控制
|
||||
return authsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前缀
|
||||
* @param prefix
|
||||
*/
|
||||
export function getPrefix(prefix: string) {
|
||||
if (prefix && !prefix.endsWith(':')) {
|
||||
return prefix + ':';
|
||||
}
|
||||
return prefix;
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import { useDefaultEnhanced } from '../hooks/useJVxeComponent';
|
||||
import { isFunction, isObject, isString } from '/@/utils/is';
|
||||
import { JVxeTypes } from '../types';
|
||||
import { JVxeComponent } from '../types/JVxeComponent';
|
||||
import { componentMap } from '../componentMap';
|
||||
|
||||
// 已注册的组件增强
|
||||
const enhancedMap = new Map<JVxeTypes, JVxeComponent.Enhanced>();
|
||||
|
||||
/**
|
||||
* 获取某个组件的增强
|
||||
* @param type JVxeTypes
|
||||
*/
|
||||
export function getEnhanced(type: JVxeTypes | string): JVxeComponent.Enhanced {
|
||||
let $type: JVxeTypes = <JVxeTypes>type;
|
||||
if (!enhancedMap.has($type)) {
|
||||
let defaultEnhanced = useDefaultEnhanced();
|
||||
if (componentMap.has($type)) {
|
||||
let enhanced = componentMap.get($type)?.enhanced ?? {};
|
||||
if (isObject(enhanced)) {
|
||||
Object.keys(defaultEnhanced).forEach((key) => {
|
||||
let def = defaultEnhanced[key];
|
||||
if (enhanced.hasOwnProperty(key)) {
|
||||
// 方法如果存在就不覆盖
|
||||
if (!isFunction(def) && !isString(def)) {
|
||||
enhanced[key] = Object.assign({}, def, enhanced[key]);
|
||||
}
|
||||
} else {
|
||||
enhanced[key] = def;
|
||||
}
|
||||
});
|
||||
enhancedMap.set($type, <JVxeComponent.Enhanced>enhanced);
|
||||
return <JVxeComponent.Enhanced>enhanced;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`[JVxeTable] ${$type} 组件尚未注册,获取增强失败`);
|
||||
}
|
||||
enhancedMap.set($type, <JVxeComponent.Enhanced>defaultEnhanced);
|
||||
}
|
||||
return <JVxeComponent.Enhanced>enhancedMap.get($type);
|
||||
}
|
||||
|
||||
/** 辅助方法:替换${...}变量 */
|
||||
export function replaceProps(col, value) {
|
||||
if (value && typeof value === 'string') {
|
||||
let text = value;
|
||||
text = text.replace(/\${title}/g, col.title);
|
||||
text = text.replace(/\${key}/g, col.key);
|
||||
text = text.replace(/\${defaultValue}/g, col.defaultValue);
|
||||
return text;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,143 @@
|
||||
import type { Component } from 'vue';
|
||||
import { h } from 'vue';
|
||||
import VXETable from 'vxe-table';
|
||||
import { definedComponent, addComponent, componentMap, spanEnds, excludeKeywords } from '../componentMap';
|
||||
import { JVxeRenderType, JVxeTypePrefix, JVxeTypes } from '../types/JVxeTypes';
|
||||
import { getEnhanced } from './enhancedUtils';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
|
||||
/**
|
||||
* 判断某个组件是否已注册
|
||||
* @param type
|
||||
*/
|
||||
export function isRegistered(type: JVxeTypes | string) {
|
||||
if (excludeKeywords.includes(<JVxeTypes>type)) {
|
||||
return true;
|
||||
}
|
||||
return componentMap.has(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册vxe自定义组件
|
||||
*
|
||||
* @param type
|
||||
* @param component 编辑状态显示的组件
|
||||
* @param spanComponent 非编辑状态显示的组件,可以为空
|
||||
*/
|
||||
export function registerComponent(type: JVxeTypes, component: Component, spanComponent?: Component) {
|
||||
addComponent(type, component, spanComponent);
|
||||
registerOneComponent(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步注册vxe自定义组件
|
||||
*
|
||||
* @param type
|
||||
* @param promise
|
||||
*/
|
||||
export async function registerAsyncComponent(type: JVxeTypes, promise: Promise<any>) {
|
||||
const result = await promise;
|
||||
if (isFunction(result.installJVxe)) {
|
||||
result.install((component: Component, spanComponent?: Component) => {
|
||||
addComponent(type, component, spanComponent);
|
||||
registerOneComponent(type);
|
||||
});
|
||||
} else {
|
||||
addComponent(type, result.default);
|
||||
registerOneComponent(type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 2024-03-08
|
||||
* liaozhiyang
|
||||
* 异步注册vxe自定义组件
|
||||
* 【QQYUN-8241】
|
||||
* @param type
|
||||
* @param promise
|
||||
*/
|
||||
export function registerASyncComponentReal(type: JVxeTypes, component) {
|
||||
addComponent(type, component);
|
||||
registerOneComponent(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装所有vxe组件
|
||||
*/
|
||||
export function registerAllComponent() {
|
||||
definedComponent();
|
||||
// 遍历所有组件批量注册
|
||||
const components = [...componentMap.keys()];
|
||||
components.forEach((type) => {
|
||||
if (!type.endsWith(spanEnds)) {
|
||||
registerOneComponent(<JVxeTypes>type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册单个vxe组件
|
||||
*
|
||||
* @param type 组件 type
|
||||
*/
|
||||
export function registerOneComponent(type: JVxeTypes) {
|
||||
const component = componentMap.get(type);
|
||||
if (component) {
|
||||
const switches = getEnhanced(type).switches;
|
||||
if (switches.editRender && !switches.visible) {
|
||||
createEditRender(type, component);
|
||||
} else {
|
||||
createCellRender(type, component);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`【registerOneComponent】"${type}"不存在于componentMap中`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 注册可编辑组件 */
|
||||
function createEditRender(type: JVxeTypes, component: Component, spanComponent?: Component) {
|
||||
// 获取当前组件的增强
|
||||
const enhanced = getEnhanced(type);
|
||||
if (!spanComponent) {
|
||||
if (componentMap.has(type + spanEnds)) {
|
||||
spanComponent = componentMap.get(type + spanEnds);
|
||||
} else {
|
||||
// 默认的 span 组件为 normal
|
||||
spanComponent = componentMap.get(JVxeTypes.normal);
|
||||
}
|
||||
}
|
||||
// 添加渲染
|
||||
VXETable.renderer.add(JVxeTypePrefix + type, {
|
||||
// 可编辑模板
|
||||
renderEdit: createRender(type, component, JVxeRenderType.editer),
|
||||
// 显示模板
|
||||
renderCell: createRender(type, spanComponent, JVxeRenderType.spaner),
|
||||
// 增强注册
|
||||
...enhanced.installOptions,
|
||||
});
|
||||
}
|
||||
|
||||
/** 注册普通组件 */
|
||||
function createCellRender(type: JVxeTypes, component: Component = <Component>componentMap.get(JVxeTypes.normal)) {
|
||||
// 获取当前组件的增强
|
||||
const enhanced = getEnhanced(type);
|
||||
VXETable.renderer.add(JVxeTypePrefix + type, {
|
||||
// 默认显示模板
|
||||
renderDefault: createRender(type, component, JVxeRenderType.default),
|
||||
// 增强注册
|
||||
...enhanced.installOptions,
|
||||
});
|
||||
}
|
||||
|
||||
function createRender(type, component, renderType) {
|
||||
return function (renderOptions, params) {
|
||||
return [
|
||||
h(component, {
|
||||
type: type,
|
||||
params: params,
|
||||
renderOptions: renderOptions,
|
||||
renderType: renderType,
|
||||
}),
|
||||
];
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
/**
|
||||
*
|
||||
* 根据 tagName 获取父级节点
|
||||
*
|
||||
* @param dom 一级dom节点
|
||||
* @param tagName 标签名,不区分大小写
|
||||
*/
|
||||
export function getParentNodeByTagName(dom: HTMLElement, tagName: string = 'body'): HTMLElement | null {
|
||||
if (tagName === 'body') {
|
||||
return document.body;
|
||||
}
|
||||
if (dom.parentElement) {
|
||||
if (dom.parentElement.tagName.toLowerCase() === tagName.trim().toLowerCase()) {
|
||||
return dom.parentElement;
|
||||
} else {
|
||||
return getParentNodeByTagName(dom.parentElement, tagName);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
118
jeecgboot-vue3/src/components/jeecg/JVxeTable/src/vxe.data.ts
Normal file
118
jeecgboot-vue3/src/components/jeecg/JVxeTable/src/vxe.data.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
export const vxeProps = () => ({
|
||||
rowKey: propTypes.string.def('id'),
|
||||
// 列信息
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
// 数据源
|
||||
dataSource: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
authPre: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
// 是否显示工具栏
|
||||
toolbar: propTypes.bool.def(false),
|
||||
// 工具栏配置
|
||||
toolbarConfig: propTypes.object.def(() => ({
|
||||
// prefix 前缀;suffix 后缀;
|
||||
slots: ['prefix', 'suffix'],
|
||||
// add 新增按钮;remove 删除按钮;clearSelection 清空选择按钮;collapse 展开收起
|
||||
btns: ['add', 'remove', 'clearSelection'],
|
||||
})),
|
||||
// 是否显示行号
|
||||
rowNumber: propTypes.bool.def(false),
|
||||
// 固定行号位置或者不固定 【QQYUN-8405】
|
||||
rowNumberFixed: propTypes.oneOf(['left', 'none']).def('left'),
|
||||
// update-begin--author:liaozhiyang---date:20240509---for:【issues/1162】JVxeTable列过长(出现横向滚动条)时无法拖拽排序
|
||||
dragSortFixed: propTypes.oneOf(['left', 'none']).def('left'),
|
||||
rowSelectionFixed: propTypes.oneOf(['left', 'none']).def('left'),
|
||||
// update-end--author:liaozhiyang---date:20240509---for:【issues/1162】JVxeTable列过长(出现横向滚动条)时无法拖拽排序
|
||||
// 是否可选择行
|
||||
rowSelection: propTypes.bool.def(false),
|
||||
// 选择行类型
|
||||
rowSelectionType: propTypes.oneOf(['checkbox', 'radio']).def('checkbox'),
|
||||
// 是否可展开行
|
||||
rowExpand: propTypes.bool.def(false),
|
||||
// 展开行配置
|
||||
expandConfig: propTypes.object.def(() => ({})),
|
||||
// 页面是否在加载中
|
||||
loading: propTypes.bool.def(false),
|
||||
// 表格高度
|
||||
height: propTypes.oneOfType([propTypes.number, propTypes.string]).def('auto'),
|
||||
// 最大高度
|
||||
maxHeight: {
|
||||
type: Number,
|
||||
default: () => null,
|
||||
},
|
||||
// 要禁用的行
|
||||
disabledRows: propTypes.object.def(() => ({})),
|
||||
// 是否禁用全部组件
|
||||
disabled: propTypes.bool.def(false),
|
||||
// 是否可拖拽排序(有固定列的情况下无法拖拽排序,仅可上下排序)
|
||||
dragSort: propTypes.bool.def(false),
|
||||
// 排序字段保存的Key
|
||||
sortKey: propTypes.string.def('orderNum'),
|
||||
// 排序序号开始值,默认为 0
|
||||
sortBegin: propTypes.number.def(0),
|
||||
// 大小,可选值有:medium(中)、small(小)、mini(微)
|
||||
size: propTypes.oneOf(['medium', 'small', 'mini']).def('medium'),
|
||||
// 是否显示边框线
|
||||
bordered: propTypes.bool.def(false),
|
||||
// 自定义列配置 默认继承 setup.toolbar.custom
|
||||
custom: propTypes.bool.def(false),
|
||||
// 分页器参数,设置了即可显示分页器
|
||||
pagination: propTypes.object.def(() => ({})),
|
||||
// 点击行时是否显示子表单
|
||||
clickRowShowSubForm: propTypes.bool.def(false),
|
||||
// 点击行时是否显示主表单
|
||||
clickRowShowMainForm: propTypes.bool.def(false),
|
||||
// 是否点击选中行,优先级最低
|
||||
clickSelectRow: propTypes.bool.def(false),
|
||||
// 是否开启 reload 数据效果
|
||||
reloadEffect: propTypes.bool.def(false),
|
||||
// 校验规则
|
||||
editRules: propTypes.object.def(() => ({})),
|
||||
// 是否异步删除行,如果你要实现异步删除,那么需要把这个选项开启,
|
||||
// 在remove事件里调用confirmRemove方法才会真正删除(除非删除的全是新增的行)
|
||||
asyncRemove: propTypes.bool.def(false),
|
||||
// 是否一直显示组件,如果为false则只有点击的时候才出现组件
|
||||
// 注:该参数不能动态修改;如果行、列字段多的情况下,会根据机器性能造成不同程度的卡顿。
|
||||
// TODO 新版vxe-table取消了 visible 参数,导致无法实现该功能
|
||||
alwaysEdit: propTypes.bool.def(false),
|
||||
// 联动配置,数组,详情配置见文档
|
||||
linkageConfig: propTypes.array.def(() => []),
|
||||
// 是否开启使用 webSocket 无痕刷新
|
||||
socketReload: propTypes.bool.def(false),
|
||||
// 相同的socketKey更改时会互相刷新
|
||||
socketKey: propTypes.string.def('vxe-default'),
|
||||
// 新增行时切换行的激活状态
|
||||
addSetActive: propTypes.bool.def(true),
|
||||
// 是否开启键盘编辑
|
||||
keyboardEdit: propTypes.bool.def(false),
|
||||
// update-begin--author:liaozhiyang---date:20231013---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
// 横向虚拟滚动配置(不支持展开行)
|
||||
// 【QQYUN-7676】x滚动条滚动时字典变成了id
|
||||
scrollX: propTypes.object.def(() => ({ enabled: false })),
|
||||
// 纵向虚拟滚动配置(不支持展开行)
|
||||
scrollY: propTypes.object.def(() => ({ enabled: true })),
|
||||
// update-end--author:liaozhiyang---date:20231013---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
//【QQYUN-8566】缓存列设置的key(路由页面内唯一)
|
||||
cacheColumnsKey: propTypes.string.def(''),
|
||||
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
rowClassName: {
|
||||
type: [String, Function],
|
||||
default: null,
|
||||
},
|
||||
// 不允许拖拽的行 [{'key':field,'value':value}]
|
||||
notAllowDrag: propTypes.array.def(() => []),
|
||||
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8785】online表单列位置的id未做限制,拖动其他列到id列上面,同步数据库时报错
|
||||
});
|
||||
|
||||
export const vxeEmits = ['save', 'added', 'removed', 'inserted', 'dragged', 'selectRowChange', 'pageChange', 'valueChange', 'blur'];
|
||||
6
jeecgboot-vue3/src/components/jeecg/JVxeTable/types.ts
Normal file
6
jeecgboot-vue3/src/components/jeecg/JVxeTable/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import JVxeTable from './src/JVxeTable';
|
||||
|
||||
export type { JVxeComponent } from './src/types/JVxeComponent';
|
||||
export type { JVxeColumn, JVxeLinkageConfig } from './src/types';
|
||||
export { JVxeTypes } from './src/types/JVxeTypes';
|
||||
export type JVxeTableInstance = InstanceType<typeof JVxeTable>;
|
||||
109
jeecgboot-vue3/src/components/jeecg/JVxeTable/utils.ts
Normal file
109
jeecgboot-vue3/src/components/jeecg/JVxeTable/utils.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import type { Ref, ComponentInternalInstance } from 'vue';
|
||||
import { unref, isRef } from 'vue';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
|
||||
type dispatchEventOptions = {
|
||||
// JVxeTable 的 props
|
||||
props;
|
||||
// 触发的 event 事件对象
|
||||
$event;
|
||||
// 行、列
|
||||
row?;
|
||||
column?;
|
||||
// JVxeTable的vue3实例
|
||||
instance?: ComponentInternalInstance | any;
|
||||
// 要寻找的className
|
||||
className: string;
|
||||
// 重写找到dom后的处理方法
|
||||
handler?: Fn;
|
||||
// 是否直接执行click方法而不是模拟click事件
|
||||
isClick?: boolean;
|
||||
};
|
||||
|
||||
/** 模拟触发事件 */
|
||||
export function dispatchEvent(options: dispatchEventOptions) {
|
||||
const { props, $event, row, column, instance, className, handler, isClick } = options;
|
||||
if ((!$event || !$event.path) && !instance) {
|
||||
return;
|
||||
}
|
||||
// alwaysEdit 下不模拟触发事件,否者会导致触发两次
|
||||
if (props && props.alwaysEdit) {
|
||||
return;
|
||||
}
|
||||
let getCell = () => {
|
||||
let paths: HTMLElement[] = [...($event?.path ?? [])];
|
||||
// 通过 instance 获取 cell dom对象
|
||||
if (row && column) {
|
||||
let selector = `table.vxe-table--body tbody tr[rowid='${row.id}'] td[colid='${column.id}']`;
|
||||
let cellDom = instance!.vnode?.el?.querySelector(selector);
|
||||
// -update-begin--author:liaozhiyang---date:20230830---for:【QQYUN-6390】解决online新增字段警告(兼容下)
|
||||
if (!cellDom) {
|
||||
cellDom = instance!.$el?.querySelector(selector);
|
||||
}
|
||||
// -update-begin--author:liaozhiyang---date:20230830---for:【QQYUN-6390】解决online新增字段警告(兼容下)
|
||||
if (cellDom) {
|
||||
paths.unshift(cellDom);
|
||||
}
|
||||
}
|
||||
for (const el of paths) {
|
||||
if (el.classList?.contains('vxe-body--column')) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
let cell = getCell();
|
||||
if (cell) {
|
||||
window.setTimeout(() => {
|
||||
let getElement = () => {
|
||||
let classList = className.split(' ');
|
||||
if (classList.length > 0) {
|
||||
const getClassName = (cls: string) => {
|
||||
if (cls.startsWith('.')) {
|
||||
return cls.substring(1, cls.length);
|
||||
}
|
||||
return cls;
|
||||
};
|
||||
let get = (target, className, idx = 0) => {
|
||||
let elements = target.getElementsByClassName(getClassName(className));
|
||||
if (elements && elements.length > 0) {
|
||||
return elements[idx];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
let element: HTMLElement = get(cell, classList[0]);
|
||||
for (let i = 1; i < classList.length; i++) {
|
||||
if (!element) {
|
||||
break;
|
||||
}
|
||||
element = get(element, classList[i]);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
let element = getElement();
|
||||
if (element) {
|
||||
if (isFunction(handler)) {
|
||||
handler(element);
|
||||
} else {
|
||||
// 模拟触发点击事件
|
||||
if (isClick) {
|
||||
element.click();
|
||||
} else {
|
||||
element.dispatchEvent($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
} else {
|
||||
console.warn('【JVxeTable】dispatchEvent 获取 cell 失败');
|
||||
}
|
||||
}
|
||||
|
||||
/** 绑定 VxeTable 数据 */
|
||||
export function vModel(value, row, column: Ref<any> | string) {
|
||||
// @ts-ignore
|
||||
let property = isRef(column) ? column.value.property : column;
|
||||
unref(row)[property] = value;
|
||||
}
|
||||
254
jeecgboot-vue3/src/components/jeecg/OnLine/JPopupOnlReport.vue
Normal file
254
jeecgboot-vue3/src/components/jeecg/OnLine/JPopupOnlReport.vue
Normal file
@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="jeecg-basic-table-form-container" v-if="showSearchFlag">
|
||||
<a-form ref="formRef" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol" @keyup.enter.native="searchQuery">
|
||||
<a-row :gutter="24">
|
||||
<template v-for="(item, index) in queryInfo">
|
||||
<template v-if="item.hidden === '1'">
|
||||
<a-col :md="6" :sm="24" :key="'query' + index" v-show="toggleSearchStatus">
|
||||
<SearchFormItem :formElRef="formRef" :queryParam="queryParam" :item="item" :dictOptions="dictOptions"></SearchFormItem>
|
||||
</a-col>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-col :md="6" :sm="24" :key="'query' + index">
|
||||
<SearchFormItem :formElRef="formRef" :queryParam="queryParam" :item="item" :dictOptions="dictOptions"></SearchFormItem>
|
||||
</a-col>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<a-col :md="6" :sm="8">
|
||||
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
|
||||
<a-col :lg="6">
|
||||
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
|
||||
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
|
||||
<a @click="handleToggleSearch" style="margin-left: 8px">
|
||||
{{ toggleSearchStatus ? '收起' : '展开' }}
|
||||
<Icon :icon="toggleSearchStatus ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
|
||||
</a>
|
||||
</a-col>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:canResize="true"
|
||||
:bordered="true"
|
||||
:loading="loading"
|
||||
:rowKey="combineRowKey"
|
||||
:columns="columns"
|
||||
:showIndexColumn="false"
|
||||
:dataSource="dataSource"
|
||||
:pagination="pagination"
|
||||
:rowSelection="rowSelection"
|
||||
@row-click="clickThenCheck"
|
||||
@change="handleChangeInTable"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 跳转Href的动态组件方式 -->
|
||||
<a-modal v-bind="hrefComponent.model" v-on="hrefComponent.on">
|
||||
<component :is="hrefComponent.is" v-bind="hrefComponent.params" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref, ref, watch, watchEffect, reactive, computed } from 'vue';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { usePopBiz } from '/@/components/jeecg/OnLine/hooks/usePopBiz';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JPopupOnlReport',
|
||||
components: {
|
||||
SearchFormItem: createAsyncComponent(() => import('/@/components/jeecg/OnLine/SearchFormItem.vue'), { loading: true }),
|
||||
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), {
|
||||
loading: true,
|
||||
}),
|
||||
},
|
||||
props: ['multi', 'code', 'id', 'sorter', 'groupId', 'param', 'clickToRowSelect'],
|
||||
emits: ['ok', 'register'],
|
||||
setup(props, { emit, refs }) {
|
||||
const { createMessage } = useMessage();
|
||||
const labelCol = reactive({
|
||||
xs: { span: 24 },
|
||||
sm: { span: 6 },
|
||||
});
|
||||
const wrapperCol = reactive({
|
||||
xs: { span: 24 },
|
||||
sm: { span: 18 },
|
||||
});
|
||||
const formRef = ref();
|
||||
const tableRef = ref();
|
||||
const toggleSearchStatus = ref(false);
|
||||
const attrs = useAttrs();
|
||||
const tableScroll = ref({ x: true });
|
||||
const route = useRoute();
|
||||
console.log('route.query = ',route.query)
|
||||
const getBindValue = Object.assign({}, {routeQuery: route.query}, unref(props), unref(attrs));
|
||||
|
||||
const [
|
||||
{
|
||||
visibleChange,
|
||||
loadColumnsInfo,
|
||||
dynamicParamHandler,
|
||||
loadData,
|
||||
loadColumnsAndData,
|
||||
handleChangeInTable,
|
||||
combineRowKey,
|
||||
clickThenCheck,
|
||||
filterUnuseSelect,
|
||||
handleExport,
|
||||
},
|
||||
{
|
||||
hrefComponent,
|
||||
visible,
|
||||
rowSelection,
|
||||
checkedKeys,
|
||||
selectRows,
|
||||
pagination,
|
||||
dataSource,
|
||||
columns,
|
||||
loading,
|
||||
title,
|
||||
iSorter,
|
||||
queryInfo,
|
||||
queryParam,
|
||||
dictOptions,
|
||||
},
|
||||
] = usePopBiz(getBindValue, tableRef);
|
||||
|
||||
pagination.pageSizeOptions = ['10', '100', '300'];
|
||||
|
||||
const showSearchFlag = computed(() => unref(queryInfo) && unref(queryInfo).length > 0);
|
||||
/**
|
||||
*监听code
|
||||
*/
|
||||
watch(
|
||||
() => props.code,
|
||||
() => {
|
||||
loadColumnsAndData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
/**
|
||||
*监听popup动态参数 支持系统变量语法
|
||||
*/
|
||||
watch(
|
||||
() => props.param,
|
||||
() => {
|
||||
if (visible) {
|
||||
dynamicParamHandler();
|
||||
//loadData();
|
||||
}
|
||||
}
|
||||
);
|
||||
/**
|
||||
*监听sorter排序字段
|
||||
*/
|
||||
watchEffect(() => {
|
||||
if (props.sorter) {
|
||||
let arr = props.sorter.split('=');
|
||||
if (arr.length === 2 && ['asc', 'desc'].includes(arr[1].toLowerCase())) {
|
||||
iSorter.value = { column: arr[0], order: arr[1].toLowerCase() };
|
||||
// 排序字段受控
|
||||
unref(columns).forEach((col) => {
|
||||
if (col.dataIndex === unref(iSorter).column) {
|
||||
col['sortOrder'] = unref(iSorter).order === 'asc' ? 'ascend' : 'descend';
|
||||
} else {
|
||||
col['sortOrder'] = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('【JPopup】sorter参数不合法');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function handleToggleSearch() {
|
||||
toggleSearchStatus.value = !unref(toggleSearchStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*/
|
||||
function onExportXls() {
|
||||
handleExport!();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
function searchQuery() {
|
||||
loadData(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function searchReset() {
|
||||
queryParam.value = {};
|
||||
loadData(1);
|
||||
}
|
||||
|
||||
return {
|
||||
attrs,
|
||||
|
||||
tableScroll,
|
||||
dataSource,
|
||||
pagination,
|
||||
columns,
|
||||
rowSelection,
|
||||
checkedKeys,
|
||||
loading,
|
||||
title,
|
||||
hrefComponent,
|
||||
|
||||
clickThenCheck,
|
||||
loadData,
|
||||
combineRowKey,
|
||||
handleChangeInTable,
|
||||
visibleChange,
|
||||
queryInfo,
|
||||
queryParam,
|
||||
tableRef,
|
||||
formRef,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
dictOptions,
|
||||
showSearchFlag,
|
||||
toggleSearchStatus,
|
||||
handleToggleSearch,
|
||||
searchQuery,
|
||||
searchReset,
|
||||
onExportXls,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.jeecg-basic-table-form-container {
|
||||
padding: 0px;
|
||||
|
||||
.table-page-search-submitButtons {
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.jeecg-basic-table .ant-table-wrapper .ant-table-title){
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-select-selector){
|
||||
min-width: 95px;
|
||||
}
|
||||
</style>
|
||||
327
jeecgboot-vue3/src/components/jeecg/OnLine/SearchFormItem.vue
Normal file
327
jeecgboot-vue3/src/components/jeecg/OnLine/SearchFormItem.vue
Normal file
@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<a-form-item v-if="item.view === DateTypeEnum.Date" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<template v-if="single_mode === item.mode">
|
||||
<a-date-picker
|
||||
:showTime="false"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
:placeholder="'请选择' + item.label"
|
||||
v-model:value="queryParam[item.field]"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-date-picker
|
||||
:showTime="false"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
v-model:value="queryParam[item.field + '_begin']"
|
||||
style="width: calc(50% - 15px)"
|
||||
></a-date-picker>
|
||||
<span class="group-query-strig">~</span>
|
||||
<a-date-picker
|
||||
:showTime="false"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="结束日期"
|
||||
v-model:value="queryParam[item.field + '_end']"
|
||||
style="width: calc(50% - 15px)"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else-if="item.view === DateTypeEnum.Datetime" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label :title="item.label">
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<template v-if="single_mode === item.mode">
|
||||
<a-date-picker
|
||||
:placeholder="'请选择' + item.label"
|
||||
:show-time="true"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
v-model:value="queryParam[item.field]"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-date-picker
|
||||
placeholder="选择开始时间"
|
||||
:show-time="true"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
v-model:value="queryParam[item.field + '_begin']"
|
||||
style="width: calc(50% - 9px); min-width: 60px"
|
||||
></a-date-picker>
|
||||
<span class="group-query-strig" style="width: auto; padding: 0 4px">~</span>
|
||||
<a-date-picker
|
||||
placeholder="选择结束时间"
|
||||
:show-time="true"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
v-model:value="queryParam[item.field + '_end']"
|
||||
style="width: calc(50% - 9px); min-width: 60px"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else-if="item.view === DateTypeEnum.Time" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<template v-if="single_mode === item.mode">
|
||||
<a-date-picker :placeholder="'请选择' + item.label" mode="time" valueFormat="HH:mm:ss" v-model:value="queryParam[item.field]"></a-date-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-date-picker
|
||||
placeholder="请选择开始时间"
|
||||
mode="time"
|
||||
valueFormat="HH:mm:ss"
|
||||
v-model:value="queryParam[item.field + '_begin']"
|
||||
style="width: calc(50% - 15px)"
|
||||
></a-date-picker>
|
||||
<span class="group-query-strig">~</span>
|
||||
<a-date-picker
|
||||
placeholder="请选择结束时间"
|
||||
mode="time"
|
||||
valueFormat="HH:mm:ss"
|
||||
v-model:value="queryParam[item.field + '_end']"
|
||||
style="width: calc(50% - 15px)"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-else-if="item.view === CompTypeEnum.List || item.view === CompTypeEnum.Radio || item.view === CompTypeEnum.Switch"
|
||||
:labelCol="labelCol"
|
||||
:class="'jeecg-online-search'"
|
||||
>
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<JDictSelectTag v-if="item.config === '1'" :placeholder="'请选择' + item.label" v-model="queryParam[item.field]" :dictCode="getDictCode(item)">
|
||||
</JDictSelectTag>
|
||||
<a-select v-else :placeholder="'请选择' + item.label" v-model:value="queryParam[item.field]">
|
||||
<template v-for="(obj, index) in dictOptions[getDictOptionKey(item)]" :key="index">
|
||||
<a-select-option :value="obj.value"> {{ obj.text }}</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else-if="item.view === CompTypeEnum.SelTree" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<JTreeSelect
|
||||
:placeholder="'请选择' + item.label"
|
||||
v-model:value="queryParam[item.field]"
|
||||
:dict="item.dict"
|
||||
:pidField="item.pidField"
|
||||
:pidValue="item.pidValue"
|
||||
:hasChildField="item.hasChildField"
|
||||
load-triggle-change
|
||||
>
|
||||
</JTreeSelect>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else-if="item.view === CompTypeEnum.CatTree" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<JCategorySelect :pcode="item.pcode" v-model:value="queryParam[item.field]" :placeholder="'请选择' + item.label" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else-if="item.view === CompTypeEnum.SelSearch" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<JOnlineSearchSelect v-model:value="queryParam[item.field]" :placeholder="'请选择'+item.label" :fieldId="item.fieldId"/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else-if="item.view === CompTypeEnum.SelUser" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<JSelectUserByDept v-model:value="queryParam[item.field]" :placeholder="'请选择' + item.label"></JSelectUserByDept>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else-if="item.view == CompTypeEnum.SelDepart" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<JSelectDept v-model:value="queryParam[item.field]" :placeholder="'请选择' + item.label" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else-if="item.view === CompTypeEnum.Popup" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<JPopup
|
||||
:placeholder="'请选择' + item.label"
|
||||
v-model:value="queryParam[item.field]"
|
||||
:formElRef="formElRef"
|
||||
:code="item.dictTable"
|
||||
:field-config="item.dictCode"
|
||||
:multi="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else-if="item.view === CompTypeEnum.Pca" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<JAreaLinkage :placeholder="'请选择' + item.label" v-model:value="queryParam[item.field]" />
|
||||
</a-form-item>
|
||||
<!--TODO 缺少的组件-->
|
||||
<a-form-item
|
||||
v-else-if="item.view === CompTypeEnum.Checkbox || item.view === CompTypeEnum.ListMulti"
|
||||
:labelCol="labelCol"
|
||||
:label="item.label"
|
||||
:class="'jeecg-online-search'"
|
||||
>
|
||||
<!-- <j-select-multiple
|
||||
v-if="item.config==='1'"
|
||||
:placeholder=" '请选择'+item.label "
|
||||
v-model="queryParam[item.field]"
|
||||
:dictCode="getDictCode(item)">
|
||||
</j-select-multiple>
|
||||
<j-select-multiple
|
||||
v-else
|
||||
:placeholder=" '请选择'+item.label "
|
||||
:options="dictOptions[item.dbField]"
|
||||
v-model="queryParam[item.field]">
|
||||
</j-select-multiple>-->
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-else :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<template v-if="single_mode === item.mode && 'string'== item.view">
|
||||
<j-input :placeholder="'请输入' + item.label" v-model:value="queryParam[item.field]"></j-input>
|
||||
</template>
|
||||
<template v-else-if="single_mode === item.mode">
|
||||
<a-input :placeholder="'请输入' + item.label" v-model:value="queryParam[item.field]"></a-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-input :placeholder="'请输入开始' + item.label" v-model:value="queryParam[item.field + '_begin']" style="width: calc(50% - 15px)"></a-input>
|
||||
<span class="group-query-strig">~</span>
|
||||
<a-input :placeholder="'请输入结束' + item.label" v-model:value="queryParam[item.field + '_end']" style="width: calc(50% - 15px)"></a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
//import JOnlineSearchSelect from '@/components/online/autoform/comp/JOnlineSearchSelect'
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { DateTypeEnum } from '/@/enums/DateTypeEnum.ts';
|
||||
import { CompTypeEnum } from '/@/enums/CompTypeEnum.ts';
|
||||
import { JDictSelectTag, JTreeSelect, JCategorySelect, JSelectUserByDept, JSelectDept, JPopup, JAreaLinkage,JInput,JSearchSelect } from '/@/components/Form';
|
||||
export default defineComponent({
|
||||
name: 'SearchFormItem',
|
||||
components: {
|
||||
//JOnlineSearchSelect
|
||||
JDictSelectTag,
|
||||
JTreeSelect,
|
||||
JCategorySelect,
|
||||
JSelectUserByDept,
|
||||
JSelectDept,
|
||||
JPopup,
|
||||
JAreaLinkage,
|
||||
JInput,
|
||||
},
|
||||
props: {
|
||||
formElRef: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true,
|
||||
},
|
||||
dictOptions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true,
|
||||
},
|
||||
queryParam: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const single_mode = ref('single');
|
||||
console.log('dictOptions===>', props.dictOptions);
|
||||
function getDictCode(item) {
|
||||
if (item.dictTable && item.dictTable.length > 0) {
|
||||
return item.dictTable + ',' + item.dictText + ',' + item.dictCode;
|
||||
} else {
|
||||
return item.dictCode;
|
||||
}
|
||||
}
|
||||
|
||||
function getSqlByDictCode(item) {
|
||||
let { dictTable, dictCode, dictText } = item;
|
||||
let temp = dictTable.toLowerCase();
|
||||
let arr = temp.split('where');
|
||||
let condition = '';
|
||||
if (arr.length > 1) {
|
||||
condition = ' where' + arr[1];
|
||||
}
|
||||
let sql = 'select ' + dictCode + " as 'value', " + dictText + " as 'text' from " + arr[0] + condition;
|
||||
console.log('sql', sql);
|
||||
return sql;
|
||||
}
|
||||
|
||||
function getDictOptionKey(item) {
|
||||
if (item.dbField) {
|
||||
return item.dbField;
|
||||
} else {
|
||||
return item.field;
|
||||
}
|
||||
}
|
||||
|
||||
// 定义查询条件 文本label的最大宽度 比起单纯的控制字体个数更好
|
||||
const labelTextMaxWidth = '120px';
|
||||
const labelCol = {
|
||||
style: {
|
||||
'max-width': labelTextMaxWidth,
|
||||
},
|
||||
};
|
||||
return {
|
||||
labelTextMaxWidth,
|
||||
labelCol,
|
||||
single_mode,
|
||||
getDictOptionKey,
|
||||
getDictCode,
|
||||
getSqlByDictCode,
|
||||
DateTypeEnum,
|
||||
CompTypeEnum,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.group-query-strig {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 查询条件左对齐样式设置 */
|
||||
.jeecg-online-search :deep(.ant-form-item-label) {
|
||||
flex: 0 0 auto !important;
|
||||
width: auto;
|
||||
}
|
||||
.jeecg-online-search :deep(.ant-form-item-control) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* label显示宽度 超出显示... */
|
||||
.jeecg-online-search :deep(.label-text) {
|
||||
max-width: v-bind(labelTextMaxWidth);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
917
jeecgboot-vue3/src/components/jeecg/OnLine/hooks/usePopBiz.ts
Normal file
917
jeecgboot-vue3/src/components/jeecg/OnLine/hooks/usePopBiz.ts
Normal file
@ -0,0 +1,917 @@
|
||||
import { reactive, ref, unref, defineAsyncComponent, toRaw, markRaw, isRef, watch, onUnmounted } from 'vue';
|
||||
import { httpGroupRequest } from '/@/components/Form/src/utils/GroupRequest';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { filterMultiDictText } from '/@/utils/dict/JDictSelectUtil.js';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { OnlineColumn } from '/@/components/jeecg/OnLine/types/onlineConfig';
|
||||
import { h } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useMethods } from '/@/hooks/system/useMethods';
|
||||
import { importViewsFile, _eval } from '/@/utils';
|
||||
import {getToken} from "@/utils/auth";
|
||||
|
||||
export function usePopBiz(ob, tableRef?) {
|
||||
// update-begin--author:liaozhiyang---date:20230811---for:【issues/675】子表字段Popup弹框数据不更新
|
||||
let props: any;
|
||||
if (isRef(ob)) {
|
||||
props = ob.value;
|
||||
const stopWatch = watch(ob, (newVal) => {
|
||||
props = newVal;
|
||||
});
|
||||
onUnmounted(() => stopWatch());
|
||||
} else {
|
||||
props = ob;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230811---for:【issues/675】子表字段Popup弹框数据不更新
|
||||
const { createMessage } = useMessage();
|
||||
//弹窗可视状态
|
||||
const visible = ref(false);
|
||||
//表格加载
|
||||
const loading = ref(false);
|
||||
//cgRpConfigId
|
||||
const cgRpConfigId = ref('');
|
||||
//标题
|
||||
const title = ref('列表');
|
||||
// 排序字段,默认无排序
|
||||
const iSorter = ref<any>('');
|
||||
// 查询对象
|
||||
const queryInfo = ref([]);
|
||||
// 查询参数
|
||||
const queryParam = ref<any>({});
|
||||
// 动态参数
|
||||
const dynamicParam = ref<any>({});
|
||||
//字典配置项
|
||||
const dictOptions = ref({});
|
||||
//数据集
|
||||
const dataSource = ref<Array<object>>([]);
|
||||
//定义表格信息
|
||||
const columns = ref<Array<object>>([]);
|
||||
// 当前路由
|
||||
const route = useRoute();
|
||||
//定义请求url信息
|
||||
const configUrl = reactive({
|
||||
//列表页加载column和data
|
||||
getColumnsAndData: '/online/cgreport/api/getColumnsAndData/',
|
||||
getColumns: '/online/cgreport/api/getRpColumns/',
|
||||
getData: '/online/cgreport/api/getData/',
|
||||
getQueryInfo: '/online/cgreport/api/getQueryInfo/',
|
||||
export: '/online/cgreport/api/exportManySheetXls/',
|
||||
});
|
||||
//已选择的值
|
||||
const checkedKeys = ref<Array<string | number>>([]);
|
||||
//选择的行记录
|
||||
const selectRows = ref<Array<any>>([]);
|
||||
// 点击单元格选中行 popup需要 但是报表预览不需要
|
||||
let clickThenCheckFlag = true;
|
||||
if (props.clickToRowSelect === false) {
|
||||
clickThenCheckFlag = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择列配置
|
||||
*/
|
||||
const rowSelection = {
|
||||
fixed: true,
|
||||
type: props.multi ? 'checkbox' : 'radio',
|
||||
selectedRowKeys: checkedKeys,
|
||||
selectionRows: selectRows,
|
||||
onChange: onSelectChange,
|
||||
};
|
||||
|
||||
/**
|
||||
* 序号列配置
|
||||
*/
|
||||
const indexColumnProps = {
|
||||
dataIndex: 'index',
|
||||
width: '15px',
|
||||
};
|
||||
/**
|
||||
* 分页配置
|
||||
*/
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: ['10', '20', '30'],
|
||||
// showTotal: (total, range) => {
|
||||
// return range[0] + '-' + range[1] + ' 共' + total + '条'
|
||||
// },
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
total: 0,
|
||||
// 合计逻辑 [待优化 3.0]
|
||||
showTotal: (total) => onShowTotal(total),
|
||||
realPageSize: 10,
|
||||
realTotal: 0,
|
||||
// 是否有合计列,默认为"",在第一次获取到数据之后会设计为ture或者false
|
||||
isTotal: <string | boolean>'',
|
||||
onShowSizeChange: (current, pageSize) => onSizeChange(current, pageSize),
|
||||
});
|
||||
|
||||
/**
|
||||
* 表格选择事件
|
||||
* @param selectedRowKeys
|
||||
* @param selectRow
|
||||
*/
|
||||
function onSelectChange(selectedRowKeys: (string | number)[]) {
|
||||
// update-begin--author:liaozhiyang---date:20240105---for:【QQYUN-7514】popup单选显示radio
|
||||
if (!props.multi) {
|
||||
selectRows.value = [];
|
||||
checkedKeys.value = [];
|
||||
selectedRowKeys = [selectedRowKeys[selectedRowKeys.length - 1]];
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240105---for:【QQYUN-7514】popup单选显示radio
|
||||
// update-begin--author:liaozhiyang---date:20230919---for:【QQYUN-4263】跨页选择导出问题
|
||||
if (!selectedRowKeys || selectedRowKeys.length == 0) {
|
||||
selectRows.value = [];
|
||||
checkedKeys.value = [];
|
||||
} else {
|
||||
if (selectRows.value.length > selectedRowKeys.length) {
|
||||
// 取消
|
||||
selectRows.value.forEach((item, index) => {
|
||||
const rowKey = combineRowKey(item);
|
||||
if (!selectedRowKeys.find((key) => key === rowKey)) {
|
||||
selectRows.value.splice(index, 1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
const append: any = [];
|
||||
const beforeRowKeys = selectRows.value.map((item) => combineRowKey(item));
|
||||
selectedRowKeys.forEach((key) => {
|
||||
if (!beforeRowKeys.find((item) => item === key)) {
|
||||
// 那就是新增选中的行
|
||||
const row = getRowByKey(key);
|
||||
row && append.push(row);
|
||||
}
|
||||
});
|
||||
selectRows.value = [...selectRows.value, ...append];
|
||||
}
|
||||
checkedKeys.value = [...selectedRowKeys];
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230919---for:【QQYUN-4263】跨页选择导出问题
|
||||
}
|
||||
/**
|
||||
* 过滤没用选项
|
||||
* @param selectedRowKeys
|
||||
*/
|
||||
function filterUnuseSelect() {
|
||||
selectRows.value = unref(selectRows).filter((item) => {
|
||||
let combineKey = combineRowKey(item);
|
||||
return unref(checkedKeys).indexOf(combineKey) >= 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据key获取row信息
|
||||
* @param key
|
||||
*/
|
||||
function getRowByKey(key) {
|
||||
let row = unref(dataSource).filter((record) => combineRowKey(record) === key);
|
||||
return row && row.length > 0 ? row[0] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载rowKey
|
||||
*/
|
||||
function combineRowKey(record) {
|
||||
let res = record?.id || '';
|
||||
Object.keys(record).forEach((key) => {
|
||||
res = key == 'rowIndex' ? record[key] + res : res + record[key];
|
||||
});
|
||||
res = res.length > 50 ? res.substring(0, 50) : res;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载列信息
|
||||
*/
|
||||
function loadColumnsInfo() {
|
||||
let url = `${configUrl.getColumns}${props.code}`;
|
||||
//缓存key
|
||||
let groupIdKey = props.groupId ? `${props.groupId}${url}` : '';
|
||||
httpGroupRequest(() => defHttp.get({ url }, { isTransformResponse: false, successMessageMode: 'none' }), groupIdKey).then((res) => {
|
||||
if (res.success) {
|
||||
initDictOptionData(res.result.dictOptions);
|
||||
cgRpConfigId.value = res.result.cgRpConfigId;
|
||||
title.value = res.result.cgRpConfigName;
|
||||
let currColumns = res.result.columns;
|
||||
for (let a = 0; a < currColumns.length; a++) {
|
||||
if (currColumns[a].customRender) {
|
||||
let dictCode = currColumns[a].customRender;
|
||||
currColumns[a].customRender = ({ text }) => {
|
||||
return filterMultiDictText(unref(dictOptions)[dictCode], text + '');
|
||||
};
|
||||
}
|
||||
// 排序字段受控
|
||||
if (unref(iSorter) && currColumns[a].dataIndex === unref(iSorter).column) {
|
||||
currColumns[a].sortOrder = unref(iSorter).order === 'asc' ? 'ascend' : 'descend';
|
||||
}
|
||||
}
|
||||
if (currColumns[0].key !== 'rowIndex') {
|
||||
currColumns.unshift({
|
||||
title: '序号',
|
||||
dataIndex: 'rowIndex',
|
||||
key: 'rowIndex',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
customRender: function ({ text }) {
|
||||
// update-begin--author:liaozhiyang---date:20231226---for:【QQYUN-7584】popup有合计时序号列会出现NaN
|
||||
if (text == undefined) {
|
||||
return '';
|
||||
} else {
|
||||
return parseInt(text) + 1;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231226---for:【QQYUN-7584】popup有合计时序号列会出现NaN
|
||||
},
|
||||
});
|
||||
}
|
||||
columns.value = [...currColumns];
|
||||
initQueryInfo(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载列和数据[列表专用]
|
||||
*/
|
||||
function loadColumnsAndData() {
|
||||
// 第一次加载 置空isTotal 在这里调用确保 该方法只是进入页面后 加载一次 其余查询不走该方法
|
||||
pagination.isTotal = '';
|
||||
let url = `${configUrl.getColumnsAndData}${props.id}`;
|
||||
//缓存key
|
||||
let groupIdKey = props.groupId ? `${props.groupId}${url}` : '';
|
||||
httpGroupRequest(() => defHttp.get({ url }, { isTransformResponse: false, successMessageMode: 'none' }), groupIdKey).then((res) => {
|
||||
if (res.success) {
|
||||
initDictOptionData(res.result.dictOptions);
|
||||
cgRpConfigId.value = props.id;
|
||||
let { columns: metaColumnList, cgreportHeadName, fieldHrefSlots, isGroupTitle } = res.result;
|
||||
title.value = cgreportHeadName;
|
||||
// href 跳转
|
||||
const fieldHrefSlotKeysMap = {};
|
||||
fieldHrefSlots.forEach((item) => (fieldHrefSlotKeysMap[item.slotName] = item));
|
||||
let currColumns = handleColumnHrefAndDict(metaColumnList, fieldHrefSlotKeysMap);
|
||||
|
||||
// popup需要序号, 普通列表不需要
|
||||
if (clickThenCheckFlag === true) {
|
||||
currColumns.unshift({
|
||||
title: '序号',
|
||||
dataIndex: 'rowIndex',
|
||||
key: 'rowIndex',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
customRender: function ({ text }) {
|
||||
return parseInt(text) + 1;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 合并表头
|
||||
if (isGroupTitle === true) {
|
||||
currColumns = handleGroupTitle(currColumns);
|
||||
}
|
||||
columns.value = [...currColumns];
|
||||
initQueryInfo(res.result.data);
|
||||
} else {
|
||||
//update-begin-author:taoyan date:20220401 for: VUEN-583【vue3】JeecgBootException: sql黑名单校验不通过,请联系管理员!,前台无提示
|
||||
createMessage.warning(res.message);
|
||||
//update-end-author:taoyan date:20220401 for: VUEN-583【vue3】JeecgBootException: sql黑名单校验不通过,请联系管理员!,前台无提示
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理求和的列 合计逻辑 [待优化 3.0]
|
||||
*/
|
||||
function handleSumColumn(metaColumnList: OnlineColumn[], dataTotal: number): void {
|
||||
// 获取需要合计列的dataIndex
|
||||
let sumColumnList = getNeedSumColumns(metaColumnList);
|
||||
// 判断是否为第一次获取数据,如果是的话,则需要重新设置pageSize
|
||||
if (pagination.isTotal == '') {
|
||||
if (sumColumnList.length > 0) {
|
||||
pagination.isTotal = true;
|
||||
// 有合计字段时,每次最多查询原pageSize-1条记录,另外需要第一次时将查询的10条中删除最后一条
|
||||
// 删除最后一条数据 如果第一次得到的数据长度等于pageSize的话,则删除最后一条
|
||||
if (dataSource.value.length == pagination.pageSize) {
|
||||
let remove_data = dataSource.value.pop();
|
||||
}
|
||||
pagination.realPageSize = pagination.pageSize - 1;
|
||||
} else {
|
||||
pagination.isTotal = false;
|
||||
}
|
||||
}
|
||||
// 需要添加合计字段
|
||||
if (pagination.isTotal) {
|
||||
let totalRow = {};
|
||||
sumColumnList.forEach((dataIndex) => {
|
||||
let count = 0;
|
||||
dataSource.value.forEach((row) => {
|
||||
// 统计去除null及空数据
|
||||
if (row[dataIndex] != null && row[dataIndex] != '') {
|
||||
count += parseFloat(row[dataIndex]);
|
||||
}
|
||||
});
|
||||
totalRow[dataIndex] = isNaN(count) ? '包含非数字内容' : count.toFixed(2);
|
||||
|
||||
// 长整形时合计不显示.00后缀
|
||||
let v = metaColumnList.find((v) => v.dataIndex == dataIndex);
|
||||
if (v && v.fieldType == 'Long') {
|
||||
totalRow[dataIndex] = parseInt(totalRow[dataIndex]);
|
||||
}
|
||||
});
|
||||
dataSource.value.push(totalRow);
|
||||
pagination.realTotal = dataTotal;
|
||||
pagination.total = Number(dataTotal) + Number(Math.floor(dataTotal / pagination.realPageSize));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要求和的列 dataIndex
|
||||
* @param columns
|
||||
*/
|
||||
function getNeedSumColumns(columns: OnlineColumn[]): string[] {
|
||||
let arr: string[] = [];
|
||||
for (let column of columns) {
|
||||
if (column.isTotal === '1') {
|
||||
arr.push(column.dataIndex!);
|
||||
}
|
||||
// 【VUEN-1569】【online报表】合计无效
|
||||
if (column.children && column.children.length > 0) {
|
||||
let subArray = getNeedSumColumns(column.children);
|
||||
if (subArray.length > 0) {
|
||||
arr.push(...subArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理列的href和字典翻译
|
||||
*/
|
||||
function handleColumnHrefAndDict(columns: OnlineColumn[], fieldHrefSlotKeysMap: {}): OnlineColumn[] {
|
||||
for (let column of columns) {
|
||||
let { customRender, hrefSlotName, fieldType } = column;
|
||||
// online 报表中类型配置为日期(yyyy-MM-dd ),但是实际展示为日期时间格式(yyyy-MM-dd HH:mm:ss) issues/3042
|
||||
if (fieldType == 'Date') {
|
||||
column.customRender = ({ text }) => {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
if (text.length > 10) {
|
||||
return text.substring(0, 10);
|
||||
}
|
||||
return text;
|
||||
};
|
||||
} else {
|
||||
if (!hrefSlotName && column.scopedSlots && column.scopedSlots.customRender) {
|
||||
//【Online报表】字典和href互斥 这里通过fieldHrefSlotKeysMap 先找到是href的列
|
||||
if (fieldHrefSlotKeysMap.hasOwnProperty(column.scopedSlots.customRender)) {
|
||||
hrefSlotName = column.scopedSlots.customRender;
|
||||
}
|
||||
}
|
||||
// 如果 customRender 有值则代表使用了字典
|
||||
// 如果 hrefSlotName 有值则代表使用了href跳转
|
||||
// 两者可以兼容。兼容的具体思路为:先获取到字典替换的值,再添加href链接跳转
|
||||
if (customRender || hrefSlotName) {
|
||||
let dictCode = customRender as string;
|
||||
let replaceFlag = '_replace_text_';
|
||||
column.customRender = ({ text, record }) => {
|
||||
let value = text;
|
||||
// 如果 dictCode 有值,就进行字典转换
|
||||
if (dictCode) {
|
||||
if (dictCode.startsWith(replaceFlag)) {
|
||||
let textFieldName = dictCode.replace(replaceFlag, '');
|
||||
value = record[textFieldName];
|
||||
} else {
|
||||
value = filterMultiDictText(unref(dictOptions)[dictCode], text + '');
|
||||
}
|
||||
}
|
||||
// 扩展参数设置列的内容长度
|
||||
if (column.showLength) {
|
||||
if (value && value.length > column.showLength) {
|
||||
value = value.substr(0, column.showLength) + '...';
|
||||
}
|
||||
}
|
||||
// 如果 hrefSlotName 有值,就生成一个 a 标签,包裹住字典替换后(或原生)的值
|
||||
if (hrefSlotName) {
|
||||
let field = fieldHrefSlotKeysMap[hrefSlotName];
|
||||
if (field) {
|
||||
return h(
|
||||
'a',
|
||||
{
|
||||
onClick: () => handleClickFieldHref(field, record),
|
||||
},
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理合并表头
|
||||
* @param columns
|
||||
*/
|
||||
function handleGroupTitle(columns: OnlineColumn[]): OnlineColumn[] {
|
||||
let newColumns: OnlineColumn[] = [];
|
||||
for (let column of columns) {
|
||||
//排序字段受控 ---- 此逻辑为新增逻辑 待
|
||||
if (unref(iSorter) && column.dataIndex === unref(iSorter).column) {
|
||||
column.sortOrder = unref(iSorter).order === 'asc' ? 'ascend' : 'descend';
|
||||
}
|
||||
//判断字段是否需要合并表头
|
||||
if (column.groupTitle) {
|
||||
let clIndex = newColumns.findIndex((im) => im.title === column.groupTitle);
|
||||
if (clIndex !== -1) {
|
||||
//表头已存在直接push children
|
||||
newColumns[clIndex].children!.push(column);
|
||||
} else {
|
||||
//表头不存在组装表头信息
|
||||
let clGroup: OnlineColumn = {},
|
||||
child: OnlineColumn[] = [];
|
||||
child.push(column);
|
||||
clGroup.title = column.groupTitle;
|
||||
clGroup.align = 'center';
|
||||
clGroup.children = child;
|
||||
newColumns.push(clGroup);
|
||||
}
|
||||
} else {
|
||||
newColumns.push(column);
|
||||
}
|
||||
}
|
||||
return newColumns;
|
||||
}
|
||||
|
||||
// 获取路由器对象 href跳转用到
|
||||
let router = useRouter();
|
||||
/**
|
||||
* href 点击事件
|
||||
* @param field
|
||||
* @param record
|
||||
*/
|
||||
function handleClickFieldHref(field, record) {
|
||||
let href = field.href;
|
||||
let urlPattern = /(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?/;
|
||||
let compPattern = /\.vue(\?.*)?$/;
|
||||
let jsPattern = /{{([^}]+)}}/g; // {{ xxx }}
|
||||
if (typeof href === 'string') {
|
||||
href = href.trim().replace(/\${([^}]+)?}/g, (s1, s2) => record[s2]);
|
||||
// 执行 {{...}} JS增强语句
|
||||
if (jsPattern.test(href)) {
|
||||
href = href.replace(jsPattern, function (text, s0) {
|
||||
try {
|
||||
// 支持 {{ ACCESS_TOKEN }} 占位符
|
||||
if (s0.trim() === 'ACCESS_TOKEN') {
|
||||
return getToken()
|
||||
}
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20230904---for:【QQYUN-6390】eval替换成new Function,解决build警告
|
||||
return _eval(s0);
|
||||
// update-end--author:liaozhiyang---date:20230904---for:【QQYUN-6390】eval替换成new Function,解决build警告
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return text;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (urlPattern.test(href)) {
|
||||
window.open(href, '_blank');
|
||||
} else if (compPattern.test(href)) {
|
||||
// 处理弹框
|
||||
openHrefCompModal(href);
|
||||
} else {
|
||||
router.push(href);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
function handleExport() {
|
||||
const { handleExportXls } = useMethods();
|
||||
let url = `${configUrl.export}${cgRpConfigId.value}`;
|
||||
let params = getQueryParams(); //查询条件
|
||||
// 【VUEN-1568】如果选中了某些行,就只导出选中的行
|
||||
let keys = unref(checkedKeys);
|
||||
if (keys.length > 0) {
|
||||
params['force_id'] = keys
|
||||
.map((i) => selectRows.value.find((item) => combineRowKey(item) === i)?.id)
|
||||
.filter((i) => i != null && i !== '')
|
||||
.join(',');
|
||||
}
|
||||
handleExportXls(title.value, url, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合计逻辑 [待优化 3.0]
|
||||
* 分页 大小改变事件
|
||||
* @param _current
|
||||
* @param size
|
||||
*/
|
||||
function onSizeChange(_current, size) {
|
||||
pagination.isTotal = '';
|
||||
pagination.pageSize = size;
|
||||
if (pagination.isTotal) {
|
||||
pagination.realPageSize = size - 1;
|
||||
} else {
|
||||
pagination.realPageSize = size;
|
||||
}
|
||||
pagination.current = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合计逻辑 [待优化 3.0]
|
||||
* 显示总条数
|
||||
* @param total
|
||||
*/
|
||||
function onShowTotal(total) {
|
||||
// 重新根据是否有合计计算每页显示的数据
|
||||
let start = (pagination.current - 1) * pagination.realPageSize + 1;
|
||||
let end = start + (pagination.isTotal ? dataSource.value.length - 1 : dataSource.value.length) - 1;
|
||||
let realTotal = pagination.isTotal ? pagination.realTotal : total;
|
||||
return start + '-' + end + ' 共' + realTotal + '条';
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出框显示隐藏触发事件
|
||||
*/
|
||||
async function visibleChange($event) {
|
||||
visible.value = $event;
|
||||
$event && loadColumnsInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化查询条件
|
||||
* @param data 数据结果集
|
||||
*/
|
||||
function initQueryInfo(data) {
|
||||
let url = `${configUrl.getQueryInfo}${unref(cgRpConfigId)}`;
|
||||
//缓存key
|
||||
let groupIdKey = props.groupId ? `${props.groupId}${url}` : '';
|
||||
httpGroupRequest(() => defHttp.get({ url }, { isTransformResponse: false, successMessageMode: 'none' }), groupIdKey).then((res) => {
|
||||
// console.log("获取查询条件", res);
|
||||
if (res.success) {
|
||||
dynamicParamHandler(res.result);
|
||||
queryInfo.value = res.result;
|
||||
console.log('queryInfo==>', queryInfo.value);
|
||||
//查询条件加载后再请求数据
|
||||
if (data) {
|
||||
setDataSource(data);
|
||||
//传递路由参数和动态参数,不生效,
|
||||
loadData(1);
|
||||
} else {
|
||||
//没有传递data时查询数据
|
||||
loadData(1);
|
||||
}
|
||||
} else {
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载表格数据
|
||||
* @param arg
|
||||
*/
|
||||
function loadData(arg?) {
|
||||
if (arg == 1) {
|
||||
pagination.current = 1;
|
||||
}
|
||||
let params = getQueryParams(); //查询条件
|
||||
params['onlRepUrlParamStr'] = getUrlParamString();
|
||||
console.log('params', params);
|
||||
loading.value = true;
|
||||
// update-begin--author:liaozhiyang---date:20240603---for:【TV360X-578】online报表SQL翻译,第二页不翻页数据
|
||||
let url = `${configUrl.getColumnsAndData}${unref(cgRpConfigId)}`;
|
||||
// update-end--author:liaozhiyang---date:20240603---for:【TV360X-578】online报表SQL翻译,第二页不翻页数据
|
||||
//缓存key
|
||||
let groupIdKey = props.groupId ? `${props.groupId}${url}${JSON.stringify(params)}` : '';
|
||||
httpGroupRequest(() => defHttp.get({ url, params }, { isTransformResponse: false, successMessageMode: 'none' }), groupIdKey).then((res) => {
|
||||
// update-begin--author:liaozhiyang---date:20240603---for:【TV360X-578】online报表SQL翻译,第二页不翻页数据
|
||||
res.result.dictOptions && initDictOptionData(res.result.dictOptions);
|
||||
// update-end--author:liaozhiyang---date:20240603---for:【TV360X-578】online报表SQL翻译,第二页不翻页数据
|
||||
loading.value = false;
|
||||
// update-begin--author:liaozhiyang---date:20240603---for:【TV360X-578】online报表SQL翻译,第二页不翻页数据
|
||||
let data = res.result.data;
|
||||
// update-end--author:liaozhiyang---date:20240603---for:【TV360X-578】online报表SQL翻译,第二页不翻页数据
|
||||
console.log('表格信息:', data);
|
||||
setDataSource(data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地址栏的参数
|
||||
*/
|
||||
function getUrlParamString() {
|
||||
let query = route.query;
|
||||
let arr:any[] = []
|
||||
if(query && Object.keys(query).length>0){
|
||||
Object.keys(query).map(k=>{
|
||||
arr.push(`${k}=${query[k]}`)
|
||||
})
|
||||
}
|
||||
return arr.join('&')
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置dataSource
|
||||
*/
|
||||
function setDataSource(data) {
|
||||
if (data) {
|
||||
pagination.total = Number(data.total);
|
||||
let currentPage = pagination?.current ?? 1;
|
||||
for (let a = 0; a < data.records.length; a++) {
|
||||
if (!data.records[a].rowIndex) {
|
||||
data.records[a].rowIndex = a + (currentPage - 1) * 10;
|
||||
}
|
||||
}
|
||||
dataSource.value = data.records;
|
||||
//update-begin-author:taoyan date:2023-2-11 for:issues/356 在线报表分页有问题
|
||||
//update-begin-author:liusq date:2023-4-04 for:issues/426 修复356时候引入的回归错误 JPopupOnlReportModal.vue 中未修改
|
||||
tableRef?.value && tableRef?.value?.setPagination({
|
||||
total: Number(data.total)
|
||||
})
|
||||
//update-end-author:liusq date:2023-4-04 for:issues/426 修复356时候引入的回归错误 JPopupOnlReportModal.vue 中未修改
|
||||
//update-end-author:taoyan date:2023-2-11 for:issues/356 在线报表分页有问题
|
||||
} else {
|
||||
pagination.total = 0;
|
||||
dataSource.value = [];
|
||||
}
|
||||
// 合计逻辑 [待优化 3.0]
|
||||
handleSumColumn(columns.value, pagination.total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询参数
|
||||
*/
|
||||
function getQueryParams() {
|
||||
let paramTarget = {};
|
||||
if (unref(dynamicParam)) {
|
||||
//处理自定义参数
|
||||
Object.keys(unref(dynamicParam)).map((key) => {
|
||||
paramTarget['self_' + key] = unref(dynamicParam)[key];
|
||||
});
|
||||
}
|
||||
let param = Object.assign(paramTarget, unref(queryParam), unref(iSorter));
|
||||
param.pageNo = pagination.current;
|
||||
// 合计逻辑 [待优化 3.0]
|
||||
// 实际查询时不使用table组件的pageSize,而使用自定义的realPageSize,realPageSize会在第一次获取到数据后变化
|
||||
param.pageSize = pagination.realPageSize;
|
||||
return filterObj(param);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理动态参数
|
||||
*/
|
||||
function dynamicParamHandler(arr?) {
|
||||
if (arr && arr.length > 0) {
|
||||
//第一次加载查询条件前 初始化queryParam为空对象
|
||||
let queryTemp = {};
|
||||
for (let item of arr) {
|
||||
if (item.mode === 'single') {
|
||||
queryTemp[item.field] = '';
|
||||
}
|
||||
}
|
||||
queryParam.value = { ...queryTemp };
|
||||
}
|
||||
// 合并路由参数
|
||||
if (props.routeQuery) {
|
||||
queryParam.value = Object.assign(queryParam.value, props.routeQuery);
|
||||
}
|
||||
|
||||
let dynamicTemp = {};
|
||||
if (props.param) {
|
||||
Object.keys(props.param).map((key) => {
|
||||
let str = props.param[key];
|
||||
if (key in queryParam) {
|
||||
if (str && str.startsWith("'") && str.endsWith("'")) {
|
||||
str = str.substring(1, str.length - 1);
|
||||
}
|
||||
//如果查询条件包含参数 设置值
|
||||
unref(queryParam)[key] = str;
|
||||
}
|
||||
dynamicTemp[key] = props.param[key];
|
||||
});
|
||||
}
|
||||
dynamicParam.value = { ...dynamicTemp };
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页
|
||||
* @param page
|
||||
* @param filters
|
||||
* @param sorter
|
||||
*/
|
||||
function handleChangeInTable(page, filters, sorter) {
|
||||
console.log(page, filters, sorter);
|
||||
//分页、排序、筛选变化时触发
|
||||
if (Object.keys(sorter).length > 0) {
|
||||
iSorter.value = {
|
||||
column: sorter.field,
|
||||
order: 'ascend' === sorter.order ? 'asc' : 'desc',
|
||||
};
|
||||
// 排序字段受控
|
||||
unref(columns).forEach((col) => {
|
||||
if (col['dataIndex'] === sorter.field) {
|
||||
col['sortOrder'] = sorter.order;
|
||||
}
|
||||
});
|
||||
}
|
||||
pagination.current = page.current;
|
||||
pagination.pageSize = page.pageSize;
|
||||
loadData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 行点击事件
|
||||
* @param record
|
||||
*/
|
||||
function clickThenCheck(record) {
|
||||
if (clickThenCheckFlag === true) {
|
||||
// update-begin--author:liaozhiyang---date:20240104---for:【QQYUN-7514】popup单选显示radio
|
||||
if (!props.multi) {
|
||||
selectRows.value = [];
|
||||
checkedKeys.value = [];
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240104---for:【QQYUN-7514】popup单选显示radio
|
||||
let rowKey = combineRowKey(record);
|
||||
if (!unref(checkedKeys) || unref(checkedKeys).length == 0) {
|
||||
let arr1: any[] = [],
|
||||
arr2: any[] = [];
|
||||
arr1.push(record);
|
||||
arr2.push(rowKey);
|
||||
checkedKeys.value = arr2;
|
||||
//selectRows.value = arr1;
|
||||
} else {
|
||||
if (unref(checkedKeys).indexOf(rowKey) < 0) {
|
||||
//不存在就选中
|
||||
checkedKeys.value.push(rowKey);
|
||||
//selectRows.value.push(record);
|
||||
} else {
|
||||
//已选中就取消
|
||||
let rowKey_index = unref(checkedKeys).indexOf(rowKey);
|
||||
checkedKeys.value.splice(rowKey_index, 1);
|
||||
//selectRows.value.splice(rowKey_index, 1);
|
||||
}
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20230914---for:【issues/5357】点击行选中
|
||||
tableRef.value.setSelectedRowKeys([...checkedKeys.value]);
|
||||
// update-end--author:liaozhiyang---date:20230914---for:【issues/5357】点击行选中
|
||||
}
|
||||
}
|
||||
|
||||
//防止字典中有垃圾数据
|
||||
function initDictOptionData(arr) {
|
||||
let obj = {};
|
||||
Object.keys(arr).map((k) => {
|
||||
obj[k] = arr[k].filter((item) => {
|
||||
return item != null;
|
||||
});
|
||||
});
|
||||
dictOptions.value = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤对象中为空的属性
|
||||
* @param obj
|
||||
* @returns {*}
|
||||
*/
|
||||
function filterObj(obj) {
|
||||
if (!(typeof obj == 'object')) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let key in obj) {
|
||||
if (obj.hasOwnProperty(key) && (obj[key] == null || obj[key] == undefined || obj[key] === '')) {
|
||||
delete obj[key];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 样式
|
||||
const dialogStyle = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
// 弹窗属性配置
|
||||
const hrefComponent = ref({
|
||||
model: {
|
||||
title: '',
|
||||
okText: '关闭',
|
||||
width: '100%',
|
||||
open: false,
|
||||
destroyOnClose: true,
|
||||
style: dialogStyle,
|
||||
// dialogStyle: dialogStyle,
|
||||
bodyStyle: {
|
||||
padding: '8px',
|
||||
height: 'calc(100vh - 108px)',
|
||||
overflow: 'auto',
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
// 隐藏掉取消按钮
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
},
|
||||
on: {
|
||||
ok: () => (hrefComponent.value.model.open = false),
|
||||
cancel: () => (hrefComponent.value.model.open = false),
|
||||
},
|
||||
is: <any>null,
|
||||
params: {},
|
||||
});
|
||||
|
||||
// 超链点击事件--> 打开一个modal窗口
|
||||
function openHrefCompModal(href) {
|
||||
// 解析 href 参数
|
||||
let index = href.indexOf('?');
|
||||
let path = href;
|
||||
if (index !== -1) {
|
||||
path = href.substring(0, index);
|
||||
let paramString = href.substring(index + 1, href.length);
|
||||
let paramArray = paramString.split('&');
|
||||
let params = {};
|
||||
paramArray.forEach((paramObject) => {
|
||||
let paramItem = paramObject.split('=');
|
||||
params[paramItem[0]] = paramItem[1];
|
||||
});
|
||||
hrefComponent.value.params = params;
|
||||
} else {
|
||||
hrefComponent.value.params = {};
|
||||
}
|
||||
hrefComponent.value.model.open = true;
|
||||
hrefComponent.value.model.title = '操作';
|
||||
hrefComponent.value.is = markRaw(defineAsyncComponent(() => importViewsFile(path)));
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2022-5-31 for: VUEN-1155 popup 选择数据时,会选择多条重复数据
|
||||
/**
|
||||
* emit事件 获取选中的行数据
|
||||
*/
|
||||
function getOkSelectRows(): any[] {
|
||||
let arr = unref(selectRows);
|
||||
let selectedRowKeys = checkedKeys.value;
|
||||
console.log('arr', arr);
|
||||
if (!selectedRowKeys || selectedRowKeys.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (!arr || arr.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
let rows: any = [];
|
||||
for (let key of selectedRowKeys) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let combineKey = combineRowKey(arr[i]);
|
||||
if (key === combineKey) {
|
||||
rows.push(toRaw(arr[i]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
//update-end-author:taoyan date:2022-5-31 for: VUEN-1155 popup 选择数据时,会选择多条重复数据
|
||||
|
||||
return [
|
||||
{
|
||||
visibleChange,
|
||||
loadColumnsInfo,
|
||||
loadColumnsAndData,
|
||||
dynamicParamHandler,
|
||||
loadData,
|
||||
handleChangeInTable,
|
||||
combineRowKey,
|
||||
clickThenCheck,
|
||||
filterUnuseSelect,
|
||||
handleExport,
|
||||
getOkSelectRows,
|
||||
},
|
||||
{
|
||||
hrefComponent,
|
||||
visible,
|
||||
rowSelection,
|
||||
checkedKeys,
|
||||
selectRows,
|
||||
pagination,
|
||||
dataSource,
|
||||
columns,
|
||||
indexColumnProps,
|
||||
loading,
|
||||
title,
|
||||
iSorter,
|
||||
queryInfo,
|
||||
queryParam,
|
||||
dictOptions,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
interface ScopedSlots {
|
||||
customRender: string;
|
||||
}
|
||||
|
||||
interface HrefSlots {
|
||||
// 链接地址
|
||||
href: string;
|
||||
// fieldHref_字段名
|
||||
slotName: string;
|
||||
}
|
||||
|
||||
interface OnlineColumn {
|
||||
dataIndex?: string;
|
||||
title?: string;
|
||||
key?: string;
|
||||
fieldType?: string;
|
||||
width?: number | string;
|
||||
align?: string;
|
||||
sorter?: string | boolean;
|
||||
isTotal?: string | number | boolean;
|
||||
groupTitle?: string;
|
||||
// 超链的时候 和HrefSlots中的slotName匹配
|
||||
scopedSlots?: ScopedSlots;
|
||||
// 一般用于字典 字典传过来的是字典编码字符串 后转函数
|
||||
customRender?: string | Function;
|
||||
// 这个类型不知道有什么用
|
||||
hrefSlotName?: string;
|
||||
showLength?: number | string;
|
||||
children?: OnlineColumn[];
|
||||
sortOrder?: string;
|
||||
// 插槽对应控件类型(列表)
|
||||
slots?: ScopedSlots;
|
||||
//超过宽度将自动省略,暂不支持和排序筛选一起使用。
|
||||
ellipsis?: boolean;
|
||||
// 是否固定列
|
||||
fixed?: boolean | 'left' | 'right';
|
||||
//字段类型 int/string
|
||||
dbType?:string;
|
||||
//他表字段用
|
||||
linkField?:string;
|
||||
}
|
||||
|
||||
export { OnlineColumn, HrefSlots };
|
||||
142
jeecgboot-vue3/src/components/jeecg/UserAvatar.vue
Normal file
142
jeecgboot-vue3/src/components/jeecg/UserAvatar.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="user-avatar-info">
|
||||
<a-popover title="" :overlayStyle="{width: '250px'}">
|
||||
<template #content>
|
||||
<div style="display: flex;flex-direction: row;align-items: center">
|
||||
<div style="width: 60px;text-align: center">
|
||||
<a-avatar v-if="userAvatar" :src="userAvatar" :size="47"/>
|
||||
<a-avatar v-else :size="47">{{ getAvatarText() }}</a-avatar>
|
||||
</div>
|
||||
<div style="flex: 1;display: flex;flex-direction: column;margin-left: 12px">
|
||||
<div style="color: #000;display: inline-block;font-size: 15px;font-weight: 700;margin-top: 3px;vertical-align: top;width: 170px;">
|
||||
{{ userLabel }}
|
||||
</div>
|
||||
<div style="color: #757575;display: block;margin-top: 4px;">
|
||||
{{ phone }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span style="cursor: pointer">
|
||||
<a-avatar v-if="userAvatar" :src="userAvatar" :loadError="loadError"/>
|
||||
<a-avatar v-else>{{ getAvatarText() }}</a-avatar>
|
||||
</span>
|
||||
</a-popover>
|
||||
<span class="realname-ellipsis">
|
||||
{{ userLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, watchEffect, defineComponent } from 'vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserAvatar',
|
||||
props: {
|
||||
username: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
detail:{
|
||||
type: Object,
|
||||
default: ()=>{},
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const userAvatar = ref('');
|
||||
const userLabel = ref('');
|
||||
const phone = ref('');
|
||||
|
||||
watchEffect(async ()=>{
|
||||
userAvatar.value = '';
|
||||
userLabel.value = '';
|
||||
phone.value = '';
|
||||
let username = props.username;
|
||||
if(username){
|
||||
await initUserInfo(username);
|
||||
}
|
||||
let userInfo = props.detail;
|
||||
if(userInfo){
|
||||
if(userInfo.avatar){
|
||||
userAvatar.value = getFileAccessHttpUrl(userInfo.avatar);
|
||||
}
|
||||
if(userInfo.realname){
|
||||
userLabel.value = userInfo.realname;
|
||||
}
|
||||
if(userInfo.phone){
|
||||
phone.value = userInfo.phone;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function initUserInfo(val) {
|
||||
const params = {
|
||||
username: val,
|
||||
};
|
||||
const url = '/sys/user/getMultiUser';
|
||||
const data = await defHttp.get({ url, params }, {isTransformResponse: false});
|
||||
if(data && data.length > 0){
|
||||
let temp = data[0].avatar;
|
||||
if (temp) {
|
||||
userAvatar.value = getFileAccessHttpUrl(temp)
|
||||
}
|
||||
userLabel.value = data[0].realname;
|
||||
phone.value = data[0].phone;
|
||||
}else{
|
||||
console.log(data)
|
||||
}
|
||||
}
|
||||
|
||||
function getAvatarText() {
|
||||
let text = userLabel.value;
|
||||
if (!text) {
|
||||
text = props.username;
|
||||
}
|
||||
if (text) {
|
||||
if (text.length > 2) {
|
||||
return text.substr(0, 2);
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function loadError() {
|
||||
userAvatar.value = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
userAvatar,
|
||||
userLabel,
|
||||
getAvatarText,
|
||||
phone,
|
||||
loadError
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-avatar-info{
|
||||
.ant-avatar-image{
|
||||
cursor: pointer;
|
||||
}
|
||||
.realname-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
133
jeecgboot-vue3/src/components/jeecg/captcha/CaptchaModal.vue
Normal file
133
jeecgboot-vue3/src/components/jeecg/captcha/CaptchaModal.vue
Normal file
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<BasicModal @register="registerModal" width="450px" :minHeight="100" :title="title" @ok="handleSubmit" destroyOnClose :canFullscreen="false">
|
||||
<BasicForm @register="registerForm">
|
||||
<template #captchaSlot="{ model, field }">
|
||||
<div style="width: 100%; display: flex">
|
||||
<a-input style="width: 200px" v-model:value="model[field]" placeholder="请输入图片验证码" />
|
||||
<div class="margin-left10">
|
||||
<img
|
||||
class="pointer"
|
||||
v-if="randCodeData.requestCodeSuccess"
|
||||
style="margin-top: 2px; max-width: initial; height: 30px"
|
||||
:src="randCodeData.randCodeImage"
|
||||
@click="getCaptchaCode"
|
||||
/>
|
||||
<img v-else class="pointer" style="margin-top: 2px; max-width: initial; height: 30px" :src="codeImage" @click="getCaptchaCode" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="CaptchaModal">
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||
import { BasicForm, FormSchema, useForm } from '@/components/Form';
|
||||
import codeImage from '@/assets/images/checkcode.png';
|
||||
import { getCodeInfo } from '@/api/sys/user';
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import {useMessage} from "@/hooks/web/useMessage";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CaptchaModal',
|
||||
components: { BasicModal, BasicForm },
|
||||
emits: ['ok','register'],
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('验证码');
|
||||
const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'captcha',
|
||||
component: 'Input',
|
||||
label: '图片验证码',
|
||||
rules: [{ required: true }],
|
||||
slot: 'captchaSlot',
|
||||
},
|
||||
];
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, validate }] = useForm({
|
||||
schemas: schemas,
|
||||
showActionButtonGroup: false,
|
||||
baseRowStyle: { "justify-content": 'center', "display": "grid", "margin-top": "10px" },
|
||||
rowProps: { "justify": "center" },
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
|
||||
//表单赋值
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
setModalProps({ confirmLoading: true });
|
||||
//重置表单
|
||||
await resetFields();
|
||||
await getCaptchaCode();
|
||||
setModalProps({ confirmLoading: false });
|
||||
});
|
||||
|
||||
//存放二维码的数据
|
||||
const randCodeData = reactive({
|
||||
randCodeImage: '',
|
||||
requestCodeSuccess: false,
|
||||
checkKey: -1,
|
||||
});
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
async function getCaptchaCode() {
|
||||
await resetFields();
|
||||
randCodeData.checkKey = new Date().getTime();
|
||||
getCodeInfo(randCodeData.checkKey).then((res) => {
|
||||
randCodeData.randCodeImage = res;
|
||||
randCodeData.requestCodeSuccess = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 第三方配置点击事件
|
||||
*/
|
||||
async function handleSubmit() {
|
||||
let values = await validate();
|
||||
defHttp.post({ url: '/sys/smsCheckCaptcha', params: { captcha: values.captcha, checkKey: randCodeData.checkKey } }, { isTransformResponse: false }).then((res)=>{
|
||||
if(res.success){
|
||||
emit('ok');
|
||||
closeModal();
|
||||
}else{
|
||||
createMessage.warning(res.message);
|
||||
getCaptchaCode();
|
||||
}
|
||||
}).catch((res) =>{
|
||||
createMessage.warning(res.message);
|
||||
getCaptchaCode();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
registerForm,
|
||||
registerModal,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
randCodeData,
|
||||
codeImage,
|
||||
getCaptchaCode,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.margin-left10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
:deep(.ant-row){
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
183
jeecgboot-vue3/src/components/jeecg/comment/CommentFiles.vue
Normal file
183
jeecgboot-vue3/src/components/jeecg/comment/CommentFiles.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<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>
|
||||
</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 {useModal} from "/@/components/Modal";
|
||||
|
||||
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 [registerModel, { openModal }] = useModal();
|
||||
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();
|
||||
}
|
||||
|
||||
function showFileModal() {
|
||||
openModal(true, {})
|
||||
}
|
||||
|
||||
function onSelectFileOk(temp) {
|
||||
// update-begin--author:liaozhiyang---date:20240603---for:【TV360X-935】从知识库选择文件判断下是否没选
|
||||
if (temp.id === '') return;
|
||||
// update-end--author:liaozhiyang---date:20240603---for:【TV360X-935】从知识库选择文件判断下是否没选
|
||||
let arr = selectFileList.value;
|
||||
arr.push({
|
||||
...temp,
|
||||
exist: true
|
||||
})
|
||||
selectFileList.value = arr;
|
||||
}
|
||||
|
||||
return {
|
||||
selectFileList,
|
||||
beforeUpload,
|
||||
handleRemove,
|
||||
getBackground,
|
||||
isImage,
|
||||
dataList,
|
||||
uploadFileUrl,
|
||||
quxiao,
|
||||
queding,
|
||||
buttonLoading,
|
||||
getImageAsBackground,
|
||||
viewImage,
|
||||
registerModel,
|
||||
showFileModal,
|
||||
onSelectFileOk,
|
||||
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import 'comment.less';
|
||||
</style>
|
||||
361
jeecgboot-vue3/src/components/jeecg/comment/CommentList.vue
Normal file
361
jeecgboot-vue3/src/components/jeecg/comment/CommentList.vue
Normal file
@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div :style="{ position: 'relative', height: allHeight + 'px' }">
|
||||
<a-list ref="listRef" 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" @openChange="(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 class="content" 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 class="comment-area">
|
||||
<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 ,inject, nextTick } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
// import dayjs from 'dayjs';
|
||||
// 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, getGloablEmojiIndex } 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),
|
||||
// 其他需要减去的高度
|
||||
otherHeight: propTypes.number.def(0),
|
||||
},
|
||||
setup(props) {
|
||||
const { createMessage } = useMessage();
|
||||
const dataList = ref([]);
|
||||
const { userInfo } = useUserStore();
|
||||
const dayjs = inject('$dayjs')
|
||||
const listRef = ref(null);
|
||||
/**
|
||||
* 获取当前用户名称
|
||||
*/
|
||||
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(() => {
|
||||
let otherHeight = props.otherHeight || 0;
|
||||
commentHeight.value = window.innerHeight - 57 - 46 - 70 - 160 - otherHeight;
|
||||
allHeight.value = window.innerHeight - 57 - 46 - 53 -20 - otherHeight;
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载数据
|
||||
* @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;
|
||||
// update-begin--author:liaozhiyang---date:20240521---for:【TV360X-18】评论之后滚动条自动触底
|
||||
// Number.MAX_SAFE_INTEGER 火狐不兼容改成 10e4
|
||||
nextTick(() => {
|
||||
listRef.value && listRef.value.$el && (listRef.value.$el.scrollTop = 10e5);
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240521---for:【TV360X-18】评论之后滚动条自动触底
|
||||
}
|
||||
}
|
||||
|
||||
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 storageEmojiIndex = inject('$globalEmojiIndex')
|
||||
const storageEmojiIndex = getGloablEmojiIndex()
|
||||
const { getHtml } = useEmojiHtml(storageEmojiIndex);
|
||||
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,
|
||||
listRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</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: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-list-items{
|
||||
.ant-list-item:last-child{
|
||||
margin-bottom: 46px;
|
||||
}
|
||||
}
|
||||
.tx{
|
||||
margin-top: 4px;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
.comment-area {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #eee;
|
||||
background-color: #fff;
|
||||
}
|
||||
html[data-theme='dark'] {
|
||||
.comment-area {
|
||||
border-color: rgba(253, 253, 253, 0.12);
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
.content {
|
||||
color:rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
</style>
|
||||
110
jeecgboot-vue3/src/components/jeecg/comment/CommentPanel.vue
Normal file
110
jeecgboot-vue3/src/components/jeecg/comment/CommentPanel.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="comment-tabs-warp" v-if="showStatus">
|
||||
<a-tabs v-if="show" @change="handleChange" :animated="false">
|
||||
<a-tab-pane v-if="showComment" tab="评论" key="comment" class="comment-list-tab">
|
||||
<comment-list :tableName="tableName" :dataId="dataId" :datetime="datetime1" :otherHeight="otherHeight"></comment-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane v-if="showFiles" tab="文件" key="file">
|
||||
<comment-files :tableName="tableName" :dataId="dataId" :datetime="datetime2"></comment-files>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane v-if="showDataLog" 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, nextTick } 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(''),
|
||||
// 显示评论
|
||||
showComment: propTypes.bool.def(true),
|
||||
// 显示文件
|
||||
showFiles: propTypes.bool.def(true),
|
||||
// 显示日志
|
||||
showDataLog: propTypes.bool.def(true),
|
||||
// 其他需要减去的高度
|
||||
otherHeight: propTypes.number.def(0),
|
||||
},
|
||||
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);
|
||||
const show = ref(true);
|
||||
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;
|
||||
// update-begin--author:liaozhiyang---date:20240527---for:【TV360X-486】再次打开重置组件内的状态
|
||||
// 再次打开重置组件内的状态
|
||||
show.value = false;
|
||||
nextTick(() => {
|
||||
show.value = true;
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240527---for:【TV360X-486】再次打开重置组件内的状态
|
||||
}
|
||||
|
||||
return {
|
||||
showStatus,
|
||||
handleChange,
|
||||
datetime1,
|
||||
datetime2,
|
||||
datetime3,
|
||||
reload,
|
||||
show,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.comment-tabs-warp {
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
> .ant-tabs {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
//antd3升级后,表单右侧讨论样式调整
|
||||
: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
jeecgboot-vue3/src/components/jeecg/comment/DataLogList.vue
Normal file
177
jeecgboot-vue3/src/components/jeecg/comment/DataLogList.vue
Normal 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.createName || 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 {inject, ref, watchEffect} from 'vue'
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
// import dayjs from 'dayjs';
|
||||
// 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 dayjs = inject('$dayjs')
|
||||
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>
|
||||
@ -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>
|
||||
420
jeecgboot-vue3/src/components/jeecg/comment/MyComment.vue
Normal file
420
jeecgboot-vue3/src/components/jeecg/comment/MyComment.vue
Normal file
@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<div :class="{'comment-active': commentActive}" class="comment-main" @click="handleClickBlank">
|
||||
<textarea ref="commentRef" v-model="myComment" @keyup.enter="sendComment" @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 rowKey="username" @register="registerModal" @selected="setValue" :multi="false"></UserSelectModal>
|
||||
<a-modal v-model:open="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, inject} 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/userSelect/UserSelectModal.vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import UploadChunk from './UploadChunk.vue';
|
||||
import 'emoji-mart-vue-fast/css/emoji-mart.css';
|
||||
import {getGloablEmojiIndex, 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,
|
||||
},
|
||||
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, closeModal }] = 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 { realname, username } = options[0];
|
||||
if (realname && username) {
|
||||
let str = `${realname}[${username}]`;
|
||||
let temp = myComment.value;
|
||||
if (!temp) {
|
||||
myComment.value = '@' + str;
|
||||
} else {
|
||||
if (temp.endsWith('@')) {
|
||||
myComment.value = temp + str +' ';
|
||||
} else {
|
||||
myComment.value = '@' + str + ' ' + temp + ' ';
|
||||
}
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2024-01-22---for:【QQYUN-8002】选完人,鼠标应该放到后面并在前面加上空格---
|
||||
showHtml.value = false;
|
||||
commentRef.value.focus();
|
||||
commentActive.value = true;
|
||||
//update-end---author:wangshuai---date:2024-01-22---for:【QQYUN-8002】选完人,鼠标应该放到后面并在前面加上空格---
|
||||
}
|
||||
}
|
||||
closeModal();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240603---for:【TV360X-931】评论表情插入光标位置
|
||||
const index = commentRef.value?.selectionStart ?? temp.length;
|
||||
// myComment.value = temp + str;
|
||||
const startStr = temp.substring(0, index);
|
||||
const endStr = temp.substring(index);
|
||||
myComment.value = startStr + str + endStr;
|
||||
// update-end--author:liaozhiyang---date:20240603---for:【TV360X-931】评论表情插入光标位置
|
||||
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 = inject('$globalEmojiIndex')
|
||||
const emojiIndex = getGloablEmojiIndex()
|
||||
const { getHtml } = useEmojiHtml(emojiIndex);
|
||||
|
||||
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">
|
||||
// update-begin--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
.comment-main {
|
||||
border: 1px solid #eee;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
.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: @primary-color !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");
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
html[data-theme='dark'] {
|
||||
.emoji-type-image.emoji-set-apple {
|
||||
background-image: url("./image/emoji_native.png");
|
||||
}
|
||||
.comment-main {
|
||||
border-color: rgba(253, 253, 253, 0.12);
|
||||
}
|
||||
.comment-content {
|
||||
background-color: #141414;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
border-color: rgba(253, 253, 253, 0.12);
|
||||
}
|
||||
.comment-buttons{
|
||||
border-color: rgba(253, 253, 253, 0.12);
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
</style>
|
||||
137
jeecgboot-vue3/src/components/jeecg/comment/UploadChunk.vue
Normal file
137
jeecgboot-vue3/src/components/jeecg/comment/UploadChunk.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<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>
|
||||
</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';
|
||||
import {useModal} from "/@/components/Modal";
|
||||
|
||||
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();
|
||||
|
||||
const [registerModel, { openModal }] = useModal();
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function showFileModal() {
|
||||
openModal(true, {})
|
||||
}
|
||||
|
||||
function onSelectFileOk(temp) {
|
||||
let arr = selectFileList.value;
|
||||
arr.push({
|
||||
...temp,
|
||||
exist: true
|
||||
})
|
||||
selectFileList.value = arr;
|
||||
}
|
||||
|
||||
return {
|
||||
selectFileList,
|
||||
beforeUpload,
|
||||
handleRemove,
|
||||
getBackground,
|
||||
getUploadFileList,
|
||||
clear,
|
||||
isImage,
|
||||
getImageSrc,
|
||||
viewImage,
|
||||
registerModel,
|
||||
showFileModal,
|
||||
onSelectFileOk
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import 'comment.less';
|
||||
</style>
|
||||
252
jeecgboot-vue3/src/components/jeecg/comment/comment.less
Normal file
252
jeecgboot-vue3/src/components/jeecg/comment/comment.less
Normal file
@ -0,0 +1,252 @@
|
||||
/*文件上传列表-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: rgba(51, 51, 51, 0.6) !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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
html[data-theme='dark'] {
|
||||
.jeecg-comment-files {
|
||||
&.ant-alert-info {
|
||||
background-color: #141414;
|
||||
border: 1px solid #3a3a3a;
|
||||
}
|
||||
.j-icon,
|
||||
.j-icon:hover {
|
||||
border-color: #3a3a3a;
|
||||
.inner-button:hover {
|
||||
color: #bebebe;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
BIN
jeecgboot-vue3/src/components/jeecg/comment/image/emoji.png
Normal file
BIN
jeecgboot-vue3/src/components/jeecg/comment/image/emoji.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 839 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 MiB |
459
jeecgboot-vue3/src/components/jeecg/comment/useComment.ts
Normal file
459
jeecgboot-vue3/src/components/jeecg/comment/useComment.ts
Normal file
@ -0,0 +1,459 @@
|
||||
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 image from '/@/assets/svg/fileType/image.png';
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { createImgPreview } from '/@/components/Preview';
|
||||
import data from "emoji-mart-vue-fast/data/apple.json";
|
||||
import { EmojiIndex } from "emoji-mart-vue-fast/src";
|
||||
import { encryptByBase64 } from '/@/utils/cipher';
|
||||
|
||||
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 });
|
||||
|
||||
export function getGloablEmojiIndex(){
|
||||
if(window['myEmojiIndex']){
|
||||
console.log("----走window['myEmojiIndex']缓存,不new新对象!")
|
||||
return window['myEmojiIndex'];
|
||||
}
|
||||
|
||||
window['myEmojiIndex'] = new EmojiIndex(data, {
|
||||
function() {
|
||||
return true;
|
||||
},
|
||||
exclude:['recent','people','nature','foods','activity','places','objects','symbols','flags']
|
||||
});
|
||||
return window['myEmojiIndex'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单条记录
|
||||
* @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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* QQYUN-4310【文件】从文件库选择文件功能未做
|
||||
* @param file
|
||||
*/
|
||||
async function saveSysFormFile(file){
|
||||
let url = '/sys/comment/addFile';
|
||||
let params = {
|
||||
fileId: file.id,
|
||||
commentId: uploadData.commentId
|
||||
}
|
||||
await defHttp.post({url, params}, { joinParamsToUrl: true, isTransformResponse: false });
|
||||
}
|
||||
|
||||
async function uploadFiles(fileList) {
|
||||
if (fileList && fileList.length > 0) {
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
let file = toRaw(fileList[i]);
|
||||
if(file.exist === true){
|
||||
await saveSysFormFile(file);
|
||||
}else{
|
||||
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,
|
||||
image
|
||||
};
|
||||
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 getImageTypeIcon() {
|
||||
return typeMap['image'];
|
||||
}
|
||||
|
||||
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(file.exist){
|
||||
return getFileAccessHttpUrl(file.url);
|
||||
}
|
||||
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;
|
||||
if(item.exist){
|
||||
url = getFileAccessHttpUrl(item.url);
|
||||
}else{
|
||||
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')
|
||||
//update-begin---author:scott ---date:2024-06-03 for:【TV360X-952】升级到kkfileview4.1.0---
|
||||
let previewUrl = encodeURIComponent(encryptByBase64(url));
|
||||
window.open(onlinePreviewDomain+'?url='+previewUrl);
|
||||
//update-end---author:scott ---date::2024-06-03 for:【TV360X-952】升级到kkfileview4.1.0----
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化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,
|
||||
getImageTypeIcon
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于emoji渲染
|
||||
*/
|
||||
export function useEmojiHtml(globalEmojiIndex){
|
||||
const COLONS_REGEX = new RegExp('([^:]+)?(:[a-zA-Z0-9-_+]+:(:skin-tone-[2-6]:)?)','g');
|
||||
|
||||
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 = globalEmojiIndex.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 {
|
||||
globalEmojiIndex,
|
||||
getHtml
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取modal窗体高度
|
||||
*/
|
||||
export function getModalHeight(){
|
||||
return window.innerHeight;
|
||||
}
|
||||
180
jeecgboot-vue3/src/components/jeecg/thirdApp/JThirdAppButton.vue
Normal file
180
jeecgboot-vue3/src/components/jeecg/thirdApp/JThirdAppButton.vue
Normal file
@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<template v-if="syncToApp || syncToLocal">
|
||||
<JThirdAppDropdown v-if="enabledTypes.wechatEnterprise" type="wechatEnterprise" name="企微" v-bind="bindAttrs" v-on="bindEvents" />
|
||||
<JThirdAppDropdown v-if="enabledTypes.dingtalk" type="dingtalk" name="钉钉" v-bind="bindAttrs" v-on="bindEvents" />
|
||||
</template>
|
||||
<template v-else>未设置任何同步方向</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { ref, computed, createVNode, h, resolveComponent } from 'vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { backEndUrl, getEnabledTypes, doSyncThirdApp } from './jThirdApp.api';
|
||||
import { Modal, Input } from 'ant-design-vue';
|
||||
import JThirdAppDropdown from './JThirdAppDropdown.vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createMessage, createWarningModal } = useMessage();
|
||||
const props = defineProps({
|
||||
// 同步类型,可以是 user、depart
|
||||
bizType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// 是否允许同步到第三方APP
|
||||
syncToApp: Boolean,
|
||||
// 是否允许第三方APP同步到本地
|
||||
syncToLocal: Boolean,
|
||||
// 选择的行
|
||||
selectedRowKeys: Array,
|
||||
});
|
||||
// 声明Emits
|
||||
const emit = defineEmits(['sync-ok', 'sync-error', 'sync-finally']);
|
||||
|
||||
const enabledTypes = ref({});
|
||||
// 绑定属性
|
||||
const bindAttrs = computed(() => {
|
||||
return {
|
||||
syncToApp: props.syncToApp,
|
||||
syncToLocal: props.syncToLocal,
|
||||
};
|
||||
});
|
||||
// 绑定方法
|
||||
const bindEvents = computed(() => {
|
||||
return {
|
||||
'to-app': onToApp,
|
||||
'to-local': onToLocal,
|
||||
};
|
||||
});
|
||||
|
||||
// 同步到第三方App
|
||||
function onToApp(e) {
|
||||
doSync(e.type, '/toApp');
|
||||
}
|
||||
|
||||
// 同步到本地
|
||||
function onToLocal(e) {
|
||||
doSync(e.type, '/toLocal');
|
||||
}
|
||||
|
||||
// 获取启用的第三方App
|
||||
async function loadEnabledTypes() {
|
||||
enabledTypes.value = await getEnabledTypes();
|
||||
}
|
||||
|
||||
// 开始同步第三方App
|
||||
function doSync(type, direction) {
|
||||
let urls = backEndUrl[type];
|
||||
if (!(urls && urls[props.bizType])) {
|
||||
console.warn('配置出错');
|
||||
return;
|
||||
}
|
||||
let url = urls[props.bizType] + direction;
|
||||
let selectedRowKeys = props.selectedRowKeys;
|
||||
let content = '确定要开始同步全部数据吗?可能花费较长时间!';
|
||||
|
||||
if (Array.isArray(selectedRowKeys) && selectedRowKeys.length > 0) {
|
||||
content = `确定要开始同步这 ${selectedRowKeys.length} 项吗?`;
|
||||
} else {
|
||||
selectedRowKeys = [];
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const model = Modal.confirm({
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
title: '同步',
|
||||
content,
|
||||
onOk: () => {
|
||||
model.update({
|
||||
keyboard: false,
|
||||
okText: '同步中…',
|
||||
cancelButtonProps: { disabled: true },
|
||||
});
|
||||
let params = { ids: selectedRowKeys.join(',') };
|
||||
return defHttp
|
||||
.get({ url, params }, { isTransformResponse: false })
|
||||
.then((res) => {
|
||||
let options = {};
|
||||
if (res.result) {
|
||||
options = {
|
||||
width: 600,
|
||||
title: res.message,
|
||||
content: () => {
|
||||
let nodes;
|
||||
let successInfo = [`成功信息如下:`, renderTextarea(h, res.result.successInfo.map((v, i) => `${i + 1}. ${v}`).join('\n'))];
|
||||
if (res.success) {
|
||||
nodes = [...successInfo, h('br'), `无失败信息!`];
|
||||
} else {
|
||||
nodes = [
|
||||
`失败信息如下:`,
|
||||
renderTextarea(h, res.result.failInfo.map((v, i) => `${i + 1}. ${v}`).join('\n')),
|
||||
h('br'),
|
||||
...successInfo,
|
||||
];
|
||||
}
|
||||
return nodes;
|
||||
},
|
||||
};
|
||||
}
|
||||
if (res.success) {
|
||||
if (options != null) {
|
||||
Modal.success(options);
|
||||
} else {
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
emit('sync-ok');
|
||||
} else {
|
||||
if (options != null) {
|
||||
Modal.warning(options);
|
||||
} else {
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
emit('sync-error');
|
||||
}
|
||||
})
|
||||
.catch(() => model.destroy())
|
||||
.finally(() => {
|
||||
resolve();
|
||||
emit('sync-finally', {
|
||||
type,
|
||||
direction,
|
||||
isToApp: direction === '/toApp',
|
||||
isToLocal: direction === '/toLocal',
|
||||
});
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderTextarea(h, value) {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
id: 'box',
|
||||
style: {
|
||||
minHeight: '100px',
|
||||
border: '1px solid #d9d9d9',
|
||||
fontSize: '14px',
|
||||
maxHeight: '250px',
|
||||
whiteSpace: 'pre',
|
||||
overflow: 'auto',
|
||||
padding: '10px',
|
||||
},
|
||||
},
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
// 获取启用的第三方App
|
||||
loadEnabledTypes();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#box:hover {
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<a-dropdown v-if="syncToApp && syncToLocal">
|
||||
<a-button type="primary" preIcon="ant-design:sync-outlined">同步{{ name }}</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleMenuClick">
|
||||
<a-menu-item v-if="syncToApp" key="to-app">同步到{{ name }}</a-menu-item>
|
||||
<a-menu-item v-if="getSyncToLocal" key="to-local">同步到本地</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-button v-else-if="syncToApp" type="primary" preIcon="ant-design:sync-outlined" @click="handleMenuClick({ key: 'to-app' })"
|
||||
>同步{{ name }}</a-button
|
||||
>
|
||||
<a-button v-else type="primary" preIcon="ant-design:sync-outlined" @click="handleMenuClick({ key: 'to-local' })">同步{{ name }}到本地</a-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* JThirdAppButton 的子组件,不可单独使用 */
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
name: String,
|
||||
syncToApp: Boolean,
|
||||
syncToLocal: Boolean,
|
||||
});
|
||||
// 声明Emits
|
||||
const emit = defineEmits(['to-app', 'to-local']);
|
||||
|
||||
const getSyncToLocal = computed(() => {
|
||||
// 由于企业微信接口变更,将不再支持同步到本地
|
||||
if (props.type === 'wechatEnterprise') {
|
||||
return false;
|
||||
}
|
||||
return props.syncToLocal;
|
||||
});
|
||||
|
||||
function handleMenuClick(event) {
|
||||
emit(event.key, { type: props.type });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -0,0 +1,37 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { cloneObject } from '/@/utils/index';
|
||||
|
||||
export const backEndUrl = {
|
||||
// 获取启用的第三方App
|
||||
getEnabledType: '/sys/thirdApp/getEnabledType',
|
||||
// 企业微信
|
||||
wechatEnterprise: {
|
||||
user: '/sys/thirdApp/sync/wechatEnterprise/user',
|
||||
depart: '/sys/thirdApp/sync/wechatEnterprise/depart',
|
||||
},
|
||||
// 钉钉
|
||||
dingtalk: {
|
||||
user: '/sys/thirdApp/sync/dingtalk/user',
|
||||
depart: '/sys/thirdApp/sync/dingtalk/depart',
|
||||
},
|
||||
};
|
||||
// 启用了哪些第三方App(在此缓存)
|
||||
let enabledTypes = null;
|
||||
|
||||
// 获取启用的第三方App
|
||||
export const getEnabledTypes = async () => {
|
||||
// 获取缓存
|
||||
if (enabledTypes != null) {
|
||||
return cloneObject(enabledTypes);
|
||||
} else {
|
||||
let { success, result } = await defHttp.get({ url: backEndUrl.getEnabledType }, { isTransformResponse: false });
|
||||
if (success) {
|
||||
// 在此缓存
|
||||
enabledTypes = cloneObject(result);
|
||||
return result;
|
||||
} else {
|
||||
console.warn('getEnabledType查询失败:');
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
Reference in New Issue
Block a user