fix: 修改文件名大小写

This commit is contained in:
lixin 2025-01-09 17:58:24 +08:00
parent 3dcd0ddf6c
commit 763bc6db3c
167 changed files with 19601 additions and 78 deletions

1103
build/cool/eps.d.ts vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -14,6 +14,11 @@
"@vueuse/core": "^10.4.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.36.0",
"@vue-flow/minimap": "^1.5.0",
"@vue-flow/node-resizer": "^1.3.6",
"axios": "^1.7.2",
"chardet": "^2.0.0",
"core-js": "^3.32.1",

152
pnpm-lock.yaml generated
View File

@ -14,6 +14,21 @@ importers:
'@element-plus/icons-vue':
specifier: ^2.3.1
version: 2.3.1(vue@3.5.13(typescript@5.7.3))
'@vue-flow/background':
specifier: ^1.3.0
version: 1.3.2(@vue-flow/core@1.41.7(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@vue-flow/controls':
specifier: ^1.1.2
version: 1.1.2(@vue-flow/core@1.41.7(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@vue-flow/core':
specifier: ^1.36.0
version: 1.41.7(vue@3.5.13(typescript@5.7.3))
'@vue-flow/minimap':
specifier: ^1.5.0
version: 1.5.0(@vue-flow/core@1.41.7(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@vue-flow/node-resizer':
specifier: ^1.3.6
version: 1.4.0(@vue-flow/core@1.41.7(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@vueuse/core':
specifier: ^10.4.0
version: 10.11.1(vue@3.5.13(typescript@5.7.3))
@ -897,6 +912,35 @@ packages:
vite: ^5.0.0 || ^6.0.0
vue: ^3.2.25
'@vue-flow/background@1.3.2':
resolution: {integrity: sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==}
peerDependencies:
'@vue-flow/core': ^1.23.0
vue: ^3.3.0
'@vue-flow/controls@1.1.2':
resolution: {integrity: sha512-6dtl/JnwDBNau5h3pDBdOCK6tdxiVAOL3cyruRL61gItwq5E97Hmjmj2BIIqX2p7gU1ENg3z80Z4zlu58fGlsg==}
peerDependencies:
'@vue-flow/core': ^1.23.0
vue: ^3.3.0
'@vue-flow/core@1.41.7':
resolution: {integrity: sha512-YAavc1wD8nll5IECnUyqzzNX+Y5WEGm7drmVOdkRPU8z9VkxlbgtokK+sTjz3N232Dm9pHuXRHD+u0HHRk0kdQ==}
peerDependencies:
vue: ^3.3.0
'@vue-flow/minimap@1.5.0':
resolution: {integrity: sha512-JhxXDF+8uTc7sgkZHDIvFpHqSl4wsK9xp8Kz5OHwNcXlgGcwqj4yad6jcc1B6bGxm+huESpNmoPotQbpMn6rVw==}
peerDependencies:
'@vue-flow/core': ^1.23.0
vue: ^3.3.0
'@vue-flow/node-resizer@1.4.0':
resolution: {integrity: sha512-S52MRcSpd6asza8Cl0bKM2sHGrbq7vBydKHDuPdoTD+cvjNX6XF4LSiPZOuzExePI6b+O6dg2EZ1378oOLGFpA==}
peerDependencies:
'@vue-flow/core': ^1.23.0
vue: ^3.3.0
'@vue/babel-helper-vue-transform-on@1.2.5':
resolution: {integrity: sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==}
@ -1290,6 +1334,44 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
d@1.0.2:
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
engines: {node: '>=0.12'}
@ -3533,6 +3615,40 @@ snapshots:
vite: 5.4.11(@types/node@20.17.12)(sass@1.83.1)(terser@5.37.0)
vue: 3.5.13(typescript@5.7.3)
'@vue-flow/background@1.3.2(@vue-flow/core@1.41.7(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@vue-flow/core': 1.41.7(vue@3.5.13(typescript@5.7.3))
vue: 3.5.13(typescript@5.7.3)
'@vue-flow/controls@1.1.2(@vue-flow/core@1.41.7(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@vue-flow/core': 1.41.7(vue@3.5.13(typescript@5.7.3))
vue: 3.5.13(typescript@5.7.3)
'@vue-flow/core@1.41.7(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@vueuse/core': 10.11.1(vue@3.5.13(typescript@5.7.3))
d3-drag: 3.0.0
d3-selection: 3.0.0
d3-zoom: 3.0.0
vue: 3.5.13(typescript@5.7.3)
transitivePeerDependencies:
- '@vue/composition-api'
'@vue-flow/minimap@1.5.0(@vue-flow/core@1.41.7(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@vue-flow/core': 1.41.7(vue@3.5.13(typescript@5.7.3))
d3-selection: 3.0.0
d3-zoom: 3.0.0
vue: 3.5.13(typescript@5.7.3)
'@vue-flow/node-resizer@1.4.0(@vue-flow/core@1.41.7(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@vue-flow/core': 1.41.7(vue@3.5.13(typescript@5.7.3))
d3-drag: 3.0.0
d3-selection: 3.0.0
vue: 3.5.13(typescript@5.7.3)
'@vue/babel-helper-vue-transform-on@1.2.5': {}
'@vue/babel-plugin-jsx@1.2.5(@babel/core@7.26.0)':
@ -4020,6 +4136,42 @@ snapshots:
csstype@3.1.3: {}
d3-color@3.1.0: {}
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-ease@3.0.1: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-selection@3.0.0: {}
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
d@1.0.2:
dependencies:
es5-ext: 0.10.64

View File

@ -0,0 +1,259 @@
<template>
<div class="cl-chat__wrap">
<el-badge :value="unCount" :hidden="!unCount">
<div class="cl-chat__icon" @click="open">
<cl-svg name="icon-notice" :size="16" />
</div>
</el-badge>
<!-- 弹框 -->
<cl-dialog
v-model="visible"
title="聊天窗口"
height="70vh"
width="1200px"
padding="0"
keep-alive
:scrollbar="false"
:close-on-click-modal="false"
close-on-press-escape
:controls="['slot-expand', 'cl-flex1', 'fullscreen', 'close']"
>
<div
class="cl-chat"
:class="{
'is-mini': browser.isMini,
'is-expand': isExpand
}"
>
<div class="cl-chat__session">
<chat-session />
</div>
<div class="cl-chat__right">
<chat-message />
</div>
</div>
<!-- 展开按钮 -->
<template #slot-expand>
<button class="cl-dialog__controls-icon">
<el-icon @click="isExpand = true" v-if="!isExpand">
<notebook />
</el-icon>
<el-icon @click="isExpand = false" v-else>
<arrow-left />
</el-icon>
</button>
</template>
</cl-dialog>
</div>
</template>
<script lang="ts" name="cl-chat" setup>
import { nextTick, provide, ref } from "vue";
import dayjs from "dayjs";
import { useCool, config, module, useBrowser } from "/@/cool";
import { useBase } from "/$/base";
import { Notebook, ArrowLeft } from "@element-plus/icons-vue";
import { debounce } from "lodash-es";
// import io from "socket.io-client";
import { Socket } from "socket.io-client";
import ChatMessage from "./message.vue";
import ChatSession from "./session.vue";
import { Chat } from "../types";
import { useStore } from "../store";
// const { mitt } = useCool();
const { browser, onScreenChange } = useBrowser();
//
const { session, message } = useStore();
//
const { user } = useBase();
//
// const { options } = module.get("chat");
//
const visible = ref(false);
//
const isExpand = ref(true);
//
const unCount = ref(parseInt(String(Math.random() * 100)));
// Socket
let socket: Socket;
//
function connect() {
refresh();
// if (!socket) {
// socket = io(config.host + options.path, {
// auth: {
// token: user.token
// }
// });
// socket.on("connect", () => {
// console.log(`connect ${user.info?.nickName}`);
// //
// socket.on("message", (msg) => {
// console.log(msg);
// mitt("chat-message", msg);
// });
// refresh();
// });
// socket.on("disconnect", (err) => {
// console.error(err);
// });
// }
}
//
function open() {
visible.value = true;
connect();
}
//
function close() {
visible.value = false;
}
//
function expand(value?: boolean) {
isExpand.value = value === undefined ? !isExpand.value : value;
}
//
function send(data: Chat.Message, isAppend?: boolean) {
// socket.emit("message", {});
if (isAppend) {
append(data);
}
}
//
function append(data: Chat.Message) {
message.list.push({
fromId: user.info?.id,
toId: session.value?.userId,
avatar: user.info?.headImg,
nickName: user.info?.nickName,
createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
...data
});
scrollToBottom();
}
//
const scrollToBottom = debounce(() => {
nextTick(() => {
const box = document.querySelector(".cl-chat .chat-message .list");
box?.scroll({
top: 100000 + Math.random(),
behavior: "smooth"
});
});
}, 300);
//
async function refresh() {
await session.get();
await message.get();
scrollToBottom();
}
provide("chat", {
get socket() {
return socket;
},
send,
append,
expand,
scrollToBottom
});
//
onScreenChange(() => {
isExpand.value = browser.isMini ? false : true;
});
defineExpose({
open,
close
});
</script>
<style lang="scss">
.cl-chat {
display: flex;
justify-content: flex-end;
background-color: #eee;
padding: 5px;
box-sizing: border-box;
position: relative;
color: #000;
height: 100%;
&__icon {
padding: 0 5px;
text-align: center;
&:hover {
color: var(--color-primary);
}
}
&__footer {
padding: 9px 0;
}
&__session {
height: calc(100% - 10px);
width: 345px;
position: absolute;
left: 5px;
top: 5px;
}
&__right {
position: relative;
z-index: 99;
transition: width 0.3s;
width: 100%;
}
&.is-mini {
&.is-expand {
.cl-chat__session {
z-index: 100;
}
}
.cl-chat__session {
width: calc(100% - 10px);
}
.cl-chat__right {
width: 100% !important;
}
}
&.is-expand {
.cl-chat__right {
width: calc(100% - 350px);
}
}
}
</style>

View File

@ -0,0 +1,354 @@
<template>
<div class="chat-message" v-loading="message?.loading" element-loading-text="消息列表加载中">
<!-- 头部 -->
<div class="head">
<template v-if="session?.value">
<div class="avatar">
<cl-avatar :size="30" shape="square" :src="session?.value.avatar" />
</div>
<span class="name">{{ session?.value.nickName }}聊天中</span>
</template>
</div>
<!-- 消息列表 -->
<el-scrollbar class="list">
<ul>
<li v-for="(item, index) in list" :key="index">
<div
class="item"
:class="{
'is-right': item.isMy
}"
>
<div class="avatar">
<cl-avatar :size="36" shape="square" :src="item.avatar" />
</div>
<div
class="det"
@contextmenu="
(e) => {
onContextMenu(e, item);
}
"
>
<div class="h">
<span class="name">{{ item.nickName }}</span>
</div>
<div class="content">
<!-- 文本 -->
<div class="is-text" v-if="item.contentType == 0">
<span>{{ item.content.text }}</span>
</div>
<!-- 图片 -->
<div class="is-image" v-else-if="item.contentType == 1">
<el-image
:src="item.content.imageUrl"
:preview-src-list="previewUrls"
:initial-index="item._index"
scroll-container=".chat-message .list"
/>
</div>
</div>
</div>
</div>
</li>
</ul>
</el-scrollbar>
<!-- 底部 -->
<div class="footer">
<div class="tools">
<ul>
<cl-upload @success="onImageSend" :show-file-list="false">
<li>
<el-icon><picture-filled /></el-icon>
</li>
</cl-upload>
<li>
<el-icon><video-camera /></el-icon>
</li>
<li>
<el-icon><microphone /></el-icon>
</li>
<li>
<el-icon><location /></el-icon>
</li>
</ul>
</div>
<div class="input">
<el-input
v-model="value"
type="textarea"
:rows="4"
resize="none"
:autosize="{
minRows: 4,
maxRows: 10
}"
placeholder="输入内容"
></el-input>
<el-button type="success" @click="onTextSend" :disabled="!value">发送</el-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { useChat } from "../hooks";
import { useStore } from "../store";
import { PictureFilled, VideoCamera, Microphone, Location } from "@element-plus/icons-vue";
import { useBase } from "/$/base";
import { ContextMenu } from "@cool-vue/crud";
import { useClipboard } from "@vueuse/core";
import { Chat } from "../types";
import { ElMessage } from "element-plus";
const { user } = useBase();
const { chat } = useChat();
const { message, session } = useStore();
const { copy } = useClipboard();
const value = ref("");
//
const list = computed(() => {
let n = 0;
return message.list.map((e) => {
if (e.contentType == 1) {
e._index = n++;
}
//
e.isMy = e.fromId == user.info?.id;
return e;
});
});
//
const previewUrls = computed(() =>
message.list
.filter((e) => e.contentType == 1)
.map((e) => e.content?.imageUrl)
.filter(Boolean)
);
//
function onTextSend() {
chat.send(
{
contentType: 0,
content: {
text: value.value
}
},
true
);
value.value = "";
}
//
function onImageSend(res: any) {
chat.send(
{
contentType: 1,
content: {
imageUrl: res.url
}
},
true
);
value.value = "";
}
//
function onContextMenu(e: Event, item: Chat.Message) {
ContextMenu.open(e, {
hover: {
target: "content"
},
list: [
{
label: "复制",
callback(done) {
copy(item.content.text || "");
ElMessage.success("复制成功");
done();
}
},
{
label: "转发"
},
{
label: "删除"
}
]
});
}
</script>
<style lang="scss" scoped>
.chat-message {
display: flex;
flex-direction: column;
background-color: #fff;
border-radius: 6px;
height: 100%;
box-sizing: border-box;
.head {
display: flex;
align-items: center;
height: 50px;
padding: 0 10px;
.name {
margin-left: 10px;
font-size: 14px;
}
ul {
li {
list-style: none;
}
}
}
.list {
flex: 1;
background-color: #f7f7f7;
ul {
& > li {
list-style: none;
.date {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
font-size: 12px;
}
.item {
display: flex;
padding: 10px;
.avatar {
margin-right: 10px;
}
.det {
.h {
display: flex;
align-items: center;
.name {
font-size: 12px;
color: #666;
}
}
.content {
display: flex;
flex-direction: column;
margin-top: 5px;
.is-text {
background-color: #fff;
padding: 8px;
border-radius: 0 6px 6px 6px;
max-width: 400px;
font-size: 14px;
}
.is-image {
background-color: #fff;
.el-image {
display: block;
min-height: 100px;
max-width: 200px;
border-radius: 4px;
}
}
}
}
&.is-right {
flex-direction: row-reverse;
.avatar {
margin-left: 10px;
margin-right: 0;
}
.det {
.h {
justify-content: flex-end;
}
.content {
.is-text {
border-radius: 6px 0 6px 6px;
}
}
}
}
}
}
}
}
.footer {
padding: 10px;
.tools {
display: flex;
margin-bottom: 10px;
ul {
display: flex;
align-items: center;
flex: 1;
li {
height: 26px;
width: 26px;
border-radius: 4px;
margin-right: 10px;
list-style: none;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 18px;
&:hover {
background-color: #f7f7f7;
}
}
}
}
.input {
display: flex;
position: relative;
.el-button {
margin-left: 10px;
position: absolute;
right: 10px;
bottom: 10px;
}
}
}
}
</style>

View File

@ -0,0 +1,209 @@
<template>
<div class="chat-session">
<div class="head">
<el-input v-model="keyWord" placeholder="关键字搜索" clearable></el-input>
<ul class="tools">
<li>
<el-icon><plus /></el-icon>
</li>
<li @click="session.get()">
<el-icon><refresh /></el-icon>
</li>
</ul>
</div>
<div class="list" v-loading="session?.loading">
<el-scrollbar class="scroller" :ref="setRefs('scroller')">
<div
class="item"
v-for="(item, index) in list"
:key="index"
:class="{
'is-active': item.id == session?.value?.id
}"
@click="toDetail(item)"
>
<div class="avatar">
<el-badge :value="item.num" :hidden="item.num == 0">
<cl-avatar shape="square" :src="item.avatar" />
</el-badge>
</div>
<div class="det">
<p class="name">{{ item.nickName }}</p>
<p class="message">
{{ item.text }}
</p>
</div>
<div class="status">
<p class="date">{{ item.createTime }}</p>
</div>
</div>
<el-empty
v-if="list.length == 0"
:image-size="100"
description="暂无会话"
></el-empty>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, ref } from "vue";
import { useChat } from "../hooks";
import { useStore } from "../store";
import { Refresh, Plus } from "@element-plus/icons-vue";
import { Chat } from "../types";
import { useBrowser, useCool } from "/@/cool";
import { useDialog } from "@cool-vue/crud";
const { browser } = useBrowser();
const { chat } = useChat();
const { session, message } = useStore();
const { refs, setRefs } = useCool();
useDialog({
onFullscreen() {
nextTick(() => {
refs.scroller?.update();
});
}
});
//
const keyWord = ref("");
//
const list = computed(() => session?.list.filter((e) => e.nickName?.includes(keyWord.value)) || []);
//
async function toDetail(item: Chat.Session) {
if (browser.isMini) {
chat.expand(false);
}
session.set(item);
await message.get({ page: 1 });
chat.scrollToBottom();
}
</script>
<style lang="scss" scoped>
.chat-session {
height: 100%;
width: 100%;
background-color: #fff;
border-radius: 6px;
.head {
display: flex;
border-bottom: 1px solid #f7f7f7;
padding: 10px;
:deep(.el-input) {
height: 30px;
.el-input__wrapper {
background-color: #eee;
box-shadow: none;
}
}
.tools {
display: inline-flex;
align-items: center;
li {
height: 30px;
width: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-left: 10px;
border-radius: 4px;
background-color: #eee;
color: #666;
.el-icon {
font-size: 16px;
}
&:hover {
background-color: #ddd;
}
}
}
}
.list {
height: calc(100% - 51px);
overflow: hidden;
.scroller {
height: 100%;
}
.item {
display: flex;
padding: 15px 10px;
cursor: pointer;
.avatar {
margin-right: 10px;
:deep(.el-badge__content) {
transform: translateY(-50%) translateX(calc(100% - 5px)) scale(0.8) !important;
}
}
.det {
flex: 1;
.name,
.message {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.name {
font-size: 14px;
margin-bottom: 4px;
}
.message {
font-size: 12px;
color: #666;
}
}
.status {
display: flex;
flex-direction: column;
align-items: flex-end;
font-size: 12px;
.date {
margin-bottom: 5px;
color: #999;
}
}
&.is-active {
background-color: #eee;
}
&:not(.is-active):hover {
background-color: #f7f7f7;
}
}
}
}
</style>

View File

@ -0,0 +1,15 @@
import type { ModuleConfig } from "/@/cool";
export default (): ModuleConfig => {
return {
// toolbar: {
// order: 2,
// h5: false,
// component: import("./components/index.vue")
// },
options: {
// socket.io 连接地址
path: "/chat"
}
};
};

View File

@ -0,0 +1,10 @@
import { inject } from "vue";
import { Chat } from "../types";
export function useChat() {
const chat = inject("chat") as Chat.Provide;
return {
chat
};
}

View File

@ -0,0 +1,56 @@
import { Service } from "/@/cool";
import Mock from "mockjs";
@Service("chat/message")
class ChatMessage {
async page() {
return new Promise((resolve) => {
const data = Mock.mock({
"list|20": [
{
id: "@id",
nickName: "@cname",
createTime: "@datetime(HH:mm:ss)",
text: "@cparagraph(5)",
"contentType|0-1": 0,
"userId|1-2": 1,
avatar() {
return Mock.Random.image(
"40x40",
Mock.Random.color(),
"#FFF",
"png",
this.nickName[0]
);
},
content() {
return JSON.stringify({
text: this.text,
imageUrl: Mock.Random.image(
"100x100",
Mock.Random.color(),
"#FFF",
"png",
this.nickName
)
});
}
}
]
});
setTimeout(() => {
resolve({
list: data.list,
pagination: {
total: 20,
page: 1,
size: 20
}
});
}, 1000);
});
}
}
export default ChatMessage;

View File

@ -0,0 +1,43 @@
import { Service } from "/@/cool";
import Mock from "mockjs";
@Service("chat/session")
class ChatSession {
async page() {
return new Promise((resolve) => {
const data = Mock.mock({
"list|20": [
{
id: "@id",
nickName: "@cname",
createTime: "@datetime(HH:mm:ss)",
text: "@cparagraph(5)",
"num|0-99": 0,
avatar() {
return Mock.Random.image(
"40x40",
Mock.Random.color(),
"#FFF",
"png",
this.nickName[0]
);
}
}
]
});
setTimeout(() => {
resolve({
list: data.list,
pagination: {
total: 20,
page: 1,
size: 20
}
});
}, 1000);
});
}
}
export default ChatSession;

View File

@ -0,0 +1,12 @@
import { useMessageStore } from "./message";
import { useSessionStore } from "./session";
export function useStore() {
const session = useSessionStore();
const message = useMessageStore();
return {
session,
message
};
}

View File

@ -0,0 +1,46 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { service } from "/@/cool";
export const useMessageStore = defineStore("chat-message", () => {
// 加载状态
const loading = ref(false);
// 列表
const list = ref<any[]>([]);
// 分页
const pagination = ref({
page: 1,
total: 0,
size: 20
});
// 获取列表
async function get(params?: any) {
loading.value = true;
// 清空
if (params?.page == 1) {
list.value = [];
}
// 发送请求
await service.chat.message.page(params).then((res) => {
list.value = res.list.map((e) => {
e.content = JSON.parse(e.content);
return e;
});
pagination.value = res.pagination;
});
loading.value = false;
}
return {
loading,
list,
pagination,
get
};
});

View File

@ -0,0 +1,46 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { service } from "/@/cool";
export const useSessionStore = defineStore("chat-session", () => {
// 加载状态
const loading = ref(false);
// 列表
const list = ref<any[]>([]);
// 选中
const value = ref<any>();
// 获取列表
async function get(params?: any) {
loading.value = true;
// 发送请求
await service.chat.session.page(params).then((res) => {
// 默认加载第一个会话的消息
if (!value.value) {
set(res.list[0]);
}
// 设置列表
list.value = res.list;
});
loading.value = false;
}
// 设置值
function set(data: any) {
// 设置值
value.value = data;
}
return {
loading,
list,
value,
get,
set
};
});

36
src/modules/chat/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,36 @@
import { Socket } from "socket.io-client";
export namespace Chat {
enum ContentType {
"text" = 0,
"image" = 1,
"video" = 2
}
interface Message {
fromId?: string;
toId?: string;
content: {
text?: string;
imageUrl?: string;
[key: string]: any;
};
contentType: ContentType;
[key: string]: any;
}
interface Session {
id: string;
avatar: string;
nickName: string;
[key: string]: any;
}
interface Provide {
socket?: Socket;
send(message: Message, isAppend?: boolean): void;
append(message: Message): void;
expand(shouldExpand?: boolean): void;
scrollToBottom(): void;
}
}

View File

@ -0,0 +1,275 @@
<template>
<div class="cl-flow" id="cl-flow">
<vue-flow
:default-viewport="{ zoom: 1.0 }"
:min-zoom="0.25"
:max-zoom="2"
@connect="onConnect"
@node-mouse-enter="onNodeMouseEnter"
@node-mouse-leave="onNodeMouseLeave"
@node-click="onNodeClick"
@node-drag-stop="onNodeDragStop"
@node-context-menu="refs.contextMenu?.onNode"
@edge-mouse-enter="onEdgeMouseEnter"
@edge-mouse-leave="onEdgeMouseLeave"
@pane-context-menu="refs.contextMenu?.onPane"
@pane-click="onPaneClick"
@pane-scroll="onPaneScroll"
@pane-ready="onPaneReady"
>
<!-- 自定义顶部栏 -->
<tools-head />
<!-- 自定义选择框 -->
<tools-selection />
<!-- 自定义面板 -->
<tools-panel />
<!-- 自定义节点添加面板 -->
<tools-node-add />
<!-- 自定义右键菜单 -->
<tools-context-menu :ref="setRefs('contextMenu')" />
<!-- 自定义控制器 -->
<tools-controls />
<!-- 自定义连接线按钮 -->
<template
#edge-button="{
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
markerEnd,
style,
data
}"
>
<tools-edge-button
:id="id"
:source-x="sourceX"
:source-y="sourceY"
:target-x="targetX"
:target-y="targetY"
:source-position="sourcePosition"
:target-position="targetPosition"
:marker-end="markerEnd"
:style="style"
:data="data"
/>
</template>
<!-- 自定义节点 -->
<template #[item.name!]="{ id }" v-for="item in flow.CustomNodes" :key="item.name">
<tools-card :node-id="id" />
</template>
<!-- 背景 -->
<background pattern-color="#aaa" :gap="16" />
<!-- 小图 -->
<mini-map position="bottom-right" :height="100" :width="150" />
</vue-flow>
</div>
</template>
<script setup lang="ts">
import {
VueFlow,
Connection,
NodeDragEvent,
EdgeMouseEvent,
type NodeMouseEvent,
type VueFlowStore
} from "@vue-flow/core";
import { Background } from "@vue-flow/background";
import { MiniMap } from "@vue-flow/minimap";
import { useFlow } from "../hooks";
import { useCool } from "/@/cool";
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
import "@vue-flow/controls/dist/style.css";
import "@vue-flow/minimap/dist/style.css";
import "@vue-flow/node-resizer/dist/style.css";
import ToolsPanel from "./tools/panel/index.vue";
import ToolsCard from "./tools/card.vue";
import ToolsEdgeButton from "./tools/edge-button.vue";
import ToolsControls from "./tools/controls.vue";
import ToolsHead from "./tools/head.vue";
import ToolsContextMenu from "./tools/context-menu.vue";
import ToolsSelection from "./tools/selection.vue";
import ToolsNodeAdd from "./tools/node-add.vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { onBeforeRouteLeave } from "vue-router";
import { onMounted, onUnmounted } from "vue";
import type { FlowNode } from "../types";
import { nextTick } from "vue";
const props = defineProps({
flowId: Number
});
const { mitt, refs, setRefs } = useCool();
const flow = useFlow();
//
function onPaneReady(store: VueFlowStore) {
flow.init();
flow.get(props.flowId!);
}
//
function onPaneScroll(e: WheelEvent | undefined) {
refs.contextMenu?.close();
}
// 线
function onConnect(connection: Connection) {
flow.addEdge(connection);
}
//
async function onNodeClick(e: NodeMouseEvent) {
//
if (flow.node?.id == e.node.id) return false;
const node = flow.findNode(e.node.id);
flow.setNode(node);
//
// flow.setViewportByNode(flow.node!);
refs.contextMenu?.close();
}
//
function onNodeMouseEnter(e: NodeMouseEvent) {
flow.activeEdge(e.node.id, true);
}
//
function onNodeMouseLeave(e: NodeMouseEvent) {
flow.activeEdge(e.node.id, false);
}
//
function onNodeDragStop(e: NodeDragEvent) {
flow.updateNode(e.node.id, {
position: e.node.position
});
}
// 线
function onEdgeMouseEnter(e: EdgeMouseEvent) {
e.edge.data.show = true;
}
// 线
function onEdgeMouseLeave(e: EdgeMouseEvent) {
e.edge.data.show = false;
}
//
function onPaneClick() {
refs.contextMenu?.close();
flow.enableDrag();
flow.clearNode();
}
//
function openForm(node: FlowNode) {
closeForm();
if (node) {
flow.updateChildrenPosition("open", node);
setTimeout(() => {
mitt.emit("flow.openForm", node);
}, 100);
}
}
//
function closeForm() {
flow.updateChildrenPosition("close", flow.node!);
mitt.emit("flow.closeForm", flow.node);
}
//
async function save() {
await flow.save();
ElMessage.success("数据保存成功");
}
onMounted(() => {
mitt.on("flow.setNode", openForm);
mitt.on("flow.clearNode", closeForm);
window.addEventListener("beforeunload", save);
});
onUnmounted(() => {
mitt.off("flow.setNode", openForm);
mitt.off("flow.clearNode", closeForm);
window.removeEventListener("beforeunload", save);
});
let lock = false;
onBeforeRouteLeave((to, from, next) => {
if (lock) {
return next();
}
lock = true;
ElMessageBox.confirm("退出前是否保存当前数据?", "提示", {
type: "warning",
confirmButtonText: "保存",
cancelButtonText: "不保存",
distinguishCancelAndClose: true,
beforeClose(action, instance, done) {
done();
if (action == "confirm") {
save();
next();
} else if (action == "cancel") {
next();
} else {
lock = false;
next(false);
}
}
}).catch(() => null);
});
</script>
<style lang="scss">
.cl-flow {
height: 100%;
.vue-flow {
height: 100%;
}
.vue-flow__minimap {
margin: 0;
bottom: 50px;
right: 10px;
height: 100px;
border-radius: 6px;
overflow: hidden;
box-shadow: 0px 0 6px 1px rgba(16, 24, 40, 0.08);
}
.vue-flow__node:has(.is-moving) {
transition: transform 0.1s;
}
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<cl-dialog v-model="visible" title="节点运行信息" width="70%">
<cl-editor-monaco v-model="nodeInfo" height="800px" />
</cl-dialog>
</template>
<script lang="ts" name="flow-tools-log-detail" setup>
import { ref } from "vue";
const nodeInfo = ref<any>(null);
//
const visible = ref<boolean>(false);
//
function open(row) {
if (row.nodeInfo) {
nodeInfo.value = row.nodeInfo;
}
visible.value = true;
}
//
function close() {
visible.value = false;
}
defineExpose({ open });
</script>

View File

@ -0,0 +1,192 @@
<template>
<cl-dialog v-model="visible" width="80%" title="调用日志">
<cl-crud ref="Crud">
<cl-row>
<!-- 刷新按钮 -->
<cl-refresh-btn />
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-filter label="状态">
<cl-select
:width="160"
:options="options.type"
prop="type"
placeholder="请选择状态"
/>
</cl-filter>
</cl-row>
<cl-row>
<!-- 数据表格 -->
<cl-table ref="Table" :auto-height="false" />
</cl-row>
<cl-row>
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination />
</cl-row>
</cl-crud>
</cl-dialog>
<log-detail :ref="setRefs('logDetailRef')" />
</template>
<script lang="ts" name="flow-tools-log" setup>
import LogDetail from "./detail.vue";
import { ref, nextTick, reactive } from "vue";
import { useCrud, useTable } from "@cool-vue/crud";
import { useCool } from "/@/cool";
import { isEmpty } from "lodash-es";
const { service, refs, setRefs } = useCool();
const options = reactive({
type: [
{
label: "失败",
type: "danger",
value: 0
},
{
label: "成功",
type: "success",
value: 1
},
{
label: "未知",
type: "info",
value: 2
}
]
});
//
const visible = ref<boolean>(false);
//
function open(row) {
visible.value = true;
nextTick(() => {
refresh({
flowId: row.id
});
});
}
//
function close() {
visible.value = false;
}
// cl-table
const Table = useTable({
columns: [
{
label: "流程名称",
prop: "flowName",
minWidth: 140
},
{
label: "流程label",
prop: "flowLabel",
minWidth: 120
},
{
label: "传入参数",
prop: "inputParams",
minWidth: 160,
showOverflowTooltip: true
},
{
label: "结果",
prop: "result",
width: 160,
showOverflowTooltip: true
},
{
label: "创建时间",
prop: "createTime",
width: 170,
sortable: "custom"
},
{
label: "执行状态",
prop: "type",
width: 140,
fixed: "right",
dict: options.type
},
{
type: "op",
width: 120,
buttons: [
{
label: "运行信息",
onClick: ({ scope }) => {
refs.logDetailRef.open(scope.row);
}
}
]
}
]
});
// cl-crud
const Crud = useCrud(
{
service: service.flow.log,
async onRefresh(params, { render, next }) {
const res = await service.flow.log.page(params);
//
const list =
res.list?.map((e) => {
let { inputParams, result, ...arg } = e;
//
inputParams = inputParams ? JSON.stringify(decodeUnicode(inputParams)) : "";
result = result ? JSON.stringify(decodeUnicode(result)) : "";
return {
...arg,
inputParams,
result
};
}) ?? [];
//
render(list, res.pagination);
}
}
// (app) => {
// app.refresh();
// }
);
//
function refresh(params?: any) {
Crud.value?.refresh(params);
}
/**
* 处理Unicode 解码参数
* @param data
* @param key
*/
function decodeUnicode(data, key = "dynamic") {
if (data && !isEmpty(data[key])) {
data[key] = JSON.parse(data[key]);
}
return data;
}
defineExpose({
open
});
</script>

View File

@ -0,0 +1,58 @@
<template>
<div class="form-input-number">
<span class="label prefix" v-if="prefix">{{ prefix }}</span>
<div>
<el-input-number
v-model="value"
:min="min"
:max="max"
:precision="precision"
></el-input-number>
</div>
<span class="label suffix" v-if="suffix">{{ suffix }}</span>
</div>
</template>
<script lang="ts" setup>
import { useModel } from "vue";
const props = defineProps({
modelValue: null,
prefix: String,
suffix: String,
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 9999999999
},
precision: {
type: Number,
default: 0
}
});
const value = useModel(props, "modelValue");
</script>
<style lang="scss" scoped>
.form-input-number {
display: flex;
align-items: center;
.label {
flex-shrink: 0;
font-size: var(--el-form-label-font-size);
// color: var(--el-text-color-regular);
color: var(--el-color-info);
&.prefix {
margin-right: 10px;
}
&.suffix {
margin-left: 10px;
}
}
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<div class="form-input-params">
<cl-svg name="add" class="btn-icon is-rt" @click="add()" v-if="!disabled" />
<div
class="item"
v-for="(item, index) in list"
:key="index"
:class="{
'is-error': errFields.includes(item.field)
}"
>
<div class="d">
<el-input
v-model="item.field"
placeholder="变量名"
class="name"
clearable
:disabled="!editField"
/>
</div>
<div class="d">
<tools-var
v-model="item.name"
v-model:node-id="item.nodeId"
v-model:type="item.type"
v-model:value="item.value"
:inputable="varInputable"
/>
</div>
<cl-svg name="delete" class="btn-icon" @click="remove(index)" v-if="!disabled" />
</div>
<el-text v-if="isEmpty(list) && placeholder" type="info" size="small">
<el-icon> <warning /> </el-icon>
{{ placeholder }}
</el-text>
</div>
</template>
<script setup lang="ts" name="node-base-form-input-params">
import type { FlowField } from "/$/flow/types";
import { useModel, type PropType, computed } from "vue";
import ToolsVar from "/$/flow/components/tools/var.vue";
import { isEmpty } from "lodash-es";
import { Warning } from "@element-plus/icons-vue";
const props = defineProps({
modelValue: {
type: Array as PropType<FlowField[]>,
default: () => []
},
field: {
type: String,
default: "arg"
},
//
editField: {
type: Boolean,
default: true
},
//
disabled: Boolean,
//
placeholder: String,
//
varInputable: Boolean
});
const list = useModel(props, "modelValue");
//
const errFields = computed(() => {
const arr: string[] = [];
list.value.forEach((a, i) => {
const n = list.value.findIndex((b) => b.field == a.field);
if (n >= 0 && i != n) {
arr.push(a.field);
}
});
return arr;
});
let id = 1;
//
function add() {
list.value.push({
field: `${props.field}${++id}`
});
}
//
function remove(index: number) {
list.value.splice(index, 1);
}
</script>
<style lang="scss" scoped>
.form-input-params {
.item {
display: flex;
align-items: center;
margin-bottom: 5px;
.d {
margin-right: 5px;
flex: 1;
overflow: hidden;
}
&:last-child {
margin-bottom: 0;
}
&.is-error {
.d {
&:first-child {
:deep(.el-input__wrapper) {
box-shadow: 0 0 0 1px var(--el-color-danger) inset;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,165 @@
<template>
<div class="form-input-params">
<cl-svg name="add" class="btn-icon is-rt" @click="add()" v-if="op" />
<div class="item" v-for="(item, index) in list" :key="index">
<div class="d">
<el-input
v-model="item.field"
placeholder="变量名"
class="name"
:disabled="onDisabled(item, 'field')"
/>
</div>
<div class="d">
<template v-if="typeInput">
<el-input v-model="item.type" placeholder="请输入描述">
<template #suffix>
<cl-svg name="fullscreen" class="btn-icon" @click="input.open(item)" />
</template>
</el-input>
</template>
<el-select
v-model="item.type"
placeholder="类型"
:disabled="onDisabled(item, 'type')"
v-else
>
<el-option v-for="t in options.type" :key="t" :label="t" :value="t" />
</el-select>
</div>
<cl-svg name="delete" class="btn-icon" @click="remove(index)" v-if="op" />
</div>
<!-- 自定义输入 -->
<cl-dialog v-model="input.visible" title="自定义输入">
<el-input type="textarea" v-model="input.value" :rows="20" placeholder="请输入" />
<template #footer>
<el-button @click="input.close">取消</el-button>
<el-button type="success" @click="input.save">保存</el-button>
</template>
</cl-dialog>
</div>
</template>
<script setup lang="ts" name="node-base-form-output-params">
import { ElMessage } from "element-plus";
import type { FlowField } from "/$/flow/types";
import { reactive, useModel, type PropType } from "vue";
const props = defineProps({
modelValue: {
type: Array as PropType<FlowField[]>,
default: () => []
},
//
typeInput: Boolean,
//
disabledFields: [Array],
//
editField: {
type: Boolean,
default: true
},
//
op: {
type: Boolean,
default: true
}
});
const list = useModel(props, "modelValue");
//
const options = reactive({
type: ["string", "number", "array", "object", "boolean", "date"]
});
let id = 0;
//
function add() {
if (!id) {
id = list.value.length;
}
list.value.push({
field: `res${++id}`,
type: props.typeInput ? "" : "string"
});
}
//
function remove(index: number) {
list.value.splice(index, 1);
}
//
function onDisabled(item: FlowField, key: "field" | "type") {
let f = props.disabledFields?.includes(item.field);
if (!f) {
if (key == "field") {
f = !props.editField;
}
}
return f;
}
//
const input = reactive({
visible: false,
value: "",
data: null as FlowField | null,
open(item: FlowField) {
input.data = item!;
input.value = item.type!;
input.visible = true;
},
close() {
input.visible = false;
},
save() {
if (!input.value) {
return ElMessage.warning("请输入内容");
}
if (input.data) {
input.data.type = input.value;
}
input.close();
}
});
</script>
<style lang="scss" scoped>
.form-input-params {
.item {
display: flex;
align-items: center;
margin-bottom: 5px;
.d {
flex: 1;
margin-right: 5px;
}
&:last-child {
margin-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div class="form-text">
<el-text tag="p" size="small" type="info" v-for="(item, index) in list" :key="index">{{
item
}}</el-text>
</div>
</template>
<script setup lang="ts" name="node-base-form-text">
import { computed } from "vue";
import { isArray } from "lodash-es";
const props = defineProps({
text: {
type: [String, Array],
default: ""
}
});
const list = computed(() => {
return isArray(props.text) ? props.text : [props.text];
});
</script>
<style lang="scss" scoped>
.form-text {
line-height: normal;
.el-text {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,11 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
export default (): FlowNode => {
return {
group: "信息",
label: "百度搜索",
description: "通过百度搜索引擎查找内容",
component
};
};

View File

@ -0,0 +1,28 @@
<template>
<div class="node-baidu"></div>
</template>
<script lang="ts" setup name="node-baidu">
import { PropType } from "vue";
import { useFlow } from "/$/flow";
import type { FlowNode } from "/$/flow/types";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
},
//
focus: {
type: Boolean,
default: false
}
});
const flow = useFlow();
</script>
<style lang="scss" scoped>
.node-baidu {
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<div class="form-adv">
<div class="textarea-item">
<div class="head">
<span>指令</span>
</div>
<el-input
type="textarea"
v-model="data.cmd"
placeholder="输入你的指令"
:autosize="{
minRows: 4,
maxRows: 6
}"
resize="none"
/>
</div>
</div>
</template>
<script setup lang="ts" name="node-problem-form-adv">
import { useModel } from "vue";
const props = defineProps({
modelValue: {
type: Object,
default: () => ({})
}
});
const data = useModel(props, "modelValue");
</script>
<style lang="scss" scoped>
.form-adv {
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<div class="form-classify">
<cl-svg name="add" class="btn-icon is-rt" @click="add()" />
<div class="list">
<div class="textarea-item" v-for="(item, index) in list" :key="index">
<cl-svg name="delete" class="btn-icon" @click="remove(index)"></cl-svg>
<el-input
type="textarea"
v-model="list[index]"
placeholder="输入你的主题内容"
:autosize="{
minRows: 2,
maxRows: 4
}"
resize="none"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="node-problem-form-classify">
import { useModel, watch, type PropType, onMounted } from "vue";
import { useFlow } from "/$/flow/hooks";
const props = defineProps({
modelValue: {
type: Array as PropType<string[]>,
default: () => []
}
});
const flow = useFlow();
const list = useModel(props, "modelValue");
//
function add() {
list.value.push("");
}
//
function remove(index: number) {
list.value.splice(index, 1);
// handle线
const edge = flow.edges.find(
(e) => e.source == flow.node?.id && e.sourceHandle == `source-${index}`
);
if (edge) {
flow.removeEdges(edge);
}
}
onMounted(() => {
watch(
list,
(arr) => {
if (flow.node) {
flow.node.handle!.next = arr.map((_, i) => {
return {
label: `分类 ${i + 1}`,
value: `source-${i}`
};
});
}
},
{
immediate: true,
deep: true
}
);
});
</script>
<style lang="scss" scoped>
.form-classify {
.list {
.textarea-item {
padding-top: 8px !important;
.btn-icon {
position: absolute;
right: 8px;
top: 8px;
z-index: 9;
background-color: var(--el-bg-color);
}
}
}
}
</style>

View File

@ -0,0 +1,119 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import FormModel from "../llm/form/model.vue";
import FormClassify from "./form/classify.vue";
import FormAdv from "./form/adv.vue";
import FormInputParams from "../_base/form/input-params.vue";
import FormText from "../_base/form/text.vue";
export default (): FlowNode => {
return {
group: "AI",
label: "分类器",
description: "根据内容调用 LLM 进行智能分类",
color: "#409eff",
component,
form: {
items: [
{
label: "输入变量",
prop: "inputParams",
component: {
vm: FormInputParams,
props: {
editField: false,
disabled: true
}
}
},
{
label: "模型",
prop: "options.model",
component: {
vm: FormModel
}
},
{
label: "分类",
prop: "options.types",
component: {
vm: FormClassify
}
},
// {
// component: {
// name: "cl-form-card",
// props: {
// label: "高级设置",
// expand: false
// }
// },
// children: [
// {
// prop: "options.adv",
// component: {
// vm: FormAdv
// }
// }
// ]
// },
{
label: "输出变量",
component: {
vm: FormText,
props: {
text: ["content<string> 内容", "index<number> 分类索引"]
}
}
}
]
},
data: {
inputParams: [
{
field: "content",
type: "string"
}
],
outputParams: [
{
field: "index",
type: "number"
}
],
options: {
model: {
options: [],
params: {
model: ""
}
},
types: [""]
}
},
handle: {
source: false,
next: []
},
validator(data) {
const { model, types } = data.options;
// 验证变量是否绑定值
const param = data.inputParams?.find((e) => !e.nodeId);
if (param) {
return `请绑定变量:${param.field}`;
}
// 验证分类内容是否填写
const type = types?.findIndex((e: string) => !e);
if (type >= 0) {
return `请填写分类${type + 1}的内容`;
}
// 验证模型是否选择
if (!model.params.model) {
return "请选择模型";
}
}
};
};

View File

@ -0,0 +1,89 @@
<template>
<div class="node-classify">
<div class="model" v-if="!focus">
<model-text :data="node.data?.options?.model" />
</div>
<div class="item" v-for="(item, index) in list" :key="index">
<span class="content">{{ item.content || "未填写内容" }}</span>
<tools-handle
type="source"
:node-id="node.id"
:id="item.value"
:position="{
right: '-24px'
}"
/>
</div>
</div>
</template>
<script lang="ts" setup name="node-classify">
import type { FlowNode } from "/$/flow/types";
import { computed, type PropType } from "vue";
import ToolsHandle from "/$/flow/components/tools/handle.vue";
import ModelText from "../llm/form/model-text.vue";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
},
//
focus: {
type: Boolean,
default: false
}
});
const list = computed(() => {
return ((props.node.data?.options?.types as any[]) || []).map((e, i) => {
return {
content: e,
value: `source-${i}`
};
});
});
</script>
<style lang="scss" scoped>
.node-classify {
padding-bottom: 15px;
.model,
.item {
display: flex;
align-items: center;
background-color: var(--el-fill-color-light);
padding: 0 10px;
border-radius: 6px;
&:last-child {
margin-bottom: 0;
}
}
.model {
height: 33px;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-bottom: 8px;
}
.item {
height: 30px;
margin-bottom: 4px;
font-size: 12px;
position: relative;
.content {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
</style>

View File

@ -0,0 +1,697 @@
export const axios = `
declare module "axios" {
// TypeScript Version: 4.7
export type AxiosHeaderValue = AxiosHeaders | string | string[] | number | boolean | null;
interface RawAxiosHeaders {
[key: string]: AxiosHeaderValue;
}
type MethodsHeaders = Partial<
{
[Key in Method as Lowercase<Key>]: AxiosHeaders;
} & { common: AxiosHeaders }
>;
type AxiosHeaderMatcher =
| string
| RegExp
| ((this: AxiosHeaders, value: string, name: string) => boolean);
type AxiosHeaderParser = (this: AxiosHeaders, value: AxiosHeaderValue, header: string) => any;
export class AxiosHeaders {
constructor(headers?: RawAxiosHeaders | AxiosHeaders | string);
[key: string]: any;
set(
headerName?: string,
value?: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher
): AxiosHeaders;
set(headers?: RawAxiosHeaders | AxiosHeaders | string, rewrite?: boolean): AxiosHeaders;
get(headerName: string, parser: RegExp): RegExpExecArray | null;
get(headerName: string, matcher?: true | AxiosHeaderParser): AxiosHeaderValue;
has(header: string, matcher?: AxiosHeaderMatcher): boolean;
delete(header: string | string[], matcher?: AxiosHeaderMatcher): boolean;
clear(matcher?: AxiosHeaderMatcher): boolean;
normalize(format: boolean): AxiosHeaders;
concat(
...targets: Array<AxiosHeaders | RawAxiosHeaders | string | undefined | null>
): AxiosHeaders;
toJSON(asStrings?: boolean): RawAxiosHeaders;
static from(thing?: AxiosHeaders | RawAxiosHeaders | string): AxiosHeaders;
static accessor(header: string | string[]): AxiosHeaders;
static concat(
...targets: Array<AxiosHeaders | RawAxiosHeaders | string | undefined | null>
): AxiosHeaders;
setContentType(value: ContentType, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentType(parser?: RegExp): RegExpExecArray | null;
getContentType(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasContentType(matcher?: AxiosHeaderMatcher): boolean;
setContentLength(
value: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher
): AxiosHeaders;
getContentLength(parser?: RegExp): RegExpExecArray | null;
getContentLength(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasContentLength(matcher?: AxiosHeaderMatcher): boolean;
setAccept(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getAccept(parser?: RegExp): RegExpExecArray | null;
getAccept(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasAccept(matcher?: AxiosHeaderMatcher): boolean;
setUserAgent(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getUserAgent(parser?: RegExp): RegExpExecArray | null;
getUserAgent(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasUserAgent(matcher?: AxiosHeaderMatcher): boolean;
setContentEncoding(
value: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher
): AxiosHeaders;
getContentEncoding(parser?: RegExp): RegExpExecArray | null;
getContentEncoding(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasContentEncoding(matcher?: AxiosHeaderMatcher): boolean;
setAuthorization(
value: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher
): AxiosHeaders;
getAuthorization(parser?: RegExp): RegExpExecArray | null;
getAuthorization(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasAuthorization(matcher?: AxiosHeaderMatcher): boolean;
[Symbol.iterator](): IterableIterator<[string, AxiosHeaderValue]>;
}
type CommonRequestHeadersList =
| "Accept"
| "Content-Length"
| "User-Agent"
| "Content-Encoding"
| "Authorization";
type ContentType =
| AxiosHeaderValue
| "text/html"
| "text/plain"
| "multipart/form-data"
| "application/json"
| "application/x-www-form-urlencoded"
| "application/octet-stream";
export type RawAxiosRequestHeaders = Partial<
RawAxiosHeaders & {
[Key in CommonRequestHeadersList]: AxiosHeaderValue;
} & {
"Content-Type": ContentType;
}
>;
export type AxiosRequestHeaders = RawAxiosRequestHeaders & AxiosHeaders;
type CommonResponseHeadersList =
| "Server"
| "Content-Type"
| "Content-Length"
| "Cache-Control"
| "Content-Encoding";
type RawCommonResponseHeaders = {
[Key in CommonResponseHeadersList]: AxiosHeaderValue;
} & {
"set-cookie": string[];
};
export type RawAxiosResponseHeaders = Partial<RawAxiosHeaders & RawCommonResponseHeaders>;
export type AxiosResponseHeaders = RawAxiosResponseHeaders & AxiosHeaders;
export interface AxiosRequestTransformer {
(this: InternalAxiosRequestConfig, data: any, headers: AxiosRequestHeaders): any;
}
export interface AxiosResponseTransformer {
(
this: InternalAxiosRequestConfig,
data: any,
headers: AxiosResponseHeaders,
status?: number
): any;
}
export interface AxiosAdapter {
(config: InternalAxiosRequestConfig): AxiosPromise;
}
export interface AxiosBasicCredentials {
username: string;
password: string;
}
export interface AxiosProxyConfig {
host: string;
port: number;
auth?: AxiosBasicCredentials;
protocol?: string;
}
export enum HttpStatusCode {
Continue = 100,
SwitchingProtocols = 101,
Processing = 102,
EarlyHints = 103,
Ok = 200,
Created = 201,
Accepted = 202,
NonAuthoritativeInformation = 203,
NoContent = 204,
ResetContent = 205,
PartialContent = 206,
MultiStatus = 207,
AlreadyReported = 208,
ImUsed = 226,
MultipleChoices = 300,
MovedPermanently = 301,
Found = 302,
SeeOther = 303,
NotModified = 304,
UseProxy = 305,
Unused = 306,
TemporaryRedirect = 307,
PermanentRedirect = 308,
BadRequest = 400,
Unauthorized = 401,
PaymentRequired = 402,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
NotAcceptable = 406,
ProxyAuthenticationRequired = 407,
RequestTimeout = 408,
Conflict = 409,
Gone = 410,
LengthRequired = 411,
PreconditionFailed = 412,
PayloadTooLarge = 413,
UriTooLong = 414,
UnsupportedMediaType = 415,
RangeNotSatisfiable = 416,
ExpectationFailed = 417,
ImATeapot = 418,
MisdirectedRequest = 421,
UnprocessableEntity = 422,
Locked = 423,
FailedDependency = 424,
TooEarly = 425,
UpgradeRequired = 426,
PreconditionRequired = 428,
TooManyRequests = 429,
RequestHeaderFieldsTooLarge = 431,
UnavailableForLegalReasons = 451,
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504,
HttpVersionNotSupported = 505,
VariantAlsoNegotiates = 506,
InsufficientStorage = 507,
LoopDetected = 508,
NotExtended = 510,
NetworkAuthenticationRequired = 511
}
export type Method =
| "get"
| "GET"
| "delete"
| "DELETE"
| "head"
| "HEAD"
| "options"
| "OPTIONS"
| "post"
| "POST"
| "put"
| "PUT"
| "patch"
| "PATCH"
| "purge"
| "PURGE"
| "link"
| "LINK"
| "unlink"
| "UNLINK";
export type ResponseType =
| "arraybuffer"
| "blob"
| "document"
| "json"
| "text"
| "stream"
| "formdata";
export type responseEncoding =
| "ascii"
| "ASCII"
| "ansi"
| "ANSI"
| "binary"
| "BINARY"
| "base64"
| "BASE64"
| "base64url"
| "BASE64URL"
| "hex"
| "HEX"
| "latin1"
| "LATIN1"
| "ucs-2"
| "UCS-2"
| "ucs2"
| "UCS2"
| "utf-8"
| "UTF-8"
| "utf8"
| "UTF8"
| "utf16le"
| "UTF16LE";
export interface TransitionalOptions {
silentJSONParsing?: boolean;
forcedJSONParsing?: boolean;
clarifyTimeoutError?: boolean;
}
export interface GenericAbortSignal {
readonly aborted: boolean;
onabort?: ((...args: any) => any) | null;
addEventListener?: (...args: any) => any;
removeEventListener?: (...args: any) => any;
}
export interface FormDataVisitorHelpers {
defaultVisitor: SerializerVisitor;
convertValue: (value: any) => any;
isVisitable: (value: any) => boolean;
}
export interface SerializerVisitor {
(
this: GenericFormData,
value: any,
key: string | number,
path: null | Array<string | number>,
helpers: FormDataVisitorHelpers
): boolean;
}
export interface SerializerOptions {
visitor?: SerializerVisitor;
dots?: boolean;
metaTokens?: boolean;
indexes?: boolean | null;
}
// tslint:disable-next-line
export interface FormSerializerOptions extends SerializerOptions {}
export interface ParamEncoder {
(value: any, defaultEncoder: (value: any) => any): any;
}
export interface CustomParamsSerializer {
(params: Record<string, any>, options?: ParamsSerializerOptions): string;
}
export interface ParamsSerializerOptions extends SerializerOptions {
encode?: ParamEncoder;
serialize?: CustomParamsSerializer;
}
type MaxUploadRate = number;
type MaxDownloadRate = number;
type BrowserProgressEvent = any;
export interface AxiosProgressEvent {
loaded: number;
total?: number;
progress?: number;
bytes: number;
rate?: number;
estimated?: number;
upload?: boolean;
download?: boolean;
event?: BrowserProgressEvent;
lengthComputable: boolean;
}
type Milliseconds = number;
type AxiosAdapterName = "fetch" | "xhr" | "http" | string;
type AxiosAdapterConfig = AxiosAdapter | AxiosAdapterName;
export type AddressFamily = 4 | 6 | undefined;
export interface LookupAddressEntry {
address: string;
family?: AddressFamily;
}
export type LookupAddress = string | LookupAddressEntry;
export interface AxiosRequestConfig<D = any> {
url?: string;
method?: Method | string;
baseURL?: string;
transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
headers?: (RawAxiosRequestHeaders & MethodsHeaders) | AxiosHeaders;
params?: any;
paramsSerializer?: ParamsSerializerOptions | CustomParamsSerializer;
data?: D;
timeout?: Milliseconds;
timeoutErrorMessage?: string;
withCredentials?: boolean;
adapter?: AxiosAdapterConfig | AxiosAdapterConfig[];
auth?: AxiosBasicCredentials;
responseType?: ResponseType;
responseEncoding?: responseEncoding | string;
xsrfCookieName?: string;
xsrfHeaderName?: string;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
maxContentLength?: number;
validateStatus?: ((status: number) => boolean) | null;
maxBodyLength?: number;
maxRedirects?: number;
maxRate?: number | [MaxUploadRate, MaxDownloadRate];
beforeRedirect?: (
options: Record<string, any>,
responseDetails: { headers: Record<string, string>; statusCode: HttpStatusCode }
) => void;
socketPath?: string | null;
transport?: any;
httpAgent?: any;
httpsAgent?: any;
proxy?: AxiosProxyConfig | false;
cancelToken?: CancelToken;
decompress?: boolean;
transitional?: TransitionalOptions;
signal?: GenericAbortSignal;
insecureHTTPParser?: boolean;
env?: {
FormData?: new (...args: any[]) => object;
};
formSerializer?: FormSerializerOptions;
family?: AddressFamily;
lookup?:
| ((
hostname: string,
options: object,
cb: (
err: Error | null,
address: LookupAddress | LookupAddress[],
family?: AddressFamily
) => void
) => void)
| ((
hostname: string,
options: object
) => Promise<
| [address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily]
| LookupAddress
>);
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
fetchOptions?: Record<string, any>;
}
// Alias
export type RawAxiosRequestConfig<D = any> = AxiosRequestConfig<D>;
export interface InternalAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
headers: AxiosRequestHeaders;
}
export interface HeadersDefaults {
common: RawAxiosRequestHeaders;
delete: RawAxiosRequestHeaders;
get: RawAxiosRequestHeaders;
head: RawAxiosRequestHeaders;
post: RawAxiosRequestHeaders;
put: RawAxiosRequestHeaders;
patch: RawAxiosRequestHeaders;
options?: RawAxiosRequestHeaders;
purge?: RawAxiosRequestHeaders;
link?: RawAxiosRequestHeaders;
unlink?: RawAxiosRequestHeaders;
}
export interface AxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, "headers"> {
headers: HeadersDefaults;
}
export interface CreateAxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, "headers"> {
headers?: RawAxiosRequestHeaders | AxiosHeaders | Partial<HeadersDefaults>;
}
export interface AxiosResponse<T = any, D = any> {
data: T;
status: number;
statusText: string;
headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
config: InternalAxiosRequestConfig<D>;
request?: any;
}
export class AxiosError<T = unknown, D = any> extends Error {
constructor(
message?: string,
code?: string,
config?: InternalAxiosRequestConfig<D>,
request?: any,
response?: AxiosResponse<T, D>
);
config?: InternalAxiosRequestConfig<D>;
code?: string;
request?: any;
response?: AxiosResponse<T, D>;
isAxiosError: boolean;
status?: number;
toJSON: () => object;
cause?: Error;
static from<T = unknown, D = any>(
error: Error | unknown,
code?: string,
config?: InternalAxiosRequestConfig<D>,
request?: any,
response?: AxiosResponse<T, D>,
customProps?: object
): AxiosError<T, D>;
static readonly ERR_FR_TOO_MANY_REDIRECTS = "ERR_FR_TOO_MANY_REDIRECTS";
static readonly ERR_BAD_OPTION_VALUE = "ERR_BAD_OPTION_VALUE";
static readonly ERR_BAD_OPTION = "ERR_BAD_OPTION";
static readonly ERR_NETWORK = "ERR_NETWORK";
static readonly ERR_DEPRECATED = "ERR_DEPRECATED";
static readonly ERR_BAD_RESPONSE = "ERR_BAD_RESPONSE";
static readonly ERR_BAD_REQUEST = "ERR_BAD_REQUEST";
static readonly ERR_NOT_SUPPORT = "ERR_NOT_SUPPORT";
static readonly ERR_INVALID_URL = "ERR_INVALID_URL";
static readonly ERR_CANCELED = "ERR_CANCELED";
static readonly ECONNABORTED = "ECONNABORTED";
static readonly ETIMEDOUT = "ETIMEDOUT";
}
export class CanceledError<T> extends AxiosError<T> {}
export type AxiosPromise<T = any> = Promise<AxiosResponse<T>>;
export interface CancelStatic {
new (message?: string): Cancel;
}
export interface Cancel {
message: string | undefined;
}
export interface Canceler {
(message?: string, config?: AxiosRequestConfig, request?: any): void;
}
export interface CancelTokenStatic {
new (executor: (cancel: Canceler) => void): CancelToken;
source(): CancelTokenSource;
}
export interface CancelToken {
promise: Promise<Cancel>;
reason?: Cancel;
throwIfRequested(): void;
}
export interface CancelTokenSource {
token: CancelToken;
cancel: Canceler;
}
export interface AxiosInterceptorOptions {
synchronous?: boolean;
runWhen?: (config: InternalAxiosRequestConfig) => boolean;
}
export interface AxiosInterceptorManager<V> {
use(
onFulfilled?: ((value: V) => V | Promise<V>) | null,
onRejected?: ((error: any) => any) | null,
options?: AxiosInterceptorOptions
): number;
eject(id: number): void;
clear(): void;
}
export class Axios {
constructor(config?: AxiosRequestConfig);
defaults: AxiosDefaults;
interceptors: {
request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
getUri(config?: AxiosRequestConfig): string;
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
get<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<R>;
delete<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<R>;
head<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<R>;
options<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<R>;
post<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
put<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
patch<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
postForm<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
putForm<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
patchForm<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
): Promise<R>;
}
export interface AxiosInstance extends Axios {
<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<R>;
defaults: Omit<AxiosDefaults, "headers"> & {
headers: HeadersDefaults & {
[key: string]: AxiosHeaderValue;
};
};
}
export interface GenericFormData {
append(name: string, value: any, options?: any): any;
}
export interface GenericHTMLFormElement {
name: string;
method: string;
submit(): void;
}
export function getAdapter(
adapters: AxiosAdapterConfig | AxiosAdapterConfig[] | undefined
): AxiosAdapter;
export function toFormData(
sourceObj: object,
targetFormData?: GenericFormData,
options?: FormSerializerOptions
): GenericFormData;
export function formToJSON(form: GenericFormData | GenericHTMLFormElement): object;
export function isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D>;
export function spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
export function isCancel(value: any): value is Cancel;
export function all<T>(values: Array<T | Promise<T>>): Promise<T[]>;
export interface AxiosStatic extends AxiosInstance {
create(config?: CreateAxiosDefaults): AxiosInstance;
Cancel: CancelStatic;
CancelToken: CancelTokenStatic;
Axios: typeof Axios;
AxiosError: typeof AxiosError;
HttpStatusCode: typeof HttpStatusCode;
readonly VERSION: string;
isCancel: typeof isCancel;
all: typeof all;
spread: typeof spread;
isAxiosError: typeof isAxiosError;
toFormData: typeof toFormData;
formToJSON: typeof formToJSON;
getAdapter: typeof getAdapter;
CanceledError: typeof CanceledError;
AxiosHeaders: typeof AxiosHeaders;
}
const axios: AxiosStatic;
export default axios;
}
`;

View File

@ -0,0 +1,639 @@
export const cool = `
/**
* Repository
*/
interface Repository {
/**
* any target that is managed by this repository.
* If this repository manages any from schema,
* then it returns a name of that schema instead.
*/
readonly target: any;
/**
* any Manager used by this repository.
*/
readonly manager: any;
/**
* Query runner provider used for this repository.
*/
readonly queryRunner?: any;
/**
* any metadata of the any current repository manages.
*/
get metadata(): any;
/**
* Creates a new query builder that can be used to build a SQL query.
*/
createQueryBuilder(alias?: string, queryRunner?: any): any;
/**
* Checks if any has an id.
* If any composite compose ids, it will check them all.
*/
hasId(any: any): boolean;
/**
* Gets any mixed id.
*/
getId(any: any): any;
/**
* Creates a new any instance.
*/
create(): any;
/**
* Creates new entities and copies all any properties from given objects into their new entities.
* Note that it copies only properties that are present in any schema.
*/
create(anyLikeArray: any): any[];
/**
* Creates a new any instance and copies all any properties from this object into a new any.
* Note that it copies only properties that are present in any schema.
*/
create(anyLike: any): any;
/**
* Merges multiple entities (or any-like objects) into a given any.
*/
merge(mergeIntoany: any, ...anyLikes: any): any;
/**
* Creates a new any from the given plain javascript object. If any already exist in the database, then
* it loads it (and everything related to it), replaces all values with the new ones from the given object
* and returns this new any. This new any is actually a loaded from the db any with all properties
* replaced from the new object.
*
* Note that given any-like object must have an any id / primary key to find any by.
* Returns undefined if any with given id was not found.
*/
preload(anyLike: any): Promise<any | undefined>;
/**
* Saves all given entities in the database.
* If entities do not exist in the database then inserts, otherwise updates.
*/
save(
entities: any[],
options: any & {
reload: false;
}
): Promise<any[]>;
/**
* Saves all given entities in the database.
* If entities do not exist in the database then inserts, otherwise updates.
*/
save(entities: any[], options?: any): Promise<any[]>;
/**
* Saves a given any in the database.
* If any does not exist in the database then inserts, otherwise updates.
*/
save(
any: any,
options: any & {
reload: false;
}
): Promise<any>;
/**
* Saves a given any in the database.
* If any does not exist in the database then inserts, otherwise updates.
*/
save(any: any, options?: any): any;
/**
* Removes a given entities from the database.
*/
remove(entities: any[], options?: any): Promise<any[]>;
/**
* Removes a given any from the database.
*/
remove(any: any, options?: any): Promise<any>;
/**
* Records the delete date of all given entities.
*/
softRemove(
entities: any[],
options: any & {
reload: false;
}
): Promise<any[]>;
/**
* Records the delete date of all given entities.
*/
softRemove(entities: any[], options?: any): Promise<any[]>;
/**
* Records the delete date of a given any.
*/
softRemove(
any: any,
options: any & {
reload: false;
}
): Promise<any>;
/**
* Records the delete date of a given any.
*/
softRemove(any: any, options?: any): Promise<any>;
/**
* Recovers all given entities in the database.
*/
recover(
entities: any[],
options: any & {
reload: false;
}
): Promise<any[]>;
/**
* Recovers all given entities in the database.
*/
recover(entities: any[], options?: any): Promise<any[]>;
/**
* Recovers a given any in the database.
*/
recover(
any: any,
options: any & {
reload: false;
}
): Promise<any>;
/**
* Recovers a given any in the database.
*/
recover(any: any, options?: any): Promise<any>;
/**
* Inserts a given any into the database.
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
* Executes fast and efficient INSERT query.
* Does not check if any exist in the database, so query will fail if duplicate any is being inserted.
*/
insert(any: any | any[]): Promise<any>;
/**
* Updates any partially. any can be found by a given conditions.
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
* Executes fast and efficient UPDATE query.
* Does not check if any exist in the database.
*/
update(criteria: any, partialany: any): Promise<any>;
/**
* Inserts a given any into the database, unless a unique constraint conflicts then updates the any
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
* Executes fast and efficient INSERT ... ON CONFLICT DO UPDATE/ON DUPLICATE KEY UPDATE query.
*/
upsert(anyOrEntities: any | any[], conflictPathsOrOptions: string[] | any): Promise<any>;
/**
* Deletes entities by a given criteria.
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
* Executes fast and efficient DELETE query.
* Does not check if any exist in the database.
*/
delete(criteria: any): Promise<any>;
/**
* Records the delete date of entities by a given criteria.
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
* Executes fast and efficient SOFT-DELETE query.
* Does not check if any exist in the database.
*/
softDelete(criteria: any): Promise<any>;
/**
* Restores entities by a given criteria.
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
* Executes fast and efficient SOFT-DELETE query.
* Does not check if any exist in the database.
*/
restore(criteria: any): Promise<any>;
/**
* Checks whether any any exists that matches the given options.
*
* @deprecated use \`exists\` method instead, for example:
*
* .exists()
*/
exist(options?: any): Promise<boolean>;
/**
* Checks whether any any exists that matches the given options.
*/
exists(options?: any): Promise<boolean>;
/**
* Checks whether any any exists that matches the given conditions.
*/
existsBy(where: any | any[]): Promise<boolean>;
/**
* Counts entities that match given options.
* Useful for pagination.
*/
count(options?: any): Promise<number>;
/**
* Counts entities that match given conditions.
* Useful for pagination.
*/
countBy(where: any | any[]): Promise<number>;
/**
* Return the SUM of a column
*/
sum(columnName: any, where?: any | any[]): Promise<number | null>;
/**
* Return the AVG of a column
*/
average(columnName: any, where?: any | any[]): Promise<number | null>;
/**
* Return the MIN of a column
*/
minimum(columnName: any, where?: any | any[]): Promise<number | null>;
/**
* Return the MAX of a column
*/
maximum(columnName: any, where?: any | any[]): Promise<number | null>;
/**
* Finds entities that match given find options.
*/
find(options?: any): Promise<any[]>;
/**
* Finds entities that match given find options.
*/
findBy(where: any | any[]): Promise<any[]>;
/**
* Finds entities that match given find options.
* Also counts all entities that match given conditions,
* but ignores pagination settings (from and take options).
*/
findAndCount(options?: any): Promise<[any[], number]>;
/**
* Finds entities that match given WHERE conditions.
* Also counts all entities that match given conditions,
* but ignores pagination settings (from and take options).
*/
findAndCountBy(where: any | any[]): Promise<[any[], number]>;
/**
* Finds entities with ids.
* Optionally find options or conditions can be applied.
*
* @deprecated use \`findBy\` method instead in conjunction with \`In\` operator, for example:
*
* .findBy({
* id: In([1, 2, 3])
* })
*/
findByIds(ids: any[]): Promise<any[]>;
/**
* Finds first any by a given find options.
* If any was not found in the database - returns null.
*/
findOne(options: any): Promise<any | null>;
/**
* Finds first any that matches given where condition.
* If any was not found in the database - returns null.
*/
findOneBy(where: any | any[]): Promise<any | null>;
/**
* Finds first any that matches given id.
* If any was not found in the database - returns null.
*
* @deprecated use \`findOneBy\` method instead in conjunction with \`In\` operator, for example:
*
* .findOneBy({
* id: 1 // where "id" is your primary column name
* })
*/
findOneById(id: number | string | Date | any): Promise<any | null>;
/**
* Finds first any by a given find options.
* If any was not found in the database - rejects with error.
*/
findOneOrFail(options: any): Promise<any>;
/**
* Finds first any that matches given where condition.
* If any was not found in the database - rejects with error.
*/
findOneByOrFail(where: any | any[]): Promise<any>;
/**
* Executes a raw SQL query and returns a raw database results.
* Raw query execution is supported only by relational databases (MongoDB is not supported).
*/
query(query: string, parameters?: any[]): Promise<any>;
/**
* Clears all the data from the given table/collection (truncates/drops it).
*
* Note: this method uses TRUNCATE and may not work as you expect in transactions on some platforms.
* @see https://stackoverflow.com/a/5972738/925151
*/
clear(): Promise<void>;
/**
* Increments some column by provided value of the entities matched given conditions.
*/
increment(conditions: any, propertyPath: string, value: number | string): Promise<any>;
/**
* Decrements some column by provided value of the entities matched given conditions.
*/
decrement(conditions: any, propertyPath: string, value: number | string): Promise<any>;
/**
* Extends repository with provided functions.
*/
extend<CustomRepository>(
customs: CustomRepository & ThisType<this & CustomRepository>
): this & CustomRepository;
[key: string]: any;
}
/**
* App
*/
interface App {
/**
*
*/
getBaseDir(): string;
/**
*
*/
getAppDir(): string;
/**
*
*/
getEnv(): string;
/**
*
*/
getFramework(): any;
/**
*
*/
getProcessType(): any;
/**
*
*/
getApplicationContext(): any;
/**
*
* @param key key
*/
getConfig(key?: string): any;
/**
*
* @param name
*/
getLogger(name?: string): any;
/**
*
*/
getCoreLogger(): any;
/**
*
* @param name
* @param options
*/
createLogger(name: string, options: any): any;
/**
*
*/
getProjectName(): string;
/**
* RequestContainer的上下文
* @param args
*/
createAnonymousContext(...args: any[]): any;
/**
* RequestContainer的上下文
* @param BaseContextLoggerClass
*/
setContextLoggerClass(BaseContextLoggerClass: any): void;
/**
*
* @param obj
*/
addConfigObject(obj: any): any;
/**
*
* @param key
* @param value
*/
setAttr(key: string, value: any): any;
/**
*
* @param key
*/
getAttr<T>(key: string): T;
/**
* 使
* @param Middleware
*/
useMiddleware<R, N>(Middleware): void;
/**
*
*/
getMiddleware<R, N>(): any;
/**
* 使
* @param Filter
*/
useFilter<R, N>(Filter): void;
/**
*
* @param guard
*/
useGuard(guard): void;
/**
*
*/
getNamespace(): string;
[key: string]: any;
}
/**
* OrmManager
*/
interface OrmManager {
/**
*
* @param dataSourceName
*/
getDataSource(dataSourceName: string): any;
/**
*
* @param dataSourceName
*/
hasDataSource(dataSourceName: string): boolean;
/**
*
*/
getDataSourceNames(): string[];
/**
*
*/
getAllDataSources(): Map<string, any>;
/**
*
* @param dataSourceName
*/
isConnected(dataSourceName: string): Promise<boolean>;
/**
*
* @param config
* @param clientName
* @param options
*/
createInstance(
config: any,
clientName: any,
options?: {
validateConnection?: boolean;
cacheInstance?: boolean | undefined;
}
): Promise<any>;
/**
* model或者repository的数据源名称
* @param modelOrRepository
*/
getDataSourceNameByModel(modelOrRepository: any): string | undefined;
/**
*
*/
getName(): string;
/**
*
*/
stop(): Promise<void>;
/**
*
*/
getDefaultDataSourceName(): string;
/**
*
* @param name
*/
getDataSourcePriority(name: string): string;
/**
*
* @param name
*/
isHighPriority(name: string): boolean;
/**
*
* @param
*/
isMediumPriority(name: string): boolean;
/**
*
* @param name
*/
isLowPriority(name: string): boolean;
[key: string]: any;
}
/**
* PluginService
*/
interface PluginService {
/**
*
* @param key key
*/
getConfig(key: string): Promise<any>;
/**
*
* @param key key
* @param method
* @param params
* @returns
*/
invoke(key: string, method: string, ...params: any[]): Promise<any>;
/**
*
* @param key key
* @returns
*/
getInstance(key: string): Promise<any>;
[key: string]: any;
}
/**
* MidwayCache
*/
interface MidwayCache {
/**
*
* @param key
* @param value
* @param ttl
* @returns
*/
set: (key: string, value: unknown, ttl?: number) => Promise<void>;
/**
*
* @param key
* @returns
*/
get: <T>(key: string) => Promise<T | undefined>;
/**
*
* @param key
* @returns
*/
del: (key: string) => Promise<void>;
/**
*
* @returns
*/
reset: () => Promise<void>;
[key: string]: any;
}
/**
*
*/
declare class Base {
/**
*
*/
typeORMDataSourceManager: OrmManager;
/**
*
*/
app: App;
/**
*
*/
pluginService: PluginService;
/**
*
*/
cache: MidwayCache;
/**
*
*/
main(params: any): Promise<any>;
/**
* sql
* @param sql sql语句
* @param params
* @param dataSource
*/
execSql(sql: string, params, dataSource): Promise<any>;
/**
* typeorm的Repository
* @param anyName
* @returns
*/
getRepository(anyName: string): Promise<Repository>;
/**
* service
* @param service
* @param method
* @param args
* @returns
*/
invokeService(service: string, method: string, ...args): Promise<any>;
/**
*
* @param key key
* @param method
* @param args
* @returns
*/
invokePlugin(key: string, method: string, ...args): Promise<any>;
}
`;

View File

@ -0,0 +1,464 @@
export const dayjs = `
declare module "dayjs" {
function dayjs(date?: dayjs.ConfigType): dayjs.Dayjs;
function dayjs(
date?: dayjs.ConfigType,
format?: dayjs.OptionType,
strict?: boolean
): dayjs.Dayjs;
function dayjs(
date?: dayjs.ConfigType,
format?: dayjs.OptionType,
locale?: string,
strict?: boolean
): dayjs.Dayjs;
namespace dayjs {
interface ConfigTypeMap {
default: string | number | Date | Dayjs | null | undefined;
}
export type ConfigType = ConfigTypeMap[keyof ConfigTypeMap];
export interface FormatObject {
locale?: string;
format?: string;
utc?: boolean;
}
export type OptionType = FormatObject | string | string[];
export type UnitTypeShort = "d" | "D" | "M" | "y" | "h" | "m" | "s" | "ms";
export type UnitTypeLong =
| "millisecond"
| "second"
| "minute"
| "hour"
| "day"
| "month"
| "year"
| "date";
export type UnitTypeLongPlural =
| "milliseconds"
| "seconds"
| "minutes"
| "hours"
| "days"
| "months"
| "years"
| "dates";
export type UnitType = UnitTypeLong | UnitTypeLongPlural | UnitTypeShort;
export type OpUnitType = UnitType | "week" | "weeks" | "w";
export type QUnitType = UnitType | "quarter" | "quarters" | "Q";
export type ManipulateType = Exclude<OpUnitType, "date" | "dates">;
class Dayjs {
constructor(config?: ConfigType);
/**
* All Day.js objects are immutable. Still, \`dayjs#clone\` can create a clone of the current object if you need one.
* \`\`\`
* dayjs().clone()// => Dayjs
* dayjs(dayjs('2019-01-25')) // passing a Dayjs object to a constructor will also clone it
* \`\`\`
* Docs: https://day.js.org/docs/en/parse/dayjs-clone
*/
clone(): Dayjs;
/**
* This returns a \`boolean\` indicating whether the Day.js object contains a valid date or not.
* \`\`\`
* dayjs().isValid()// => boolean
* \`\`\`
* Docs: https://day.js.org/docs/en/parse/is-valid
*/
isValid(): boolean;
/**
* Get the year.
* \`\`\`
* dayjs().year()// => 2020
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/year
*/
year(): number;
/**
* Set the year.
* \`\`\`
* dayjs().year(2000)// => Dayjs
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/year
*/
year(value: number): Dayjs;
/**
* Get the month.
*
* Months are zero indexed, so January is month 0.
* \`\`\`
* dayjs().month()// => 0-11
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/month
*/
month(): number;
/**
* Set the month.
*
* Months are zero indexed, so January is month 0.
*
* Accepts numbers from 0 to 11. If the range is exceeded, it will bubble up to the next year.
* \`\`\`
* dayjs().month(0)// => Dayjs
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/month
*/
month(value: number): Dayjs;
/**
* Get the date of the month.
* \`\`\`
* dayjs().date()// => 1-31
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/date
*/
date(): number;
/**
* Set the date of the month.
*
* Accepts numbers from 1 to 31. If the range is exceeded, it will bubble up to the next months.
* \`\`\`
* dayjs().date(1)// => Dayjs
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/date
*/
date(value: number): Dayjs;
/**
* Get the day of the week.
*
* Returns numbers from 0 (Sunday) to 6 (Saturday).
* \`\`\`
* dayjs().day()// 0-6
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/day
*/
day(): 0 | 1 | 2 | 3 | 4 | 5 | 6;
/**
* Set the day of the week.
*
* Accepts numbers from 0 (Sunday) to 6 (Saturday). If the range is exceeded, it will bubble up to next weeks.
* \`\`\`
* dayjs().day(0)// => Dayjs
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/day
*/
day(value: number): Dayjs;
/**
* Get the hour.
* \`\`\`
* dayjs().hour()// => 0-23
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/hour
*/
hour(): number;
/**
* Set the hour.
*
* Accepts numbers from 0 to 23. If the range is exceeded, it will bubble up to the next day.
* \`\`\`
* dayjs().hour(12)// => Dayjs
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/hour
*/
hour(value: number): Dayjs;
/**
* Get the minutes.
* \`\`\`
* dayjs().minute()// => 0-59
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/minute
*/
minute(): number;
/**
* Set the minutes.
*
* Accepts numbers from 0 to 59. If the range is exceeded, it will bubble up to the next hour.
* \`\`\`
* dayjs().minute(59)// => Dayjs
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/minute
*/
minute(value: number): Dayjs;
/**
* Get the seconds.
* \`\`\`
* dayjs().second()// => 0-59
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/second
*/
second(): number;
/**
* Set the seconds.
*
* Accepts numbers from 0 to 59. If the range is exceeded, it will bubble up to the next minutes.
* \`\`\`
* dayjs().second(1)// Dayjs
* \`\`\`
*/
second(value: number): Dayjs;
/**
* Get the milliseconds.
* \`\`\`
* dayjs().millisecond()// => 0-999
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/millisecond
*/
millisecond(): number;
/**
* Set the milliseconds.
*
* Accepts numbers from 0 to 999. If the range is exceeded, it will bubble up to the next seconds.
* \`\`\`
* dayjs().millisecond(1)// => Dayjs
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/millisecond
*/
millisecond(value: number): Dayjs;
/**
* Generic setter, accepting unit as first argument, and value as second, returns a new instance with the applied changes.
*
* In general:
* \`\`\`
* dayjs().set(unit, value) === dayjs()[unit](value)
* \`\`\`
* Units are case insensitive, and support plural and short forms.
* \`\`\`
* dayjs().set('date', 1)
* dayjs().set('month', 3) // April
* dayjs().set('second', 30)
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/set
*/
set(unit: UnitType, value: number): Dayjs;
/**
* String getter, returns the corresponding information getting from Day.js object.
*
* In general:
* \`\`\`
* dayjs().get(unit) === dayjs()[unit]()
* \`\`\`
* Units are case insensitive, and support plural and short forms.
* \`\`\`
* dayjs().get('year')
* dayjs().get('month') // start 0
* dayjs().get('date')
* \`\`\`
* Docs: https://day.js.org/docs/en/get-set/get
*/
get(unit: UnitType): number;
/**
* Returns a cloned Day.js object with a specified amount of time added.
* \`\`\`
* dayjs().add(7, 'day')// => Dayjs
* \`\`\`
* Units are case insensitive, and support plural and short forms.
*
* Docs: https://day.js.org/docs/en/manipulate/add
*/
add(value: number, unit?: ManipulateType): Dayjs;
/**
* Returns a cloned Day.js object with a specified amount of time subtracted.
* \`\`\`
* dayjs().subtract(7, 'year')// => Dayjs
* \`\`\`
* Units are case insensitive, and support plural and short forms.
*
* Docs: https://day.js.org/docs/en/manipulate/subtract
*/
subtract(value: number, unit?: ManipulateType): Dayjs;
/**
* Returns a cloned Day.js object and set it to the start of a unit of time.
* \`\`\`
* dayjs().startOf('year')// => Dayjs
* \`\`\`
* Units are case insensitive, and support plural and short forms.
*
* Docs: https://day.js.org/docs/en/manipulate/start-of
*/
startOf(unit: OpUnitType): Dayjs;
/**
* Returns a cloned Day.js object and set it to the end of a unit of time.
* \`\`\`
* dayjs().endOf('month')// => Dayjs
* \`\`\`
* Units are case insensitive, and support plural and short forms.
*
* Docs: https://day.js.org/docs/en/manipulate/end-of
*/
endOf(unit: OpUnitType): Dayjs;
/**
* Get the formatted date according to the string of tokens passed in.
*
* To escape characters, wrap them in square brackets (e.g. [MM]).
* \`\`\`
* dayjs().format()// => current date in ISO8601, without fraction seconds e.g. '2020-04-02T08:02:17-05:00'
* dayjs('2019-01-25').format('[YYYYescape] YYYY-MM-DDTHH:mm:ssZ[Z]')// 'YYYYescape 2019-01-25T00:00:00-02:00Z'
* dayjs('2019-01-25').format('DD/MM/YYYY') // '25/01/2019'
* \`\`\`
* Docs: https://day.js.org/docs/en/display/format
*/
format(template?: string): string;
/**
* This indicates the difference between two date-time in the specified unit.
*
* To get the difference in milliseconds, use \`dayjs#diff\`
* \`\`\`
* const date1 = dayjs('2019-01-25')
* const date2 = dayjs('2018-06-05')
* date1.diff(date2) // 20214000000 default milliseconds
* date1.diff() // milliseconds to current time
* \`\`\`
*
* To get the difference in another unit of measurement, pass that measurement as the second argument.
* \`\`\`
* const date1 = dayjs('2019-01-25')
* date1.diff('2018-06-05', 'month') // 7
* \`\`\`
* Units are case insensitive, and support plural and short forms.
*
* Docs: https://day.js.org/docs/en/display/difference
*/
diff(date?: ConfigType, unit?: QUnitType | OpUnitType, float?: boolean): number;
/**
* This returns the number of **milliseconds** since the Unix Epoch of the Day.js object.
* \`\`\`
* dayjs('2019-01-25').valueOf() // 1548381600000
* +dayjs(1548381600000) // 1548381600000
* \`\`\`
* To get a Unix timestamp (the number of seconds since the epoch) from a Day.js object, you should use Unix Timestamp \`dayjs#unix()\`.
*
* Docs: https://day.js.org/docs/en/display/unix-timestamp-milliseconds
*/
valueOf(): number;
/**
* This returns the Unix timestamp (the number of **seconds** since the Unix Epoch) of the Day.js object.
* \`\`\`
* dayjs('2019-01-25').unix() // 1548381600
* \`\`\`
* This value is floored to the nearest second, and does not include a milliseconds component.
*
* Docs: https://day.js.org/docs/en/display/unix-timestamp
*/
unix(): number;
/**
* Get the number of days in the current month.
* \`\`\`
* dayjs('2019-01-25').daysInMonth() // 31
* \`\`\`
* Docs: https://day.js.org/docs/en/display/days-in-month
*/
daysInMonth(): number;
/**
* To get a copy of the native \`Date\` object parsed from the Day.js object use \`dayjs#toDate\`.
* \`\`\`
* dayjs('2019-01-25').toDate()// => Date
* \`\`\`
*/
toDate(): Date;
/**
* To serialize as an ISO 8601 string.
* \`\`\`
* dayjs('2019-01-25').toJSON() // '2019-01-25T02:00:00.000Z'
* \`\`\`
* Docs: https://day.js.org/docs/en/display/as-json
*/
toJSON(): string;
/**
* To format as an ISO 8601 string.
* \`\`\`
* dayjs('2019-01-25').toISOString() // '2019-01-25T02:00:00.000Z'
* \`\`\`
* Docs: https://day.js.org/docs/en/display/as-iso-string
*/
toISOString(): string;
/**
* Returns a string representation of the date.
* \`\`\`
* dayjs('2019-01-25').toString() // 'Fri, 25 Jan 2019 02:00:00 GMT'
* \`\`\`
* Docs: https://day.js.org/docs/en/display/as-string
*/
toString(): string;
/**
* Get the UTC offset in minutes.
* \`\`\`
* dayjs().utcOffset()
* \`\`\`
* Docs: https://day.js.org/docs/en/manipulate/utc-offset
*/
utcOffset(): number;
/**
* This indicates whether the Day.js object is before the other supplied date-time.
* \`\`\`
* dayjs().isBefore(dayjs('2011-01-01')) // default milliseconds
* \`\`\`
* If you want to limit the granularity to a unit other than milliseconds, pass it as the second parameter.
* \`\`\`
* dayjs().isBefore('2011-01-01', 'year')// => boolean
* \`\`\`
* Units are case insensitive, and support plural and short forms.
*
* Docs: https://day.js.org/docs/en/query/is-before
*/
isBefore(date?: ConfigType, unit?: OpUnitType): boolean;
/**
* This indicates whether the Day.js object is the same as the other supplied date-time.
* \`\`\`
* dayjs().isSame(dayjs('2011-01-01')) // default milliseconds
* \`\`\`
* If you want to limit the granularity to a unit other than milliseconds, pass it as the second parameter.
* \`\`\`
* dayjs().isSame('2011-01-01', 'year')// => boolean
* \`\`\`
* Docs: https://day.js.org/docs/en/query/is-same
*/
isSame(date?: ConfigType, unit?: OpUnitType): boolean;
/**
* This indicates whether the Day.js object is after the other supplied date-time.
* \`\`\`
* dayjs().isAfter(dayjs('2011-01-01')) // default milliseconds
* \`\`\`
* If you want to limit the granularity to a unit other than milliseconds, pass it as the second parameter.
* \`\`\`
* dayjs().isAfter('2011-01-01', 'year')// => boolean
* \`\`\`
* Units are case insensitive, and support plural and short forms.
*
* Docs: https://day.js.org/docs/en/query/is-after
*/
isAfter(date?: ConfigType, unit?: OpUnitType): boolean;
locale(): string;
locale(preset: string | ILocale, object?: Partial<ILocale>): Dayjs;
}
export type PluginFunc<T = unknown> = (option: T, c: typeof Dayjs, d: typeof dayjs) => void;
export function extend<T = unknown>(plugin: PluginFunc<T>, option?: T): Dayjs;
export function locale(
preset?: string | ILocale,
object?: Partial<ILocale>,
isLocal?: boolean
): string;
export function isDayjs(d: any): d is Dayjs;
export function unix(t: number): Dayjs;
const Ls: { [key: string]: ILocale };
}
export default dayjs;
}
`;

View File

@ -0,0 +1,13 @@
import { axios } from "./axios";
import { cool } from "./cool";
import { dayjs } from "./dayjs";
import { lodash } from "./lodash";
import { moment } from "./moment";
export const declares = {
axios,
cool,
dayjs,
lodash,
moment
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,927 @@
export const moment = `
declare module "moment" {
/**
* @param strict Strict parsing disables the deprecated fallback to the native Date constructor when
* parsing a string.
*/
function moment(inp?: Moment.MomentInput, strict?: boolean): Moment.Moment;
/**
* @param strict Strict parsing requires that the format and input match exactly, including delimiters.
* Strict parsing is frequently the best parsing option. For more information about choosing strict vs
* forgiving parsing, see the [parsing guide](https://momentjs.com/guides/#/parsing/).
*/
function moment(
inp?: Moment.MomentInput,
format?: Moment.MomentFormatSpecification,
strict?: boolean
): Moment.Moment;
/**
* @param strict Strict parsing requires that the format and input match exactly, including delimiters.
* Strict parsing is frequently the best parsing option. For more information about choosing strict vs
* forgiving parsing, see the [parsing guide](https://momentjs.com/guides/#/parsing/).
*/
function moment(
inp?: Moment.MomentInput,
format?: Moment.MomentFormatSpecification,
language?: string,
strict?: boolean
): Moment.Moment;
namespace Moment {
type RelativeTimeKey =
| "s"
| "ss"
| "m"
| "mm"
| "h"
| "hh"
| "d"
| "dd"
| "w"
| "ww"
| "M"
| "MM"
| "y"
| "yy";
type CalendarKey =
| "sameDay"
| "nextDay"
| "lastDay"
| "nextWeek"
| "lastWeek"
| "sameElse"
| string;
type LongDateFormatKey =
| "LTS"
| "LT"
| "L"
| "LL"
| "LLL"
| "LLLL"
| "lts"
| "lt"
| "l"
| "ll"
| "lll"
| "llll";
interface Locale {
calendar(key?: CalendarKey, m?: Moment, now?: Moment): string;
longDateFormat(key: LongDateFormatKey): string;
invalidDate(): string;
ordinal(n: number): string;
preparse(inp: string): string;
postformat(inp: string): string;
relativeTime(
n: number,
withoutSuffix: boolean,
key: RelativeTimeKey,
isFuture: boolean
): string;
pastFuture(diff: number, absRelTime: string): string;
set(config: Object): void;
months(): string[];
months(m: Moment, format?: string): string;
monthsShort(): string[];
monthsShort(m: Moment, format?: string): string;
monthsParse(monthName: string, format: string, strict: boolean): number;
monthsRegex(strict: boolean): RegExp;
monthsShortRegex(strict: boolean): RegExp;
week(m: Moment): number;
firstDayOfYear(): number;
firstDayOfWeek(): number;
weekdays(): string[];
weekdays(m: Moment, format?: string): string;
weekdaysMin(): string[];
weekdaysMin(m: Moment): string;
weekdaysShort(): string[];
weekdaysShort(m: Moment): string;
weekdaysParse(weekdayName: string, format: string, strict: boolean): number;
weekdaysRegex(strict: boolean): RegExp;
weekdaysShortRegex(strict: boolean): RegExp;
weekdaysMinRegex(strict: boolean): RegExp;
isPM(input: string): boolean;
meridiem(hour: number, minute: number, isLower: boolean): string;
}
interface StandaloneFormatSpec {
format: string[];
standalone: string[];
isFormat?: RegExp;
}
interface WeekSpec {
dow: number;
doy?: number;
}
type CalendarSpecVal = string | ((m?: MomentInput, now?: Moment) => string);
interface CalendarSpec {
sameDay?: CalendarSpecVal;
nextDay?: CalendarSpecVal;
lastDay?: CalendarSpecVal;
nextWeek?: CalendarSpecVal;
lastWeek?: CalendarSpecVal;
sameElse?: CalendarSpecVal;
// any additional properties might be used with moment.calendarFormat
[x: string]: CalendarSpecVal | void; // undefined
}
type RelativeTimeSpecVal =
| string
| ((
n: number,
withoutSuffix: boolean,
key: RelativeTimeKey,
isFuture: boolean
) => string);
type RelativeTimeFuturePastVal = string | ((relTime: string) => string);
interface RelativeTimeSpec {
future?: RelativeTimeFuturePastVal;
past?: RelativeTimeFuturePastVal;
s?: RelativeTimeSpecVal;
ss?: RelativeTimeSpecVal;
m?: RelativeTimeSpecVal;
mm?: RelativeTimeSpecVal;
h?: RelativeTimeSpecVal;
hh?: RelativeTimeSpecVal;
d?: RelativeTimeSpecVal;
dd?: RelativeTimeSpecVal;
w?: RelativeTimeSpecVal;
ww?: RelativeTimeSpecVal;
M?: RelativeTimeSpecVal;
MM?: RelativeTimeSpecVal;
y?: RelativeTimeSpecVal;
yy?: RelativeTimeSpecVal;
}
interface LongDateFormatSpec {
LTS: string;
LT: string;
L: string;
LL: string;
LLL: string;
LLLL: string;
// lets forget for a sec that any upper/lower permutation will also work
lts?: string;
lt?: string;
l?: string;
ll?: string;
lll?: string;
llll?: string;
}
type MonthWeekdayFn = (momentToFormat: Moment, format?: string) => string;
type WeekdaySimpleFn = (momentToFormat: Moment) => string;
interface EraSpec {
since: string | number;
until: string | number;
offset: number;
name: string;
narrow: string;
abbr: string;
}
interface LocaleSpecification {
months?: string[] | StandaloneFormatSpec | MonthWeekdayFn;
monthsShort?: string[] | StandaloneFormatSpec | MonthWeekdayFn;
weekdays?: string[] | StandaloneFormatSpec | MonthWeekdayFn;
weekdaysShort?: string[] | StandaloneFormatSpec | WeekdaySimpleFn;
weekdaysMin?: string[] | StandaloneFormatSpec | WeekdaySimpleFn;
meridiemParse?: RegExp;
meridiem?: (hour: number, minute: number, isLower: boolean) => string;
isPM?: (input: string) => boolean;
longDateFormat?: LongDateFormatSpec;
calendar?: CalendarSpec;
relativeTime?: RelativeTimeSpec;
invalidDate?: string;
ordinal?: (n: number) => string;
ordinalParse?: RegExp;
week?: WeekSpec;
eras?: EraSpec[];
// Allow anything: in general any property that is passed as locale spec is
// put in the locale object so it can be used by locale functions
[x: string]: any;
}
interface MomentObjectOutput {
years: number;
/* One digit */
months: number;
/* Day of the month */
date: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
}
interface argThresholdOpts {
ss?: number;
s?: number;
m?: number;
h?: number;
d?: number;
w?: number | void;
M?: number;
}
interface Duration {
clone(): Duration;
humanize(argWithSuffix?: boolean, argThresholds?: argThresholdOpts): string;
humanize(argThresholds?: argThresholdOpts): string;
abs(): Duration;
as(units: unitOfTime.Base): number;
get(units: unitOfTime.Base): number;
milliseconds(): number;
asMilliseconds(): number;
seconds(): number;
asSeconds(): number;
minutes(): number;
asMinutes(): number;
hours(): number;
asHours(): number;
days(): number;
asDays(): number;
weeks(): number;
asWeeks(): number;
months(): number;
asMonths(): number;
years(): number;
asYears(): number;
add(inp?: DurationInputArg1, unit?: DurationInputArg2): Duration;
subtract(inp?: DurationInputArg1, unit?: DurationInputArg2): Duration;
locale(): string;
locale(locale: LocaleSpecifier): Duration;
localeData(): Locale;
toISOString(): string;
toJSON(): string;
isValid(): boolean;
/**
* @deprecated since version 2.8.0
*/
lang(locale: LocaleSpecifier): Moment;
/**
* @deprecated since version 2.8.0
*/
lang(): Locale;
/**
* @deprecated
*/
toIsoString(): string;
}
interface MomentRelativeTime {
future: any;
past: any;
s: any;
ss: any;
m: any;
mm: any;
h: any;
hh: any;
d: any;
dd: any;
M: any;
MM: any;
y: any;
yy: any;
}
interface MomentLongDateFormat {
L: string;
LL: string;
LLL: string;
LLLL: string;
LT: string;
LTS: string;
l?: string;
ll?: string;
lll?: string;
llll?: string;
lt?: string;
lts?: string;
}
interface MomentParsingFlags {
empty: boolean;
unusedTokens: string[];
unusedInput: string[];
overflow: number;
charsLeftOver: number;
nullInput: boolean;
invalidMonth: string | void; // null
invalidFormat: boolean;
userInvalidated: boolean;
iso: boolean;
parsedDateParts: any[];
meridiem: string | void; // null
}
interface MomentParsingFlagsOpt {
empty?: boolean;
unusedTokens?: string[];
unusedInput?: string[];
overflow?: number;
charsLeftOver?: number;
nullInput?: boolean;
invalidMonth?: string;
invalidFormat?: boolean;
userInvalidated?: boolean;
iso?: boolean;
parsedDateParts?: any[];
meridiem?: string;
}
interface MomentBuiltinFormat {
__momentBuiltinFormatBrand: any;
}
type MomentFormatSpecification =
| string
| MomentBuiltinFormat
| (string | MomentBuiltinFormat)[];
export namespace unitOfTime {
type Base =
| "year"
| "years"
| "y"
| "month"
| "months"
| "M"
| "week"
| "weeks"
| "w"
| "day"
| "days"
| "d"
| "hour"
| "hours"
| "h"
| "minute"
| "minutes"
| "m"
| "second"
| "seconds"
| "s"
| "millisecond"
| "milliseconds"
| "ms";
type _quarter = "quarter" | "quarters" | "Q";
type _isoWeek = "isoWeek" | "isoWeeks" | "W";
type _date = "date" | "dates" | "D";
type DurationConstructor = Base | _quarter | _isoWeek;
export type DurationAs = Base;
export type StartOf = Base | _quarter | _isoWeek | _date | void; // null
export type Diff = Base | _quarter;
export type MomentConstructor = Base | _date;
export type All =
| Base
| _quarter
| _isoWeek
| _date
| "weekYear"
| "weekYears"
| "gg"
| "isoWeekYear"
| "isoWeekYears"
| "GG"
| "dayOfYear"
| "dayOfYears"
| "DDD"
| "weekday"
| "weekdays"
| "e"
| "isoWeekday"
| "isoWeekdays"
| "E";
}
type numberlike = number | string;
interface MomentInputObject {
years?: numberlike;
year?: numberlike;
y?: numberlike;
months?: numberlike;
month?: numberlike;
M?: numberlike;
days?: numberlike;
day?: numberlike;
d?: numberlike;
dates?: numberlike;
date?: numberlike;
D?: numberlike;
hours?: numberlike;
hour?: numberlike;
h?: numberlike;
minutes?: numberlike;
minute?: numberlike;
m?: numberlike;
seconds?: numberlike;
second?: numberlike;
s?: numberlike;
milliseconds?: numberlike;
millisecond?: numberlike;
ms?: numberlike;
}
interface DurationInputObject extends MomentInputObject {
quarters?: numberlike;
quarter?: numberlike;
Q?: numberlike;
weeks?: numberlike;
week?: numberlike;
w?: numberlike;
}
interface MomentSetObject extends MomentInputObject {
weekYears?: numberlike;
weekYear?: numberlike;
gg?: numberlike;
isoWeekYears?: numberlike;
isoWeekYear?: numberlike;
GG?: numberlike;
quarters?: numberlike;
quarter?: numberlike;
Q?: numberlike;
weeks?: numberlike;
week?: numberlike;
w?: numberlike;
isoWeeks?: numberlike;
isoWeek?: numberlike;
W?: numberlike;
dayOfYears?: numberlike;
dayOfYear?: numberlike;
DDD?: numberlike;
weekdays?: numberlike;
weekday?: numberlike;
e?: numberlike;
isoWeekdays?: numberlike;
isoWeekday?: numberlike;
E?: numberlike;
}
interface FromTo {
from: MomentInput;
to: MomentInput;
}
type MomentInput =
| Moment
| Date
| string
| number
| (number | string)[]
| MomentInputObject
| void; // null | undefined
type DurationInputArg1 = Duration | number | string | FromTo | DurationInputObject | void; // null | undefined
type DurationInputArg2 = unitOfTime.DurationConstructor;
type LocaleSpecifier = string | Moment | Duration | string[] | boolean;
interface MomentCreationData {
input: MomentInput;
format?: MomentFormatSpecification;
locale: Locale;
isUTC: boolean;
strict?: boolean;
}
interface Moment extends Object {
format(format?: string): string;
startOf(unitOfTime: unitOfTime.StartOf): Moment;
endOf(unitOfTime: unitOfTime.StartOf): Moment;
add(amount?: DurationInputArg1, unit?: DurationInputArg2): Moment;
/**
* @deprecated reverse syntax
*/
add(unit: unitOfTime.DurationConstructor, amount: number | string): Moment;
subtract(amount?: DurationInputArg1, unit?: DurationInputArg2): Moment;
/**
* @deprecated reverse syntax
*/
subtract(unit: unitOfTime.DurationConstructor, amount: number | string): Moment;
calendar(): string;
calendar(formats: CalendarSpec): string;
calendar(time: MomentInput, formats?: CalendarSpec): string;
clone(): Moment;
/**
* @return Unix timestamp in milliseconds
*/
valueOf(): number;
// current date/time in local mode
local(keepLocalTime?: boolean): Moment;
isLocal(): boolean;
// current date/time in UTC mode
utc(keepLocalTime?: boolean): Moment;
isUTC(): boolean;
/**
* @deprecated use isUTC
*/
isUtc(): boolean;
parseZone(): Moment;
isValid(): boolean;
invalidAt(): number;
hasAlignedHourOffset(other?: MomentInput): boolean;
creationData(): MomentCreationData;
parsingFlags(): MomentParsingFlags;
year(y: number): Moment;
year(): number;
/**
* @deprecated use year(y)
*/
years(y: number): Moment;
/**
* @deprecated use year()
*/
years(): number;
quarter(): number;
quarter(q: number): Moment;
quarters(): number;
quarters(q: number): Moment;
month(M: number | string): Moment;
month(): number;
/**
* @deprecated use month(M)
*/
months(M: number | string): Moment;
/**
* @deprecated use month()
*/
months(): number;
day(d: number | string): Moment;
day(): number;
days(d: number | string): Moment;
days(): number;
date(d: number): Moment;
date(): number;
/**
* @deprecated use date(d)
*/
dates(d: number): Moment;
/**
* @deprecated use date()
*/
dates(): number;
hour(h: number): Moment;
hour(): number;
hours(h: number): Moment;
hours(): number;
minute(m: number): Moment;
minute(): number;
minutes(m: number): Moment;
minutes(): number;
second(s: number): Moment;
second(): number;
seconds(s: number): Moment;
seconds(): number;
millisecond(ms: number): Moment;
millisecond(): number;
milliseconds(ms: number): Moment;
milliseconds(): number;
weekday(): number;
weekday(d: number): Moment;
isoWeekday(): number;
isoWeekday(d: number | string): Moment;
weekYear(): number;
weekYear(d: number): Moment;
isoWeekYear(): number;
isoWeekYear(d: number): Moment;
week(): number;
week(d: number): Moment;
weeks(): number;
weeks(d: number): Moment;
isoWeek(): number;
isoWeek(d: number): Moment;
isoWeeks(): number;
isoWeeks(d: number): Moment;
weeksInYear(): number;
weeksInWeekYear(): number;
isoWeeksInYear(): number;
isoWeeksInISOWeekYear(): number;
dayOfYear(): number;
dayOfYear(d: number): Moment;
from(inp: MomentInput, suffix?: boolean): string;
to(inp: MomentInput, suffix?: boolean): string;
fromNow(withoutSuffix?: boolean): string;
toNow(withoutPrefix?: boolean): string;
diff(b: MomentInput, unitOfTime?: unitOfTime.Diff, precise?: boolean): number;
toArray(): number[];
toDate(): Date;
toISOString(keepOffset?: boolean): string;
inspect(): string;
toJSON(): string;
unix(): number;
isLeapYear(): boolean;
/**
* @deprecated in favor of utcOffset
*/
zone(): number;
zone(b: number | string): Moment;
utcOffset(): number;
utcOffset(b: number | string, keepLocalTime?: boolean): Moment;
isUtcOffset(): boolean;
daysInMonth(): number;
isDST(): boolean;
zoneAbbr(): string;
zoneName(): string;
isBefore(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean;
isAfter(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean;
isSame(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean;
isSameOrAfter(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean;
isSameOrBefore(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean;
isBetween(
a: MomentInput,
b: MomentInput,
granularity?: unitOfTime.StartOf,
inclusivity?: "()" | "[)" | "(]" | "[]"
): boolean;
/**
* @deprecated as of 2.8.0, use locale
*/
lang(language: LocaleSpecifier): Moment;
/**
* @deprecated as of 2.8.0, use locale
*/
lang(): Locale;
locale(): string;
locale(locale: LocaleSpecifier): Moment;
localeData(): Locale;
/**
* @deprecated no reliable implementation
*/
isDSTShifted(): boolean;
// NOTE(constructor): Same as moment constructor
/**
* @deprecated as of 2.7.0, use moment.min/max
*/
max(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean): Moment;
/**
* @deprecated as of 2.7.0, use moment.min/max
*/
max(
inp?: MomentInput,
format?: MomentFormatSpecification,
language?: string,
strict?: boolean
): Moment;
// NOTE(constructor): Same as moment constructor
/**
* @deprecated as of 2.7.0, use moment.min/max
*/
min(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean): Moment;
/**
* @deprecated as of 2.7.0, use moment.min/max
*/
min(
inp?: MomentInput,
format?: MomentFormatSpecification,
language?: string,
strict?: boolean
): Moment;
get(unit: unitOfTime.All): number;
set(unit: unitOfTime.All, value: number): Moment;
set(objectLiteral: MomentSetObject): Moment;
toObject(): MomentObjectOutput;
}
export let version: string;
export let fn: Moment;
// NOTE(constructor): Same as moment constructor
/**
* @param strict Strict parsing disables the deprecated fallback to the native Date constructor when
* parsing a string.
*/
export function utc(inp?: MomentInput, strict?: boolean): Moment;
/**
* @param strict Strict parsing requires that the format and input match exactly, including delimiters.
* Strict parsing is frequently the best parsing option. For more information about choosing strict vs
* forgiving parsing, see the [parsing guide](https://momentjs.com/guides/#/parsing/).
*/
export function utc(
inp?: MomentInput,
format?: MomentFormatSpecification,
strict?: boolean
): Moment;
/**
* @param strict Strict parsing requires that the format and input match exactly, including delimiters.
* Strict parsing is frequently the best parsing option. For more information about choosing strict vs
* forgiving parsing, see the [parsing guide](https://momentjs.com/guides/#/parsing/).
*/
export function utc(
inp?: MomentInput,
format?: MomentFormatSpecification,
language?: string,
strict?: boolean
): Moment;
export function unix(timestamp: number): Moment;
export function invalid(flags?: MomentParsingFlagsOpt): Moment;
export function isMoment(m: any): m is Moment;
export function isDate(m: any): m is Date;
export function isDuration(d: any): d is Duration;
/**
* @deprecated in 2.8.0
*/
export function lang(language?: string): string;
/**
* @deprecated in 2.8.0
*/
export function lang(language?: string, definition?: Locale): string;
export function locale(language?: string): string;
export function locale(language?: string[]): string;
export function locale(language?: string, definition?: LocaleSpecification | void): string; // null | undefined
export function localeData(key?: string | string[]): Locale;
export function duration(inp?: DurationInputArg1, unit?: DurationInputArg2): Duration;
// NOTE(constructor): Same as moment constructor
export function parseZone(
inp?: MomentInput,
format?: MomentFormatSpecification,
strict?: boolean
): Moment;
export function parseZone(
inp?: MomentInput,
format?: MomentFormatSpecification,
language?: string,
strict?: boolean
): Moment;
export function months(): string[];
export function months(index: number): string;
export function months(format: string): string[];
export function months(format: string, index: number): string;
export function monthsShort(): string[];
export function monthsShort(index: number): string;
export function monthsShort(format: string): string[];
export function monthsShort(format: string, index: number): string;
export function weekdays(): string[];
export function weekdays(index: number): string;
export function weekdays(format: string): string[];
export function weekdays(format: string, index: number): string;
export function weekdays(localeSorted: boolean): string[];
export function weekdays(localeSorted: boolean, index: number): string;
export function weekdays(localeSorted: boolean, format: string): string[];
export function weekdays(localeSorted: boolean, format: string, index: number): string;
export function weekdaysShort(): string[];
export function weekdaysShort(index: number): string;
export function weekdaysShort(format: string): string[];
export function weekdaysShort(format: string, index: number): string;
export function weekdaysShort(localeSorted: boolean): string[];
export function weekdaysShort(localeSorted: boolean, index: number): string;
export function weekdaysShort(localeSorted: boolean, format: string): string[];
export function weekdaysShort(localeSorted: boolean, format: string, index: number): string;
export function weekdaysMin(): string[];
export function weekdaysMin(index: number): string;
export function weekdaysMin(format: string): string[];
export function weekdaysMin(format: string, index: number): string;
export function weekdaysMin(localeSorted: boolean): string[];
export function weekdaysMin(localeSorted: boolean, index: number): string;
export function weekdaysMin(localeSorted: boolean, format: string): string[];
export function weekdaysMin(localeSorted: boolean, format: string, index: number): string;
export function min(moments: Moment[]): Moment;
export function min(...moments: Moment[]): Moment;
export function max(moments: Moment[]): Moment;
export function max(...moments: Moment[]): Moment;
/**
* Returns unix time in milliseconds. Overwrite for profit.
*/
export function now(): number;
export function defineLocale(
language: string,
localeSpec: LocaleSpecification | void
): Locale; // null
export function updateLocale(
language: string,
localeSpec: LocaleSpecification | void
): Locale; // null
export function locales(): string[];
export function normalizeUnits(unit: unitOfTime.All): string;
export function relativeTimeThreshold(threshold: string): number | boolean;
export function relativeTimeThreshold(threshold: string, limit: number): boolean;
export function relativeTimeRounding(fn: (num: number) => number): boolean;
export function relativeTimeRounding(): (num: number) => number;
export function calendarFormat(m: Moment, now: Moment): string;
export function parseTwoDigitYear(input: string): number;
/**
* Constant used to enable explicit ISO_8601 format parsing.
*/
export let ISO_8601: MomentBuiltinFormat;
export let RFC_2822: MomentBuiltinFormat;
export let defaultFormat: string;
export let defaultFormatUtc: string;
export let suppressDeprecationWarnings: boolean;
export let deprecationHandler: ((name: string | void, msg: string) => void) | void;
export let HTML5_FMT: {
DATETIME_LOCAL: string;
DATETIME_LOCAL_SECONDS: string;
DATETIME_LOCAL_MS: string;
DATE: string;
TIME: string;
TIME_SECONDS: string;
TIME_MS: string;
WEEK: string;
MONTH: string;
};
}
export = moment;
}
`;

View File

@ -0,0 +1,138 @@
<template>
<div class="editor">
<div class="opbar">
<el-tooltip content="全屏">
<cl-svg name="fullscreen" class="btn-icon" @click="fullscreen.open" />
</el-tooltip>
<el-tooltip content="调试">
<cl-svg name="debug" class="btn-icon" @click="toDebug" />
</el-tooltip>
</div>
<cl-editor-monaco
:ref="setRefs('monaco')"
v-model="value"
:height="300"
:language="language"
/>
<cl-dialog
v-model="fullscreen.visible"
title="代码编辑"
keep-alive
fullscreen
:scrollbar="false"
height="500px"
padding="5px"
>
<cl-editor-monaco
:ref="setRefs('monaco')"
v-model="value"
height="100%"
:language="language"
/>
</cl-dialog>
</div>
</template>
<script setup lang="ts" name="node-code-editor">
import { onMounted, reactive, useModel, watch, computed } from "vue";
import { useFlow } from "/$/flow/hooks";
import { addDeclare } from "/@/plugins/editor-monaco";
import { useCool } from "/@/cool";
import { declares } from "./declares";
import { ctx } from "virtual:ctx";
const props = defineProps({
modelValue: String
});
const { refs, setRefs, mitt } = useCool();
const flow = useFlow();
//
const language = computed(() => {
switch (ctx.serviceLang) {
case "Java":
return "java";
case "Python":
return "python";
default:
return "typescript";
}
});
//
const value = useModel(props, "modelValue");
//
function insertCode(code: string) {
refs.monaco.appendContent(code);
refs.popover?.hide();
}
//
function toDebug() {
mitt.emit("flow.runOpen", flow.node);
}
//
function updateDeclare() {
watch(
() => flow.node?.data,
(data) => {
const params = data?.inputParams
?.map((e) => `${e.field}: ${e.type || "string"};`)
.join("");
const result = data?.outputParams
?.map((e) => `${e.field}: ${e.type || "string"};`)
.join("");
addDeclare({
path: "flow.d.ts",
content: `
declare interface Params { ${params} }
declare interface Result { ${result} }`
});
},
{
immediate: true,
deep: true
}
);
}
//
function init() {
if (ctx.serviceLang == "Node") {
["axios", "dayjs", "lodash", "moment", "cool"].forEach((k) => {
addDeclare({ path: `${k}.d.ts`, content: declares[k] });
});
updateDeclare();
}
}
//
const fullscreen = reactive({
visible: false,
open() {
fullscreen.visible = true;
}
});
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
.opbar {
position: absolute;
right: 0;
top: -30px;
}
</style>

View File

@ -0,0 +1,65 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import FormInputParams from "../_base/form/input-params.vue";
import FormOutputParams from "../_base/form/output-params.vue";
import Editor from "./editor/index.vue";
import { Snippet } from "./shippets";
export default (): FlowNode => {
return {
group: "行为",
label: "执行代码",
description: "执行一段自定义代码可以调用框架的插件、数据库、service等",
color: "#67c23a",
component,
form: {
width: "500px",
items: [
{
label: "输入变量",
prop: "inputParams",
component: {
vm: FormInputParams
}
},
{
label: "代码编辑",
prop: "options.code",
component: {
vm: Editor
}
},
{
label: "输出变量",
prop: "outputParams",
component: {
vm: FormOutputParams
}
}
]
},
data: {
inputParams: [
{
field: "arg1"
}
],
outputParams: [
{
field: "result",
type: "string"
}
],
options: {
code: Snippet.base
}
},
validator(data) {
// 验证变量是否绑定值
const param = data.inputParams?.find((e) => !e.nodeId);
if (param) {
return `请绑定变量:${param.field}`;
}
}
};
};

View File

@ -0,0 +1,20 @@
<template>
<div class="node-code"></div>
</template>
<script lang="ts" setup name="node-code">
import type { FlowNode } from "/$/flow/types";
import type { PropType } from "vue";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
},
//
focus: {
type: Boolean,
default: false
}
});
</script>

View File

@ -0,0 +1,48 @@
import { ctx } from "virtual:ctx";
const Java = {
base: `import java.util.Map;
/**
*
*/
public class DynamicClass {
/**
*
*/
public Map<String, Object> main(Map<String, Object> params) {
System.out.println("Cool main " + params);
return params;
}
}`
};
const Node = {
base: `import axios from 'axios';
import * as _ from 'lodash';
import * as moment from 'moment';
/**
*
*/
export class Cool extends Base {
/**
*
*/
async main(params: Params): Promise<Result> {
console.log('Cool main', params);
return {
result: ""
};
}
}`,
simple: `async function main(params: Params): Promise<Params> {
return params;
}`
};
export const Snippets = {
Java,
Node
};
export const Snippet = Snippets[ctx.serviceLang];

View File

@ -0,0 +1,44 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import FormInputParams from "../_base/form/input-params.vue";
export default (): FlowNode => {
return {
group: "逻辑",
label: "结束",
description: "结束节点",
color: "#f56c6c",
component,
form: {
items: [
{
label: "输出变量",
prop: "outputParams",
component: {
vm: FormInputParams,
props: {
field: "res"
}
}
}
]
},
data: {
outputParams: [
{
field: "res"
}
]
},
handle: {
source: false
},
validator(data) {
// 验证变量是否绑定值
const param = data.outputParams?.find((e) => !e.nodeId);
if (param) {
return `请绑定变量到${param.field}`;
}
}
};
};

View File

@ -0,0 +1,3 @@
<template>
<div class="node-end"></div>
</template>

View File

@ -0,0 +1,68 @@
<template>
<div class="form-info" v-if="info">
<el-tag disable-transitions type="success" effect="dark" size="small">
v{{ info?.version }}
</el-tag>
<el-text :truncated="true" size="small">
{{ info?.name }}
</el-text>
</div>
</template>
<script setup lang="ts" name="node-flow-form-info">
import { useCool } from "/@/cool";
import { ref, watch, onMounted } from "vue";
const props = defineProps({
flowId: Number
});
const { service } = useCool();
const info = ref<Eps.FlowInfoEntity>();
async function refresh() {
info.value = await service.flow.info.info({
id: props.flowId
});
}
onMounted(() => {
watch(
() => props.flowId,
(val) => {
if (val) {
refresh();
}
},
{
immediate: true
}
);
});
</script>
<style lang="scss" scoped>
.form-info {
display: flex;
align-items: center;
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
padding: 0 10px;
cursor: pointer;
height: 32px;
width: 100%;
position: relative;
transition: all 0.2s;
box-sizing: border-box;
.el-tag {
margin-right: 5px;
}
&:hover {
border-color: var(--el-border-color-hover);
}
}
</style>

View File

@ -0,0 +1,234 @@
<template>
<div class="form-select">
<div class="empty" v-if="!modelValue">
<span>未选择流程</span>
<span @click="open()">点击选择</span>
</div>
<info :flow-id="modelValue" @click="open()" v-else />
<cl-dialog v-model="visible" title="选择流程" width="1000px" :controls="['close']">
<cl-crud ref="Crud" padding="0">
<cl-row>
<cl-table
ref="Table"
:auto-height="false"
@row-dblclick="select"
@row-click="onRowClick"
>
<template #column-op="{ scope }">
<el-button
size="small"
type="success"
round
bg
@click="select(scope.row)"
>
选择
</el-button>
</template>
</cl-table>
</cl-row>
<cl-row>
<cl-flex1 />
<cl-pagination />
</cl-row>
</cl-crud>
</cl-dialog>
</div>
</template>
<script setup lang="ts" name="node-flow-form-select">
import { ref, nextTick } from "vue";
import { useCool } from "/@/cool";
import { useCrud, useTable } from "@cool-vue/crud";
import Info from "./info.vue";
import { useFlow } from "/$/flow/hooks";
import type { FlowNode } from "/$/flow/types";
const props = defineProps({
modelValue: Number
});
const emit = defineEmits(["update:modelValue"]);
const { service, route, mitt } = useCool();
const flow = useFlow();
// ID
const flowId = ref();
//
const visible = ref(false);
// cl-crud
const Crud = useCrud({
service: service.flow.info
});
// cl-table
const Table = useTable({
columns: [
{
label: "操作",
prop: "op"
},
{ label: "名称", prop: "name", minWidth: 140 },
{
label: "标签",
prop: "label",
minWidth: 140
},
{ label: "版本", prop: "version", minWidth: 100 },
{ label: "描述", prop: "description", showOverflowTooltip: true, minWidth: 200 },
{
label: "发布时间",
prop: "releaseTime",
minWidth: 170,
sortable: "desc",
formatter(row) {
return row.releaseTime || "未发布";
}
}
]
});
//
async function refresh(params?: any) {
return Crud.value?.refresh(params);
}
//
function onRowClick(row: Eps.FlowInfoEntity) {
flowId.value = row.id;
}
//
function select(row: Eps.FlowInfoEntity) {
const { nodes } = row.data || row.draft;
//
const start = (nodes as FlowNode[]).find((e) => e.type == "start");
//
if (flow.node?.data && start) {
const params = start.data?.inputParams?.map((e) => {
return {
field: e.field,
label: e.label,
name: e.name,
required: e.required,
type: e.type
};
});
mitt.emit("flow.updateForm", {
inputParams: params
});
}
flowId.value = row.id;
emit("update:modelValue", flowId.value);
close();
}
//
function open() {
visible.value = true;
nextTick(async () => {
await refresh({
status: 1,
isRelease: true,
flowId: route.query.id
});
//
flowId.value = props.modelValue;
if (flowId.value) {
Table.value?.setCurrentRow(Table.value.data.find((e) => e.id == flowId.value));
}
});
}
//
function close() {
visible.value = false;
}
</script>
<style lang="scss" scoped>
.op {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.list {
.item {
display: flex;
align-items: center;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
height: 35px;
line-height: normal;
padding: 0 5px;
margin-bottom: 5px;
border: 1px solid var(--el-border-color);
box-sizing: border-box;
.icon {
margin: 0 5px;
font-size: 16px;
}
.name,
.desc {
font-size: 12px;
}
.desc {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover {
background-color: var(--el-fill-color-lighter);
}
&.active {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
&:last-child {
margin-bottom: 0;
}
}
}
.empty {
text-align: center;
line-height: normal;
padding: 15px 0;
border-radius: 4px;
font-size: 12px;
border: 1px solid var(--el-border-color);
span {
&:first-child {
color: var(--el-text-color-placeholder);
}
&:last-child {
color: var(--el-color-success);
cursor: pointer;
}
}
}
</style>

View File

@ -0,0 +1,64 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import FormInputParams from "../_base/form/input-params.vue";
import FormOutputParams from "../_base/form/output-params.vue";
import FormSelect from "./form/select.vue";
export default (): FlowNode => {
return {
group: "扩展",
label: "流程",
description: "执行其他流程",
color: "#fd9d2f",
component,
form: {
items: [
{
label: "输入变量",
prop: "inputParams",
component: {
vm: FormInputParams,
props: {
disabled: true,
editField: false,
placeholder: "请先选择流程"
}
}
},
{
label: "选择流程",
prop: "options.flowId",
component: {
vm: FormSelect
}
},
{
label: "输出变量",
prop: "outputParams",
component: {
vm: FormOutputParams
}
}
]
},
data: {
options: {},
inputParams: [],
outputParams: [
{
field: "res1",
type: "string"
}
]
},
validator(data) {
// 验证if条件是否设置
const param = data.options.IF?.find(
(e: JudgeItem) => !e.nodeId || !e.condition || !e.value
);
if (param) {
return "条件判断格式异常";
}
}
};
};

View File

@ -0,0 +1,29 @@
<template>
<div class="node-flow" v-if="node.data?.options.flowId && !focus">
<info :flow-id="node.data?.options.flowId" />
</div>
</template>
<script lang="ts" setup name="node-flow">
import type { FlowNode } from "/$/flow/types";
import type { PropType } from "vue";
import Info from "./form/info.vue";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
},
//
focus: {
type: Boolean,
default: false
}
});
</script>
<style lang="scss" scoped>
.node-flow {
padding-bottom: 15px;
}
</style>

View File

@ -0,0 +1,32 @@
import type { FlowNode } from "../../types";
import { shallowRef } from "vue";
const files: { [key: string]: { default: () => FlowNode } } = import.meta.glob(
"./*/index.{ts,tsx}",
{
eager: true
}
);
const CustomNodes = shallowRef<FlowNode[]>([]);
for (const i in files) {
const [, type] = i.split("/");
const d = files[i].default();
if (d.enable !== false) {
const configWidth = d.form?.width || '400px';
const width = `${parseFloat(configWidth) + 30}px`
CustomNodes.value.push({
...d,
type,
name: `node-${type}`,
icon: type,
cardWidth: width
});
}
}
export { CustomNodes };

View File

@ -0,0 +1,188 @@
<template>
<div class="form-if">
<cl-svg name="add" class="btn-icon is-rt" @click="add()" />
<div class="item" v-for="(item, index) in list" :key="index">
<tools-var
v-model="item.field"
v-model:nodeId="item.nodeId"
v-model:nodeType="item.nodeType"
/>
<el-select
v-model="item.condition"
placeholder="操作符"
:style="{
width: '100px'
}"
popper-class="cl-flow__popper"
>
<el-option
v-for="c in condition"
:key="c.value"
:label="c.label"
:value="c.value"
/>
</el-select>
<el-input
v-model="item.value"
placeholder="输入值"
:style="{
width: '80px'
}"
/>
<cl-svg class="btn-icon" name="delete" @click="del(index)" />
<div class="operator">
<el-button-group size="small">
<el-button
v-for="o in operator"
:key="o.value"
:type="item.operator == o.value ? 'primary' : ''"
@click="item.operator = o.value"
>
{{ o.label }}
</el-button>
</el-button-group>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="node-judge-form-if">
import { type PropType, onMounted, useModel } from "vue";
import ToolsVar from "/$/flow/components/tools/var.vue";
const props = defineProps({
modelValue: {
type: Object as PropType<JudgeItem[]>,
default: () => ({})
}
});
const list = useModel(props, "modelValue");
const condition = [
{
label: "包含",
value: "include"
},
{
label: "不包含",
value: "exclude"
},
{
label: "开始是",
value: "startWith"
},
{
label: "结束是",
value: "endWith"
},
{
label: "等于",
value: "equal"
},
{
label: "不等于",
value: "notEqual"
},
{
label: "大于",
value: "greaterThan"
},
{
label: "大于等于",
value: "greaterThanOrEqual"
},
{
label: "小于",
value: "lessThan"
},
{
label: "小于等于",
value: "lessThanOrEqual"
},
{
label: "为空",
value: "isNull"
},
{
label: "不为空",
value: "isNotNull"
}
];
const operator = [
{
label: "OR",
value: "OR"
},
{
label: "AND",
value: "AND"
}
];
function del(index: number) {
list.value.splice(index, 1);
}
function add() {
list.value.push({
field: "",
condition: "",
value: "",
operator: "OR"
});
}
onMounted(() => {
list.value.forEach((e) => {
if (!e.operator) {
e.operator = "OR";
}
});
});
</script>
<style lang="scss" scoped>
.form-if {
.item {
display: flex;
align-items: center;
margin-bottom: 10px;
padding-bottom: 35px;
position: relative;
.el-select {
margin: 0 5px;
}
.el-select,
.el-input {
flex-shrink: 0;
}
.btn-icon {
margin-left: 5px;
}
.operator {
position: absolute;
bottom: 0;
}
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
.operator {
display: none;
}
}
}
}
</style>

View File

@ -0,0 +1,74 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import FormIf from "./form/if.vue";
import { h, resolveComponent, toRaw } from "vue";
export default (): FlowNode => {
return {
group: "逻辑",
label: "条件判断",
description: "条件判断节点",
color: "#f56c6c",
component,
form: {
items: [
{
label: "满足",
prop: "options.IF",
component: {
vm: FormIf
}
},
{
label: "不满足",
component: () => {
return h(
toRaw(resolveComponent("el-text")),
{
type: "info",
size: "small"
},
() => {
return "用于定义当条件不满足时应执行的逻辑。";
}
);
}
}
]
},
data: {
options: {
IF: [{}] as JudgeItem[],
ELSE: [] as JudgeItem[]
},
outputParams: [
{
type: "boolean",
field: "result"
}
]
},
handle: {
source: false,
next: [
{
label: "满足",
value: "source-if"
},
{
label: "不满足",
value: "source-else"
}
]
},
validator(data) {
// 验证if条件是否设置
const param = data.options.IF?.find(
(e: JudgeItem) => !e.nodeId || !e.condition || !e.value
);
if (param) {
return "条件判断格式异常";
}
}
};
};

View File

@ -0,0 +1,61 @@
<template>
<div class="node-judge">
<div class="item">
<span> 满足 </span>
<tools-handle
type="source"
:node-id="node.id"
id="source-if"
:position="{
right: '-24px'
}"
/>
</div>
<div class="item">
<span> 不满足 </span>
<tools-handle
type="source"
:node-id="node.id"
id="source-else"
:position="{
right: '-24px'
}"
/>
</div>
</div>
</template>
<script setup lang="ts" name="node-judge">
import type { FlowNode } from "/$/flow/types";
import { type PropType } from "vue";
import { useFlow } from "/$/flow/hooks";
import ToolsHandle from "/$/flow/components/tools/handle.vue";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
}
});
const flow = useFlow();
</script>
<style lang="scss" scoped>
.node-judge {
padding-bottom: 15px;
.item {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 5px;
font-size: 12px;
position: relative;
&:last-child {
margin-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,8 @@
declare interface JudgeItem {
nodeType?: string;
nodeId?: string;
field: string;
condition: string;
value?: string;
operator: string;
}

View File

@ -0,0 +1,112 @@
<template>
<div class="list">
<div class="item" v-for="(item, index) in list" :key="index">
<cl-svg name="know" class="icon" />
<span class="name">{{ item.name }}</span>
<cl-svg
class="btn-icon del"
name="delete"
@click="
() => {
ids.splice(index, 1);
}
"
v-if="deletable"
/>
</div>
</div>
</template>
<script setup lang="ts" name="node-know-form-list">
import { PropType, ref, onMounted, computed, useModel } from "vue";
import { useCool } from "/@/cool";
const props = defineProps({
modelValue: {
type: Array as PropType<number[]>,
default: () => []
},
deletable: Boolean
});
const { service } = useCool();
//
const ids = useModel(props, "modelValue");
//
const knows = ref<{ id: number; name: string; description: string }[]>([]);
//
const list = computed(() => {
return ids.value
.map((id) => {
return knows.value.find((e) => e.id === id)!;
})
.filter(Boolean);
});
//
function refresh() {
service.flow.config
.config({
node: "know"
})
.then((res) => {
knows.value = res.knows || [];
});
}
onMounted(() => {
refresh();
});
</script>
<style lang="scss" scoped>
.list {
.item {
display: flex;
align-items: center;
border-radius: var(--el-border-radius-base);
cursor: pointer;
height: 32px;
line-height: normal;
padding: 0 5px;
margin-bottom: 5px;
border: 1px solid var(--el-border-color);
box-sizing: border-box;
.icon {
margin: 0 5px;
font-size: 16px;
color: var(--el-text-color-regular);
}
.name {
font-size: 12px;
}
.del {
font-size: 14px;
margin-left: auto;
padding: 4px;
outline: none;
border-radius: 6px;
&:hover {
background-color: var(--el-fill-color-light);
color: var(--el-text-color-primary);
}
}
&:hover {
border-color: var(--el-border-color-hover);
}
&:last-child {
margin-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,183 @@
<template>
<div class="form-select">
<cl-svg name="add" class="btn-icon is-rt" @click="open()" />
<list v-model="ids" deletable />
<div class="empty border" v-if="isEmpty(ids)">
<span>未选择知识库</span>
<span @click="open()">点击选择</span>
</div>
<cl-dialog v-model="visible" title="选择知识库" width="400px" :controls="['close']">
<div class="list">
<div
class="item"
v-for="(item, index) in knows"
:key="index"
:class="{
active: selected.includes(item.id)
}"
@click="select(item.id)"
>
<p class="name">{{ item.name }}</p>
<p class="desc" v-if="item.description">{{ item.description }}</p>
</div>
</div>
<div class="empty" v-if="isEmpty(knows)">
<span>暂无知识库</span>
<span @click="router.push('/know/data/type')">点击添加</span>
</div>
<div class="op">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</div>
</cl-dialog>
</div>
</template>
<script setup lang="ts" name="node-know-form-select">
import { ref, useModel, type PropType } from "vue";
import { useCool } from "/@/cool";
import { isEmpty } from "lodash-es";
import List from "./list.vue";
import { ElMessage } from "element-plus";
const props = defineProps({
modelValue: {
type: Array as PropType<number[]>,
default: () => []
}
});
const { service, router } = useCool();
const visible = ref(false);
//
const ids = useModel(props, "modelValue");
//
const knows = ref<{ id: number; name: string; description: string }[]>([]);
//
function refresh() {
service.flow.config
.config({
node: "know"
})
.then((res) => {
knows.value = res.knows || [];
});
}
//
const selected = ref<number[]>([]);
//
function select(id: number) {
const index = selected.value.findIndex((e) => e == id);
if (index >= 0) {
selected.value.splice(index, 1);
} else {
selected.value.push(id);
}
}
//
function open() {
visible.value = true;
selected.value = [...ids.value];
refresh();
}
//
function close() {
visible.value = false;
}
//
function save() {
if (isEmpty(selected.value)) {
return ElMessage.warning("至少选择一个知识库");
}
ids.value = [...selected.value];
close();
}
</script>
<style lang="scss" scoped>
.op {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.list {
.item {
border-radius: 6px;
cursor: pointer;
line-height: normal;
padding: 10px;
margin-bottom: 10px;
border: 1px solid var(--el-border-color);
box-sizing: border-box;
user-select: none;
.icon {
margin: 0 5px;
font-size: 16px;
}
.name {
font-size: 14px;
}
.desc {
margin-top: 2px;
font-size: 12px;
color: var(--el-color-info);
}
&.active {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
&:not(.active):hover {
background-color: var(--el-fill-color-lighter);
}
&:last-child {
margin-bottom: 0;
}
}
}
.empty {
text-align: center;
line-height: normal;
padding: 15px 0;
border-radius: 4px;
font-size: 12px;
span {
&:first-child {
color: var(--el-text-color-placeholder);
}
&:last-child {
color: var(--el-color-success);
cursor: pointer;
}
}
&.border {
border: 1px solid var(--el-border-color);
}
}
</style>

View File

@ -0,0 +1,92 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import FormInputParams from "../_base/form/input-params.vue";
import FormSelect from "./form/select.vue";
import FormText from "../_base/form/text.vue";
import { isEmpty } from "lodash-es";
export default (): FlowNode => {
return {
group: "信息",
label: "知识库",
description: "从知识库中检索出相关的内容",
component,
form: {
items: [
{
label: "输入变量",
prop: "inputParams",
component: {
vm: FormInputParams,
props: {
editField: false,
disabled: true
}
}
},
{
label: "选择知识库",
prop: "options.knowIds",
component: {
vm: FormSelect
}
},
{
label: "结果条数",
prop: "options.size",
component: {
name: "el-input-number",
props: {
min: 1,
max: 100
}
}
},
{
label: "输出变量",
component: {
vm: FormText,
props: {
text: ["documents<object[]> 文档列表", "text<string> 文档内容"]
}
}
}
]
},
data: {
inputParams: [
{
field: "text"
}
],
outputParams: [
{
field: "documents",
type: "object[]"
},
{
field: "text",
type: "string"
}
],
options: {
knowIds: [],
size: 3
}
},
validator(data) {
const { knowIds } = data.options;
// 验证变量是否绑定值
const param = data.inputParams?.find((e) => !e.nodeId);
if (param) {
return `请绑定变量:${param.field}`;
}
// 验证知识库是否选择
if (isEmpty(knowIds)) {
return "请选择知识库";
}
}
};
};

View File

@ -0,0 +1,30 @@
<template>
<div class="node-know" v-if="node?.data && !isEmpty(node.data.options.knowIds) && !focus">
<list v-model="node.data.options.knowIds" />
</div>
</template>
<script lang="ts" setup name="node-know">
import type { FlowNode } from "/$/flow/types";
import { type PropType } from "vue";
import { isEmpty } from "lodash-es";
import List from "./form/list.vue";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
},
//
focus: {
type: Boolean,
default: false
}
});
</script>
<style lang="scss" scoped>
.node-know {
padding-bottom: 15px;
}
</style>

View File

@ -0,0 +1,376 @@
<template>
<div class="form-content">
<cl-svg name="add" class="btn-icon is-rt" @click="add()" />
<draggable class="list" tag="div" v-model="list" :animation="300" item-key="id">
<template #item="{ element: item, index }">
<div
class="item textarea-item"
:class="{
error: !item.content
}"
>
<div class="head">
<el-select
class="role"
v-model="item.role"
size="small"
popper-class="cl-flow__popper"
:disabled="item.role == 'system'"
>
<el-option
v-for="t in getRoles(item)"
:label="t.label"
:value="t.value"
:key="t.value"
/>
</el-select>
<div class="op">
<cl-svg
name="delete"
class="btn-icon"
@click="remove(index)"
v-if="item.role != 'system'"
/>
</div>
</div>
<div
:ref="setRefs(`input-${index}`)"
class="content"
:class="{
'is-empty': !item.content
}"
contenteditable="true"
@input="(e) => onContentInput(e, item)"
@focus="(e) => onContentInput(e, item)"
@keydown="(e) => onContentKeydown(e)"
v-html="item.text"
></div>
</div>
</template>
</draggable>
<!-- 选择变量 -->
<tools-var
:ref="setRefs('toolsVar')"
only-select
position="absolute"
:show-picker="false"
:autofocus="false"
use-input-params
/>
</div>
</template>
<script setup lang="ts" name="node-llm-form-content">
import { type PropType, onMounted, onUnmounted, useModel } from "vue";
import { useCool } from "/@/cool";
import { last } from "lodash-es";
import { sleep } from "/@/cool/utils";
import { useFlow } from "/$/flow/hooks";
import ToolsVar from "/$/flow/components/tools/var.vue";
import Draggable from "vuedraggable";
const props = defineProps({
modelValue: {
type: Array as PropType<LLMMessage[]>,
default: () => []
}
});
const { refs, setRefs } = useCool();
const flow = useFlow();
//
const list = useModel(props, "modelValue");
//
function getRoles(item: LLMMessage) {
return [
{
label: "SYSTEM",
value: "system"
},
{
label: "USER",
value: "user"
},
{
label: "ASSISTANT",
value: "assistant"
}
].filter((e) => {
return item.role == "system" ? true : e.value != "system";
});
}
//
function add() {
const item = last(list.value);
list.value.push({
role: item?.role == "user" ? "assistant" : "user",
content: ""
});
}
//
function remove(index: number) {
list.value.splice(index, 1);
}
//
function setContent(el: HTMLElement, item: LLMMessage) {
let val = "";
function deep(e: Node) {
if (e.nodeType == 1) {
const node = e as HTMLElement;
const field = node.getAttribute("data-field");
if (field) {
val += `{${field}}`;
} else {
if (val) {
val += "\n";
}
if (e.textContent) {
e.childNodes.forEach(deep);
}
}
} else {
val += e.textContent;
}
}
deep(el);
item.content = val;
}
//
async function onContentInput(e: any, item: LLMMessage) {
setContent(e.target, item);
const sel = window.getSelection();
if (!sel || sel.rangeCount == 0) {
return;
}
await sleep(10);
const range = sel.getRangeAt(0);
//
const startContainer: any = range.startContainer;
const startOffset = range.startOffset;
//
if (startOffset > 0 && startContainer.textContent?.[startOffset - 1] === "/") {
range.setStart(sel.anchorNode!, sel.anchorOffset);
range.collapse(true);
//
const rect = range.getBoundingClientRect();
//
const container = startContainer.parentElement.closest(".form-content");
const containerRect = container.getBoundingClientRect();
//
const relativeReact = {
top: rect.top - containerRect.top + 8,
left: rect.left - containerRect.left,
width: rect.width,
height: rect.height
};
//
refs.toolsVar.open({
rect: relativeReact,
onSelect(item: any) {
const pos = range.startOffset;
if (pos > 0) {
// /
range.setStart(range.startContainer, pos - 1);
range.deleteContents();
//
const node = document.createElement("div");
node.setAttribute("contenteditable", "false");
node.setAttribute("data-field", item.field);
node.className = "field";
node.innerText = item.field;
//
node.addEventListener("click", () => {
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
});
//
range.insertNode(node);
//
range.setStartAfter(node);
range.setEndAfter(node);
// range
const sel2 = window.getSelection();
sel2?.removeAllRanges();
sel2?.addRange(range);
}
}
});
} else {
refs.toolsVar.close();
}
}
//
function onContentKeydown(e: KeyboardEvent) {
if (e.key == "ArrowUp" || e.key == "ArrowDown") {
refs.toolsVar.focus(e);
}
}
//
function onSelectionChange() {
function isRangeInsideElement(range: Range, element: Element) {
const elementRange = document.createRange();
elementRange.selectNodeContents(element);
return range.intersectsNode(element);
}
const selection = document.getSelection();
if (selection && selection.rangeCount != 0) {
const selectedRange = selection.getRangeAt(0);
for (let i in refs) {
//
if (i.includes("input-")) {
if (refs[i]?.contains(selectedRange.commonAncestorContainer)) {
const list = refs[i].querySelectorAll(".field") as HTMLElement[];
//
list.forEach((e) => {
if (isRangeInsideElement(selectedRange, e)) {
e.className = "field active";
} else {
e.className = "field";
}
});
return;
}
}
}
}
}
onMounted(() => {
list.value.forEach((e) => {
let start = -1;
let arr: any[] = [];
for (let i = 0; i < e.content.length; i++) {
let v = e.content[i];
if (v == "{") {
start = i;
} else if (v == "}") {
const field = e.content.substring(start + 1, i);
const param = flow.node?.data?.inputParams?.find((e) => e.field == field);
if (param) {
arr.push(
`<div class="field" contenteditable="false" data-field="${field}">${field}</div>`
);
}
start = -1;
} else {
if (start == -1) {
arr.push(v);
}
}
}
e.text = arr.join("");
});
document.addEventListener("selectionchange", onSelectionChange);
});
onUnmounted(() => {
document.removeEventListener("selectionchange", onSelectionChange);
});
</script>
<style lang="scss" scoped>
.form-content {
// pointer-events: all;
position: relative;
.list {
.item {
.head {
padding: 5px 10px !important;
.role {
width: 100px;
}
}
.content {
outline: none;
line-height: 1.5;
padding: 0 10px;
min-height: 50px;
user-select: text;
white-space: pre-wrap;
word-break: break-word;
cursor: text;
:deep(.field) {
display: inline-block;
border-radius: 4px;
padding: 0 4px;
margin: 0 2px;
font-size: 12px;
cursor: pointer;
border: 1px solid var(--el-border-color);
box-sizing: border-box;
user-select: none;
color: var(--el-color-primary);
&:hover,
&.active {
border-color: var(--el-color-primary);
}
}
&.is-empty {
color: var(--el-text-color-placeholder);
cursor: text;
&::before {
content: "在这里写你的提示词,输入 “/” 插入变量";
}
}
}
&.error {
border-color: var(--el-color-danger) !important;
}
}
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="form-model-text">
<template v-if="data?.params?.model">
<span class="supplier">{{ data.supplier }}</span>
<el-text>{{ data.params.model }}</el-text>
</template>
<el-text color="info" size="small" v-else>未选择模型</el-text>
</div>
</template>
<script setup lang="ts" name="node-llm-form-model-text">
import { type PropType } from "vue";
const props = defineProps({
data: {
type: Object as PropType<LLMData>,
default: () => ({})
}
});
</script>
<style lang="scss" scoped>
.form-model-text {
display: flex;
.supplier {
border-right: 1px solid var(--el-border-color);
padding-right: 10px;
margin-right: 10px;
}
}
</style>

View File

@ -0,0 +1,382 @@
<template>
<el-popover
trigger="click"
:teleported="false"
:offset="5"
popper-class="cl-flow__popper"
width="100%"
@show="onShow"
>
<template #reference>
<div class="inner-item">
<model-text :data="value" />
</div>
</template>
<div class="form-model">
<div class="model">
<p class="title">模型</p>
<el-popover
:ref="setRefs('modelPopover')"
trigger="click"
:teleported="false"
:offset="5"
popper-class="cl-flow__popper"
:popper-style="{
padding: 0
}"
placement="bottom-end"
width="300px"
>
<template #reference>
<div class="inner">
<span v-if="value?.params.model">{{ value.params.model }}</span>
<span class="placeholder" v-else>选择模型</span>
<el-icon class="arrow">
<arrow-down />
</el-icon>
</div>
</template>
<div class="search">
<el-input
placeholder="搜索模型"
:prefix-icon="Search"
v-model="keyWord"
clearable
/>
</div>
<el-scrollbar max-height="400px">
<div class="list" v-for="(item, index) in list" :key="index">
<p class="label">{{ item.title }}</p>
<div
class="item"
v-for="m in item.select"
:key="m"
:class="{
'is-check': value.params?.model == m
}"
@click="model.select(m, item)"
>
<span>{{ m }}</span>
<el-icon class="check">
<check />
</el-icon>
</div>
</div>
</el-scrollbar>
<div class="empty" v-if="isEmpty(list)">未找到匹配项</div>
</el-popover>
</div>
<div class="params" v-if="!isEmpty(value.options)">
<p class="title">参数</p>
<view class="row" v-for="(item, index) in value.options" :key="index">
<span class="label">{{ item.title }}</span>
<el-switch size="small" v-model="item.enable" />
<el-slider
class="slider"
v-model="item.value"
show-input
size="small"
:max="item.max || 1"
:min="item.min || 0"
:step="(item.max || 1) / 10"
:show-input-controls="false"
v-if="item.type == 'number'"
/>
<el-input
class="string"
v-model="item.value"
clearable
size="small"
placeholder="请输入"
v-else-if="item.type == 'string'"
/>
<el-switch class="boolean" v-model="item.value" v-if="item.type == 'boolean'" />
</view>
</div>
</div>
</el-popover>
</template>
<script setup lang="ts" name="node-llm-form-model">
import { computed, onMounted, type PropType, reactive, ref, useModel, watch } from "vue";
import { Search, Check, ArrowDown } from "@element-plus/icons-vue";
import { useCool } from "/@/cool";
import { isEmpty } from "lodash-es";
import ModelText from "./model-text.vue";
const props = defineProps({
modelValue: {
type: Object as PropType<LLMData>,
default: () => ({})
}
});
const emit = defineEmits(["update:modelValue"]);
const { refs, setRefs, service } = useCool();
//
const value = useModel(props, "modelValue");
//
const keyWord = ref("");
//
const model = reactive({
list: [] as LLMItem[],
get() {
service.flow.config.getByNode({ node: "llm" }).then((res) => {
model.list = (res as Eps.FlowConfigEntity[]).map((e) => {
const d: LLMItem = {
options: [],
title: e.name!,
type: e.type!,
id: e.id!,
select: []
};
(e.options?.options || []).forEach((e: LLMOption) => {
if (e.field == "model") {
d.select = e.select!;
} else {
d.options.push(e);
}
});
return d;
});
//
let name = value.value?.params?.model;
//
let item = model.list.find((e) => e.select.includes(name));
if (item) {
item.options.forEach((a) => {
const d = value.value.options.find((b) => a.field == b.field);
if (d) {
//
a.value = d.value;
a.enable = d.enable;
}
});
} else {
//
item = model.list[0];
if (item) {
name = item.select[0];
}
}
if (item) {
model.select(name, item);
}
});
},
select(name: string, item: LLMItem) {
if (!name) {
return false;
}
//
value.value.params.model = name;
//
value.value.supplier = item.type;
// ID
value.value.configId = item.id;
//
value.value.options = item.options.map((e) => {
if (e.value === undefined) {
if (e.default !== undefined) {
e.value = e.default;
}
}
return e;
});
//
refs.modelPopover?.hide();
}
});
//
const list = computed(() => {
return model.list.filter((item) => item.title.includes(keyWord.value));
});
//
function onShow() {
model.get();
}
onMounted(() => {
model.get();
watch(
() => value.value.options,
(arr) => {
arr.forEach((e) => {
if (e.enable) {
value.value.params[e.field] = e.value;
} else {
delete value.value.params[e.field];
}
});
},
{
deep: true
}
);
});
</script>
<style lang="scss" scoped>
.form-model {
padding: 0 5px;
.title {
font-size: 14px;
font-weight: bold;
}
.model {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
.selector {
width: 120px;
}
.inner {
display: flex;
align-items: center;
height: 30px;
min-width: 100px;
border-radius: 6px;
background-color: var(--el-fill-color-light);
cursor: pointer;
padding: 0 30px 0 10px;
font-size: 12px;
position: relative;
.arrow {
position: absolute;
right: 8px;
}
&:hover {
background-color: var(--el-fill-color-lighter);
}
}
.search {
padding: 5px;
:deep(.el-input__wrapper) {
box-shadow: none;
background-color: var(--el-fill-color-light);
border-radius: 6px;
}
}
.empty {
padding: 10px 5px 15px 5px;
line-height: 1;
font-size: 12px;
color: var(--el-text-color-secondary);
text-align: center;
}
.list {
padding: 0 5px 5px 5px;
.label {
padding: 5px;
font-size: 12px;
color: var(--el-color-info);
}
.item {
display: flex;
align-items: center;
height: 30px;
padding: 0 5px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
.check {
display: none;
margin-left: auto;
}
&.is-check {
color: var(--el-color-primary);
.check {
display: inline-block;
}
}
&:hover {
background-color: var(--el-fill-color-lighter);
}
}
}
}
.params {
padding-top: 10px;
border-top: 1px solid var(--el-fill-color-light);
.row {
display: flex;
align-items: center;
padding: 5px 0;
.label {
margin-right: 5px;
}
.slider {
margin-left: auto;
width: 200px;
:deep(.el-input-number) {
width: 60px;
}
}
.string {
width: 200px;
margin-left: auto;
}
}
}
}
</style>

View File

@ -0,0 +1,122 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import FormMessage from "./form/message.vue";
import FormModel from "./form/model.vue";
import FormInputParams from "../_base/form/input-params.vue";
import FormInputNumber from "../_base/form/input-number.vue";
import FormText from "../_base/form/text.vue";
export default (): FlowNode => {
return {
group: "AI",
label: "LLM",
description: "调用大语言模型回答问题",
color: "#409eff",
component,
isDisableDrag: true, // 是否允许拖拽部分组件跟拖拽事件冲突需禁用。例伪富文本dom
form: {
items: [
{
label: "输入变量",
prop: "inputParams",
component: {
vm: FormInputParams,
props: {
field: "input"
}
}
},
{
label: "模型",
prop: "options.model",
component: {
vm: FormModel
}
},
{
label: "消息",
prop: "options.messages",
component: {
vm: FormMessage
}
},
{
prop: "options.history",
span: 12,
component: {
vm: FormInputNumber,
props: {
prefix: "保存",
suffix: "条历史数据"
}
}
},
{
label: "输出变量",
component: {
vm: FormText,
props: {
text: ["text<string> 回复内容"]
}
}
}
]
},
data: {
inputParams: [
{
field: "input"
}
],
outputParams: [
{
type: "string",
field: "text"
},
{
type: "stream",
field: "stream"
}
],
options: {
model: {
options: [],
params: {
model: ""
}
},
messages: [
{
role: "system",
content: ""
},
{
role: "user",
content: ""
},
] as LLMMessage[],
history: 0
}
},
validator(data) {
const { model, messages } = data.options;
// 验证变量是否绑定值
const param = data.inputParams?.find((e) => !e.nodeId);
if (param) {
return `请绑定变量:${param.field}`;
}
// 验证模型是否选择
if (!model.params.model) {
return "请选择模型";
}
// 验证消息是否填写
const msg = messages.find((e: LLMMessage) => !e.content);
if (msg) {
return `请填写${msg.role}消息`;
}
}
};
};

View File

@ -0,0 +1,45 @@
<template>
<div class="node-llm" v-if="!focus">
<div class="model">
<model-text :data="node.data?.options?.model" />
</div>
</div>
</template>
<script lang="ts" setup name="node-llm">
import type { FlowNode } from "/$/flow/types";
import { type PropType } from "vue";
import ModelText from "./form/model-text.vue";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
},
//
focus: {
type: Boolean,
default: false
}
});
</script>
<style lang="scss" scoped>
.node-llm {
padding-bottom: 7px;
.model {
display: flex;
align-items: center;
background-color: var(--el-fill-color-light);
padding: 0 10px;
border-radius: 6px;
height: 33px;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-bottom: 8px;
}
}
</style>

View File

@ -0,0 +1,38 @@
declare interface LLMMessage {
role: string;
content: string;
text?: string;
[key: string]: any;
}
declare interface LLMData {
supplier: string;
options: LLMOption[];
params: {
model: string;
[key: string]: any;
};
configId: number;
}
declare interface LLMItem {
id: number;
options: LLMOption[];
title: string;
type: string;
select: string[];
}
declare interface LLMOption {
field: string;
value?: any;
type: "number" | "string" | "boolean";
title: string;
default?: any;
max?: number;
min?: number;
supports?: string[];
select?: string[];
status?: boolean;
[key: string]: any;
}

View File

@ -0,0 +1,83 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import FormModel from "../llm/form/model.vue";
import FormInputParams from "../_base/form/input-params.vue";
import FormOutputParams from "../_base/form/output-params.vue";
export default (): FlowNode => {
return {
group: "扩展",
label: "智能解析",
description: "智能提取内容的关键信息",
color: "#fd9d2f",
component,
form: {
items: [
{
label: "输入变量",
prop: "inputParams",
component: {
vm: FormInputParams,
props: {
editField: false,
disabled: true
}
}
},
{
label: "模型",
prop: "options.model",
component: {
vm: FormModel
}
},
{
label: "输出变量",
prop: "outputParams",
component: {
vm: FormOutputParams,
props: {
typeInput: true,
disabledFields: ["result"]
}
}
}
]
},
data: {
options: {
model: {
options: [],
params: {
model: ""
}
}
},
inputParams: [
{
field: "text"
}
],
outputParams: [
{
field: "result",
type: "输入结果"
}
]
},
validator(data) {
const { model } = data.options;
// 验证变量是否绑定值
const param = data.inputParams?.find((e) => !e.nodeId);
if (param) {
return `请绑定变量:${param.field}`;
}
// 验证模型是否选择
if (!model.params.model) {
return "请选择模型";
}
}
};
};

View File

@ -0,0 +1,20 @@
<template>
<div class="node-parse"></div>
</template>
<script lang="ts" setup name="node-parse">
import type { FlowNode } from "/$/flow/types";
import type { PropType } from "vue";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
},
//
focus: {
type: Boolean,
default: false
}
});
</script>

View File

@ -0,0 +1,164 @@
<template>
<div class="form-fields">
<cl-svg name="add" class="btn-icon is-rt" @click="open()" />
<div class="empty" v-if="isEmpty(list)">
<el-text size="small">设置的输入可在工作流程中使用</el-text>
<el-text size="small" type="primary" @click="open()">立即添加</el-text>
</div>
<tools-fields v-model="list" @edit="open" />
</div>
<cl-form ref="Form" />
</template>
<script setup lang="ts" name="node-start-form-fields">
import type { FlowField } from "/$/flow/types";
import { setFocus, useForm } from "@cool-vue/crud";
import { assign, isEmpty } from "lodash-es";
import { PropType, useModel } from "vue";
import ToolsFields from "/$/flow/components/tools/fields.vue";
import { ElMessage } from "element-plus";
const props = defineProps({
modelValue: Array as PropType<FlowField[]>,
scope: null,
disabled: Boolean,
prop: String,
isDisabled: Boolean
});
const Form = useForm();
const list = useModel(props, "modelValue");
function open(item?: FlowField) {
Form.value?.open(
{
title: "添加变量",
width: "500px",
dialog: {
controls: ["close"]
},
props: {
labelPosition: "top"
},
form: {
...item
},
items: [
{
label: "字段类型",
prop: "type",
value: "text",
component: {
name: "el-radio-group",
options: [
{
label: "文本",
value: "text"
},
{
label: "数字",
value: "number"
},
{
label: "图片",
value: "image"
}
]
},
required: true
},
{
label: "变量名称",
prop: "field",
component: {
name: "el-input",
props: {
clearable: true,
maxlength: 20
}
},
rules: [
{
required: true,
validator(rule, value, callback) {
if (isEmpty(value)) {
callback(new Error("请输入变量名称"));
} else if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(value)) {
callback(
new Error("只能由字母、数字、下划线组成,且以字母开头")
);
} else {
callback();
}
}
}
]
},
{
label: "显示名称",
prop: "label",
component: {
name: "el-input",
props: {
clearable: true,
maxlength: 20
}
},
required: true
},
{
label: "必填",
prop: "required",
value: 1,
component: {
name: "cl-switch"
},
required: true
}
],
on: {
submit(data, { close, done }) {
if (list.value?.find((e) => e.field == data.field)) {
if (item?.field != data.field) {
done();
return ElMessage.warning("变量名称已存在");
}
}
if (item) {
assign(item, data);
} else {
list.value?.push(data);
}
close();
}
}
},
[setFocus("value")]
);
}
</script>
<style lang="scss" scoped>
.form-fields {
position: relative;
.empty {
display: flex;
align-items: center;
justify-content: center;
height: 50px;
background-color: var(--el-fill-color-lighter);
border-radius: 6px;
.el-text:last-child {
cursor: pointer;
}
}
}
</style>

View File

@ -0,0 +1,42 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import FormFields from "./form/fields.vue";
import { isEmpty } from "lodash-es";
export default (): FlowNode => {
return {
label: "开始",
description: "开始节点",
color: "#409eff",
form: {
items: [
{
label: "输入字段",
prop: "inputParams",
component: {
vm: FormFields
}
}
]
},
data: {
inputParams: [
{
label: "内容",
field: "content",
type: "text",
required: true
}
]
},
handle: {
target: false
},
component,
validator(data) {
if (isEmpty(data.inputParams)) {
return "至少要输入一个字段";
}
}
};
};

View File

@ -0,0 +1,30 @@
<template>
<div class="node-start" v-if="!isEmpty(node?.data?.inputParams) && !focus">
<tools-fields v-model="node.data!.inputParams" disabled />
</div>
</template>
<script setup lang="ts" name="node-start">
import type { FlowNode } from "/$/flow/types";
import { type PropType } from "vue";
import ToolsFields from "/$/flow/components/tools/fields.vue";
import { isEmpty } from "lodash-es";
defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
},
//
focus: {
type: Boolean,
default: false
}
});
</script>
<style lang="scss" scoped>
.node-start {
padding-bottom: 15px;
}
</style>

View File

@ -0,0 +1,71 @@
import type { FlowNode } from "/$/flow/types";
import component from "./index.vue";
import { isEmpty } from "lodash-es";
import FormInputParams from "../_base/form/input-params.vue";
import FormOutputParams from "../_base/form/output-params.vue";
import CodeEditor from "../code/editor/index.vue";
import { Snippet } from "../code/shippets";
export default (): FlowNode => {
return {
group: "行为",
label: "变量",
description: "变量转换或赋值",
color: "#67c23a",
form: {
width: "500px",
items: [
{
label: "输入变量",
prop: "inputParams",
component: {
vm: FormInputParams,
props: {
varInputable: true
}
}
},
{
label: "代码编辑",
prop: "options.code",
component: {
vm: CodeEditor
}
},
{
label: "输出变量",
prop: "outputParams",
component: {
vm: FormOutputParams
}
}
]
},
data: {
options: {
code: Snippet.simple || Snippet.base
},
inputParams: [
{
field: "arg1"
}
],
outputParams: [
{
field: "string"
}
]
},
component,
validator(data) {
if (isEmpty(data.inputParams)) {
return "至少要输入一个字段";
}
const param = data.inputParams?.find((e) => !e.value && !e.nodeId);
if (param) {
return `请绑定变量或填写内容:${param.field}`;
}
}
};
};

View File

@ -0,0 +1,15 @@
<template>
<div class="node-variable"></div>
</template>
<script setup lang="ts" name="node-variable">
import type { FlowNode } from "/$/flow/types";
import { type PropType } from "vue";
defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
}
});
</script>

View File

@ -0,0 +1,509 @@
<template>
<div
class="tools-card"
:class="[
`is-${result.status}`,
{
'is-active': isActive,
'is-moving': node.isMoving
}
]"
>
<!-- 源选择点 -->
<tools-handle
type="source"
id="source"
:node-id="nodeId"
:position="{
right: '-9px',
top: '14px'
}"
v-if="node?.handle?.source ?? true"
/>
<!-- 目标选择点 -->
<tools-handle
type="target"
id="target"
:node-id="nodeId"
:position="{
left: '-9px',
top: '14px'
}"
v-if="node?.handle?.target ?? true"
/>
<div class="head">
<tools-icon :name="node?.icon" :color="node?.color" />
<el-input
v-if="isActive"
class="label"
v-model="flow.node!.label"
placeholder="标题"
/>
<span v-else>{{ node?.label }}</span>
<div class="btns" @click.stop>
<template v-if="!isStart">
<!-- <tools-nodes :node="node" is-change>
<cl-svg name="change" />
</tools-nodes> -->
<el-tooltip content="调试" placement="top" v-if="isRun && isActive">
<cl-svg class="btn-icon is-bg" name="run" @click="run" />
</el-tooltip>
<el-tooltip content="删除" placement="top">
<cl-svg
name="delete"
:style="{
marginLeft: 'auto'
}"
@click="remove"
/>
</el-tooltip>
<!-- <el-tooltip content="关闭" placement="top">
<cl-svg
v-if="isActive"
class="btn-icon is-bg"
name="close"
@click.stop="close"
/>
</el-tooltip> -->
</template>
<!-- <el-tooltip :content="node?.description" placement="top">
<cl-svg name="info" />
</el-tooltip> -->
<tools-more :node="node">
<cl-svg name="more" />
</tools-more>
</div>
</div>
<div class="desc">
<el-input
v-if="isActive"
v-model="flow.node!.desc"
:placeholder="node?.description"
clearable
/>
<span class="text" v-else>{{ node?.desc || node?.description }}</span>
</div>
<div class="container">
<component :is="component()" :node="node" :focus="isActive" />
<card-form :ref="setRefs('form')" v-if="isActive" />
</div>
<div class="tips" v-if="result.status !== 'none'">
<el-icon class="is-loading" v-if="result.status == 'running'">
<loading />
</el-icon>
<span>{{ result.message }}</span>
</div>
</div>
</template>
<script setup lang="ts" name="tools-card">
import CardForm from "./card/form.vue";
import { useFlow } from "/$/flow/hooks";
import { computed, onMounted, onUnmounted, reactive, watch } from "vue";
import ToolsIcon from "./icon.vue";
import ToolsHandle from "./handle.vue";
import ToolsMore from "./more.vue";
import { useCool } from "/@/cool";
import { Loading } from "@element-plus/icons-vue";
import type { FlowNode, FlowNodeResult } from "/$/flow/types";
import { nextTick } from "vue";
const props = defineProps({
nodeId: String
});
const { mitt, refs, setRefs } = useCool();
const flow = useFlow();
//
const isRun = computed(() => {
return flow.node ? !["start", "end"].includes(flow.node.type!) : false;
});
//
const width = computed(() => {
return flow.node?.form?.width || "400px";
});
//
const node = computed(() => {
return flow.findNode(props.nodeId!);
});
//
const isStart = computed(() => {
return node.value?.type == "start";
});
//
const isActive = computed(() => {
return node.value?.id == flow.node?.id;
});
//
function component() {
return flow.CustomNodes.find((e) => e.type == node.value?.type)?.component;
}
//
function run() {
mitt.emit("flow.runOpen", flow.node);
}
//
function remove() {
flow.removeNodes(node.value);
}
//
function close() {
flow.enableDrag();
flow.clearNode();
}
//
function getColor(color: string) {
return getComputedStyle(document.documentElement).getPropertyValue(`--el-color-${color}`);
}
//
const result = reactive({
status: "none" as "none" | "running" | "waiting" | "success" | "fail",
message: "",
clear() {
result.status = "none";
result.message = "";
result.updateEdge({ animated: false, style: {} });
},
check() {
result.clear();
},
ready(_, node?: FlowNode) {
function done() {
result.status = "waiting";
result.message = "等待中...";
}
if (node) {
if (props.nodeId == node.id) {
done();
}
} else {
const startNode = flow.findNodeByType("start");
if (startNode) {
const childrens = flow.childrenAllNodes(startNode.id!);
if (childrens.find((e) => e.id == props.nodeId) || startNode.id == props.nodeId) {
done();
result.updateEdge({
animated: true,
style: {
stroke: getColor("info")
}
});
}
}
}
},
start(nodeId: string) {
if (nodeId == props.nodeId) {
flow.setViewportByNode(flow.findNode(nodeId));
result.status = "running";
result.message = "运行中...";
result.updateEdge({
style: {
stroke: getColor("primary")
}
});
}
},
result(res: FlowNodeResult, node?: FlowNode) {
const nodeId = node?.id || res.nodeId;
if (nodeId == props.nodeId) {
//
if (res.result.success) {
const duration = parseFloat(((res.duration || 1) / 1000).toFixed(3));
result.status = "success";
result.message = `执行成功,耗时:${duration}s`;
//
if (!node) {
if (res.nextNodeId != nodeId) {
mitt.emit("flow.run", { action: "start", data: res.nextNodeId });
}
}
} else {
result.status = "fail";
result.message = res.result.error.message;
//
flow.setViewportByNode(flow.findNode(res.nodeId!));
}
// 线
if (!node) {
// 线
result.updateEdge({
animated: true,
style: {
stroke: res.result.success ? getColor("success") : getColor("danger")
}
});
}
}
},
fail({ nodeId, message }: { nodeId: string; message: string }) {
if (props.nodeId == nodeId) {
result.status = "fail";
result.message = message;
flow.setViewportByNode(flow.findNode(nodeId));
}
},
end() {
if (["waiting", "running"].includes(result.status)) {
result.close();
}
},
close() {
result.clear();
},
updateEdge(data: any) {
const edge = flow.edges.find((e) => e.target == props.nodeId);
if (edge) {
flow.updateEdge(edge.id, data);
}
},
onRun({ action, data, node }: { action: string; data: any; node?: FlowNode }) {
result[action](data, node);
}
});
//
async function openForm(data: FlowNode) {
nextTick(() => {
if (data) {
if (data.id == props.nodeId) {
refs.form?.open();
}
}
});
}
//
function closeForm(data: FlowNode) {
nextTick(() => {
if (data?.id == props.nodeId) {
refs.form?.close();
}
});
}
onMounted(() => {
mitt.on("flow.openForm", openForm);
mitt.on("flow.closeForm", closeForm);
mitt.on("flow.run", result.onRun);
mitt.on("flow.runClose", result.close);
});
onUnmounted(() => {
mitt.off("flow.openForm", openForm);
mitt.off("flow.closeForm", closeForm);
mitt.off("flow.run", result.onRun);
mitt.off("flow.runClose", result.close);
});
</script>
<style lang="scss" scoped>
.tools-card {
border-radius: 12px;
background-color: var(--el-bg-color);
width: 300px;
position: relative;
border: 2px solid var(--el-fill-color-light);
box-sizing: border-box;
transition: all 0.2s;
.btns {
display: none;
align-items: center;
justify-content: space-between;
position: absolute;
top: 8px;
right: 10px;
box-sizing: border-box;
// border: 1px solid var(--el-fill-color-light);
background-color: var(--el-bg-color);
border-radius: 6px;
font-size: 12px;
padding: 3px;
box-sizing: border-box;
.cl-svg {
color: var(--el-text-color-regular);
cursor: pointer;
border-radius: 4px;
height: 15px;
width: 15px;
padding: 4px;
&:hover,
&:focus {
background-color: var(--el-fill-color-light);
}
&:focus {
outline: none;
}
}
&:has(.cl-svg:focus) {
display: flex;
}
}
.head {
display: flex;
align-items: center;
padding: 0 15px;
height: 44px;
.icon {
display: flex;
align-items: center;
justify-content: center;
height: 20px;
width: 20px;
border-radius: 4px;
background-color: var(--el-color-primary);
color: #fff;
}
span {
font-size: 14px;
font-weight: bold;
margin-left: 10px;
line-height: 1;
}
:deep(.el-input__wrapper) {
box-shadow: none;
padding: 0 5px;
.el-input__inner {
font-weight: bold;
padding: 0 5px;
border-radius: 4px;
&:hover {
background-color: var(--el-fill-color-lighter);
}
}
}
}
.container {
padding: 0 15px;
}
.desc {
padding: 0 15px 15px 15px;
font-size: 12px;
color: var(--el-color-info);
word-break: break-all;
:deep(.el-input__wrapper) {
box-shadow: none;
font-size: 12px;
background-color: var(--el-fill-color-lighter);
border-radius: var(--el-border-radius-base);
}
}
.tips {
padding: 0 15px 15px 15px;
font-size: 12px;
.is-loading {
font-size: 14px;
position: relative;
top: 2px;
margin-right: 3px;
}
}
&:hover {
box-shadow:
0px 4px 6px -2px rgba(16, 24, 40, 0.03),
0px 12px 16px -4px rgba(16, 24, 40, 0.08);
.btns {
display: flex;
}
}
&.is-active {
width: calc(v-bind("width") + 30px);
border-color: var(--el-color-primary);
}
&.is-waiting,
&.is-running {
border-color: var(--el-color-info);
.tips {
color: var(--el-color-info);
}
}
&.is-running {
border-color: var(--el-color-primary);
.tips {
color: var(--el-color-primary);
}
}
&.is-fail {
border-color: var(--el-color-danger);
.tips {
color: var(--el-color-danger);
}
:deep(.rod) {
color: var(--el-color-danger);
}
}
&.is-success {
border-color: var(--el-color-success);
.tips {
color: var(--el-color-success);
}
:deep(.rod) {
color: var(--el-color-success);
}
}
}
</style>

View File

@ -0,0 +1,458 @@
<template>
<div class="tools-card-form nodrag" v-if="visible && flow.node">
<div class="form">
<cl-form inner ref="Form">
<template #slot-next>
<div class="next-step">
<el-text size="small" type="info"> 添加此工作流程中的下一个节点 </el-text>
<div class="link">
<div
class="item"
:class="{
active: !!item.node
}"
v-for="(item, index) in nextNodes"
:key="index"
>
<div class="a" v-if="index == 0">
<tools-icon :name="flow.node?.icon" :color="flow.node?.color" />
</div>
<div class="b" @click="next(item.node)" v-if="item.node">
<tools-icon :name="item.node?.icon" :color="item.node?.color" />
<el-text size="small">{{ item.node?.label }}</el-text>
</div>
<tools-nodes :node="flow.node" :handle="item.value" v-else>
<div class="b">
<tools-icon name="add" />
<el-text size="small">选择下一个节点</el-text>
</div>
</tools-nodes>
<span class="name" v-if="item.label">{{ item.label }}</span>
</div>
</div>
</div>
</template>
</cl-form>
</div>
</div>
</template>
<script setup lang="ts" name="tools-card-form">
import { useForm } from "@cool-vue/crud";
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { useCool } from "/@/cool";
import { useFlow } from "/$/flow/hooks";
import type { FlowNode } from "/$/flow/types";
import { setFocus } from "@cool-vue/crud";
import { cloneDeep, assign } from "lodash-es";
import ToolsIcon from "./../icon.vue";
import ToolsNodes from "./../nodes.vue";
const { mitt } = useCool();
const Form = useForm();
const flow = useFlow();
//
const visible = ref(false);
//
const minWidth = computed(() => {
return flow.node?.form?.width || "400px";
});
//
const nextNodes = computed(() => {
const list = flow.node?.handle?.next || [{ label: "", value: "" }];
return list.map((h) => {
const edge = flow.edges.find(
(e) => e.source == flow.node?.id && (h.value ? e.sourceHandle == h.value : true)
);
return {
...h,
node: edge ? flow.findNode(edge.target!) : null
};
});
});
//
function open() {
visible.value = true;
//
// if (flow.node?.isDisableDrag === true) {
// flow.disabledDrag();
// }
nextTick(() => {
const { focus, items } = flow.node?.form || {};
Form.value?.open(
{
props: {
labelPosition: "top"
},
form: flow.node?.data || {},
items: [...(items || [])],
op: {
hidden: true
}
},
[setFocus(focus || items?.[0]?.prop)]
);
});
}
//
function close() {
visible.value = false;
flow.enableDrag();
}
//
function next(node: FlowNode) {
flow.setNode(node);
open();
}
//
function update(data: any) {
assign(Form.value?.form, data);
}
onMounted(() => {
// node
watch(
() => flow.nodes.length,
() => {
if (flow.node) {
if (!flow.findNode(flow.node.id!)) {
close();
}
}
}
);
//
watch(
() => Form.value?.form,
(val) => {
const d = cloneDeep(val);
Form.value?.invokeData(d);
assign(flow.node?.data, d);
},
{
deep: true
}
);
mitt.on("flow.updateForm", update);
mitt.on("flow.run", close);
});
onUnmounted(() => {
mitt.off("flow.updateForm", update);
mitt.off("flow.run", close);
});
defineExpose({
open,
close
});
</script>
<style lang="scss" scoped>
.tools-card-form {
width: v-bind(minWidth);
max-width: 800px;
border-radius: 12px;
.head {
display: flex;
align-items: center;
padding: 10px 15px;
.label {
flex: 1;
:deep(.el-input__wrapper) {
box-shadow: none;
padding: 0 5px;
.el-input__inner {
font-weight: bold;
padding: 0 5px;
border-radius: 4px;
&:hover {
background-color: var(--el-fill-color-lighter);
}
}
}
}
.btns {
display: flex;
align-items: center;
.btn-icon {
margin-left: 6px;
}
}
}
.desc {
border-bottom: 1px solid var(--el-border-color-lighter);
padding: 0 5px 7px 5px;
:deep(.el-input__wrapper) {
box-shadow: none;
font-size: 12px;
}
}
.form {
height: calc(100% - 88px);
.cl-form {
:deep(.cl-form-card) {
.cl-form-card__header {
background-color: transparent;
padding: 0;
font-weight: normal;
color: var(--el-text-color-primary);
}
}
}
.next-step {
line-height: normal;
.link {
margin-top: 10px;
.item {
display: flex;
align-items: center;
position: relative;
margin-bottom: 10px;
.a,
.b {
height: 35px;
width: 35px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background-color: var(--el-bg-color);
box-sizing: border-box;
}
.a {
position: absolute;
left: 0;
top: 0;
z-index: 9;
border-color: var(--el-color-primary);
}
.b {
flex: 1;
justify-content: flex-start;
margin-left: 62px;
padding-left: 10px;
.el-text {
margin-left: 10px;
}
}
.name {
display: flex;
align-items: center;
position: absolute;
right: 0;
top: 0;
height: 35px;
font-size: 12px;
padding: 0 10px;
box-sizing: border-box;
border-left: 1px solid var(--el-border-color);
color: var(--el-text-color-regular);
}
&:last-child {
margin-bottom: 0;
}
&::after {
content: "";
display: block;
height: 44px;
width: 44px;
position: absolute;
left: 18px;
bottom: 18px;
border: 1px solid var(--el-border-color);
border-top: 0;
border-right: 0;
}
&:first-child::after {
border-left: 0;
}
&:last-child::after {
border-radius: 0 0 0 6px;
}
&.active {
.name {
color: var(--el-color-primary);
}
.name,
&::after {
border-color: var(--el-color-primary);
}
.b {
border-color: var(--el-color-primary);
.el-text {
color: var(--el-color-primary);
}
}
}
&:not(.active) .b:hover {
background-color: var(--el-fill-color-lighter);
}
}
}
}
}
//
// & > div[class^="tools-card"] {
// position: relative;
// background-color: var(--el-bg-color);
// height: 100%;
// border-radius: 10px;
// box-sizing: border-box;
// box-shadow: 0px 0 10px 1px rgba(16, 24, 40, 0.05);
// margin-left: 10px;
:deep(.inner-item) {
display: flex;
align-items: center;
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
padding: 0 10px;
cursor: pointer;
height: 32px;
width: 100%;
position: relative;
transition: all 0.2s;
box-sizing: border-box;
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 20px;
}
.placeholder {
color: var(--el-text-color-placeholder);
font-size: 14px;
}
.close {
position: absolute;
right: 6px;
display: none;
font-size: 12px !important;
color: var(--el-color-info);
}
&:hover {
border-color: var(--el-border-color-hover);
.close {
display: block;
}
}
}
:deep(.textarea-item) {
border: 1px solid var(--el-border-color);
padding: 0 0 8px 0;
border-radius: 8px;
margin-bottom: 10px;
.el-textarea__inner {
background-color: transparent;
box-shadow: none;
padding: 0 10px;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
height: 30px;
line-height: normal;
span {
font-size: 12px;
color: var(--el-color-info);
}
}
&:last-child {
margin-bottom: 0;
}
}
:deep(.btn-icon) {
color: var(--el-text-color-regular);
cursor: pointer;
border-radius: 6px;
font-size: 14px;
padding: 4px;
flex-shrink: 0;
outline: none;
&:focus,
&:hover {
background-color: var(--el-fill-color-light);
color: var(--el-text-color-primary);
}
&.is-rt {
position: absolute;
top: -30px;
right: 0;
}
&.is-bg {
background-color: var(--el-fill-color-lighter);
&:hover {
color: var(--el-color-primary) !important;
}
}
}
// }
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<g>
<path
class="vue-flow__connection animated"
fill="none"
stroke="#6F3381"
:stroke-width="1.5"
:d="`M${sourceX},${sourceY} C ${sourceX} ${targetY} ${sourceX} ${targetY} ${targetX},${targetY}`"
/>
<circle
:cx="targetX"
:cy="targetY"
fill="#fff"
:r="4"
stroke="#6F3381"
:stroke-width="1.5"
/>
</g>
</template>
<script setup lang="ts" name="tools-connection-line">
defineProps({
sourceX: {
type: Number,
required: true
},
sourceY: {
type: Number,
required: true
},
targetX: {
type: Number,
required: true
},
targetY: {
type: Number,
required: true
}
});
</script>

View File

@ -0,0 +1,190 @@
<template>
<cl-context-menu ref="PaneContextMenu" />
</template>
<script setup lang="tsx" name="tools-context-menu">
import { defineComponent, ref } from "vue";
import {
CopyDocument,
Grid,
Delete,
DocumentCopy,
Plus,
CaretRight
} from "@element-plus/icons-vue";
import { useFlow } from "../../hooks";
import { NodeMouseEvent } from "@vue-flow/core";
import { ElMessageBox } from "element-plus";
import { ContextMenu } from "@cool-vue/crud";
import ToolsNodes from "./nodes.vue";
import { useCool } from "/@/cool";
const { mitt } = useCool();
const flow = useFlow();
//
const PaneContextMenu = ref<ClContextMenu.Ref>();
//
const PlusIcon = defineComponent({
components: {
Plus,
ToolsNodes
},
setup() {
return () => {
return (
<div>
<plus />
<tools-nodes is-auto-insert onHide={close}>
<span class="cl-flow__context-menu-btn"></span>
</tools-nodes>
</div>
);
};
}
});
//
let closeContextMenu: () => void;
function close() {
PaneContextMenu.value?.close();
closeContextMenu?.();
}
//
function onPane(e: MouseEvent) {
PaneContextMenu.value?.open(e, {
class: "cl-flow__context-menu",
list: [
{
label: "粘贴到这里",
suffixIcon: CopyDocument,
hidden: !flow.copyData,
callback(done) {
if (flow.copyData) {
const { zoom, x, y } = flow.viewport;
const node = flow.addNode(flow.copyData.type!, {
...flow.copyData,
position: {
x: (e.layerX - x) / zoom,
y: (e.layerY - y) / zoom
}
});
flow.setCopyNode(null);
flow.setNode(node);
flow.setViewportByNode(node);
}
done();
}
},
{
label: "运行",
suffixIcon: CaretRight,
callback(done) {
mitt.emit("flow.runOpen");
done();
}
},
{
label: "添加节点",
suffixIcon: PlusIcon,
callback() {
// @ts-ignore
document.querySelector(".cl-flow__context-menu-btn")?.click();
}
},
{
label: "整理节点",
suffixIcon: Grid,
callback(done) {
flow.arrange();
done();
}
},
{
label: "清空节点",
suffixIcon: Delete,
callback(done) {
ElMessageBox.confirm("请确认是否清空所有节点?", "提示", {
type: "warning"
})
.then(() => {
flow.clear();
})
.catch(() => null);
done();
}
}
]
});
e.stopPropagation();
e.preventDefault();
}
//
function onNode({ event, node }: NodeMouseEvent) {
const e = event as MouseEvent;
const { close } = ContextMenu.open(e, {
class: "cl-flow__context-menu",
list: [
{
label: "复制",
suffixIcon: DocumentCopy,
hidden: node.type == "start",
callback(done) {
flow.setCopyNode(node);
done();
}
},
{
label: "删除",
suffixIcon: Delete,
hidden: node.type == "start",
callback(done) {
flow.removeNodes(node);
done();
}
}
]
});
closeContextMenu = close;
e.stopPropagation();
e.preventDefault();
}
defineExpose({
onPane,
onNode,
close
});
</script>
<style lang="scss">
.cl-flow__context-menu {
border-radius: 8px;
padding: 0 5px;
& > div {
border-radius: 6px;
height: 30px;
font-size: 12px;
padding: 0 10px;
&:hover {
background-color: var(--el-fill-color-light);
}
}
}
</style>

View File

@ -0,0 +1,224 @@
<template>
<div class="tools-controls">
<el-popover
trigger="click"
width="100px"
placement="top-start"
popper-class="cl-flow__popper"
:popper-style="{
minWidth: '100px'
}"
:offset="5"
:teleported="false"
>
<template #reference>
<div class="zoom-op">
<cl-svg name="reduce-btn-fill" @click.stop="vueFlow.zoomOut()" />
<span class="val">{{ zoom }}%</span>
<cl-svg name="add-btn-fill" @click.stop="vueFlow.zoomIn()" />
</div>
</template>
<div class="list">
<div
class="item"
v-for="item in ratio"
:key="item.value"
@click="vueFlow.zoomTo(item.value)"
>
{{ item.label }}
</div>
<div class="item" @click="zoomFit">自适应</div>
</div>
</el-popover>
<div class="hr"></div>
<div class="btn-op">
<el-tooltip content="指针模式">
<cl-svg
name="pointer"
:class="{
active: flow.controlMode == 'pointer'
}"
@click="toPointer"
/>
</el-tooltip>
<el-tooltip content="手模式">
<cl-svg
name="hand"
:class="{
active: flow.controlMode == 'hand'
}"
@click="toHand"
/>
</el-tooltip>
<el-tooltip content="整理节点">
<cl-svg name="arrange" @click="toArrange" />
</el-tooltip>
<el-tooltip content="节点定位">
<cl-svg name="center" @click="toCenter" />
</el-tooltip>
</div>
<div class="hr"></div>
<tools-history />
</div>
</template>
<script setup lang="ts" name="tools-controls">
import { useVueFlow } from "@vue-flow/core";
import { computed } from "vue";
import { useCool } from "/@/cool";
import { useFlow } from "../../hooks";
import ToolsHistory from "./history.vue";
const vueFlow = useVueFlow();
const flow = useFlow();
const { mitt } = useCool();
const zoom = computed(() => {
return Math.ceil(flow.viewport.zoom * 100 || 100);
});
//
const ratio = [
{
label: "200%",
value: 2
},
{
label: "150%",
value: 1.5
},
{
label: "100%",
value: 1
},
{
label: "75%",
value: 0.75
},
{
label: "50%",
value: 0.5
},
{
label: "25%",
value: 0.25
}
];
//
function toPointer() {
flow.setControlMode("pointer");
}
//
function toHand() {
flow.setControlMode("hand");
}
//
function zoomFit() {
vueFlow.fitView();
}
//
function toArrange() {
flow.arrange();
//
mitt.emit("flow.closeForm");
}
//
function toCenter() {
flow.setViewportByNode(flow.node!);
}
</script>
<style lang="scss" scoped>
.tools-controls {
display: flex;
align-items: center;
position: absolute;
bottom: 10px;
right: 10px;
z-index: 9;
border-radius: 6px;
background-color: var(--el-bg-color);
box-shadow: 0px 0 6px 1px rgba(16, 24, 40, 0.08);
padding: 0 3px;
.hr {
height: 10px;
width: 1px;
background-color: var(--el-border-color-light);
margin: 0 5px;
}
:deep(.cl-svg) {
border-radius: 6px;
height: 16px;
width: 16px;
padding: 4px;
color: var(--el-color-info-dark-2);
&:focus,
&:hover {
color: var(--el-color-primary);
background-color: var(--el-fill-color);
}
&:focus {
outline: none;
}
&.active {
color: var(--el-color-primary);
}
}
.zoom-op {
width: 100px;
.val {
flex: 1;
text-align: center;
user-select: none;
font-size: 12px;
}
}
.zoom-op,
.btn-op {
display: flex;
align-items: center;
height: 30px;
box-sizing: border-box;
cursor: pointer;
}
.list {
.item {
cursor: pointer;
font-size: 12px;
padding: 5px 10px;
border-radius: 4px;
text-align: center;
user-select: none;
&:hover {
background-color: var(--el-fill-color-lighter);
}
}
}
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<base-edge :id="id" :style="style" :path="path[0]" :marker-end="markerEnd" />
<edge-label-renderer>
<div
:style="{
height: '16px',
width: '16px',
pointerEvents: 'all',
position: 'absolute',
transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`
}"
class="nodrag nopan"
>
<!-- <tools-nodes
:ref="setRefs('toolsNodes')"
:node="node"
:edge-id="id"
:handle="edge?.sourceHandle!"
is-insert
>
<el-icon
class="icon"
:class="{
active: refs.toolsNodes?.visible,
show: data?.show
}"
>
<circle-plus-filled />
</el-icon>
</tools-nodes> -->
<el-icon
class="icon"
:class="{
show: data?.show
}"
@click="del"
>
<circle-close-filled />
</el-icon>
</div>
</edge-label-renderer>
</template>
<script setup lang="ts" name="tools-edge-button">
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from "@vue-flow/core";
import { computed } from "vue";
import ToolsNodes from "./nodes.vue";
import { CirclePlusFilled, CircleCloseFilled } from "@element-plus/icons-vue";
import { useCool } from "/@/cool";
import { useFlow } from "../../hooks";
const props = defineProps({
id: {
type: String,
required: true
},
sourceX: {
type: Number,
required: true
},
sourceY: {
type: Number,
required: true
},
targetX: {
type: Number,
required: true
},
targetY: {
type: Number,
required: true
},
sourcePosition: {
type: String,
required: true
},
targetPosition: {
type: String,
required: true
},
markerEnd: {
type: String,
required: false
},
style: {
type: Object,
required: false
},
data: Object
});
const { refs, setRefs } = useCool();
const flow = useFlow();
const path = computed(() => getBezierPath(props as any));
const edge = computed(() => {
return flow.findEdge(props.id);
});
const node = computed(() => {
return flow.nodes.find((e) => e.id == edge.value?.source);
});
function del() {
const edge = flow.findEdge(props.id);
if (edge) {
flow.removeEdges(edge);
}
}
</script>
<style lang="scss" scoped>
.icon {
display: none;
color: var(--el-color-danger);
font-size: 16px;
cursor: pointer;
background-color: var(--el-bg-color);
border-radius: 100%;
transition: transform 0.15s;
&.show {
display: inline-block;
}
&.active,
&:hover {
transform: scale(1.3);
display: inline-block;
}
}
</style>

View File

@ -0,0 +1,124 @@
<template>
<div class="tools-fields">
<div
class="item"
:class="{
disabled
}"
v-for="(item, index) in list"
:key="index"
@click="edit(item)"
>
<cl-svg :name="item.type" class="type" />
<div class="name">
{{ item.field }}
<span v-if="!disabled"> · {{ item.label }}</span>
</div>
<span class="required" v-if="item.required">必填</span>
<div class="op">
<cl-svg name="delete" @click.stop="remove(index)" />
</div>
</div>
</div>
</template>
<script setup lang="ts" name="tools-field">
import { PropType, useModel } from "vue";
import type { FlowField } from "/$/flow/types";
const props = defineProps({
modelValue: {
type: Array as PropType<FlowField[]>,
default: () => []
},
disabled: Boolean
});
const emit = defineEmits(["update:modelValue", "edit"]);
//
const list = useModel(props, "modelValue");
//
function edit(item: FlowField) {
emit("edit", item);
}
//
function remove(index: number) {
list.value.splice(index, 1);
}
</script>
<style lang="scss" scoped>
.tools-fields {
.item {
display: flex;
align-items: center;
margin-bottom: 5px;
background-color: var(--el-fill-color-lighter);
border-radius: 6px;
padding: 0 5px;
height: 30px;
cursor: pointer;
position: relative;
overflow: hidden;
.type {
height: 15px;
width: 15px;
margin-right: 5px;
padding: 3px;
}
.name {
font-size: 12px;
flex: 1;
}
.required {
font-size: 12px;
margin-right: 5px;
}
.op {
display: none;
align-items: center;
justify-content: flex-end;
height: 100%;
position: absolute;
right: 0;
top: 0;
width: 80px;
padding: 0 5px;
background-color: var(--el-fill-color-light);
.cl-svg {
height: 15px;
width: 15px;
padding: 3px;
border-radius: 4px;
&:hover {
background-color: var(--el-fill-color-darker);
}
}
}
&:last-child {
margin-bottom: 0;
}
&:not(.disabled):hover {
background-color: var(--el-fill-color-light);
.op {
display: flex;
}
}
}
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<tools-nodes :node="node" :handle="id" :disabled="true || isLink">
<el-icon
class="tools-handle"
:class="[
`is-${type}`,
{
'is-link': isLink
}
]"
:style="[position]"
@click.stop
>
<span class="rod"></span>
<!-- <circle-plus-filled class="icon" /> -->
<cl-svg name="arrow-right" class="icon" />
<handle
:id="id"
:type="type"
:position="align"
:is-valid-connection="onValidConnection"
/>
</el-icon>
</tools-nodes>
</template>
<script setup lang="ts" name="tools-handle">
import { computed, PropType, StyleValue } from "vue";
import { CirclePlusFilled } from "@element-plus/icons-vue";
import { Handle, Position, Connection } from "@vue-flow/core";
import { useFlow } from "../../hooks";
import ToolsNodes from "./nodes.vue";
const props = defineProps({
nodeId: String,
id: String,
position: Object as PropType<StyleValue>,
type: {
type: String as PropType<"target" | "source">,
default: "source"
}
});
const flow = useFlow();
//
const node = computed(() => {
return flow.findNode(props.nodeId!);
});
//
const align = computed(() => {
return props.type == "target" ? Position.Left : Position.Right;
});
//
const isLink = computed(() => {
return !!flow.edges.find((e) => {
if (e[props.type] == props.nodeId) {
return e[`${props.type}Handle`] == props.id;
}
return false;
});
});
//
function onValidConnection({ targetHandle, sourceHandle, source, target }: Connection) {
//
const pNodes = flow.parentAllNodes(source);
if (pNodes.find((e) => e.id == target)) {
return false;
}
//
if (target == source) {
return false;
}
//
if (targetHandle == "target" && sourceHandle != "target") {
return true;
}
return false;
}
</script>
<style lang="scss" scoped>
.tools-handle {
position: absolute;
color: var(--el-color-primary);
font-size: 16px;
transition: transform 0.2s;
background-color: var(--el-bg-color);
border-radius: 100%;
&:hover {
transform: scale(1.2);
}
.vue-flow__handle {
position: absolute;
left: -8px;
top: 8px;
height: 14px;
width: 14px;
opacity: 0;
&-left {
left: auto;
right: -8px;
}
}
&.is-link {
display: flex;
align-items: center;
background-color: transparent;
.icon {
display: none;
}
.rod {
width: 0;
height: 0;
border-radius: 1px;
position: absolute;
border: 5px solid transparent;
color: var(--el-color-primary);
}
}
&.is-source {
.rod {
right: -5px;
border-left-color: currentColor;
}
}
&.is-target {
.rod {
left: -5px;
border-right-color: currentColor;
}
}
}
</style>

View File

@ -0,0 +1,218 @@
<template>
<div class="tools-head">
<div class="info">
<p class="title">{{ flow.info?.name }}</p>
<p class="desc">保存于 {{ dayjs(flow.info?.updateTime).format("MM-DD HH:mm:ss") }}</p>
</div>
<div class="op">
<div class="item" @click="save()">
<el-tooltip content="保存" placement="top">
<cl-svg name="save2" />
</el-tooltip>
</div>
<div class="item" @click="run()">
<el-tooltip content="运行" placement="top">
<cl-svg name="run2" />
</el-tooltip>
</div>
<el-popover
:ref="setRefs('publishPopover')"
trigger="click"
:teleported="false"
:offset="5"
width="240px"
popper-class="cl-flow__popper"
placement="bottom"
@show="publish.onShow"
>
<template #reference>
<div class="item">
<el-tooltip content="发布" placement="top">
<cl-svg name="publish" />
</el-tooltip>
</div>
</template>
<div class="publish">
<el-text size="small" tag="p">{{ publish.tips }}</el-text>
<div class="btn">
<el-button class="a" type="primary" @click="publish.next">发布</el-button>
</div>
</div>
</el-popover>
<div class="item" @click="refs.nodeConfig?.open()">
<el-tooltip content="配置" placement="top">
<cl-svg name="set" />
</el-tooltip>
</div>
<div class="item" @click="exportFlow()">
<el-tooltip content="导出" placement="top">
<cl-svg name="export" />
</el-tooltip>
</div>
</div>
<node-config :ref="setRefs('nodeConfig')" />
</div>
</template>
<script setup lang="ts" name="tools-head">
import dayjs from "dayjs";
import { useFlow } from "/$/flow/hooks";
import { useCool } from "/@/cool";
import { ElMessage, ElMessageBox } from "element-plus";
import { reactive } from "vue";
import { formatTime } from "/$/flow/utils";
import NodeConfig from "./node-config.vue";
const { mitt, refs, setRefs, service } = useCool();
const flow = useFlow();
function run() {
mitt.emit("flow.runOpen");
mitt.emit("flow.closeForm");
}
async function save() {
mitt.emit("flow.closeForm");
await flow.save();
ElMessage.success("数据保存成功");
}
const publish = reactive({
loading: false,
tips: "-",
onShow() {
const { releaseTime } = flow?.info || {};
if (releaseTime) {
publish.tips = `上次发布于${formatTime(releaseTime)}`;
} else {
publish.tips = "当前未发布";
}
},
next() {
refs.publishPopover?.hide();
mitt.emit("flow.runCheck", async (status: boolean) => {
if (status) {
ElMessage.success("检测通过,发布中");
publish.loading = true;
// 稿
await flow.save();
//
await service.flow.info
.release({
flowId: flow.info?.id
})
.then(() => {
ElMessage.success("发布成功");
flow.get();
})
.catch((err) => {
ElMessage.error(err.message);
});
publish.loading = false;
}
});
}
});
//
function exportFlow() {
ElMessageBox.confirm("是否要导出当前流程?", "提示", {
type: "success",
confirmButtonText: "确认",
cancelButtonText: "取消"
}).then(() => {
flow.exportFlow();
});
}
</script>
<style lang="scss" scoped>
.tools-head {
display: flex;
align-items: center;
justify-content: space-between;
position: absolute;
left: 0;
top: 0;
height: 50px;
width: 100%;
padding: 0 10px;
z-index: 10;
user-select: none;
box-sizing: border-box;
pointer-events: none;
.info {
pointer-events: all;
.title {
font-size: 14px;
}
.desc {
color: var(--el-text-color-regular);
font-size: 10px;
}
}
.op {
display: flex;
pointer-events: all;
.item {
display: flex;
align-items: center;
justify-content: center;
height: 36px;
width: 36px;
border-radius: 8px;
background-color: var(--el-bg-color);
margin-left: 10px;
cursor: pointer;
.cl-svg {
font-size: 22px;
color: var(--el-color-info);
outline: none;
}
&:hover {
background-color: var(--el-fill-color-lighter);
.cl-svg {
color: var(--el-text-color-primary);
}
}
}
}
.publish {
padding: 5px;
.btn {
display: flex;
margin-top: 10px;
.a {
flex: 1;
}
}
}
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<div class="tools-history">
<el-tooltip content="上一步">
<cl-svg
name="prev"
:class="{
disabled: !isPrev
}"
@click="prev"
/>
</el-tooltip>
<el-tooltip content="下一步">
<cl-svg
name="next"
:class="{
disabled: !isNext
}"
@click="next"
/>
</el-tooltip>
</div>
</template>
<script setup lang="ts" name="tools-history ">
import { ref, onMounted, watch } from "vue";
import { useFlow } from "/$/flow/hooks";
import { debounce, cloneDeep } from "lodash-es";
import { computed } from "vue";
import { sleep } from "/@/cool/utils";
import { useCool } from "/@/cool";
import { onUnmounted } from "vue";
const { mitt } = useCool();
const flow = useFlow();
const list = ref<any[]>([]);
const active = ref(0);
//
const isPrev = computed(() => active.value > 0);
//
const isNext = computed(() => active.value < list.value.length - 1);
//
function prev() {
if (isPrev.value) {
active.value -= 1;
update();
}
}
//
function next() {
if (isNext.value) {
active.value += 1;
update();
}
}
let lock = false;
//
async function update() {
if (lock) {
return false;
}
const d = list.value[active.value];
if (d) {
lock = true;
await flow.restore(d);
await sleep(300);
lock = false;
}
}
//
const append = debounce(() => {
if (lock) {
return false;
}
list.value.splice(
active.value + 1,
list.value.length,
cloneDeep({
nodes: flow.nodes,
edges: flow.edges,
viewport: flow.viewport
})
);
active.value = list.value.length - 1;
}, 300);
// flow
const fns = ["addNodes", "updateNode", "removeNodes", "addEdge", "removeEdges", "clear", "arrange"];
onMounted(() => {
append();
fns.forEach((k) => {
mitt.on(`flow.${k}`, append);
});
});
onUnmounted(() => {
fns.forEach((k) => {
mitt.off(`flow.${k}`, append);
});
});
</script>
<style lang="scss" scoped>
.tools-history {
display: flex;
cursor: pointer;
.disabled {
color: var(--el-text-color-disabled) !important;
&:focus {
color: var(--el-text-color-disabled) !important;
}
}
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div
class="tools-icon"
:class="[`is-${name}`]"
:style="{
height: size + 'px',
width: size + 'px',
backgroundColor: color
}"
>
<cl-svg :name="name" />
</div>
</template>
<script lang="ts" setup name="tools-icon">
const props = defineProps({
name: String,
size: {
type: Number,
default: 20
},
color: String
});
</script>
<style lang="scss" scoped>
.tools-icon {
display: flex;
align-items: center;
justify-content: center;
border-radius: 100%;
background-color: var(--el-color-primary);
color: #fff;
font-size: 14px;
flex-shrink: 0;
&.is-add {
background-color: var(--el-color-info-light-5);
}
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<cl-upload
type="file"
multiple
:limit-size="limitSize"
:limit="limit"
:accept="accept"
:disabled="saving || disabled"
:auto-upload="false"
:before-upload="onUpload"
:size="[220, '100%']"
:showFileList="false"
>
<el-button type="success">
<cl-svg name="import" />
<span class="text">导入</span>
</el-button>
</cl-upload>
</template>
<script lang="ts" name="flow-tools-import" setup>
import { useCool } from "/@/cool";
import { ElMessage } from "element-plus";
import { reactive, type PropType, computed, ref } from "vue";
import { debounce } from "lodash-es";
const { service } = useCool();
const props = defineProps({
limit: {
type: Number,
default: 2
},
limitSize: {
type: Number,
default: 10
},
type: {
type: String as PropType<
"default" | "success" | "warning" | "info" | "text" | "primary" | "danger"
>,
default: "success"
},
icon: String,
disabled: Boolean,
accept: {
type: String,
default: ".json"
}
});
const emits = defineEmits(["change", "success"]);
//
const upload = reactive({
filename: "",
file: null as File | null,
list: [] as any[],
reset: () => {
upload.filename = "";
upload.file = null;
upload.list = [];
}
});
//
const saving = ref<boolean>(false);
//
function onUpload(raw: File, _: any, { next }: any) {
const reader = new FileReader();
reader.onload = (event: any) => {
let data = event.target.result;
try {
const result = JSON.parse(data);
upload.list.push(result);
upload.filename = raw.name;
upload.file = raw;
emits("change", result);
} catch (error) {
ElMessage.error("导入失败,请检查文件后重新上传");
}
};
reader.readAsText(raw, "UTF-8");
// reader.readAsBinaryString(raw);
next();
//
debouncedOnSubmit();
return false;
}
// onSubmit
const debouncedOnSubmit = debounce(onSubmit, 500); // 1000
//
function onSubmit() {
if (saving.value) return false;
if (upload.list.length > props.limit) {
// ElMessage.warning(`${props.limit}`);
return false;
}
saving.value = true;
service.flow.info
.add(upload.list)
.finally(() => {
saving.value = false;
upload.reset();
})
.then(() => {
ElMessage.success("导入成功");
emits("success");
})
.catch((err) => {
ElMessage.error(`导入失败:${err.message}`);
});
}
</script>
<style lang="scss" scoped>
.text {
margin-left: 6px;
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<el-popover
:ref="setRefs('popover')"
trigger="click"
width="200px"
placement="bottom-start"
popper-class="cl-flow__popper"
:offset="5"
>
<template #reference>
<slot></slot>
</template>
<div class="tools-more">
<div class="list">
<template v-for="(item, index) in list" :key="index">
<div class="item" @click="toCommand(item.value)" v-if="!item.hidden">
<span class="label">{{ item.label }}</span>
<span class="desc" v-if="item.desc">{{ item.desc }}</span>
</div>
</template>
</div>
</div>
</el-popover>
</template>
<script setup lang="ts" name="tools-more">
import { type PropType } from "vue";
import { useFlow } from "../../hooks";
import type { FlowNode } from "../../types";
import { useCool } from "/@/cool";
import { cloneDeep } from "lodash-es";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
}
});
const { refs, setRefs } = useCool();
const flow = useFlow();
const list = [
{
label: "克隆",
value: "clone"
},
{
label: "复制",
value: "copy"
},
{
label: "删除",
value: "remove",
desc: "Del",
hidden: ["start", "end"].includes(props.node.type!)
}
];
function toCommand(value: string) {
const node = cloneDeep(props.node);
switch (value) {
case "clone":
const { x = 0, y = 0 } = node.position || {};
flow.addNode(node.type!, {
...node,
position: {
x: x + 340,
y
}
});
break;
case "copy":
flow.setCopyNode(node);
break;
case "remove":
flow.removeNodes(node);
break;
}
refs.popover.hide();
}
</script>
<style lang="scss" scoped>
.tools-more {
.list {
.item {
display: flex;
align-items: center;
justify-content: space-between;
height: 30px;
cursor: pointer;
border-radius: 6px;
padding: 0 10px;
.label {
font-size: 12px;
}
.desc {
color: var(--el-color-info);
font-size: 10px;
}
&.active {
color: var(--el-color-primary);
}
&:hover {
background-color: var(--el-fill-color-light);
}
}
}
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div
class="tools-node-add__btn"
:class="{
expand: visible
}"
@click="visible = !visible"
>
<cl-svg name="add2"></cl-svg>
</div>
<div
class="tools-node-add"
:class="{
show: visible
}"
>
<el-scrollbar height="100%">
<div class="wrap">
<div class="group" v-for="a in list" :key="a.label">
<p class="label" v-if="a.label">{{ a.label }}</p>
<div class="list">
<div
class="cl-flow__tools-node-add-item item"
:id="`cl-flow__tools-node-add--${b.type}`"
v-for="b in a.children"
:key="b.label"
:data-type="b.type"
@mousedown="(e) => mousedown(e, b)"
>
<tools-icon :name="b.icon" :color="b.color" :size="30" />
<div class="det">
<p>{{ b.label }}</p>
<p>{{ b.description }}</p>
</div>
</div>
</div>
</div>
<div class="empty" v-if="isEmpty(list)">未找到匹配项</div>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts" name="tools-node-add">
import { PropType, computed, reactive, ref } from "vue";
import { useFlow } from "../../hooks";
import { isEmpty } from "lodash-es";
import { useCool } from "/@/cool";
import { sleep } from "/@/cool/utils";
import type { FlowNode } from "../../types";
import ToolsIcon from "./icon.vue";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
}
});
const emit = defineEmits(["select", "hide"]);
const { mitt } = useCool();
const flow = useFlow();
//
const keyWord = ref("");
//
const visible = ref(false);
//
const list = computed(() => flow.getGroup(keyWord.value));
//
async function select(node: FlowNode) {
//
const source = props.node;
//
let target: FlowNode | undefined;
//
const arr = flow.nodes.map((e) => {
return (
(e.position?.y || 0) +
(document.querySelector(`div[data-id="${e.id}"]`)?.clientHeight || 0)
);
});
const y = Math.max(...arr.concat(0)) + flow.offset.g;
target = flow.addNode(node.type!, {
position: {
x: 100,
y
}
});
//
flow.setNode(target);
//
flow.setViewportByNode(target!);
//
// await sleep(100);
// mitt.emit("flow.openForm", target);
emit("select", { source, target });
}
//
const drag = reactive({
startX: 0,
startY: 0,
el: null as HTMLElement | null
});
function mousedown(e: MouseEvent, node: FlowNode) {
const el = document.getElementById(`cl-flow__tools-node-add--${node.type}`);
if (el) {
const elRect = el.getBoundingClientRect();
const wRect = document.querySelector(".cl-flow .tools-node-add")?.getBoundingClientRect();
if (wRect && elRect) {
drag.startX = e.pageX - elRect.left + wRect.left - 10;
drag.startY = e.pageY - elRect.top + wRect.top - 55;
}
drag.el = el.cloneNode(true) as HTMLElement;
}
document.addEventListener("mousemove", mousemove);
document.addEventListener("mouseup", mouseup);
}
function mousemove(e: MouseEvent) {
if (drag.el) {
if (!drag.el.className.includes("is-drag")) {
drag.el.className += " is-drag";
document.querySelector(".cl-flow .vue-flow")?.appendChild(drag.el);
}
drag.el.style.left = `${e.pageX - drag.startX}px`;
drag.el.style.top = `${e.pageY - drag.startY}px`;
}
}
function mouseup(e: MouseEvent) {
if (drag.el) {
if (drag.el.className.includes("is-drag")) {
const [x, y] = flow.viewPx(e.pageX - drag.startX, e.pageY - drag.startY);
flow.addNode(drag.el.dataset.type!, {
position: {
x,
y
}
});
drag.el.remove();
}
}
document.removeEventListener("mousemove", mousemove);
document.removeEventListener("mouseup", mouseup);
}
</script>
<style lang="scss">
.cl-flow__tools-node-add-item {
user-select: none;
display: flex;
align-items: center;
cursor: pointer;
padding: 10px;
border-radius: 6px;
width: 250px;
box-sizing: border-box;
.tools-icon {
font-size: 18px;
}
.det {
margin-left: 10px;
p {
font-size: 13px;
color: var(--el-text-color-regular);
line-height: 1.5;
&:last-child {
font-size: 10px;
color: var(--el-color-info);
}
}
}
&.is-drag {
position: absolute;
background-color: var(--el-bg-color);
z-index: 99;
box-shadow: 0px 0 10px 1px rgba(16, 24, 40, 0.05);
}
&:not(.is-drag):hover {
background-color: var(--el-fill-color-light);
}
}
</style>
<style lang="scss" scoped>
.tools-node-add {
position: absolute;
left: 10px;
top: 50px;
z-index: 10;
background-color: var(--el-bg-color);
border-radius: 6px;
height: calc(100% - 50px);
width: 260px;
transform: translateX(-400px);
transition: all 0.3s;
box-shadow: 0px 0 10px 1px rgba(16, 24, 40, 0.05);
.wrap {
padding: 5px 0;
.group {
line-height: 1;
padding: 0 5px;
.label {
color: var(--el-text-color-secondary);
font-size: 12px;
padding: 10px;
}
}
.empty {
padding: 10px 15px;
line-height: 1;
font-size: 12px;
color: var(--el-text-color-secondary);
text-align: center;
}
}
&.show {
transform: translateX(0);
}
&__btn {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 10px;
top: 55px;
font-size: 30px;
color: var(--el-color-primary);
border-radius: 100%;
z-index: 11;
cursor: pointer;
transition: transform 0.3s;
&:hover {
color: var(--el-color-primary-light-1);
}
&.expand {
transform: translate(225px, 0) rotate(225deg);
}
}
}
</style>

View File

@ -0,0 +1,358 @@
<template>
<cl-dialog
v-model="visible"
title="配置"
height="700px"
width="1200px"
padding="0"
:scrollbar="false"
>
<cl-view-group ref="ViewGroup">
<template #item="{ item, selected }">
<div
class="node-item"
:class="{
'is-active': selected?.type == item.type
}"
>
<span>{{ item.title }}</span>
<cl-svg :name="item.type" />
</div>
</template>
<template #right>
<div class="opbar">
<el-button type="success" size="small" @click="Crud?.rowAdd()">添加</el-button>
</div>
<cl-crud ref="Crud" padding="0 10px">
<el-row :gutter="10">
<el-col :span="8" :xs="24" v-for="(item, index) in list" :key="index">
<div class="data-item">
<div class="head">
<span class="name">{{ item.name }}</span>
<cl-svg name="delete" @click="Crud?.rowDelete(item)" />
<cl-svg name="set" @click="Crud?.rowEdit(item)" />
</div>
<div class="content">
<el-text size="small" type="info" :line-clamp="4">
{{ item.description || "暂无描述" }}
</el-text>
</div>
</div>
</el-col>
</el-row>
<cl-upsert ref="Upsert">
<template #slot-options="{ scope }">
<cl-editor-monaco v-model="scope.options" />
<el-tooltip content="重置">
<cl-svg name="change" class="options-btn" @click="reset" />
</el-tooltip>
</template>
</cl-upsert>
<div class="empty" v-if="isEmpty(list)">
<el-empty :image-size="100" />
</div>
</cl-crud>
</template>
</cl-view-group>
</cl-dialog>
</template>
<script setup lang="ts" name="tools-set-node-config">
import { ref } from "vue";
import { useCool } from "/@/cool";
import { useViewGroup } from "/@/plugins/view";
import { useCrud, useUpsert } from "@cool-vue/crud";
import { isEmpty, keys } from "lodash-es";
const { service } = useCool();
//
const list = ref<Eps.FlowConfigEntity>([]);
//
const config = ref({});
//
const { ViewGroup } = useViewGroup({
enableAdd: false,
enableContextMenu: false,
enableKeySearch: false,
label: "节点",
title: "配置",
service: {
async page(params) {
return service.flow.config.all(params).then((res) => {
return {
list: res.map((e) => {
return {
...e,
name: e.title
};
}),
pagination: {
total: res.length,
page: 1,
size: 100
}
};
});
}
},
onSelect(item) {
refresh({
node: item.type
});
}
});
// Crud
const Crud = useCrud({
service: service.flow.config,
async onRefresh(params, { next }) {
const res = await next({
...params,
size: 1000
});
list.value = res.list;
}
});
// Upsert
const Upsert = useUpsert({
dialog: {
width: "800px"
},
props: {
labelPosition: "top"
},
items: [
{
label: "类型",
prop: "type",
component: {
name: "el-select",
options: [],
props: {
onChange(val: string) {
Upsert.value?.setForm(
"options",
JSON.stringify(config.value[val], null, 4)
);
}
}
},
span: 8,
required: true
},
{
label: "名称",
prop: "name",
component: {
name: "el-input"
},
span: 16,
required: true
},
{
label: "描述",
prop: "description",
component: {
name: "el-input",
props: {
type: "textarea",
rows: 3
}
}
},
{
label: "配置",
prop: "options",
component: {
name: "slot-options"
}
}
],
onOpen() {
service.flow.config
.config({
node: ViewGroup.value?.selected?.type
})
.then((res) => {
config.value = res;
//
const types = keys(res).map((e) => {
return {
label: e,
value: e
};
});
//
Upsert.value?.setOptions("type", types);
//
if (Upsert.value?.mode == "add") {
const type = types[0]?.value;
if (type) {
Upsert.value?.setForm("type", type);
Upsert.value?.setForm("options", config.value[type]);
}
}
});
},
onSubmit(data, { next }) {
next({
...data,
node: ViewGroup.value?.selected?.type
});
}
});
//
function refresh(params?: any) {
Crud.value?.refresh(params);
}
//
function reset() {
const type = Upsert.value?.getForm("type");
Upsert.value?.setForm("options", JSON.stringify(config.value[type], null, 4));
}
//
const visible = ref(false);
//
function open() {
visible.value = true;
}
//
function close() {
visible.value = false;
}
defineExpose({
open,
close
});
</script>
<style lang="scss" scoped>
.node-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 10px;
margin: 0 10px;
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
.cl-svg {
font-size: 20px;
color: var(--el-color-info);
}
&.is-active,
&:hover {
background-color: var(--el-bg-color-page);
}
}
.data-item {
display: flex;
flex-direction: column;
height: 120px;
width: 100%;
border-radius: 10px;
box-sizing: border-box;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
position: relative;
padding: 10px;
border: 1px solid var(--el-bg-color-page);
.head {
display: flex;
align-items: center;
margin-bottom: 5px;
.name {
font-size: 14px;
font-weight: bold;
margin-right: auto;
}
.cl-svg {
padding: 3px;
border-radius: 6px;
font-size: 16px;
color: var(--el-text-color-regular);
margin-left: 5px;
&:hover {
background-color: var(--el-fill-color-light);
color: var(--el-text-color-primary);
}
}
}
.content {
flex: 1;
}
&:hover {
box-shadow: 0 25px 40px -25px var(--el-bg-color-page);
}
}
.opbar {
position: absolute;
right: 12px;
top: 8px;
.el-button {
border-radius: 4px;
}
}
.empty {
margin-top: 100px;
}
.options-btn {
position: absolute;
right: 0;
top: -26px;
color: var(--el-text-color-regular);
cursor: pointer;
border-radius: 6px;
font-size: 14px;
padding: 4px;
flex-shrink: 0;
position: absolute;
top: -30px;
right: 0;
outline: none;
&:hover {
background-color: var(--el-fill-color-light);
color: var(--el-text-color-primary);
}
}
</style>

View File

@ -0,0 +1,357 @@
<template>
<el-popover
:ref="setRefs('popover')"
:disabled="disabled"
trigger="click"
:persistent="false"
placement="right-start"
width="280px"
popper-class="cl-flow__popper not-padding"
:offset="5"
@show="onShow"
@hide="onHide"
>
<template #reference>
<slot></slot>
</template>
<div class="tools-nodes">
<div class="search">
<el-input
v-model="keyWord"
:prefix-icon="Search"
placeholder="搜索节点"
clearable
/>
</div>
<el-scrollbar :max-height="400">
<div class="wrap">
<div class="group" v-for="a in list" :key="a.label">
<p class="label" v-if="a.label">{{ a.label }}</p>
<div class="list">
<el-popover
placement="right"
width="200px"
:hide-after="0"
:popper-style="{
padding: '0px'
}"
:persistent="false"
:offset="10"
popper-class="cl-flow__popper"
v-for="b in a.children"
:key="b.label"
>
<template #reference>
<div class="item" @click="select(b)">
<tools-icon :name="b.icon" :color="b.color" :size="22" />
<span>{{ b.label }}</span>
</div>
</template>
<div class="tools-nodes__description">
<div class="inner">
<tools-icon :name="b.icon" :color="b.color" :size="22" />
<span>{{ b.label }}</span>
</div>
<p class="desc">{{ b.description || "暂无描述" }}</p>
</div>
</el-popover>
</div>
</div>
<div class="empty" v-if="isEmpty(list)">未找到匹配项</div>
</div>
</el-scrollbar>
</div>
</el-popover>
</template>
<script setup lang="ts" name="tools-nodes">
import { Search } from "@element-plus/icons-vue";
import { PropType, computed, ref } from "vue";
import { useFlow } from "../../hooks";
import { isEmpty } from "lodash-es";
import { useCool } from "/@/cool";
import { sleep } from "/@/cool/utils";
import type { FlowNode } from "../../types";
import ToolsIcon from "./icon.vue";
const props = defineProps({
node: {
type: Object as PropType<FlowNode>,
default: () => ({})
},
handle: String,
edgeId: String,
isChange: Boolean,
isInsert: Boolean,
isAutoInsert: Boolean,
disabled: Boolean
});
const emit = defineEmits(["select", "hide"]);
const { refs, setRefs, mitt } = useCool();
const flow = useFlow();
//
const visible = ref(false);
//
const keyWord = ref("");
//
const list = computed(() => flow.getGroup(keyWord.value));
//
async function select(node: FlowNode) {
//
const source = props.node;
//
let target: FlowNode | undefined;
//
if (props.isChange) {
if (node) {
flow.updateNode(source.id!, {
...node,
position: source.position
});
}
target = flow.findNode(source.id!);
}
//
else if (props.isAutoInsert) {
const arr = flow.nodes.map((e) => {
return (
(e.position?.y || 0) +
(document.querySelector(`div[data-id="${e.id}"]`)?.clientHeight || 0)
);
});
const y = Math.max(...arr.concat(0)) + flow.offset.g;
target = flow.addNode(node.type!, {
position: {
x: 100,
y
}
});
}
//
else if (props.isInsert) {
//
const children = flow.childrenNodes(source.id!);
//
const allChildren = flow.childrenAllNodes(source.id!, props.handle);
// 线
const edge = flow.findEdge(props.edgeId!);
//
if (edge) {
flow.removeEdges(edge);
}
//
target = flow.insertNode(node.type!, source, {});
if (target) {
const h = target.handle || {};
//
if (!h.next || h.next?.length == 1) {
//
flow.addEdge({
source: source!.id!,
target: target.id!,
sourceHandle: edge?.sourceHandle || "source",
targetHandle: "target"
});
if (h.source !== false) {
//
children.forEach((e) => {
if (edge?.target == e.id) {
flow.addEdge({
source: target!.id!,
target: e!.id!,
sourceHandle: h.next?.[0]?.value || "source",
targetHandle: "target"
});
}
});
}
}
}
//
allChildren.forEach((e) => {
flow.updateNode(e!.id!, {
position: {
x: (e?.position?.x || 0) + flow.offset.x,
y: e?.position?.y
}
});
});
} else {
//
if (props.handle == "target") {
target = flow.addNode(node.type!, {
position: source.position
});
//
const allChildren = [source, ...flow.childrenAllNodes(source.id!)];
//
if (!target?.handle?.next || target?.handle?.next?.length == 1) {
//
const sourceHandle = target?.handle?.next?.[0]?.value || "source";
//
flow.addEdge({
source: target!.id!,
target: source!.id!,
sourceHandle,
targetHandle: "target"
});
}
//
allChildren.forEach((e) => {
flow.updateNode(e!.id!, {
position: {
x: (e?.position?.x || 0) + flow.offset.x,
y: e?.position?.y
}
});
});
}
//
else {
target = flow.insertNode(node.type!, source);
flow.addEdge({
source: source!.id!,
target: target!.id!,
sourceHandle: props.handle || "source",
targetHandle: "target"
});
}
}
//
flow.setNode(target);
//
flow.setViewportByNode(target!);
//
refs.popover.hide();
//
// await sleep(100);
// mitt.emit("flow.openForm", target);
emit("select", { source, target });
}
function onShow() {
visible.value = true;
}
function onHide() {
visible.value = false;
emit("hide");
}
defineExpose({
visible
});
</script>
<style lang="scss" scoped>
.tools-nodes {
.search {
padding: 5px;
:deep(.el-input__wrapper) {
box-shadow: none;
background-color: var(--el-fill-color-light);
}
}
.wrap {
padding: 5px 0;
.group {
line-height: 1;
padding: 0 5px;
.label {
color: var(--el-text-color-secondary);
font-size: 12px;
padding: 10px;
}
.list {
.item {
display: flex;
align-items: center;
cursor: pointer;
padding: 10px;
border-radius: 6px;
span {
font-size: 13px;
margin-left: 8px;
color: var(--el-text-color-regular);
}
&:hover {
background-color: var(--el-fill-color-light);
}
}
}
}
.empty {
padding: 10px 15px;
line-height: 1;
font-size: 12px;
color: var(--el-text-color-secondary);
text-align: center;
}
}
}
.tools-nodes__description {
.inner {
display: flex;
align-items: center;
cursor: pointer;
padding: 10px;
border-radius: 6px;
span {
font-size: 13px;
margin-left: 8px;
color: var(--el-text-color-regular);
}
}
.desc {
font-size: 12px;
padding: 0 10px 10px 10px;
}
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<panel position="top-right" class="tools-panel-right">
<panel-run />
</panel>
</template>
<script setup lang="ts" name="tools-panel">
import { Panel } from "@vue-flow/core";
import PanelRun from "./run.vue";
</script>
<style lang="scss" scoped>
.vue-flow__panel {
display: flex;
&.right {
height: calc(100% - 50px);
z-index: 10;
margin: 0 10px 0 0;
margin-top: 50px;
& > div[class^="tools-panel"] {
position: relative;
background-color: var(--el-bg-color);
height: 100%;
border-radius: 10px;
box-sizing: border-box;
box-shadow: 0px 0 10px 1px rgba(16, 24, 40, 0.05);
margin-left: 10px;
:deep(.inner-item) {
display: flex;
align-items: center;
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
padding: 0 10px;
cursor: pointer;
height: 32px;
width: 100%;
position: relative;
transition: all 0.2s;
box-sizing: border-box;
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 20px;
}
.placeholder {
color: var(--el-text-color-placeholder);
font-size: 14px;
}
.close {
position: absolute;
right: 6px;
display: none;
font-size: 12px !important;
color: var(--el-color-info);
}
&:hover {
border-color: var(--el-border-color-hover);
.close {
display: block;
}
}
}
:deep(.textarea-item) {
border: 1px solid var(--el-border-color);
padding: 0 0 8px 0;
border-radius: 8px;
margin-bottom: 10px;
.el-textarea__inner {
background-color: transparent;
box-shadow: none;
padding: 0 10px;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
height: 30px;
line-height: normal;
span {
font-size: 12px;
color: var(--el-color-info);
}
}
&:last-child {
margin-bottom: 0;
}
}
:deep(.btn-icon) {
color: var(--el-text-color-regular);
cursor: pointer;
border-radius: 6px;
font-size: 14px;
padding: 4px;
flex-shrink: 0;
outline: none;
&:focus,
&:hover {
background-color: var(--el-fill-color-light);
color: var(--el-text-color-primary);
}
&.is-rt {
position: absolute;
top: -30px;
right: 0;
}
&.is-bg {
background-color: var(--el-fill-color-lighter);
&:hover {
color: var(--el-color-primary) !important;
}
}
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,289 @@
<template>
<div
class="mouse-selection"
:style="{
height: mouse.height + 'px',
width: mouse.width + 'px',
left: mouse.left + 'px',
top: mouse.top + 'px'
}"
v-show="mouse.show"
></div>
<div
class="node-selection"
:style="[boxStyle]"
@mousedown="selection.onmousedown"
v-show="box.show"
></div>
</template>
<script setup lang="ts" name="node-selection">
import { computed, onMounted, onUnmounted, reactive } from "vue";
import { useFlow } from "../../hooks";
import { FlowNode } from "../../types";
import { isEmpty } from "lodash-es";
const flow = useFlow();
const mouse = reactive({
show: false,
startX: 0,
startY: 0,
endX: 0,
endY: 0,
x: 0,
y: 0,
left: 0,
top: 0,
width: 0,
height: 0
});
const box = reactive({
show: false,
left: 0,
top: 0,
width: 0,
height: 0
});
const boxStyle = computed(() => {
const { x, y } = flow.viewport;
return {
height: scale(box.height) + "px",
width: scale(box.width) + "px",
left: scale(box.left) + x + "px",
top: scale(box.top) + y + "px"
};
});
function scale(v: number) {
const { zoom } = flow.viewport;
return v * zoom;
}
function clear() {
mouse.show = false;
mouse.height = 0;
mouse.width = 0;
mouse.endX = 0;
mouse.endY = 0;
box.show = false;
box.height = 0;
box.width = 0;
selection.clear();
}
function onmousedown(e: MouseEvent) {
clear();
const el = e.target as HTMLElement;
if (el.nodeName != "DIV") {
return false;
}
if (!el.className?.includes("vue-flow__container")) {
return false;
}
if (flow.controlMode == "pointer") {
mouse.show = true;
mouse.x = e.offsetX;
mouse.y = e.offsetY;
mouse.left = mouse.x;
mouse.top = mouse.y;
mouse.startX = e.pageX;
mouse.startY = e.pageY;
document.addEventListener("mousemove", mousemove);
document.addEventListener("mouseup", onmouseup);
}
}
function mousemove(e: MouseEvent) {
if (mouse.show) {
mouse.width = Math.abs(e.pageX - mouse.startX);
mouse.height = Math.abs(e.pageY - mouse.startY);
if (e.pageY < mouse.startY) {
mouse.top = mouse.y - mouse.height;
} else {
mouse.top = mouse.y;
}
if (e.pageX < mouse.startX) {
mouse.left = mouse.x - mouse.width;
} else {
mouse.left = mouse.x;
}
mouse.endY = mouse.top;
mouse.endX = mouse.left;
}
}
function onmouseup() {
const { x: vx, y: vy } = flow.viewport;
let x1 = 0;
let x2 = 0;
let y1 = 0;
let y2 = 0;
let isSelected = false;
flow.nodes.forEach((e) => {
const { x = 0, y = 0 } = e.position || {};
//
const nodeEl = document.querySelector(`div[data-id="${e.id}"]`);
const h = nodeEl?.clientHeight || 0;
const w = nodeEl?.clientWidth || 0;
//
if (
nodeEl &&
mouse.endX < scale(x + w) + vx &&
mouse.endX + mouse.width > scale(x) + vx &&
mouse.endY < scale(y + h) + vy &&
mouse.endY + mouse.height > scale(y) + vy
) {
isSelected = true;
//
selection.list.push(e);
if (!x1 || x1 > x) {
x1 = x;
}
if (!y1 || y1 > y) {
y1 = y;
}
if (!x2 || x2 < x + w) {
x2 = x + w;
}
if (!y2 || y2 < y + h) {
y2 = y + h;
}
}
});
// y1 -= 36;
//
if (isSelected) {
box.show = true;
box.left = x1;
box.top = y1;
box.width = x2 - x1;
box.height = y2 - y1;
}
mouse.show = false;
document.removeEventListener("mousemove", mousemove);
document.removeEventListener("mouseup", onmouseup);
}
const selection = reactive({
list: [] as FlowNode[],
startX: 0,
startY: 0,
x: 0,
y: 0,
lock: false,
clear() {
selection.list = [];
},
onmousedown(e: MouseEvent) {
e.stopPropagation();
selection.lock = true;
selection.startX = e.pageX;
selection.startY = e.pageY;
document.addEventListener("mouseup", selection.onmouseup);
document.addEventListener("mousemove", selection.onmousemove);
},
onmousemove(e: MouseEvent) {
if (selection.lock) {
const { zoom } = flow.viewport;
const x = (e.pageX - selection.startX) / zoom;
const y = (e.pageY - selection.startY) / zoom;
box.left += x;
box.top += y;
selection.list.forEach((e) => {
flow.updateNode(e.id!, {
position: {
x: (e.position?.x || 0) + x,
y: (e.position?.y || 0) + y
}
});
});
selection.startX = e.pageX;
selection.startY = e.pageY;
}
},
onmouseup() {
selection.lock = false;
document.removeEventListener("mouseup", selection.onmouseup);
document.removeEventListener("mousemove", selection.onmousemove);
},
onkeydown(e: KeyboardEvent) {
if (e.key == "Delete") {
if (!isEmpty(selection.list)) {
flow.removeNodes(selection.list);
} else {
if (flow.node) {
flow.removeNodes(flow.node);
}
}
clear();
}
}
});
onMounted(() => {
document.getElementById("cl-flow")?.addEventListener("mousedown", onmousedown);
document.addEventListener("keydown", selection.onkeydown);
});
onUnmounted(() => {
document.getElementById("cl-flow")?.removeEventListener("mousedown", onmousedown);
document.removeEventListener("keydown", selection.onkeydown);
});
</script>
<style lang="scss" scoped>
.mouse-selection,
.node-selection {
position: absolute;
border: 1px solid var(--el-color-primary);
background-color: rgba(var(--el-color-primary-rgb), 0.05);
z-index: 9;
box-sizing: border-box;
}
.node-selection {
cursor: move;
}
</style>

View File

@ -0,0 +1,466 @@
<template>
<el-popover
:ref="setRefs('popover')"
trigger="click"
width="220px"
placement="bottom-start"
popper-class="cl-flow__popper"
:popper-style="{
padding: '0'
}"
:offset="5"
@show="onShow"
@hide="onHide"
>
<template #reference>
<div class="inner-item" v-if="showPicker">
<el-tooltip content="自定义输入" v-if="inputable">
<cl-svg
name="t"
class="btn-icon is-bg"
:style="{ margin: '0 5px 0 -5px' }"
@click.stop="input.open"
/>
</el-tooltip>
<span class="text" v-if="text">{{ text }}</span>
<span class="placeholder" v-else>{{ value ? "输入值" : "选择变量" }}</span>
<cl-svg name="close" class="btn-icon close" @click.stop="clear" v-if="text" />
</div>
<div class="tools-var__btn" :ref="setRefs('btn')" :style="{ position }" v-else></div>
</template>
<div class="tools-var">
<div class="search">
<el-input v-model="keyWord" placeholder="搜索变量" :prefix-icon="Search" />
</div>
<el-scrollbar max-height="500px">
<div
class="list"
:ref="setRefs('list')"
:tabindex="0"
@keydown.stop.prevent="onKeyDown"
>
<div class="group" v-for="(item, index) in list" :key="index">
<p class="label">{{ item.label }}</p>
<div
class="item"
v-for="param in item.params"
:key="param.field"
:class="{
active: `${item.id}_${param.field}` == `${nodeId}_${field}`,
focus: focusKey == `${item.id}_${param.field}`
}"
@click="select(param, item)"
>
<cl-svg class="icon" name="var" />
<span class="value">{{ param.field }}</span>
<span class="type">{{ param.type }}</span>
</div>
</div>
</div>
<div class="empty" v-if="isEmpty(list)">未找到匹配项</div>
</el-scrollbar>
</div>
<!-- 自定义输入 -->
<cl-dialog v-model="input.visible" title="自定义输入">
<el-input
type="textarea"
v-model="input.value"
:rows="20"
placeholder="请输入"
@change="input.onChange"
/>
<template #footer>
<el-button @click="input.close">取消</el-button>
<el-button type="success" @click="input.save">保存</el-button>
</template>
</cl-dialog>
</el-popover>
</template>
<script setup lang="ts" name="tools-var">
import { computed, ref, useModel, reactive, PropType } from "vue";
import { useFlow } from "../../hooks";
import { isEmpty } from "lodash-es";
import { Search } from "@element-plus/icons-vue";
import type { FlowField } from "../../types";
import { useCool } from "/@/cool";
import { ElMessage } from "element-plus";
const props = defineProps({
modelValue: {
type: String,
default: ""
},
nodeId: {
type: String,
default: ""
},
//
value: {
type: String,
default: ""
},
//
onlySelect: Boolean,
//
useInputParams: Boolean,
//
inputable: Boolean,
//
showPicker: {
type: Boolean,
default: true
},
//
showSearch: {
type: Boolean,
default: true
},
//
autofocus: {
type: Boolean,
default: true
},
//
position: {
type: String as PropType<"fixed" | "relative" | "absolute">,
default: "fixed"
}
});
const emit = defineEmits([
"update:modelValue",
"update:nodeId",
"update:nodeType",
"update:value",
"update:type",
"select"
]);
const { refs, setRefs } = useCool();
const flow = useFlow();
//
const field = useModel(props, "modelValue");
//
const nodeId = useModel(props, "nodeId");
//
const value = useModel(props, "value");
//
const keyWord = ref("");
//
const visible = ref(false);
//
const es = {
onSelect: (() => {}) as (item: FlowField) => void
};
//
const list = computed(() => {
const arr = flow.parentAllNodes(flow.node!.id!).map((e) => {
let params: FlowField[] = [];
if (e.type == "start") {
params = e.data?.inputParams || [];
} else {
params = e.data?.outputParams || [];
}
return {
id: e.id,
type: e.type,
label: e.label,
params
};
});
if (props.useInputParams) {
if (flow.node) {
const { id, type, label, data } = flow.node;
arr.splice(0, arr.length, {
id,
type,
label,
params: (data?.inputParams || []).filter((e) => e.nodeId)
});
}
}
return arr.filter((e) => {
return !isEmpty(e.params) && (keyWord.value ? e.label?.includes(keyWord.value) : true);
});
});
//
const text = computed(() => {
const node = list.value.find(
(e) => e.id == props.nodeId && e.params.find((p) => p.field == field.value)
);
return value.value || (node ? `${node?.label} / ${field.value}` : "");
});
//
function select(param: FlowField, node: any) {
close();
const d = {
...param,
nodeType: node.type,
nodeId: node.id,
nodeLabel: node.label,
value: ""
};
if (props.onlySelect) {
es.onSelect(d);
} else {
field.value = d.field;
emit("update:nodeType", d.nodeType);
emit("update:nodeId", d.nodeId);
emit("update:type", d.type);
emit("update:value", d.value);
}
emit("select", d);
}
//
function clear() {
field.value = "";
value.value = "";
emit("update:nodeType", "");
emit("update:nodeId", "");
emit("update:type", "");
emit("update:value", "");
}
//
function open(options: { rect?: { top: number; left: number }; onSelect?: () => void } = {}) {
if (options.rect) {
const { top, left } = options.rect || {};
refs.btn.style.top = `${top}px`;
refs.btn.style.left = `${left}px`;
}
if (options.onSelect) {
es.onSelect = options.onSelect;
}
if (!visible.value) {
refs.btn.click();
}
}
//
function close() {
refs.popover.hide();
}
//
const focusKey = ref("");
function focus(e?: KeyboardEvent) {
refs.list.focus();
if (e) {
onKeyDown(e);
}
}
//
const input = reactive({
visible: false,
value: "",
open() {
input.value = value.value;
input.visible = true;
},
close() {
input.visible = false;
},
save() {
if (!input.value) {
return ElMessage.warning("请输入内容");
}
value.value = input.value;
input.close();
},
onChange() {
field.value = "";
emit("update:nodeType", "");
emit("update:nodeId", "");
emit("update:type", "");
}
});
//
function onKeyDown(e: KeyboardEvent) {
const arr = list.value
.map((e) => {
return e.params.map((p) => {
return {
...p,
node: e
};
});
})
.flat();
let index = 0;
const current = arr.findIndex((e) => focusKey.value == `${e.node.id}_${e.field}`);
switch (e.key) {
case "ArrowUp":
index = current - 1;
break;
case "ArrowDown":
index = current + 1;
break;
case "Enter":
select(arr[current], arr[current].node);
break;
}
const d = arr[index];
if (d) {
focusKey.value = `${d.node.id}_${d.field}`;
}
}
//
function onShow() {
if (props.autofocus) {
focus();
}
visible.value = true;
}
//
function onHide() {
visible.value = false;
focusKey.value = "";
}
defineExpose({
open,
close,
focus
});
</script>
<style lang="scss" scoped>
.tools-var {
padding-bottom: 5px;
.list {
outline: none;
.group {
padding: 0 5px;
.label {
display: flex;
align-items: center;
color: var(--el-text-color-secondary);
font-size: 12px;
height: 30px;
padding: 0 10px;
}
.item {
display: flex;
align-items: center;
height: 30px;
padding: 0 10px;
cursor: pointer;
border-radius: 6px;
font-size: 12px;
.icon {
height: 18px;
width: 18px;
flex-shrink: 0;
}
.value {
flex: 1;
margin: 0 5px;
}
.type {
color: var(--el-color-info);
max-width: 80px;
}
.value,
.type {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&.active {
color: var(--el-color-primary);
}
&:hover,
&.focus {
background-color: var(--el-fill-color-lighter);
}
}
}
}
.empty {
padding: 10px 0 5px 0;
color: var(--el-text-color-placeholder);
font-size: 12px;
text-align: center;
}
.search {
border-bottom: 1px solid var(--el-fill-color-light);
padding: 5px;
:deep(.el-input__wrapper) {
box-shadow: none;
background-color: var(--el-fill-color-light);
}
&:last-child {
border-bottom: 0;
margin-bottom: -5px;
}
}
&__btn {
height: 16px;
width: 4px;
// position: fixed;
}
}
</style>

View File

@ -0,0 +1,6 @@
import { ModuleConfig } from "/@/cool";
import "./static/index.scss";
export default (): ModuleConfig => {
return {};
};

View File

@ -0,0 +1,917 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { sleep } from "/@/cool/utils";
import { Connection, useVueFlow } from "@vue-flow/core";
import { CustomNodes } from "../components/nodes";
import {
assign,
cloneDeep,
debounce,
groupBy,
isArray,
isEmpty,
keys,
last,
orderBy
} from "lodash-es";
import type { FlowEdge, FlowField, FlowNode } from "../types";
import { router, service, useCool } from "/@/cool";
import { ElMessage, ElMessageBox } from "element-plus";
const offset = {
x: 400,
y_t: -10,
y_b: 50,
g: 150
};
let id = 1;
let req: Promise<any>;
export const useFlow = () => {
const vueFlow = useVueFlow();
const { mitt } = useCool();
const store = defineStore(`flow-${vueFlow.id}`, () => {
// 所有节点
const nodes = computed(() => vueFlow.nodes.value as FlowNode[]);
// 所有线
const edges = computed(() => vueFlow.edges.value as FlowEdge[]);
// 当前选中的节点
const node = ref<FlowNode>();
// 选中节点
function setNode(data: any) {
node.value = data;
mitt.emit("flow.setNode", node.value);
}
// 清空节点
function clearNode() {
mitt.emit("flow.clearNode");
setTimeout(() => {
node.value = undefined;
}, 0);
}
// 添加节点
function addNode(type: string, options?: FlowNode) {
const item = CustomNodes.value.find((e) => e.type == type);
const data = {
type,
data: {
inputParams: [],
outputParams: [],
options: {}
},
position: {
x: 0,
y: 0
},
...item,
...options,
id: String(id++)
};
vueFlow.addNodes(cloneDeep(data));
return findNode(data.id);
}
// 插入节点,自动计算位置
function insertNode(type: string, source: FlowNode, options?: FlowNode) {
if (source) {
return addNode(type, {
position: calcPosition(source, undefined),
...options
});
} else {
return;
}
}
// 更新节点
function updateNode(id: string, data: any) {
const node = findNode(id);
if (node) {
// 不同类型,清空相关连线
if (data.name && data.name != node.name) {
removeEdgeByNodeId(id);
["handle", "data", "form"].forEach((k) => {
if (!data[k]) {
data[k] = {};
}
});
}
assign(node, data);
vueFlow.updateNode(id, data);
}
}
// 移除节点
function removeNodes(nodes: any[] | any, force?: boolean) {
// start 节点不能删除,除非 force 强制删除
const vals = (isArray(nodes) ? nodes : [nodes]).filter((e) =>
force ? true : e.type != "start"
);
// 移除节点
vueFlow.removeNodes(vals);
// 移除连线
vals.forEach((e) => {
removeEdgeByNodeId(e.id);
// 如果是当前选中节点,则清空
if (e.id == node.value?.id) {
clearNode();
}
});
}
// 搜索节点
function findNode(id: string) {
return nodes.value.find((n) => n.id === id) as FlowNode;
}
// 搜索节点
function findNodeByType(type: string) {
return nodes.value.find((n) => n.type === type) as FlowNode;
}
// 叶子节点
function leafNodes(id: string) {
return childrenAllNodes(id).filter((e) => isEmpty(childrenNodes(e.id!)));
}
// 兄弟节点
function slibingNodes(id: string, hasOwn: boolean = true) {
// 找上级节点
const pNode = parentNode(id);
// 找同级
return edges.value
.filter((e) => e.source == pNode?.id)
.map((e) => findNode(e.target))
.filter((e) => (hasOwn ? true : e.id != id));
}
// 父节点
function parentNode(id: string) {
const edge = edges.value.find((e) => e.target == id);
return edge ? findNode(edge.source!) : null;
}
// 所有父节点
function parentAllNodes(id: string) {
const nodes: FlowNode[] = [];
function next(id: string) {
const pNode = parentNode(id);
if (pNode) {
nodes.push(pNode);
next(pNode.id!);
}
}
next(id);
return nodes;
}
// 子节点
function childrenNodes(id: string, handle?: string): FlowNode[] {
const node = findNode(id);
// 下级连接线
const childrenEdges = edges.value.filter((e) => e.source == id);
// 下级连接点
const handles = node?.handle?.next || [{ value: "source" }];
// 根据连接点顺序返回
return handles
.filter((e) => {
return handle ? e.value == handle : true;
})
.map((a: { value: string }) => {
const edge = childrenEdges.find((e) => e.sourceHandle == a.value);
return (edge ? findNode(edge.target!) : null)!;
})
.filter((e) => !!e);
}
// 子所有节点
function childrenAllNodes(id: string, handle?: string) {
const nodes: FlowNode[] = [];
function next(id: string, i = 0) {
const children = childrenNodes(id, i == 0 ? handle : undefined);
children.forEach((e) => {
nodes.push(e);
next(e.id!, i + 1);
});
}
next(id, 0);
return nodes;
}
// 获取与节点所有连接的其他节点
function getConnectedNodes(nodeId: string) {
const pNodes = parentAllNodes(nodeId);
const cNodes = childrenAllNodes(nodeId);
return [...pNodes, findNode(nodeId), ...cNodes].filter((e) => !!e);
}
// 是否存在线
function hasEdge(connection: Connection) {
return edges.value.filter((e) => {
if (e.source == connection.source) {
if (connection.sourceHandle == e.sourceHandle) {
return true;
}
}
if (e.target == connection.target) {
if (connection.targetHandle == e.targetHandle) {
return true;
}
}
return false;
});
}
// 添加边线
function addEdge(connection: Connection) {
const list = hasEdge(connection);
if (!isEmpty(list)) {
removeEdges(list);
}
vueFlow.addEdges({
...connection,
animated: false,
updatable: true,
// markerEnd: MarkerType.ArrowClosed,
style: {
strokeWidth: 1.5
},
type: "button"
});
}
// 查找边线
function findEdge(id: string) {
return vueFlow.findEdge(id);
}
// 更新边线
function updateEdge(id: string, data: any) {
const edge = findEdge(id);
if (edge) {
vueFlow.updateEdge(edge, assign(edge, data), false);
}
}
// 移除线
function removeEdges(edge: any | any[]) {
vueFlow.removeEdges(edge);
}
// 根据节点移除线
function removeEdgeByNodeId(nodeId: string, type?: "source" | "target") {
const list = edges.value.filter((e) => {
if (type) {
return e[type] == nodeId;
}
return e.source == nodeId || e.target == nodeId;
});
removeEdges(list);
return list;
}
// 突出已连接的线
function activeEdge(nodeId: string, isShow: boolean) {
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue(
`--el-color-primary`
);
edges.value
.filter((e) => e.target == nodeId || e.source == nodeId)
.forEach((e) => {
const stroke = isShow ? primaryColor : e._stroke || "";
// 记录之前的颜色
e._stroke = e.style?.stroke;
updateEdge(e.id, {
style: {
stroke
}
});
});
}
// 默认
function def() {
if (isEmpty(nodes.value)) {
const node = addNode("start", {
position: {
x: 100,
y: 100
}
});
setNode(node);
}
}
// 清空画布
function clear() {
// 清空线
removeEdges(edges.value);
// 清空节点
removeNodes(nodes.value, true);
// 默认
def();
// 设置视图
setViewport({ x: 0, y: 0 });
}
// 复制节点
const copyData = ref<FlowNode>();
function setCopyNode(node: any) {
copyData.value = cloneDeep(node);
}
// 缩放
const zoom = ref(100);
function setZoom(val: number) {
zoom.value = val;
}
// 控制模式
const controlMode = ref("hand");
function setControlMode(val: "pointer" | "hand") {
controlMode.value = val;
vueFlow.panOnDrag.value = val == "hand";
}
// 滚动距离 、缩放大小
const viewport = computed(() => {
return vueFlow.viewport.value || { zoom: 1.0 };
});
function setViewport(
{ x, y, zoom }: { x: number; y: number; zoom?: number },
duration = 300
) {
vueFlow.setViewport({ x, y, zoom: zoom || viewport.value.zoom }, { duration });
}
// 设置节点中心位置
async function setViewportByNode(node: FlowNode) {
if (!node) {
return false;
}
await sleep(10);
const { zoom } = viewport.value;
if (node) {
const flowEl = document.querySelector(".cl-flow");
const panelEl = document.querySelector(".cl-flow .tools-panel-right");
const nodeEl = document.querySelector(`div[data-id="${node.id}"]`);
const { x = 0, y = 0 } = node.position || {};
const top =
((flowEl?.clientHeight || 0) - (nodeEl?.clientHeight || 0) * zoom) / 2 -
y * zoom;
const left =
((flowEl?.clientWidth || 0) -
((panelEl?.clientWidth || 0) + 10) -
(nodeEl?.clientWidth || 0) * zoom) /
2 -
x * zoom;
setViewport({ x: left, y: top });
} else {
setViewport({ x: 0, y: 0 });
}
}
// 计算位置信息
function calcPosition(source: FlowNode, target?: FlowNode) {
const { x = 0, y = 0 } = source.position || {};
const position = {
x: x + offset.x,
y: 0
};
// 子节点
const nodes = childrenNodes(source.id!);
// 第一个子节点
const fNode = nodes[0];
// 【是否有相邻节点】有目标节点则判断是否与第一个相同,否则判断节点长度
if (target ? fNode?.id != target.id : !isEmpty(nodes)) {
// 计算到第几个子节点
const end = target ? nodes.findIndex((e) => e.id == target?.id) : nodes.length;
// 前几个节点高度之和,从第一个节点的 y 开始算
const height = nodes
.filter((_, i) => i < end)
.reduce(
(a, b) => {
return (
a +
(document.querySelector(`div[data-id="${b.id}"]`)?.clientHeight ||
0) +
offset.y_b
);
},
fNode?.position?.y || 0
);
position.y = height;
} else {
position.y = y - offset.y_t;
}
return position;
}
// 整理
async function arrange() {
if (node.value) {
clearNode();
await sleep(200);
}
const list = nodes.value as FlowNode[];
// 是否已连接
const conntected: string[] = [];
// 多个连接线
const group: FlowNode[][] = [];
list.forEach((e) => {
if (!conntected.includes(e.id!)) {
const nodes = [e, ...childrenAllNodes(e.id!)];
nodes.forEach((e) => {
conntected.push(e.id!);
});
// 判断最后一个是否相同
const sn = group.findIndex((e) => last(e)?.id == last(nodes)?.id);
if (sn >= 0) {
// 长的覆盖短的
if (nodes.length > group[sn].length) {
group[sn] = nodes;
}
} else {
group.push(nodes);
}
}
});
// 遍历组,计算每个节点
group.forEach((row, i) => {
// 起始
const x = 100;
let y = 100;
// 根据上一组的 y 值来计算当前
const prev = group[i - 1];
if (prev) {
const arr = prev.map((e) => {
return (
(e.position?.y || 0) +
(document.querySelector(`div[data-id="${e.id}"]`)?.clientHeight || 0)
);
});
y = Math.max(...arr.concat(0)) + offset.g;
}
const node = row[0];
if (node) {
// 更新首节点
updateNode(node.id!, {
position: {
x,
y
}
});
// 依次遍历子节点
function next(item: FlowNode) {
const children = childrenNodes(item.id!);
children.forEach((e) => {
updateNode(e.id!, {
position: calcPosition(item, e)
});
next(e);
});
}
next(node);
}
});
// 回到初始位置
setViewport({ x: 0, y: 0, zoom: 1 });
}
// 初始化
function init() {
// 设置删除键
vueFlow.deleteKeyCode.value = "none";
// 禁用点击连接
vueFlow.connectOnClick.value = false;
}
// 基本信息
const info = ref<Eps.FlowInfoEntity>();
// 获取
async function get(flowId?: number) {
await req;
return service.flow.info
.info({
id: flowId || info.value?.id
})
.then((res) => {
if (res) {
info.value = res;
// 还原节点
restore(res.draft);
// 开始节点
if (isEmpty(nodes.value)) {
def();
}
} else {
ElMessageBox.alert("流程不存在或异常,请重新选择。", "提示", {
callback() {
router.back();
}
});
}
return res;
});
}
/**
*
* @param nodes
* @param edges
* @returns
*/
function extractData(nodes, edges) {
const nodesResult = (nodes as FlowNode[]).map((e, i) => {
// 开始节点
if (e.type == "start") {
e.data?.inputParams?.forEach((p) => {
p.name = p.field;
p.nodeId = e.id;
p.nodeType = "start";
});
}
// 其他节点
else {
["inputParams", "outputParams"].forEach((k) => {
if (e.data?.[k]) {
e.data?.[k].forEach((e: FlowField) => {
const d = findNode(e.nodeId!);
if (d) {
e.nodeType = d.type;
}
});
}
});
}
return {
...e,
component: undefined,
form: undefined
};
});
const edgesResult = edges.map((e) => {
return {
...e,
animated: false,
style: {}
};
});
return {
nodes: nodesResult,
edges: edgesResult
};
}
// 保存
async function save() {
if (!info.value?.id) {
return false;
}
const { nodes, edges, viewport } = vueFlow.toObject();
// 提取有用数据
const { nodes: nodesResult, edges: edgesResult } = extractData(nodes, edges);
req = service.flow.info.update({
id: info.value?.id,
draft: {
nodes: nodesResult,
edges: edgesResult,
viewport
}
});
await req;
}
// 还原
async function restore(data: any) {
if (data) {
await vueFlow.fromObject(data);
// 还原缩放比例
zoom.value = viewport.value.zoom * 100;
// 替换为本地的配置
if (!isEmpty(nodes.value)) {
id = Math.max(...nodes.value.map((e) => Number(e.id))) + 1;
nodes.value.forEach((e) => {
const cn = CustomNodes.value.find((a) => a.type == e.type);
if (cn) {
// 处理卡片宽度
const configWidth = cn.form?.width || "400px";
const width = `${parseFloat(configWidth) + 30}px`;
e.component = cn.component;
e.form = cn.form;
e.color = cn.color;
e.group = cn.group;
e.validator = cn.validator;
e.cardWidth = width;
}
});
}
}
}
// 分组显示
function getGroup(keyWord: string) {
const d = groupBy(
CustomNodes.value.filter((e) => e.type != "start"),
"group"
);
let list = keys(d)
.map((k) => {
return {
label: k,
children: d[k].filter((e) => {
return keyWord ? e.label?.includes(keyWord) : true;
})
};
})
.filter((e) => {
return !isEmpty(e.children);
});
list = orderBy(list, "label", "asc");
return list;
}
// 视图x,y偏移
function viewPx(x: number, y: number) {
return [
(x - viewport.value.x) / viewport.value.zoom,
(y - viewport.value.y) / viewport.value.zoom
];
}
// 禁用拖拽
function disabledDrag() {
vueFlow.nodesDraggable.value = false;
vueFlow.panOnDrag.value = false;
}
// 启用拖拽
function enableDrag() {
vueFlow.nodesDraggable.value = true;
vueFlow.panOnDrag.value = true;
}
/**
*
* @param type open|close
* @param node
* @param id
* @returns
*/
async function updateChildrenPosition(type: "open" | "close", node: FlowNode) {
if (node) {
let list = [] as FlowNode[];
if (type == "open") {
list = childrenAllNodes(node.id!);
} else {
list = nodes.value;
}
list.forEach((e) => {
e.isMoving = true;
if (type == "open") {
e._offset = {
x: parseInt(node.form?.width || "400px") - 300 + 30,
y: 0
};
e.position!.x += e._offset.x;
} else {
if (e._offset) {
e.position!.x -= e._offset.x;
delete e._offset;
}
}
updateNode(e.id!, e);
setTimeout(() => {
e.isMoving = false;
}, 200);
});
}
}
/**
*
*/
async function exportFlow() {
if (!info.value?.id) {
return false;
}
const { nodes, edges, viewport } = vueFlow.toObject();
// 提取有用数据
const { nodes: nodesResult, edges: edgesResult } = extractData(nodes, edges);
const flowInfo = cloneDeep(info.value);
// 处理无用数据
delete flowInfo.id;
delete flowInfo.createTime;
delete flowInfo.updateTime;
delete flowInfo.releaseTime;
delete flowInfo.draft;
// 导出的数据
const result = {
...flowInfo,
draft: {
nodes: nodesResult,
edges: edgesResult,
viewport
}
};
const dataStr = JSON.stringify(result, null, 2);
const dataBlob = new Blob([dataStr], { type: "application/json" });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `${info.value?.name || "flow"}.json`);
link.click();
URL.revokeObjectURL(url);
ElMessage.success("导出成功");
}
const expose = {
CustomNodes,
nodes,
node,
addNode,
removeNodes,
setCopyNode,
insertNode,
setNode,
findNode,
findNodeByType,
slibingNodes,
parentNode,
parentAllNodes,
childrenNodes,
childrenAllNodes,
leafNodes,
getConnectedNodes,
clearNode,
updateNode,
edges,
addEdge,
findEdge,
updateEdge,
activeEdge,
removeEdges,
removeEdgeByNodeId,
copyData,
clear,
zoom,
setZoom,
controlMode,
setControlMode,
viewport,
setViewport,
setViewportByNode,
arrange,
init,
info,
get,
save,
restore,
getGroup,
viewPx,
offset,
disabledDrag,
enableDrag,
updateChildrenPosition,
exportFlow
};
// 事件传递
[
"addNodes",
"updateNode",
"removeNodes",
"addEdge",
"updateEdge",
"removeEdges",
"clear",
"arrange"
].forEach((k) => {
const fn = expose[k];
expose[k] = (...args: any[]) => {
const d = fn(...args);
mitt.emit(`flow.${k}`, d);
return d;
};
});
return expose;
});
return store();
};

View File

@ -0,0 +1 @@
export * from "./flow";

View File

@ -0,0 +1 @@
export * from "./hooks";

View File

@ -0,0 +1,36 @@
.el-popper {
&.cl-flow__popper {
border-radius: 8px;
.el-popper__arrow {
display: none;
}
.el-scrollbar__view {
padding: 0;
}
.el-select-dropdown__item {
border-radius: 6px;
height: 30px;
font-size: 12px;
line-height: 30px;
}
&.el-select__popper {
margin: -7px 0;
.el-select-dropdown__wrap {
padding: 5px;
}
}
&:not(.el-select__popper) {
padding: 5px;
}
&.not-padding {
padding: 0;
}
}
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1718440749256" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2677" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m36.571429 195.047619h-73.142858v182.832761L292.571429 475.428571v73.142858l182.857142-0.024381V731.428571h73.142858v-182.857142H731.428571v-73.142858h-182.857142V292.571429z" p-id="2678"></path></svg>

After

Width:  |  Height:  |  Size: 711 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1718421202036" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2220" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m0 73.142857C323.486476 170.666667 170.666667 323.486476 170.666667 512s152.81981 341.333333 341.333333 341.333333 341.333333-152.81981 341.333333-341.333333S700.513524 170.666667 512 170.666667z m36.571429 121.904762v182.857142H731.428571v73.142858h-182.857142V731.428571h-73.142858v-182.881523L292.571429 548.571429v-73.142858l182.857142-0.024381V292.571429h73.142858z" p-id="2221"></path></svg>

After

Width:  |  Height:  |  Size: 907 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1716370927117" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1680" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M548.571429 170.666667v304.761904H853.333333v73.142858H548.547048L548.571429 853.333333h-73.142858l-0.024381-304.761904H170.666667v-73.142858h304.761904V170.666667h73.142858z" p-id="1681"></path></svg>

After

Width:  |  Height:  |  Size: 534 B

View File

@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1718345666348"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2211"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="128"
height="128"
>
<path
d="M3.43139366 512c0 280.86857143 227.70003492 508.56860634 508.56860634 508.56860634s508.56860634-227.70003492 508.56860634-508.56860634S792.86857143 3.43139366 512 3.43139366 3.43139366 231.13142857 3.43139366 512z m508.56860634-231.16754814a46.23351006 46.23351006 0 0 1 46.23351006 46.23350898v138.7005291h138.7005291a46.23351006 46.23351006 0 1 1 0 92.46702012h-138.7005291v138.7005291a46.23351006 46.23351006 0 1 1-92.46702012 0v-138.7005291H327.06596084a46.23351006 46.23351006 0 1 1 0-92.46702012h138.7005291V327.06596084a46.23351006 46.23351006 0 0 1 46.23351006-46.23350898z"
p-id="2212"
></path>
</svg>

After

Width:  |  Height:  |  Size: 962 B

View File

@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1716793445789"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1488"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="128"
height="128"
>
<path
d="M226.909091 186.181818c22.481455 0 40.727273 18.245818 40.727273 40.727273v570.181818a40.727273 40.727273 0 1 1-81.454546 0V226.909091C186.181818 204.427636 204.427636 186.181818 226.909091 186.181818zM791.272727 552.727273a46.545455 46.545455 0 0 1 46.545455 46.545454v69.818182a46.545455 46.545455 0 0 1-46.545455 46.545455H395.636364a46.545455 46.545455 0 0 1-46.545455-46.545455v-69.818182a46.545455 46.545455 0 0 1 46.545455-46.545454H791.272727z m-122.181818-244.363637a46.545455 46.545455 0 0 1 46.545455 46.545455v69.818182a46.545455 46.545455 0 0 1-46.545455 46.545454H395.636364a46.545455 46.545455 0 0 1-46.545455-46.545454v-69.818182a46.545455 46.545455 0 0 1 46.545455-46.545455h273.454545z"
p-id="1489"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1718461931984" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2465" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 962.56C263.168 962.56 61.44 760.832 61.44 512S263.168 61.44 512 61.44s450.56 201.728 450.56 450.56-201.728 450.56-450.56 450.56z m180.85888-397.312c31.92832-29.61408 31.92832-76.88192 0-106.496L477.5936 259.13344a47.0016 47.0016 0 0 0-1.00352-0.90112c-19.456-17.01888-50.44224-16.52736-69.2224 1.10592-20.52096 19.29216-23.81824 48.82432-7.96672 71.4752l105.49248 150.58944c13.1072 18.67776 13.1072 42.55744 0 61.2352L398.9504 693.8624c-15.5648 22.26176-12.34944 51.3024 7.84384 70.26688l0.57344 0.55296 1.024 0.90112c19.31264 17.14176 50.29888 16.83456 69.2224-0.69632l215.26528-199.65952z" p-id="2466"></path></svg>

After

Width:  |  Height:  |  Size: 955 B

View File

@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1717073489022"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1759"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="128"
height="128"
>
<path
d="M597.17744141 310.61005859H245.90849609l105.47666016-105.54257812a33.28154297 33.28154297 0 0 0 8.60625-32.15039063 33.28681641 33.28681641 0 0 0-23.54238281-23.52304687 33.27802734 33.27802734 0 0 0-32.13984375 8.62207031L162.66904297 299.62636719c-12.38730469 12.35039062-19.24541016 28.96523437-19.31132813 46.61455078a65.57255859 65.57255859 0 0 0 19.31132813 47.11201172l141.64013672 141.67529297c6.22880859 6.26484375 14.70322266 9.79101563 23.53886718 9.79101562s17.30830078-3.52617188 23.53710938-9.79101562a33.24638672 33.24638672 0 0 0 9.76464844-23.54238282 33.24814453 33.24814453 0 0 0-9.76464844-23.53886718L240.31425781 377.140625h356.8631836c119.53476562 0 216.75585938 97.08837891 216.75585937 216.42099609s-97.22109375 216.421875-216.75585937 216.421875H364.77265625c-18.38671875 0-33.29736328 14.91064453-33.29736328 33.29736329s14.91064453 33.29824219 33.29736328 33.29824218h232.40566406c156.22646484 0 283.34707031-126.95888672 283.34707031-283.01748047s-127.11972656-283.01660156-283.34707031-283.01660156"
p-id="1760"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,21 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
t="1718439017245"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2457"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="128"
height="128"
>
<path
d="M415.33999999 352.33999999c48.96 0 88.2-56.16 88.20000002-125.63999998 0-69.48-39.42000001-125.64-88.2-125.64s-88.38000001 56.16-88.38000001 125.64c0 69.48 39.6 125.64 88.38000001 125.64zM625.58000001 360.61999999c65.34000001 8.46000001 107.28-61.2 115.55999998-113.93999999 8.46000001-52.74000001-33.48-113.94000001-79.92-124.38000001-46.26000001-10.62000001-104.04 63.54000001-109.25999999 111.78000001-6.30000001 59.04 8.46000001 118.08 73.61999999 126.54000001zM251.17999999 529.45999999c88.38000001-19.08 76.32-124.56 73.62000001-147.77999999-4.32-35.64-46.08-97.74000001-102.96-92.88-71.46000001 6.30000001-81.90000001 109.8-81.90000001 109.8-9.72 47.70000001 23.04 149.94000001 111.24000002 130.86000001zM785.42000001 348.01999999c-77.94000001 0-88.38000001 71.64-88.38000001 122.40000002 0 48.42000001 4.14000001 116.10000001 100.98000001 113.93999999 96.84-2.16 86.22000001-109.62000001 86.21999999-135 0-25.38000001-20.88-101.34000001-98.82000001-101.33999999zM345.13999999 713.24c-2.52 7.38000001-8.28 26.46000001-3.41999999 43.02000001 9.90000001 37.08 42.12 38.70000001 42.12 38.69999999h46.26000001v-113.22000001H380.6c-22.32 6.66000001-33.12 24.12-35.46000001 31.50000001z"
p-id="2458"
></path>
<path
d="M785.42000001 670.94000001s-100.98000001-78.12-159.84000002-162.54000001c-79.92-124.38000001-193.50000001-73.8-231.29999999-10.62000001-37.8 63.36-96.66000001 103.32-105.12 114.12000002-8.46000001 10.44-121.86000001 71.64-96.66000001 183.59999998 25.2 111.78000001 113.58000001 109.62000001 113.58000001 109.62000001s65.16 6.48 140.94000001-10.44c75.6-16.74000001 140.76 4.14000001 140.75999998 4.14000001s176.76 59.22000001 225.18000001-54.90000001c48.24-113.94000001-27.54000001-172.98000001-27.54000001-172.98000001z m-302.40000002 169.55999998h-114.83999998c-49.68-9.90000001-69.48-43.74000001-71.82000001-49.49999999-2.52-5.76-16.56-33.12-9.17999999-79.38000001 21.42000001-69.48 82.62000001-74.34000001 82.61999999-74.33999999h61.2v-75.24l52.02000001 0.90000001v277.55999998z m214.02000001-0.89999999h-132.30000001c-51.30000001-13.14000001-53.64-49.50000001-53.63999998-49.49999999v-146.34000001l53.64-0.90000001v131.40000002c3.24 14.04 20.70000001 16.56 20.69999999 16.56h54.54000001v-147.06000001h57.05999999V839.6z"
p-id="2459"
></path>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1720424518483" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1014" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 140m-138 0a138 138 0 1 0 276 0 138 138 0 1 0-276 0Z" fill="#A8B7BD" p-id="1015"></path><path d="M596.5 223.1c-59.6 47.5-146.4 37.7-193.9-21.8-15.6-19.6-25.1-42.2-28.6-65.3-0.9 31.5 8.9 63.5 30 90 47.5 59.6 134.3 69.3 193.9 21.8 40-31.9 57.5-81.4 50.4-128.6-1 39.2-18.7 77.6-51.8 103.9z" opacity=".52" p-id="1016"></path><path d="M512 886m-138 0a138 138 0 1 0 276 0 138 138 0 1 0-276 0Z" fill="#A8B7BD" p-id="1017"></path><path d="M427.5 802.9c59.6-47.5 146.4-37.7 193.9 21.8 15.6 19.6 25.1 42.2 28.6 65.3 0.9-31.5-8.9-63.5-30-90-47.5-59.6-134.3-69.3-193.9-21.8-40 31.9-57.5 81.4-50.4 128.6 1-39.2 18.7-77.6 51.8-103.9z" opacity=".52" p-id="1018"></path><path d="M512 767.3c72.7 0 132.1 56.3 137.4 127.6 0.3-3.4 0.5-6.9 0.5-10.4 0-76.2-61.8-138-138-138s-138 61.8-138 138c0 3.5 0.3 6.9 0.5 10.4 5.5-71.3 64.9-127.6 137.6-127.6z" fill="#63777C" opacity=".59" p-id="1019"></path><path d="M512 258.7c-72.7 0-132.1-56.3-137.4-127.6-0.3 3.4-0.5 6.9-0.5 10.4 0 76.2 61.8 138 138 138s138-61.8 138-138c0-3.5-0.3-6.9-0.5-10.4-5.5 71.3-64.9 127.6-137.6 127.6z" fill="#63777C" opacity=".59" p-id="1020"></path><path d="M512 929c-40.2 0-73.1-32.9-73.1-73.1v-689c0-40.2 32.9-73.1 73.1-73.1 40.2 0 73.1 32.9 73.1 73.1v689c0 40.2-32.9 73.1-73.1 73.1z" fill="#D3DCE0" p-id="1021"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Some files were not shown because too many files have changed in this diff Show More