mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-05-04 11:01:26 +08:00
【初始化】前端工程项目
This commit is contained in:
BIN
frontend/apps/allin-ssl/src/components/.DS_Store
vendored
Normal file
BIN
frontend/apps/allin-ssl/src/components/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @description 基础组件
|
||||
* @example
|
||||
* ```tsx
|
||||
* <BaseComponent>
|
||||
* <template #header-left>左侧头部内容</template>
|
||||
* <template #header-right>右侧头部内容</template>
|
||||
* <template #content>主要内容</template>
|
||||
* <template #footer-left>左侧底部内容</template>
|
||||
* <template #footer-right>右侧底部内容</template>
|
||||
* <template #popup>弹窗内容</template>
|
||||
* </BaseComponent>
|
||||
* ```
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'BaseComponent',
|
||||
setup(_, { slots }) {
|
||||
// 获取插槽内容,支持驼峰和短横线两种命名方式
|
||||
const slotHL = slots['header-left'] || slots['headerLeft']
|
||||
const slotHR = slots['header-right'] || slots['headerRight']
|
||||
const slotHeader = slots['header'] || slots['header']
|
||||
const slotFL = slots['footer-left'] || slots['footerLeft']
|
||||
const slotFR = slots['footer-right'] || slots['footerRight']
|
||||
const slotFooter = slots['footer'] || slots['footer']
|
||||
|
||||
return () => (
|
||||
<div class="flex flex-col">
|
||||
{/* 头部区域 */}
|
||||
{(slotHL || slotHR) && (
|
||||
<div class="flex justify-between flex-wrap" style={{ rowGap: '0.8rem' }}>
|
||||
<div class="flex flex-shrink-0">{slotHL && slotHL()}</div>
|
||||
<div class="flex flex-shrink-0">{slotHR && slotHR()}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 头部区域 */}
|
||||
{slotHeader && <div class="flex justify-between flex-wrap w-full">{slotHeader && slotHeader()}</div>}
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div class={`w-full content ${slotHL || slotHR ? 'mt-[1.2rem]' : ''} ${slotFL || slotFR ? 'mb-[1.2rem]' : ''}`}>
|
||||
{slots.content && slots.content()}
|
||||
</div>
|
||||
|
||||
{/* 底部区域 */}
|
||||
{(slotFL || slotFR) && (
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-shrink-0">{slotFL && slotFL()}</div>
|
||||
<div class="flex flex-shrink-0">{slotFR && slotFR()}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部区域 */}
|
||||
{slotFooter && <div class="flex justify-between w-full">{slotFooter()}</div>}
|
||||
|
||||
{/* 弹窗区域 */}
|
||||
{slots.popup && slots.popup()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,300 @@
|
||||
import { NButton, NFormItemGi, NGrid, NSelect, NText, NSpin, NFlex } from 'naive-ui'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { $t } from '@locales/index'
|
||||
import { useStore } from '@layout/useStore'
|
||||
import SvgIcon from '@components/svgIcon'
|
||||
|
||||
interface DnsProviderOption {
|
||||
label: string
|
||||
value: string
|
||||
type: string
|
||||
}
|
||||
|
||||
type DnsProviderType = 'btpanel' | 'aliyun' | 'ssh' | 'tencentcloud' | '1panel' | 'dns' | ''
|
||||
|
||||
interface DnsProviderSelectProps {
|
||||
// 表单类型,用于获取不同的下拉列表
|
||||
type: DnsProviderType
|
||||
// 表单,用于绑定表单的值
|
||||
path: string
|
||||
// 表单的值
|
||||
value: string
|
||||
// 表单的值类型
|
||||
valueType: 'value' | 'type'
|
||||
// 是否为添加模式
|
||||
isAddMode: boolean
|
||||
// 是否禁用
|
||||
disabled?: boolean
|
||||
// 自定义样式
|
||||
customClass?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @component DnsProviderSelect
|
||||
* @description DNS提供商选择组件,支持多种DNS提供商类型,并提供刷新和跳转到授权页面的功能
|
||||
*
|
||||
* @example 基础使用
|
||||
* <DnsProviderSelect
|
||||
* type="dns"
|
||||
* path="form.dnsProvider"
|
||||
* v-model:value="formValue.dnsProvider"
|
||||
* valueType="value"
|
||||
* :isAddMode="true"
|
||||
* />
|
||||
*
|
||||
* @example 仅显示选择器(无添加按钮)
|
||||
* <DnsProviderSelect
|
||||
* type="aliyun"
|
||||
* path="form.dnsProvider"
|
||||
* v-model:value="formValue.dnsProvider"
|
||||
* valueType="value"
|
||||
* :isAddMode="false"
|
||||
* />
|
||||
*
|
||||
* @example 禁用状态
|
||||
* <DnsProviderSelect
|
||||
* type="dns"
|
||||
* path="form.dnsProvider"
|
||||
* v-model:value="formValue.dnsProvider"
|
||||
* valueType="value"
|
||||
* :isAddMode="true"
|
||||
* :disabled="true"
|
||||
* />
|
||||
*
|
||||
* @property {string} type - DNS提供商类型,支持 'btpanel'|'aliyun'|'ssh'|'tencentcloud'|'1panel'|'dns'|''
|
||||
* @property {string} path - 表单路径,用于绑定表单的值
|
||||
* @property {string} value - 表单的值,通过v-model:value绑定
|
||||
* @property {string} valueType - 表单的值类型,可选值为 'value'(默认) 或 'type'
|
||||
* @property {boolean} isAddMode - 是否显示添加和刷新按钮,默认为true
|
||||
* @property {boolean} disabled - 是否禁用选择器,默认为false
|
||||
* @property {string} customClass - 自定义CSS类名
|
||||
*
|
||||
* @emits update:value - 当选择的DNS提供商变更时触发
|
||||
*/
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DnsProviderSelect',
|
||||
props: {
|
||||
// 表单类型,用于获取不同的下拉列表
|
||||
type: {
|
||||
type: String as PropType<DnsProviderType>,
|
||||
default: '',
|
||||
},
|
||||
// 表单,用于绑定表单的值
|
||||
path: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 表单的值
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 表单的值类型
|
||||
valueType: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
// 是否为添加模式
|
||||
isAddMode: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 自定义样式
|
||||
customClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update:value'],
|
||||
setup(props: DnsProviderSelectProps, { emit }) {
|
||||
// 错误处理
|
||||
const { handleError } = useError()
|
||||
// 获取DNS提供商
|
||||
const { fetchDnsProvider, dnsProvider } = useStore()
|
||||
// 表单的值
|
||||
const param = ref<DnsProviderOption>({
|
||||
label: '',
|
||||
value: '',
|
||||
type: '',
|
||||
})
|
||||
const dnsProviderRef = ref<DnsProviderOption[]>([])
|
||||
// 加载状态
|
||||
const isLoading = ref(false)
|
||||
// 错误信息
|
||||
const errorMessage = ref('')
|
||||
|
||||
/**
|
||||
* @description 跳转到DNS提供商授权页面
|
||||
*/
|
||||
const goToAddDnsProvider = () => {
|
||||
window.open('/auth-api-manage', '_blank')
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染单选标签
|
||||
* @param option - 选项
|
||||
* @returns 渲染后的VNode
|
||||
*/
|
||||
const renderSingleSelectTag = ({ option }: Record<string, any>): VNode => {
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
{option.label ? (
|
||||
<NFlex>
|
||||
<SvgIcon icon={`resources-${option.type}`} size="2rem" />
|
||||
<NText>{option.label}</NText>
|
||||
</NFlex>
|
||||
) : (
|
||||
<NText>{props.type === 'dns' ? $t('t_3_1745490735059') : $t('t_19_1745735766810')}</NText>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染标签
|
||||
* @param option - 选项
|
||||
* @returns 渲染后的VNode
|
||||
*/
|
||||
const renderLabel = (option: { type: string; label: string }): VNode => {
|
||||
return (
|
||||
<NFlex>
|
||||
<SvgIcon icon={`resources-${option.type}`} size="2rem" />
|
||||
<NText>{option.label}</NText>
|
||||
</NFlex>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 更新类型
|
||||
*/
|
||||
const handleUpdateType = async () => {
|
||||
const items = dnsProvider.value.find((item) => {
|
||||
return item.value === param.value.value
|
||||
})
|
||||
if (items) {
|
||||
param.value = {
|
||||
label: items.label,
|
||||
value: items.value,
|
||||
type: items.type,
|
||||
}
|
||||
}
|
||||
if (dnsProvider.value.length > 0 && param.value.value === '') {
|
||||
param.value = {
|
||||
label: dnsProvider.value[0]?.label || '',
|
||||
value: dnsProvider.value[0]?.value || '',
|
||||
type: dnsProvider.value[0]?.type || '',
|
||||
}
|
||||
}
|
||||
emit('update:value', param.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新表单的值
|
||||
* @param value - 表单的值
|
||||
*/
|
||||
const handleUpdateValue = (value: string) => {
|
||||
param.value.value = value
|
||||
handleUpdateType()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 加载DNS提供商选项
|
||||
*/
|
||||
const loadDnsProviders = async (type: DnsProviderType = '') => {
|
||||
isLoading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await fetchDnsProvider(type)
|
||||
} catch (error) {
|
||||
errorMessage.value = typeof error === 'string' ? error : $t('t_0_1746760933542')
|
||||
handleError(error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 搜索过滤函数
|
||||
* @param pattern - 搜索文本
|
||||
* @param option - 选项
|
||||
*/
|
||||
const handleFilter = (pattern: string, option: any) => {
|
||||
return option.label.toLowerCase().includes(pattern.toLowerCase())
|
||||
}
|
||||
|
||||
// 监听消息通知提供商
|
||||
watch(
|
||||
() => dnsProvider.value,
|
||||
(newVal) => {
|
||||
dnsProviderRef.value =
|
||||
newVal.map((item) => ({
|
||||
label: item.label,
|
||||
value: props.valueType === 'value' ? item.value : item.type,
|
||||
type: props.valueType === 'value' ? item.type : item.value,
|
||||
})) || []
|
||||
handleUpdateType()
|
||||
},
|
||||
)
|
||||
|
||||
// 监听父组件的值
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
loadDnsProviders(props.type)
|
||||
handleUpdateValue(props.value)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return () => (
|
||||
<NSpin show={isLoading.value}>
|
||||
<NGrid cols={24} class={props.customClass}>
|
||||
<NFormItemGi
|
||||
span={props.isAddMode ? 13 : 24}
|
||||
label={props.type === 'dns' ? $t('t_3_1745735765112') : $t('t_0_1745744902975')}
|
||||
path={props.path}
|
||||
>
|
||||
<NSelect
|
||||
class="flex-1 w-full"
|
||||
options={dnsProviderRef.value}
|
||||
renderLabel={renderLabel}
|
||||
renderTag={renderSingleSelectTag}
|
||||
filterable
|
||||
filter={handleFilter}
|
||||
placeholder={props.type === 'dns' ? $t('t_3_1745490735059') : $t('t_1_1745744905566')}
|
||||
v-model:value={param.value.value}
|
||||
onUpdateValue={handleUpdateValue}
|
||||
disabled={props.disabled}
|
||||
v-slots={{
|
||||
empty: () => {
|
||||
return (
|
||||
<span class="text-[1.4rem]">
|
||||
{errorMessage.value || (props.type === 'dns' ? $t('t_3_1745490735059') : $t('t_1_1745744905566'))}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</NFormItemGi>
|
||||
{props.isAddMode && (
|
||||
<NFormItemGi span={11}>
|
||||
<NButton class="mx-[8px]" onClick={goToAddDnsProvider} disabled={props.disabled}>
|
||||
{props.type === 'dns' ? $t('t_1_1746004861166') : $t('t_0_1745748292337')}
|
||||
</NButton>
|
||||
<NButton onClick={() => loadDnsProviders(props.type)} loading={isLoading.value} disabled={props.disabled}>
|
||||
{$t('t_0_1746497662220')}
|
||||
</NButton>
|
||||
</NFormItemGi>
|
||||
)}
|
||||
</NGrid>
|
||||
</NSpin>
|
||||
)
|
||||
},
|
||||
})
|
||||
BIN
frontend/apps/allin-ssl/src/components/flowChart/.DS_Store
vendored
Normal file
BIN
frontend/apps/allin-ssl/src/components/flowChart/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
frontend/apps/allin-ssl/src/components/flowChart/components/.DS_Store
vendored
Normal file
BIN
frontend/apps/allin-ssl/src/components/flowChart/components/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,182 @@
|
||||
.node {
|
||||
@apply flex flex-col items-center relative mx-[1.2rem];
|
||||
}
|
||||
|
||||
.nodeArrows::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -1.2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0.4rem;
|
||||
border-style: solid;
|
||||
border-width: 0.8rem 0.6rem 0.4rem;
|
||||
border-color: #cacaca transparent transparent;
|
||||
background-color: #f5f5f7;
|
||||
}
|
||||
|
||||
.nodeContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 20rem;
|
||||
min-height: 8rem;
|
||||
font-size: 1.4rem;
|
||||
box-shadow: .2rem .2rem .5rem .2rem rgba(0, 0, 0, 0.2);
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0.5rem;
|
||||
transition: box-shadow 0.1s;
|
||||
}
|
||||
|
||||
.nodeContent:hover {
|
||||
box-shadow: 0.3rem 0.3rem .6rem 0.3rem rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.nodeSelected {
|
||||
box-shadow: 0 0 0 2px #1e83e9;
|
||||
border: 1px solid #1e83e9;
|
||||
}
|
||||
|
||||
.nodeHeader {
|
||||
@apply w-full flex relative items-center justify-center bg-[#1e83e9] rounded-t-[0.5rem] p-[0.5rem_1rem] text-white box-border;
|
||||
}
|
||||
|
||||
.nodeHeaderBranch{
|
||||
@apply flex-1 justify-between;
|
||||
}
|
||||
|
||||
.nodeCondition{
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.nodeConditionHeader {
|
||||
min-height: 5rem;
|
||||
border-radius: 1rem;
|
||||
color: #333 !important;
|
||||
background-color: #f8fafc !important;
|
||||
}
|
||||
|
||||
.nodeConditionHeader input{
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
.nodeConditionHeader input:focus{
|
||||
background-color: #efefef !important;
|
||||
}
|
||||
|
||||
|
||||
.nodeConditionHeader .nodeIcon{
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
.nodeIcon {
|
||||
@apply text-[1.6rem];
|
||||
}
|
||||
|
||||
.nodeHeaderTitle {
|
||||
@apply flex flex-row items-center justify-center relative px-[2rem];
|
||||
}
|
||||
|
||||
.nodeHeaderTitleText {
|
||||
@apply max-w-[11rem] min-w-[2rem] mr-[0.5rem] whitespace-nowrap overflow-hidden text-ellipsis;
|
||||
}
|
||||
|
||||
.nodeHeaderTitleInput {
|
||||
@apply w-auto ;
|
||||
}
|
||||
|
||||
.nodeHeaderTitleInput input {
|
||||
@apply w-full text-center border border-none rounded px-2 py-1 text-[#fff] focus:outline-none bg-transparent;
|
||||
}
|
||||
|
||||
.nodeHeaderTitleInput input:focus {
|
||||
@apply border-[#1e83e9] bg-white text-[#333];
|
||||
}
|
||||
|
||||
.nodeHeaderTitleEdit {
|
||||
@apply w-[3rem] cursor-pointer hidden;
|
||||
}
|
||||
|
||||
.nodeHeaderTitle:hover .nodeHeaderTitleEdit {
|
||||
@apply inline;
|
||||
}
|
||||
|
||||
.nodeClose {
|
||||
@apply text-[1.6rem] text-center cursor-pointer;
|
||||
}
|
||||
|
||||
.nodeBody {
|
||||
@apply w-full flex-1 flex flex-col justify-center bg-white rounded-b-[0.5rem] p-[1rem] text-[#5a5e66] cursor-pointer box-border;
|
||||
}
|
||||
|
||||
.nodeConditionBody {
|
||||
@apply bg-[#f8fafc] rounded-[0.5rem];
|
||||
}
|
||||
|
||||
.nodeError {
|
||||
|
||||
box-shadow: 0 0 1rem 0.2rem rgba(243, 5, 5, 0.5);
|
||||
}
|
||||
|
||||
.nodeError:hover {
|
||||
box-shadow: 0 0 1.2rem 0.4rem rgba(243, 5, 5, 0.5);
|
||||
}
|
||||
|
||||
.nodeErrorMsg {
|
||||
@apply absolute top-1/2 -translate-y-1/2 -right-[5.5rem] z-[1];
|
||||
}
|
||||
|
||||
.nodeErrorMsgBox {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.nodeErrorIcon {
|
||||
@apply w-[2.5rem] h-[2.5rem] cursor-pointer;
|
||||
}
|
||||
|
||||
.nodeErrorTips {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 4.5rem;
|
||||
min-width: 15rem;
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0.5rem 0.5rem 1rem 0.2rem rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
|
||||
.nodeErrorTips::before {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 1rem;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -2rem;
|
||||
transform: translateY(-50%);
|
||||
border-color: transparent #FFFFFF transparent transparent;
|
||||
}
|
||||
|
||||
.nodeMove {
|
||||
@apply absolute top-1/2 -translate-y-1/2;
|
||||
}
|
||||
|
||||
.nodeMoveLeft {
|
||||
@apply -left-[3rem];
|
||||
}
|
||||
|
||||
.nodeMoveRight {
|
||||
@apply -right-[3rem];
|
||||
}
|
||||
|
||||
.nodeMoveIcon {
|
||||
@apply w-[3.5rem] h-[3.5rem] cursor-pointer;
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { useController } from '@components/flowChart/useController'
|
||||
import nodeOptions from '@components/flowChart/lib/config'
|
||||
import { useDialog } from '@baota/naive-ui/hooks'
|
||||
import { $t } from '@locales/index'
|
||||
import { CONDITION, EXECUTE_RESULT_CONDITION, START } from '@components/flowChart/lib/alias'
|
||||
import { useNodeValidator } from '@components/flowChart/lib/verify'
|
||||
|
||||
import AddNode from '@components/flowChart/components/other/addNode/index'
|
||||
import SvgIcon from '@components/svgIcon'
|
||||
|
||||
import type { BaseNodeData, NodeNum, BaseRenderNodeOptions, BaseNodeProps } from '@components/flowChart/types'
|
||||
|
||||
import styles from './index.module.css'
|
||||
import ErrorNode from '../errorNode'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BaseNode',
|
||||
props: {
|
||||
// 节点数据
|
||||
node: {
|
||||
type: Object as PropType<BaseNodeData>,
|
||||
required: true, // 自读
|
||||
},
|
||||
},
|
||||
setup(props: BaseNodeProps) {
|
||||
// ====================== 基础状态数据 ======================
|
||||
const { validator, validate } = useNodeValidator() // 验证器
|
||||
const tempNodeId = ref(props.node.id || uuidv4()) // 节点id
|
||||
const config = ref<BaseRenderNodeOptions<BaseNodeData>>(nodeOptions[props.node.type]() || {}) // 节点配置
|
||||
const nodeNameRef = ref<HTMLInputElement | null>(null) // 节点名称输入框
|
||||
const isShowEditNodeName = ref(false) // 是否显示编辑节点名称
|
||||
const inputValue = ref(props.node.name) // 输入框值
|
||||
const renderNodeContent = shallowRef() // 节点组件
|
||||
const { removeNode, updateNode } = useStore()
|
||||
const { handleSelectNode } = useController()
|
||||
|
||||
// ====================== 节点状态数据 ======================
|
||||
// 错误状态
|
||||
const errorState = ref({
|
||||
isError: false,
|
||||
message: null as string | null,
|
||||
showTips: false,
|
||||
})
|
||||
|
||||
// ====================== 计算属性 ======================
|
||||
// 是否是开始节点
|
||||
const isStart = computed(() => props.node.type === START)
|
||||
|
||||
// 是否可以删除
|
||||
const isRemoved = computed(() => config.value?.operateNode?.remove)
|
||||
|
||||
// 是否是条件节点
|
||||
const isCondition = computed(() => [CONDITION, EXECUTE_RESULT_CONDITION].includes(props.node.type))
|
||||
|
||||
// 根据节点类型获取图标
|
||||
const typeIcon: ComputedRef<string> = computed(() => {
|
||||
const type = {
|
||||
success: 'flow-success',
|
||||
fail: 'flow-error',
|
||||
}
|
||||
// console.log(props.node.config?.type)
|
||||
if (props.node.type === EXECUTE_RESULT_CONDITION)
|
||||
return (type[props.node.config?.type as keyof typeof type] || '') as string
|
||||
return ''
|
||||
})
|
||||
|
||||
// 根据节点类型获取图标颜色
|
||||
const typeIconColor: ComputedRef<string> = computed(() => {
|
||||
if (props.node.type === EXECUTE_RESULT_CONDITION) return (props.node.config?.type || '') as string
|
||||
return '#FFFFFF'
|
||||
})
|
||||
|
||||
const nodeComponents = import.meta.glob('../../task/**/index.tsx')
|
||||
// ====================== 数据监听与副作用 ======================
|
||||
// 监听节点数据,更新节点配置
|
||||
watch(
|
||||
() => props.node,
|
||||
() => {
|
||||
config.value = nodeOptions[props.node.type as NodeNum]() // 更新节点配置
|
||||
inputValue.value = props.node.name // 更新节点名称
|
||||
tempNodeId.value = props.node.id || uuidv4() // 更新节点id
|
||||
validator.validateAll() // 验证器验证
|
||||
const NodeComp =
|
||||
nodeComponents[`../../task/${props.node.type}Node/index.tsx`] ||
|
||||
import('@components/flowChart/components/base/errorNode')
|
||||
renderNodeContent.value = defineAsyncComponent({
|
||||
loader: NodeComp as Promise<Component>,
|
||||
loadingComponent: () => <div>Loading...</div>,
|
||||
errorComponent: () => <ErrorNode />,
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// ====================== 渲染节点内容 ======================
|
||||
|
||||
// // 渲染节点内容
|
||||
// const renderNodeContent = defineAsyncComponent({
|
||||
// loader: () =>
|
||||
// (nodeComp ? nodeComp : import('@components/flowChart/components/base/errorNode')) as Promise<Component>,
|
||||
// loadingComponent: () => <div>Loading...</div>,
|
||||
// errorComponent: () => <ErrorNode />,
|
||||
// })
|
||||
|
||||
// ====================== 节点操作方法 ======================
|
||||
// 显示错误提示
|
||||
const showErrorTips = (flag: boolean) => {
|
||||
errorState.value.showTips = flag
|
||||
}
|
||||
|
||||
// 删除节点
|
||||
const removeFindNode = (ev: MouseEvent, id: string, node: BaseNodeData) => {
|
||||
const validator = validate(id)
|
||||
console.log(validator)
|
||||
if (validator.valid) {
|
||||
useDialog({
|
||||
type: 'warning',
|
||||
title: $t('t_1_1745765875247', { name: node.name }),
|
||||
content: node.type === CONDITION ? $t('t_2_1745765875918') : $t('t_3_1745765920953'),
|
||||
onPositiveClick: () => removeNode(id),
|
||||
})
|
||||
}
|
||||
// 如果节点类型是条件节点或验证不通过,则删除节点
|
||||
if ([EXECUTE_RESULT_CONDITION].includes(node.type) || !validator.valid) {
|
||||
removeNode(id)
|
||||
}
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
// 点击节点
|
||||
const handleNodeClick = () => {
|
||||
handleSelectNode(props.node.id || '', props.node.type)
|
||||
}
|
||||
|
||||
// ====================== 事件处理函数 ======================
|
||||
// 回车保存
|
||||
const keyupSaveNodeName = (e: KeyboardEvent) => {
|
||||
if (e.keyCode === 13) {
|
||||
isShowEditNodeName.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存节点名称
|
||||
const saveNodeName = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
inputValue.value = target.value
|
||||
updateNode(tempNodeId.value, { name: inputValue.value })
|
||||
}
|
||||
|
||||
// ====================== 渲染函数 ======================
|
||||
return () => (
|
||||
<div class={[styles.node, !isStart.value && styles.nodeArrows]}>
|
||||
<div class={[styles.nodeContent, isCondition.value && styles.nodeCondition]} onClick={handleNodeClick}>
|
||||
{/* 节点头部 */}
|
||||
<div
|
||||
class={[
|
||||
styles.nodeHeader,
|
||||
isCondition.value && styles.nodeConditionHeader,
|
||||
!typeIcon.value ? styles.nodeHeaderBranch : '',
|
||||
]}
|
||||
style={{
|
||||
color: config.value?.title?.color,
|
||||
backgroundColor: config.value?.title?.bgColor,
|
||||
}}
|
||||
>
|
||||
{/* 节点图标 */}
|
||||
{typeIcon.value ? (
|
||||
<SvgIcon
|
||||
icon={typeIcon.value ? typeIcon.value : config.value?.icon?.name || ''}
|
||||
class={[styles.nodeIcon, '!absolute top-[50%] left-[1rem] -mt-[.8rem]']}
|
||||
color={typeIconColor.value}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* 节点标题 */}
|
||||
<div class={styles.nodeHeaderTitle} title="点击编辑">
|
||||
<div class={styles.nodeHeaderTitleInput}>
|
||||
<input
|
||||
ref={nodeNameRef}
|
||||
value={inputValue.value}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onInput={saveNodeName}
|
||||
onBlur={() => (isShowEditNodeName.value = false)}
|
||||
onKeyup={keyupSaveNodeName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
{isRemoved.value && (
|
||||
<span
|
||||
onClick={(ev) => removeFindNode(ev, tempNodeId.value, props.node)}
|
||||
class="flex items-center justify-center absolute top-[50%] right-[1rem] -mt-[.9rem]"
|
||||
>
|
||||
<SvgIcon class={styles.nodeClose} icon="close" color={isCondition.value ? '#333' : '#FFFFFF'} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 节点主体 */}
|
||||
{!isCondition.value ? (
|
||||
<div class={[styles.nodeBody]}>
|
||||
{renderNodeContent.value &&
|
||||
h(renderNodeContent.value, {
|
||||
id: props.node.id,
|
||||
node: props.node || {},
|
||||
class: 'text-center',
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{/* 错误提示 */}
|
||||
{errorState.value.showTips && (
|
||||
<div class={styles.nodeErrorMsg}>
|
||||
<div class={styles.nodeErrorMsgBox}>
|
||||
<span onMouseenter={() => showErrorTips(true)} onMouseleave={() => showErrorTips(false)}>
|
||||
<SvgIcon class={styles.nodeErrorIcon} icon="tips" color="red" />
|
||||
</span>
|
||||
{errorState.value.message && <div class={styles.nodeErrorTips}>{errorState.value.message}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 添加节点组件 */}
|
||||
<AddNode node={props.node} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
.flowNodeBranch {
|
||||
@apply flex flex-col justify-center w-full relative max-w-full overflow-visible;
|
||||
}
|
||||
|
||||
/* 多列分支样式 */
|
||||
.multipleColumns {
|
||||
@apply w-full
|
||||
}
|
||||
|
||||
.flowNodeBranchBox {
|
||||
@apply flex flex-row w-full flex-nowrap min-h-[50px] relative overflow-visible;
|
||||
}
|
||||
|
||||
/* 有嵌套分支的容器样式 */
|
||||
.hasNestedBranch {
|
||||
@apply w-full justify-around;
|
||||
}
|
||||
|
||||
.flowNodeBranchCol {
|
||||
@apply flex flex-col items-center border-t-2 border-b-2 border-[#cacaca] pt-[50px] bg-[#f8fafc] flex-1 relative max-w-[50%];
|
||||
}
|
||||
|
||||
|
||||
/* 有嵌套分支时列宽调整 */
|
||||
.hasNestedBranch .flowNodeBranchCol {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
/* 多级嵌套分支样式调整 */
|
||||
.flowNodeBranchCol .flowNodeBranchCol {
|
||||
@apply min-w-[20rem] w-[24rem]
|
||||
}
|
||||
|
||||
|
||||
.flowNodeBranchCol::before {
|
||||
@apply content-[''] absolute top-0 left-0 right-0 bottom-0 z-0 m-auto w-[2px] h-full bg-[#cacaca];
|
||||
}
|
||||
|
||||
.coverLine {
|
||||
@apply absolute h-[8px] w-[calc(50%-1px)] bg-[#f8fafc];
|
||||
}
|
||||
|
||||
.topLeftCoverLine {
|
||||
@apply -top-[4px] left-0;
|
||||
}
|
||||
|
||||
.topRightCoverLine {
|
||||
@apply -top-[4px] right-0;
|
||||
}
|
||||
|
||||
.bottomLeftCoverLine {
|
||||
@apply -bottom-[4px] left-0;
|
||||
}
|
||||
|
||||
.bottomRightCoverLine {
|
||||
@apply -bottom-[4px] right-0;
|
||||
}
|
||||
|
||||
.rightCoverLine{
|
||||
@apply absolute w-[2px] bg-[#f8fafc] top-0 right-0 h-full;
|
||||
}
|
||||
|
||||
.leftCoverLine{
|
||||
@apply absolute w-[2px] bg-[#f8fafc] top-0 left-0 h-full;
|
||||
}
|
||||
|
||||
.flowConditionNodeAdd {
|
||||
@apply absolute left-1/2 -translate-x-1/2 -top-[15px] flex justify-center items-center z-[2] w-[70px] h-[30px] text-[12px] text-[#1c84c6] bg-white rounded-[20px] cursor-pointer shadow-md;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import nodeOptions from '@components/flowChart/lib/config'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { CONDITION } from '@components/flowChart/lib/alias'
|
||||
|
||||
import NodeWrap from '@components/flowChart/components/render/nodeWrap'
|
||||
import AddNode from '@components/flowChart/components/other/addNode'
|
||||
import styles from './index.module.css'
|
||||
import type { BaseRenderNodeOptions, BranchNodeData } from '@components/flowChart/types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BranchNode',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as () => BranchNodeData,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
setup(props: { node: BranchNodeData }) {
|
||||
const { addNode } = useStore() // 流程图数据
|
||||
const config = ref<BaseRenderNodeOptions<BranchNodeData>>(nodeOptions[props.node.type]() || {}) // 节点配置
|
||||
|
||||
watch(
|
||||
() => props.node.type,
|
||||
(newVal) => {
|
||||
config.value = nodeOptions[newVal]() || {}
|
||||
},
|
||||
)
|
||||
|
||||
// 添加分支
|
||||
const addCondition = () => {
|
||||
const tempNodeId = uuidv4() // 临时节点id
|
||||
addNode(
|
||||
props.node.id || '',
|
||||
CONDITION,
|
||||
{
|
||||
id: tempNodeId,
|
||||
name: `分支${(props.node.conditionNodes?.length || 0) + 1}`,
|
||||
},
|
||||
props.node.conditionNodes?.length,
|
||||
)
|
||||
}
|
||||
|
||||
// 计算容器类名,根据分支数量调整样式
|
||||
const getContainerClass = () => {
|
||||
const count = props.node.conditionNodes?.length || 0
|
||||
const baseClass = styles.flowNodeBranch
|
||||
|
||||
// 分支数量多时添加特殊类
|
||||
if (count > 3) {
|
||||
return `${baseClass} ${styles.multipleColumns}`
|
||||
}
|
||||
|
||||
return baseClass
|
||||
}
|
||||
|
||||
// 计算分支盒子类名,处理多层嵌套情况
|
||||
const getBoxClass = () => {
|
||||
// 检查是否有嵌套的分支节点
|
||||
const hasNestedBranch = props.node.conditionNodes?.some(
|
||||
(node) => node.childNode && ['branch', 'execute_result_branch'].includes(node.childNode.type),
|
||||
)
|
||||
|
||||
const baseClass = styles.flowNodeBranchBox
|
||||
if (hasNestedBranch) {
|
||||
return `${baseClass} ${styles.hasNestedBranch}`
|
||||
}
|
||||
|
||||
return baseClass
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={getContainerClass()}>
|
||||
{config.value.operateNode?.addBranch && (
|
||||
<div class={styles.flowConditionNodeAdd} onClick={addCondition}>
|
||||
{config.value.operateNode?.addBranchTitle || '添加分支'}
|
||||
</div>
|
||||
)}
|
||||
<div class={getBoxClass()}>
|
||||
{props.node.conditionNodes?.map((condition, index: number) => (
|
||||
<div
|
||||
class={styles.flowNodeBranchCol}
|
||||
key={index}
|
||||
data-branch-index={index}
|
||||
data-branches-count={props.node.conditionNodes?.length}
|
||||
>
|
||||
{/* 条件节点 */}
|
||||
<NodeWrap node={condition} />
|
||||
{/* 用来遮挡最左列的线 */}
|
||||
{index === 0 && (
|
||||
<div>
|
||||
<div class={`${styles.coverLine} ${styles.topLeftCoverLine}`} />
|
||||
<div class={`${styles.coverLine} ${styles.bottomLeftCoverLine}`} />
|
||||
<div class={`${styles.rightCoverLine}`} />
|
||||
</div>
|
||||
)}
|
||||
{/* 用来遮挡最右列的线 */}
|
||||
{index === (props.node.conditionNodes?.length || 0) - 1 && (
|
||||
<div>
|
||||
<div class={`${styles.coverLine} ${styles.topRightCoverLine}`} />
|
||||
<div class={`${styles.coverLine} ${styles.bottomRightCoverLine}`} />
|
||||
<div class={`${styles.leftCoverLine}`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<AddNode node={props.node} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import nodeOptions from '@components/flowChart/lib/config'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { CONDITION } from '@components/flowChart/lib/alias'
|
||||
|
||||
import NodeWrap from '@components/flowChart/components/render/nodeWrap'
|
||||
import AddNode from '@components/flowChart/components/other/addNode'
|
||||
import styles from '../branchNode/index.module.css'
|
||||
import type { BaseRenderNodeOptions, ExecuteResultBranchNodeData } from '@components/flowChart/types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BranchNode',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as () => ExecuteResultBranchNodeData,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
setup(props: { node: ExecuteResultBranchNodeData }) {
|
||||
const { addNode } = useStore() // 流程图数据
|
||||
const config = ref<BaseRenderNodeOptions<ExecuteResultBranchNodeData>>(nodeOptions[props.node.type]() || {}) // 节点配置
|
||||
|
||||
watch(
|
||||
() => props.node.type,
|
||||
(newVal) => {
|
||||
config.value = nodeOptions[newVal]() || {}
|
||||
},
|
||||
)
|
||||
|
||||
// 添加条件
|
||||
const addCondition = () => {
|
||||
const tempNodeId = uuidv4() // 临时节点id
|
||||
addNode(
|
||||
props.node.id || '',
|
||||
CONDITION,
|
||||
{
|
||||
id: tempNodeId,
|
||||
name: `分支${(props.node.conditionNodes?.length || 0) + 1}`,
|
||||
},
|
||||
props.node.conditionNodes?.length,
|
||||
)
|
||||
}
|
||||
|
||||
// 计算容器类名,根据分支数量调整样式
|
||||
const getContainerClass = () => {
|
||||
const count = props.node.conditionNodes?.length || 0
|
||||
const baseClass = styles.flowNodeBranch
|
||||
|
||||
// 分支数量多时添加特殊类
|
||||
if (count > 3) {
|
||||
return `${baseClass} ${styles.multipleColumns}`
|
||||
}
|
||||
|
||||
return baseClass
|
||||
}
|
||||
|
||||
// 计算分支盒子类名,处理多层嵌套情况
|
||||
const getBoxClass = () => {
|
||||
// 检查是否有嵌套的分支节点
|
||||
const hasNestedBranch = props.node.conditionNodes?.some(
|
||||
(node) => node.childNode && ['branch', 'execute_result_branch'].includes(node.childNode.type),
|
||||
)
|
||||
const baseClass = styles.flowNodeBranchBox
|
||||
if (hasNestedBranch) {
|
||||
return `${baseClass} ${styles.hasNestedBranch}`
|
||||
}
|
||||
return baseClass
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={getContainerClass()}>
|
||||
{config.value.operateNode?.addBranch && (
|
||||
<div class={styles.flowConditionNodeAdd} onClick={addCondition}>
|
||||
{config.value.operateNode?.addBranchTitle || '添加分支'}
|
||||
</div>
|
||||
)}
|
||||
<div class={getBoxClass()}>
|
||||
{props.node.conditionNodes?.map((condition, index: number) => (
|
||||
<div
|
||||
class={styles.flowNodeBranchCol}
|
||||
key={index}
|
||||
data-branch-index={index}
|
||||
data-branches-count={props.node.conditionNodes?.length}
|
||||
>
|
||||
{/* 条件节点 */}
|
||||
<NodeWrap node={condition} />
|
||||
{/* 用来遮挡最左列的线 */}
|
||||
{index === 0 && (
|
||||
<div>
|
||||
<div class={`${styles.coverLine} ${styles.topLeftCoverLine}`} />
|
||||
<div class={`${styles.coverLine} ${styles.bottomLeftCoverLine}`} />
|
||||
<div class={`${styles.rightCoverLine}`} />
|
||||
</div>
|
||||
)}
|
||||
{/* 用来遮挡最右列的线 */}
|
||||
{index === (props.node.conditionNodes?.length || 0) - 1 && (
|
||||
<div>
|
||||
<div class={`${styles.coverLine} ${styles.topRightCoverLine}`} />
|
||||
<div class={`${styles.coverLine} ${styles.bottomRightCoverLine}`} />
|
||||
<div class={`${styles.leftCoverLine}`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<AddNode node={props.node} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
export default defineComponent({
|
||||
name: 'EndNode',
|
||||
setup() {
|
||||
return () => (
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="w-[1.5rem] h-[1.5rem] rounded-[1rem] bg-[#cacaca]"></div>
|
||||
<div class="text-[#5a5e66] mb-[10rem]">流程结束</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { BaseNodeData } from '@components/flowChart/types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BranchNode',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as () => BaseNodeData,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return () => <div>渲染节点失败,请检查类型是否支持</div>
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,129 @@
|
||||
.add {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.add::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
margin: auto;
|
||||
width: 0.2rem;
|
||||
height: 100%;
|
||||
background-color: #cacaca;
|
||||
}
|
||||
|
||||
.addBtn {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -1.2rem;
|
||||
margin-top: -2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
border-radius: 4rem;
|
||||
background-color: #1c84c6;
|
||||
box-shadow: 0.5rem 0.5rem 1rem 0.2rem rgba(0, 0, 0, 0.2);
|
||||
transition-property: width, height;
|
||||
transition-duration: 0.1s;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* .addBtn:hover {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
} */
|
||||
|
||||
.addBtnIcon {
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.addSelectBox {
|
||||
position: absolute;
|
||||
z-index: 9999999999999999;
|
||||
top: -.8rem;
|
||||
min-width: 160px;
|
||||
padding: 4px;
|
||||
list-style-type: none;
|
||||
background-color: #ffffff;
|
||||
background-clip: padding-box;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.addSelectBox::before {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 1rem solid;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.addSelectItem {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
padding: 5px 12px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 1.5714285714285714;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.addSelectItem:hover {
|
||||
background-color: #1e83e9 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.addSelectItemIcon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.addSelectItemTitle {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.addSelected {
|
||||
background-color: #1e83e9 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.addLeft {
|
||||
right: 3.4rem;
|
||||
}
|
||||
|
||||
.addLeft::before {
|
||||
right: -2rem;
|
||||
border-color: transparent transparent transparent #FFFFFF;
|
||||
}
|
||||
|
||||
.addRight {
|
||||
left: 3.4rem;
|
||||
}
|
||||
|
||||
.addRight::before {
|
||||
left: -2rem;
|
||||
border-color: transparent #FFFFFF transparent transparent;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useAddNodeController } from '@components/flowChart/useController'
|
||||
import SvgIcon from '@components/svgIcon'
|
||||
import styles from './index.module.css'
|
||||
|
||||
import type {
|
||||
NodeIcon,
|
||||
NodeNum,
|
||||
NodeTitle,
|
||||
BaseNodeData,
|
||||
BranchNodeData,
|
||||
BaseRenderNodeOptions,
|
||||
} from '@components/flowChart/types'
|
||||
import nodeOptions from '@components/flowChart/lib/config'
|
||||
|
||||
interface NodeSelect {
|
||||
title: NodeTitle
|
||||
type: NodeNum
|
||||
icon: NodeIcon
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AddNode',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<BaseNodeData>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const {
|
||||
isShowAddNodeSelect,
|
||||
nodeSelectList,
|
||||
addNodeBtnRef,
|
||||
addNodeSelectRef,
|
||||
addNodeSelectPostion,
|
||||
showNodeSelect,
|
||||
addNodeData,
|
||||
itemNodeSelected,
|
||||
excludeNodeSelectList,
|
||||
} = useAddNodeController()
|
||||
|
||||
const config = ref<BaseRenderNodeOptions<BaseNodeData | BranchNodeData>>() // 节点配置
|
||||
|
||||
watch(
|
||||
() => props.node.type,
|
||||
(newVal) => {
|
||||
config.value = nodeOptions[newVal]() || {}
|
||||
},
|
||||
)
|
||||
|
||||
return () => (
|
||||
<div class={styles.add}>
|
||||
<div
|
||||
ref={addNodeBtnRef}
|
||||
class={styles.addBtn}
|
||||
onMouseenter={() => showNodeSelect(true, props.node.type as NodeNum)}
|
||||
onMouseleave={() => showNodeSelect(false)}
|
||||
>
|
||||
<SvgIcon icon="plus" class={styles.addBtnIcon} color="#FFFFFF" />
|
||||
{isShowAddNodeSelect.value && (
|
||||
<ul
|
||||
ref={addNodeSelectRef}
|
||||
class={[styles.addSelectBox, addNodeSelectPostion.value === 1 ? styles.addLeft : styles.addRight]}
|
||||
>
|
||||
{nodeSelectList.value.map((item: NodeSelect) => {
|
||||
// 判断类型是否支持添加
|
||||
if (!excludeNodeSelectList.value?.includes(item.type)) {
|
||||
return (
|
||||
<li
|
||||
key={item.type}
|
||||
class={[styles.addSelectItem, item.selected && styles.addSelected]}
|
||||
onClick={() => addNodeData(props.node, item.type)}
|
||||
onMouseenter={itemNodeSelected}
|
||||
>
|
||||
<SvgIcon
|
||||
icon={'flow-' + item.icon.name}
|
||||
class={styles.addSelectItemIcon}
|
||||
color={item.selected ? '#FFFFFF' : item.icon.color}
|
||||
/>
|
||||
<div class={styles.addSelectItemTitle}>{item.title.name}</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import { NEmpty } from 'naive-ui'
|
||||
import type { BaseNodeData } from '@/components/flowChart/types'
|
||||
import { $t } from '@locales/index'
|
||||
|
||||
type AsyncComponentLoader = () => Promise<Component>
|
||||
|
||||
/**
|
||||
* 节点配置抽屉组件
|
||||
* 用于显示节点配置界面,根据节点类型动态加载对应的配置组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'FlowChartDrawer',
|
||||
props: {
|
||||
/**
|
||||
* 节点数据
|
||||
*/
|
||||
node: {
|
||||
type: Object as PropType<BaseNodeData | null>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
/**
|
||||
* 节点配置组件Map
|
||||
*/
|
||||
const nodeConfigComponents = shallowRef<Record<string, any>>({})
|
||||
|
||||
/**
|
||||
* 动态导入节点配置组件
|
||||
* 使用import.meta.glob导入所有drawer组件
|
||||
* 1. 任务节点配置组件
|
||||
* 2. 基础节点配置组件
|
||||
*/
|
||||
const taskDrawers: Record<string, AsyncComponentLoader> = import.meta.glob('../task/*/drawer.tsx') as Record<
|
||||
string,
|
||||
AsyncComponentLoader
|
||||
>
|
||||
|
||||
// 预加载所有抽屉组件
|
||||
const loadComponents = () => {
|
||||
// 加载任务节点组件
|
||||
Object.keys(taskDrawers).forEach((path) => {
|
||||
const matches = path.match(/\.\.\/task\/(\w+)\/drawer\.tsx/)
|
||||
if (matches && matches[1]) {
|
||||
const nodeType = matches[1].replace('Node', '').toLowerCase()
|
||||
const loaderFn = taskDrawers[path]
|
||||
if (loaderFn) {
|
||||
nodeConfigComponents.value[nodeType] = defineAsyncComponent(loaderFn)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染节点配置组件
|
||||
*/
|
||||
const renderConfigComponent = computed(() => {
|
||||
if (!props.node || !props.node.type) {
|
||||
return h(NEmpty, {
|
||||
description: $t('t_2_1744870863419'),
|
||||
})
|
||||
}
|
||||
const nodeType = props.node.type
|
||||
// 查找对应类型的配置组件
|
||||
if (nodeConfigComponents.value[nodeType]) {
|
||||
return h(nodeConfigComponents.value[nodeType], { node: props.node })
|
||||
}
|
||||
// 找不到对应的配置组件时显示提示
|
||||
return h(NEmpty, {
|
||||
description: $t('t_3_1744870864615'),
|
||||
})
|
||||
})
|
||||
|
||||
loadComponents()
|
||||
|
||||
return () => (
|
||||
<div class=" h-full w-full bg-white transform transition-transform duration-300 flex flex-col p-[1.5rem]">
|
||||
{renderConfigComponent.value}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import { BRANCH, EXECUTE_RESULT_BRANCH } from '@components/flowChart/lib/alias'
|
||||
import BranchNode from '@components/flowChart/components/base/branchNode'
|
||||
import ConditionNode from '@components/flowChart/components/base/conditionNode'
|
||||
import BaseNode from '@components/flowChart/components/base/baseNode'
|
||||
import NodeWrap from '@components/flowChart/components/render/nodeWrap'
|
||||
|
||||
import type { BaseNodeData, BranchNodeData, ExecuteResultBranchNodeData } from '@components/flowChart/types'
|
||||
|
||||
interface NodeWrapProps {
|
||||
node?: BaseNodeData | BranchNodeData
|
||||
depth?: number
|
||||
}
|
||||
|
||||
// 自定义样式
|
||||
const styles = {
|
||||
flowNodeWrap: 'flex flex-col items-center w-full relative',
|
||||
flowNodeWrapNested: 'nested-node-wrap w-full',
|
||||
flowNodeWrapDeep: 'deep-nested-node-wrap w-full',
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeWrap',
|
||||
props: {
|
||||
// 节点数据
|
||||
node: {
|
||||
type: Object as PropType<BaseNodeData | BranchNodeData | ExecuteResultBranchNodeData>,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 嵌套深度
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: ['select'],
|
||||
setup(props: NodeWrapProps, { emit }) {
|
||||
// 计算当前节点的嵌套深度样式类
|
||||
const getDepthClass = () => {
|
||||
if (props.depth && props.depth > 1) {
|
||||
return props.depth > 2 ? styles.flowNodeWrapDeep : styles.flowNodeWrapNested
|
||||
}
|
||||
return styles.flowNodeWrap
|
||||
}
|
||||
|
||||
// 选中节点
|
||||
const handleSelect = (node: BaseNodeData | BranchNodeData | ExecuteResultBranchNodeData) => {
|
||||
if (node.id) emit('select', node.id)
|
||||
}
|
||||
|
||||
return {
|
||||
getDepthClass,
|
||||
handleSelect,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
if (!this.node) return null
|
||||
const currentDepth = this.depth || 0
|
||||
const nextDepth = currentDepth + 1
|
||||
|
||||
return (
|
||||
<div class={this.getDepthClass()}>
|
||||
{/* 判断是否为分支节点或普通节点 */}
|
||||
{this.node.type === BRANCH ? <BranchNode node={this.node as BranchNodeData} /> : null}
|
||||
|
||||
{/* 判断是否为条件节点 */}
|
||||
{this.node.type === EXECUTE_RESULT_BRANCH ? (
|
||||
<ConditionNode node={this.node as ExecuteResultBranchNodeData} />
|
||||
) : null}
|
||||
|
||||
{/* 判断是否为普通节点 */}
|
||||
{![BRANCH, EXECUTE_RESULT_BRANCH].includes(this.node.type) ? <BaseNode node={this.node} /> : null}
|
||||
|
||||
{/* 判断是否存在子节点 */}
|
||||
{this.node.childNode?.type && (
|
||||
<NodeWrap node={this.node.childNode} depth={nextDepth} onSelect={(nodeId) => this.$emit('select', nodeId)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
BIN
frontend/apps/allin-ssl/src/components/flowChart/components/task/.DS_Store
vendored
Normal file
BIN
frontend/apps/allin-ssl/src/components/flowChart/components/task/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,103 @@
|
||||
import { NFormItem, NInputNumber } from 'naive-ui'
|
||||
import { useForm, useFormHooks, useModalHooks } from '@baota/naive-ui/hooks'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { $t } from '@locales/index'
|
||||
import rules from './verify'
|
||||
import DnsProviderSelect from '@components/dnsProviderSelect'
|
||||
|
||||
import type { ApplyNodeConfig } from '@components/flowChart/types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ApplyNodeDrawer',
|
||||
props: {
|
||||
// 节点配置数据
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; config: ApplyNodeConfig }>,
|
||||
default: () => ({
|
||||
id: '',
|
||||
config: {},
|
||||
}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { updateNodeConfig, isRefreshNode } = useStore()
|
||||
// 弹窗辅助
|
||||
const { confirm } = useModalHooks()
|
||||
// 获取表单助手函数
|
||||
const { useFormInput } = useFormHooks()
|
||||
|
||||
// 表单参数
|
||||
const param = ref<ApplyNodeConfig>(
|
||||
Object.keys(props.node.config).length > 0
|
||||
? props.node.config
|
||||
: { domains: '', email: '', provider_id: '', provider: '', end_day: 30 },
|
||||
)
|
||||
|
||||
// 表单渲染配置
|
||||
const config = computed(() => {
|
||||
// 基本选项
|
||||
return [
|
||||
useFormInput($t('t_17_1745227838561'), 'domains', {
|
||||
placeholder: $t('t_0_1745735774005'),
|
||||
}),
|
||||
useFormInput($t('t_1_1745735764953'), 'email', {
|
||||
placeholder: $t('t_2_1745735773668'),
|
||||
}),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => {
|
||||
return (
|
||||
<DnsProviderSelect
|
||||
type="dns"
|
||||
path="provider_id"
|
||||
value={param.value.provider_id}
|
||||
onUpdate:value={(val: { value: string; type: string }) => {
|
||||
param.value.provider_id = val.value
|
||||
param.value.provider = val.type
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => {
|
||||
return (
|
||||
<NFormItem label={$t('t_5_1745735769112')} path="end_day">
|
||||
<NInputNumber
|
||||
v-model:value={param.value.end_day}
|
||||
showButton={false}
|
||||
min={1}
|
||||
class="w-[180px]"
|
||||
placeholder={$t('t_6_1745735765205')}
|
||||
/>
|
||||
<span class="text-[1.4rem] ml-[1.2rem]">{$t('t_7_1745735768326')}</span>
|
||||
</NFormItem>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// 创建表单实例
|
||||
const { component: Form, data, example } = useForm<ApplyNodeConfig>({ defaultValue: param, config, rules })
|
||||
|
||||
// 确认事件触发
|
||||
confirm(async (close) => {
|
||||
try {
|
||||
await example.value?.validate()
|
||||
updateNodeConfig(props.node.id, data.value) // 更新节点配置
|
||||
isRefreshNode.value = props.node.id // 刷新节点
|
||||
close()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class="apply-node-drawer">
|
||||
<Form labelPlacement="top" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useNodeValidator } from '@components/flowChart/lib/verify'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { useThemeCssVar } from '@baota/naive-ui/theme'
|
||||
import { $t } from '@locales/index'
|
||||
import rules from './verify'
|
||||
import type { ApplyNodeConfig } from '@components/flowChart/types'
|
||||
|
||||
interface NodeProps {
|
||||
node: {
|
||||
id: string
|
||||
config: ApplyNodeConfig
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ApplyNode',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; config: ApplyNodeConfig }>,
|
||||
default: () => ({ id: '', config: {} }),
|
||||
},
|
||||
},
|
||||
setup(props: NodeProps) {
|
||||
// 注册验证器
|
||||
const { isRefreshNode } = useStore()
|
||||
// 初始化节点状态
|
||||
const { registerCompatValidator, validate, validationResult, unregisterValidator } = useNodeValidator()
|
||||
// 主题色
|
||||
const cssVar = useThemeCssVar(['warningColor', 'primaryColor'])
|
||||
|
||||
// 是否有效
|
||||
const validColor = computed(() => {
|
||||
return validationResult.value.valid ? 'var(--n-primary-color)' : 'var(--n-warning-color)'
|
||||
})
|
||||
|
||||
// 监听是否刷新节点
|
||||
watch(
|
||||
() => isRefreshNode.value,
|
||||
(newVal) => {
|
||||
useTimeoutFn(() => {
|
||||
registerCompatValidator(props.node.id, rules, props.node.config)
|
||||
validate(props.node.id)
|
||||
isRefreshNode.value = null
|
||||
}, 500)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
//
|
||||
onUnmounted(() => unregisterValidator(props.node.id))
|
||||
|
||||
// 渲染节点状态
|
||||
return () => (
|
||||
<div style={cssVar.value} class="text-[12px]">
|
||||
<div style={{ color: validColor.value }}>
|
||||
{validationResult.value.valid ? '域名:' + props.node.config?.domains : $t('t_9_1745735765287')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { FormRules, FormItemRule } from 'naive-ui'
|
||||
import { isDomainGroup, isEmail } from '@baota/utils/business'
|
||||
import { $t } from '@locales/index'
|
||||
|
||||
export default {
|
||||
domains: {
|
||||
required: true,
|
||||
trigger: 'input',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!isDomainGroup(value)) {
|
||||
reject(new Error($t('t_0_1745553910661')))
|
||||
} else if (!value) {
|
||||
reject(new Error($t('t_0_1746697487119')))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
email: {
|
||||
required: true,
|
||||
trigger: 'input',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!isEmail(value)) {
|
||||
reject(new Error($t('t_1_1745553909483')))
|
||||
} else if (!value) {
|
||||
reject(new Error($t('t_1_1746697485188')))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
provider_id: {
|
||||
required: true,
|
||||
trigger: 'change',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!value) {
|
||||
reject(new Error($t('t_3_1745490735059')))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
end_day: {
|
||||
required: true,
|
||||
trigger: 'input',
|
||||
validator: (rule: FormItemRule, value: number) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!value) {
|
||||
reject(new Error($t('t_2_1745553907423')))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
} as FormRules
|
||||
@@ -0,0 +1,293 @@
|
||||
import { NButton, NCard, NStep, NSteps, NText, NTooltip } from 'naive-ui'
|
||||
import { useForm, useFormHooks, useModalClose, useModalOptions, useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useThemeCssVar } from '@baota/naive-ui/theme'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { DeployNodeConfig, DeployNodeInputsConfig } from '@components/flowChart/types'
|
||||
import { $t } from '@locales/index'
|
||||
import SvgIcon from '@components/svgIcon'
|
||||
import DnsProviderSelect from '@/components/dnsProviderSelect'
|
||||
|
||||
import styles from './index.module.css'
|
||||
import verifyRules from './verify'
|
||||
|
||||
type StepStatus = 'process' | 'wait' | 'finish' | 'error'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DeployNodeDrawer',
|
||||
props: {
|
||||
// 节点配置数据
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; config: DeployNodeConfig; inputs: DeployNodeInputsConfig[] }>,
|
||||
default: () => ({
|
||||
id: '',
|
||||
inputs: [],
|
||||
config: {
|
||||
provider: '',
|
||||
provider_id: '',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { updateNode, updateNodeConfig, findApplyUploadNodesUp, isRefreshNode } = useStore()
|
||||
// 获取表单助手函数
|
||||
const { useFormInput, useFormTextarea, useFormSelect } = useFormHooks()
|
||||
// 样式支持
|
||||
const cssVar = useThemeCssVar(['primaryColor', 'borderColor'])
|
||||
// 错误处理
|
||||
const { handleError } = useError()
|
||||
// 消息处理
|
||||
const message = useMessage()
|
||||
// 弹窗配置
|
||||
const modalOptions = useModalOptions()
|
||||
// 弹窗关闭
|
||||
const closeModal = useModalClose()
|
||||
|
||||
// 部署类型选项
|
||||
const deployTypeOptions = [
|
||||
{ label: $t('t_5_1744958839222'), value: 'ssh' },
|
||||
{ label: $t('t_10_1745735765165'), value: 'btpanel' },
|
||||
{ label: $t('t_11_1745735766456'), value: 'btpanel-site' },
|
||||
{ label: $t('t_12_1745735765571'), value: '1panel' },
|
||||
{ label: $t('t_13_1745735766084'), value: '1panel-site' },
|
||||
{ label: $t('t_14_1745735766121'), value: 'tencentcloud-cdn' },
|
||||
{ label: $t('t_15_1745735768976'), value: 'tencentcloud-cos' },
|
||||
{ label: $t('t_16_1745735766712'), value: 'aliyun-cdn' },
|
||||
{ label: $t('t_2_1746697487164'), value: 'aliyun-oss' },
|
||||
]
|
||||
const certOptions = ref<{ label: string; value: string }[]>([]) // 证书选项
|
||||
const current = ref(1) // 当前步骤
|
||||
const next = ref(true) // 是否是下一步
|
||||
const currentStatus = ref<StepStatus>('process') // 当前步骤状态
|
||||
// 表单参数
|
||||
const param = ref(
|
||||
Object.keys(props.node.config).length > 0
|
||||
? {
|
||||
...props.node.config,
|
||||
inputs: Array.isArray(props.node.inputs) ? props.node.inputs[0] : { fromNodeId: '', name: '' },
|
||||
}
|
||||
: {
|
||||
provider: '',
|
||||
provider_id: '',
|
||||
inputs: {
|
||||
fromNodeId: '',
|
||||
name: '',
|
||||
},
|
||||
},
|
||||
) as Ref<DeployNodeConfig & { inputs: DeployNodeInputsConfig }>
|
||||
|
||||
// 表单配置
|
||||
const formConfig = computed(() => {
|
||||
const config = []
|
||||
config.push(
|
||||
...[
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => {
|
||||
return (
|
||||
<DnsProviderSelect
|
||||
type={param.value.provider}
|
||||
path="provider_id"
|
||||
value={param.value.provider_id}
|
||||
onUpdate:value={(val: { value: number; type: string }) => {
|
||||
param.value.provider_id = val.value
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
useFormSelect($t('t_1_1745748290291'), 'inputs.fromNodeId', certOptions.value, {
|
||||
onUpdateValue: (val, option: { label: string; value: string }) => {
|
||||
param.value.inputs.fromNodeId = val
|
||||
param.value.inputs.name = option?.label
|
||||
},
|
||||
}),
|
||||
)
|
||||
switch (param.value.provider) {
|
||||
case 'ssh':
|
||||
config.push(
|
||||
...[
|
||||
useFormInput('证书文件路径(仅支持PEM格式)', 'certPath', { placeholder: $t('t_30_1746667591892') }),
|
||||
useFormInput('私钥文件路径', 'keyPath', { placeholder: $t('t_31_1746667593074') }),
|
||||
useFormTextarea(
|
||||
'前置命令',
|
||||
'beforeCmd',
|
||||
{ placeholder: $t('t_21_1745735769154') },
|
||||
{ showRequireMark: false },
|
||||
),
|
||||
useFormTextarea(
|
||||
'后置命令',
|
||||
'afterCmd',
|
||||
{ placeholder: $t('t_22_1745735767366') },
|
||||
{ showRequireMark: false },
|
||||
),
|
||||
],
|
||||
)
|
||||
break
|
||||
case 'btpanel-site':
|
||||
config.push(...[useFormInput('站点名称', 'siteName', { placeholder: $t('t_23_1745735766455') })])
|
||||
break
|
||||
case '1panel-site':
|
||||
config.push(...[useFormInput('站点ID', 'site_id', { placeholder: $t('t_24_1745735766826') })])
|
||||
break
|
||||
case 'tencentcloud-cdn':
|
||||
case 'aliyun-cdn':
|
||||
config.push(...[useFormInput('域名', 'domain', { placeholder: $t('t_0_1744958839535') })])
|
||||
break
|
||||
case 'tencentcloud-cos':
|
||||
case 'aliyun-oss':
|
||||
config.push(...[useFormInput('域名', 'domain', { placeholder: $t('t_0_1744958839535') })])
|
||||
config.push(...[useFormInput('区域', 'region', { placeholder: $t('t_25_1745735766651') })])
|
||||
config.push(...[useFormInput('存储桶', 'bucket', { placeholder: $t('t_26_1745735767144') })])
|
||||
break
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
/**
|
||||
* @description 下一步
|
||||
* @returns
|
||||
*/
|
||||
const nextStep = async () => {
|
||||
if (!param.value.provider) return message.error($t('t_19_1745735766810'))
|
||||
|
||||
// 加载证书来源选项
|
||||
certOptions.value = findApplyUploadNodesUp(props.node.id).map((item) => {
|
||||
return { label: item.name, value: item.id }
|
||||
})
|
||||
if (!certOptions.value.length) {
|
||||
message.warning($t('t_3_1745748298161'))
|
||||
} else if (!(param.value.inputs && param.value.inputs.fromNodeId)) {
|
||||
param.value.inputs = {} as DeployNodeInputsConfig
|
||||
param.value.inputs.name = certOptions.value[0]?.label || ''
|
||||
param.value.inputs.fromNodeId = certOptions.value[0]?.value || ''
|
||||
}
|
||||
current.value++
|
||||
next.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 上一步
|
||||
* @returns
|
||||
*/
|
||||
const prevStep = () => {
|
||||
current.value--
|
||||
next.value = true
|
||||
param.value.provider_id = ''
|
||||
param.value.provider = ''
|
||||
}
|
||||
|
||||
// 表单组件
|
||||
const { component: Form, example } = useForm<DeployNodeConfig>({
|
||||
config: formConfig,
|
||||
defaultValue: param,
|
||||
rules: verifyRules,
|
||||
})
|
||||
|
||||
/**
|
||||
* @description 提交
|
||||
* @returns
|
||||
*/
|
||||
const submit = async () => {
|
||||
try {
|
||||
await example.value?.validate()
|
||||
const tempData = param.value
|
||||
const inputs = tempData.inputs
|
||||
console.log(inputs, 'inputs', props.node)
|
||||
updateNode(
|
||||
props.node.id,
|
||||
{
|
||||
inputs: [inputs],
|
||||
config: {},
|
||||
},
|
||||
false,
|
||||
)
|
||||
delete tempData.inputs
|
||||
updateNodeConfig(props.node.id, {
|
||||
...tempData,
|
||||
})
|
||||
isRefreshNode.value = props.node.id
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 隐藏底部按钮
|
||||
modalOptions.value.footer = false
|
||||
// 如果已经选择了部署类型,则跳转到下一步
|
||||
if (param.value.provider) {
|
||||
if (props.node.inputs) param.value.inputs = props.node.inputs
|
||||
nextStep()
|
||||
}
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class={styles.container} style={cssVar.value}>
|
||||
<NSteps size="small" current={current.value} status={currentStatus.value}>
|
||||
<NStep title={$t('t_28_1745735766626')} description={$t('t_19_1745735766810')}></NStep>
|
||||
<NStep title={$t('t_29_1745735768933')} description={$t('t_2_1745738969878')}></NStep>
|
||||
</NSteps>
|
||||
{current.value === 1 && (
|
||||
<div class={styles.cardContainer}>
|
||||
{deployTypeOptions.map((item) => (
|
||||
<div
|
||||
key={item.value}
|
||||
class={`${styles.optionCard} ${param.value.provider === item.value ? styles.optionCardSelected : ''}`}
|
||||
onClick={() => {
|
||||
param.value.provider = item.value
|
||||
}}
|
||||
>
|
||||
<NCard contentClass={styles.cardContent} hoverable bordered={false}>
|
||||
<SvgIcon
|
||||
icon={`resources-${item.value.replace(/-[a-z]+$/, '')}`}
|
||||
size="2rem"
|
||||
class={`${styles.icon} ${param.value.provider === item.value ? styles.iconSelected : ''}`}
|
||||
/>
|
||||
<NText type={param.value.provider === item.value ? 'primary' : 'default'}>{item.label}</NText>
|
||||
</NCard>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{current.value === 2 && (
|
||||
<NCard class={styles.formContainer}>
|
||||
<Form labelPlacement="top" />
|
||||
</NCard>
|
||||
)}
|
||||
<div class={styles.footer}>
|
||||
<NButton class={styles.footerButton} onClick={closeModal}>
|
||||
{$t('t_4_1744870861589')}
|
||||
</NButton>
|
||||
<NTooltip
|
||||
trigger="hover"
|
||||
disabled={!!param.value.provider}
|
||||
v-slots={{
|
||||
trigger: () => (
|
||||
<NButton
|
||||
type={next.value ? 'primary' : 'default'}
|
||||
class={styles.footerButton}
|
||||
disabled={!param.value.provider}
|
||||
onClick={next.value ? nextStep : prevStep}
|
||||
>
|
||||
{next.value ? $t('t_27_1745735764546') : $t('t_0_1745738961258')}
|
||||
</NButton>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{next.value ? $t('t_4_1745765868807') : null}
|
||||
</NTooltip>
|
||||
{!next.value && (
|
||||
<NButton type="primary" onClick={submit}>
|
||||
{$t('t_1_1745738963744')}
|
||||
</NButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
/* Deploy Node Drawer Styles */
|
||||
|
||||
/* Card container styles */
|
||||
.cardContainer {
|
||||
@apply grid grid-cols-3 gap-4 mt-[2.4rem];
|
||||
}
|
||||
|
||||
/* Option card styles */
|
||||
.optionCard {
|
||||
@apply flex items-center justify-center rounded-[0.4rem] transition-all border-[1px] border-transparent;
|
||||
border-color: var(--n-border-color);
|
||||
}
|
||||
|
||||
.optionCardSelected {
|
||||
@apply border-[1px] relative overflow-hidden;
|
||||
border-color: var(--n-primary-color);
|
||||
}
|
||||
|
||||
/* Add checkmark for selected item */
|
||||
.optionCardSelected::after {
|
||||
content: '';
|
||||
@apply absolute bottom-[.1rem] right-[.1rem] w-[1rem] h-[1rem] rounded-full z-10;
|
||||
@apply bg-[length:14px_14px] bg-center bg-no-repeat;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Add triangle in bottom-right corner for selected item */
|
||||
.optionCardSelected::before {
|
||||
content: '';
|
||||
@apply absolute -bottom-[.1rem] -right-[.1rem] w-0 h-0 z-10 text-white text-xs flex items-center justify-center;
|
||||
border-style: solid;
|
||||
border-width: 0 0 20px 20px;
|
||||
border-color: transparent transparent var(--n-primary-color) transparent;
|
||||
line-height: 0;
|
||||
padding-left: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Card content styles */
|
||||
.cardContent {
|
||||
@apply flex flex-col items-center justify-center p-[4px] cursor-pointer;
|
||||
}
|
||||
|
||||
/* Icon styles */
|
||||
.icon {
|
||||
@apply mb-[0.4rem];
|
||||
}
|
||||
|
||||
.iconSelected {
|
||||
color: var(--n-primary-color);
|
||||
}
|
||||
|
||||
/* Footer styles */
|
||||
.footer {
|
||||
@apply flex justify-end absolute right-[1.2rem] -bottom-[1.2rem];
|
||||
}
|
||||
|
||||
.footerButton {
|
||||
@apply mr-[0.8rem];
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
.container {
|
||||
@apply pb-[3.2rem];
|
||||
}
|
||||
|
||||
/* Form container */
|
||||
.formContainer {
|
||||
@apply mt-[2.4rem];
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useNodeValidator } from '@components/flowChart/lib/verify'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { useThemeCssVar } from '@baota/naive-ui/theme'
|
||||
import { $t } from '@locales/index'
|
||||
import TypeIcon from '@components/typeIcon'
|
||||
import rules from './verify'
|
||||
import type { DeployNodeConfig, DeployNodeInputsConfig } from '@components/flowChart/types'
|
||||
|
||||
interface NodeProps {
|
||||
node: {
|
||||
id: string
|
||||
inputs: DeployNodeInputsConfig
|
||||
config: DeployNodeConfig
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DeployNode',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; inputs: DeployNodeInputsConfig; config: DeployNodeConfig }>,
|
||||
default: () => ({ id: '', inputs: {}, config: {} }),
|
||||
},
|
||||
},
|
||||
setup(props: NodeProps) {
|
||||
// 注册验证器
|
||||
const { isRefreshNode } = useStore()
|
||||
// 初始化节点状态
|
||||
const { registerCompatValidator, validate, validationResult, unregisterValidator } = useNodeValidator()
|
||||
// 主题色
|
||||
const cssVar = useThemeCssVar(['warningColor', 'primaryColor'])
|
||||
|
||||
// 是否有效
|
||||
const validColor = computed(() => {
|
||||
return validationResult.value.valid ? 'var(--n-primary-color)' : 'var(--n-warning-color)'
|
||||
})
|
||||
|
||||
// 提示内容
|
||||
const verificationPrompt = computed(() => {
|
||||
console.log(props.node.config.provider, 'validationResult')
|
||||
if (validationResult.value.valid) return <TypeIcon icon={props.node.config.provider} type="success" />
|
||||
return $t('t_9_1745735765287')
|
||||
})
|
||||
|
||||
// 监听是否刷新节点
|
||||
watch(
|
||||
() => isRefreshNode.value,
|
||||
(newVal) => {
|
||||
useTimeoutFn(() => {
|
||||
registerCompatValidator(props.node.id, rules, props.node.config)
|
||||
validate(props.node.id)
|
||||
isRefreshNode.value = null
|
||||
}, 500)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
//
|
||||
onUnmounted(() => unregisterValidator(props.node.id))
|
||||
|
||||
// 渲染节点状态
|
||||
return () => (
|
||||
<div style={cssVar.value} class="text-[12px]">
|
||||
<div style={{ color: validColor.value }}>{verificationPrompt.value}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { FormRules, FormItemRule } from 'naive-ui'
|
||||
import { $t } from '@locales/index'
|
||||
import { isDomain } from '@baota/utils/business'
|
||||
|
||||
export default {
|
||||
provider: {
|
||||
required: true,
|
||||
message: $t('t_19_1745735766810'),
|
||||
type: 'string',
|
||||
trigger: 'change',
|
||||
},
|
||||
provider_id: {
|
||||
required: true,
|
||||
trigger: 'change',
|
||||
type: 'string',
|
||||
validator: (rule: FormItemRule, value: number) => {
|
||||
if (!value) {
|
||||
return new Error($t('t_1_1745744905566'))
|
||||
}
|
||||
},
|
||||
},
|
||||
'inputs.fromNodeId': {
|
||||
required: true,
|
||||
message: $t('t_3_1745748298161'),
|
||||
trigger: 'change',
|
||||
},
|
||||
|
||||
certPath: {
|
||||
required: true,
|
||||
message: $t('t_30_1746667591892'),
|
||||
trigger: 'input',
|
||||
},
|
||||
keyPath: {
|
||||
required: true,
|
||||
message: $t('t_31_1746667593074'),
|
||||
trigger: 'input',
|
||||
},
|
||||
|
||||
// btpanel相关字段
|
||||
siteName: {
|
||||
required: true,
|
||||
message: $t('t_23_1745735766455'),
|
||||
trigger: 'input',
|
||||
},
|
||||
// 1panel相关字段
|
||||
site_id: {
|
||||
required: true,
|
||||
message: $t('t_24_1745735766826'),
|
||||
trigger: 'input',
|
||||
},
|
||||
// CDN相关字段
|
||||
domain: {
|
||||
required: true,
|
||||
trigger: 'input',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
if (!isDomain(value)) {
|
||||
return new Error($t('t_0_1744958839535'))
|
||||
}
|
||||
},
|
||||
},
|
||||
// 存储桶相关字段
|
||||
region: {
|
||||
required: true,
|
||||
message: $t('t_25_1745735766651'),
|
||||
trigger: 'input',
|
||||
},
|
||||
bucket: {
|
||||
required: true,
|
||||
message: $t('t_26_1745735767144'),
|
||||
trigger: 'input',
|
||||
},
|
||||
} as FormRules
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useForm, useFormHooks, useModalHooks } from '@baota/naive-ui/hooks'
|
||||
import { FormConfig } from '@baota/naive-ui/types/form'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { $t } from '@locales/index'
|
||||
// 假设这个类型需要在types文件中定义
|
||||
|
||||
import NotifyProviderSelect from '@components/notifyProviderSelect'
|
||||
import verify from './verify'
|
||||
|
||||
import { NotifyNodeConfig } from '@components/flowChart/types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NotifyNodeDrawer',
|
||||
props: {
|
||||
// 节点配置数据
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; config: NotifyNodeConfig }>,
|
||||
default: () => ({
|
||||
id: '',
|
||||
config: {
|
||||
provider: '',
|
||||
provider_id: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { updateNodeConfig, isRefreshNode } = useStore()
|
||||
const { useFormInput, useFormTextarea, useFormCustom } = useFormHooks()
|
||||
const { confirm } = useModalHooks()
|
||||
const { handleError } = useError()
|
||||
const param = ref(
|
||||
Object.keys(props.node.config).length > 0
|
||||
? props.node.config
|
||||
: {
|
||||
provider: '',
|
||||
provider_id: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
)
|
||||
|
||||
// 表单渲染配置
|
||||
const formConfig: FormConfig = [
|
||||
useFormInput($t('t_0_1745920566646'), 'subject', {
|
||||
placeholder: $t('t_3_1745887835089'),
|
||||
}),
|
||||
useFormTextarea($t('t_1_1745920567200'), 'body', {
|
||||
placeholder: $t('t_4_1745887835265'),
|
||||
rows: 4,
|
||||
}),
|
||||
useFormCustom(() => (
|
||||
<NotifyProviderSelect
|
||||
path="provider_id"
|
||||
value={param.value.provider_id}
|
||||
isAddMode={true}
|
||||
onUpdate:value={(item) => {
|
||||
param.value.provider_id = item.value
|
||||
param.value.provider = item.type
|
||||
}}
|
||||
/>
|
||||
)),
|
||||
]
|
||||
|
||||
// 创建表单实例
|
||||
const {
|
||||
component: Form,
|
||||
data,
|
||||
example,
|
||||
} = useForm<NotifyNodeConfig>({
|
||||
defaultValue: param,
|
||||
config: formConfig,
|
||||
rules: verify,
|
||||
})
|
||||
|
||||
// 确认事件触发
|
||||
confirm(async (close) => {
|
||||
try {
|
||||
await example.value?.validate()
|
||||
updateNodeConfig(props.node.id, data.value)
|
||||
isRefreshNode.value = props.node.id
|
||||
close()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class="notify-node-drawer">
|
||||
<Form labelPlacement="top" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useThemeCssVar } from '@baota/naive-ui/theme'
|
||||
import { useNodeValidator } from '@components/flowChart/lib/verify'
|
||||
import { NotifyNodeConfig } from '@components/flowChart/types'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
|
||||
import rules from './verify'
|
||||
import TypeIcon from '@components/typeIcon'
|
||||
import { $t } from '@locales/index'
|
||||
export default defineComponent({
|
||||
name: 'NotifyNode',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; config: NotifyNodeConfig }>,
|
||||
default: () => ({ id: '', config: {} }),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// 注册验证器
|
||||
const { isRefreshNode } = useStore()
|
||||
const { validate, validationResult, registerCompatValidator, unregisterValidator } = useNodeValidator()
|
||||
// 主题色
|
||||
const cssVar = useThemeCssVar(['warningColor', 'primaryColor'])
|
||||
|
||||
// 是否有效
|
||||
const validColor = computed(() => {
|
||||
return validationResult.value.valid && props.node.config.provider
|
||||
? 'var(--n-primary-color)'
|
||||
: 'var(--n-warning-color)'
|
||||
})
|
||||
|
||||
// 提示内容
|
||||
const verificationPrompt = computed(() => {
|
||||
if (validationResult.value.valid && props.node.config.provider)
|
||||
return <TypeIcon icon={props.node.config.provider} type="success" />
|
||||
return $t('t_9_1745735765287')
|
||||
})
|
||||
|
||||
// 监听是否刷新节点
|
||||
watch(
|
||||
() => isRefreshNode.value,
|
||||
(newVal) => {
|
||||
useTimeoutFn(() => {
|
||||
registerCompatValidator(props.node.id, rules, props.node.config)
|
||||
validate(props.node.id)
|
||||
isRefreshNode.value = null
|
||||
}, 500)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 卸载验证器
|
||||
onUnmounted(() => unregisterValidator(props.node.id))
|
||||
|
||||
// 渲染节点状态
|
||||
return () => (
|
||||
<div style={cssVar.value} class="text-[12px]">
|
||||
<div style={{ color: validColor.value }}>{verificationPrompt.value}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { FormItemRule, FormRules } from 'naive-ui'
|
||||
import { $t } from '@locales/index'
|
||||
|
||||
export default {
|
||||
subject: {
|
||||
trigger: 'input',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!value) {
|
||||
reject(new Error($t('t_3_1745887835089')))
|
||||
} else if (value.length > 100) {
|
||||
reject(new Error($t('t_3_1745887835089') + '长度不能超过100个字符'))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
body: {
|
||||
trigger: 'input',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!value) {
|
||||
reject(new Error($t('t_4_1745887835265')))
|
||||
} else if (value.length > 1000) {
|
||||
reject(new Error($t('t_4_1745887835265') + '长度不能超过1000个字符'))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
provider_id: {
|
||||
trigger: 'change',
|
||||
type: 'string',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!value) {
|
||||
reject(new Error($t('t_0_1745887835267')))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
} as FormRules
|
||||
@@ -0,0 +1,206 @@
|
||||
import { NFormItemGi, NGrid, NInputGroup, NInputGroupLabel, NInputNumber, NSelect } from 'naive-ui'
|
||||
import { useForm, useFormHooks, useModalHooks } from '@baota/naive-ui/hooks'
|
||||
import { $t } from '@locales/index'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import rules from './verify'
|
||||
|
||||
import type { StartNodeConfig } from '@components/flowChart/types'
|
||||
import type { FormConfig } from '@baota/naive-ui/types/form'
|
||||
|
||||
// 类型
|
||||
import type { VNode } from 'vue'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StartNodeDrawer',
|
||||
props: {
|
||||
// 节点配置数据
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; config: StartNodeConfig }>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { updateNodeConfig, isRefreshNode } = useStore()
|
||||
// 弹窗辅助
|
||||
const { confirm } = useModalHooks()
|
||||
// 错误处理
|
||||
const { handleError } = useError()
|
||||
// 获取表单助手函数
|
||||
const { useFormRadio, useFormCustom } = useFormHooks()
|
||||
// 表单参数
|
||||
const param = ref<StartNodeConfig>(
|
||||
Object.values(props.node.config).length > 0 ? props.node.config : { exec_type: 'manual' },
|
||||
)
|
||||
|
||||
// 周期类型选项
|
||||
const cycleTypeOptions = [
|
||||
{ label: $t('t_2_1744875938555'), value: 'day' },
|
||||
{ label: $t('t_0_1744942117992'), value: 'week' },
|
||||
{ label: $t('t_3_1744875938310'), value: 'month' },
|
||||
]
|
||||
|
||||
// 星期选项
|
||||
const weekOptions = [
|
||||
{ label: $t('t_1_1744942116527'), value: 1 },
|
||||
{ label: $t('t_2_1744942117890'), value: 2 },
|
||||
{ label: $t('t_3_1744942117885'), value: 3 },
|
||||
{ label: $t('t_4_1744942117738'), value: 4 },
|
||||
{ label: $t('t_5_1744942117167'), value: 5 },
|
||||
{ label: $t('t_6_1744942117815'), value: 6 },
|
||||
{ label: $t('t_7_1744942117862'), value: 0 },
|
||||
]
|
||||
|
||||
// 定义默认值常量,避免重复
|
||||
const DEFAULT_AUTO_SETTINGS: Record<string, StartNodeConfig> = {
|
||||
day: { exec_type: 'auto', type: 'day', hour: 1, minute: 0 },
|
||||
week: { exec_type: 'auto', type: 'week', hour: 1, minute: 0, week: 1 },
|
||||
month: { exec_type: 'auto', type: 'month', hour: 1, minute: 0, month: 1 },
|
||||
}
|
||||
|
||||
// 节点配置
|
||||
const { config } = toRefs(props.node)
|
||||
|
||||
// 创建时间输入input
|
||||
const createTimeInput = (value: number, updateFn: (val: number) => void, max: number, label: string): VNode => (
|
||||
<NInputGroup>
|
||||
<NInputNumber
|
||||
value={value}
|
||||
onUpdateValue={(val: number | null) => {
|
||||
if (val !== null) {
|
||||
updateFn(val)
|
||||
}
|
||||
}}
|
||||
max={max}
|
||||
min={0}
|
||||
showButton={false}
|
||||
class="w-full"
|
||||
/>
|
||||
<NInputGroupLabel>{label}</NInputGroupLabel>
|
||||
</NInputGroup>
|
||||
)
|
||||
|
||||
// 表单渲染
|
||||
const formRender = computed(() => {
|
||||
const formItems: FormConfig = []
|
||||
if (param.value.exec_type === 'auto') {
|
||||
formItems.push(
|
||||
useFormCustom<StartNodeConfig>(() => {
|
||||
return (
|
||||
<NGrid cols={24} xGap={24}>
|
||||
<NFormItemGi label={$t('t_2_1744879616413')} span={8} showRequireMark path="type">
|
||||
<NSelect class="w-full" options={cycleTypeOptions} v-model:value={param.value.type} />
|
||||
</NFormItemGi>
|
||||
|
||||
{param.value.type !== 'day' && (
|
||||
<NFormItemGi span={5} path={param.value.type === 'week' ? 'week' : 'month'}>
|
||||
{param.value.type === 'week' ? (
|
||||
<NSelect
|
||||
value={param.value.week}
|
||||
onUpdateValue={(val: number) => {
|
||||
if (typeof val === 'number') {
|
||||
param.value.week = val
|
||||
}
|
||||
}}
|
||||
options={weekOptions}
|
||||
/>
|
||||
) : (
|
||||
createTimeInput(
|
||||
param.value.month || 0,
|
||||
(val: number) => (param.value.month = val),
|
||||
31,
|
||||
$t('t_29_1744958838904'),
|
||||
)
|
||||
)}
|
||||
</NFormItemGi>
|
||||
)}
|
||||
|
||||
<NFormItemGi span={config.value.type === 'day' ? 7 : 5} path="hour">
|
||||
{createTimeInput(
|
||||
param.value.hour || 0,
|
||||
(val: number) => (param.value.hour = val),
|
||||
23,
|
||||
$t('t_5_1744879615277'),
|
||||
)}
|
||||
</NFormItemGi>
|
||||
|
||||
<NFormItemGi span={config.value.type === 'day' ? 7 : 5} path="minute">
|
||||
{createTimeInput(
|
||||
param.value.minute || 0,
|
||||
(val: number) => (param.value.minute = val),
|
||||
59,
|
||||
$t('t_3_1744879615723'),
|
||||
)}
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return [
|
||||
// 运行模式选择
|
||||
useFormRadio($t('t_30_1745735764748'), 'exec_type', [
|
||||
{ label: $t('t_4_1744875940750'), value: 'auto' },
|
||||
{ label: $t('t_5_1744875940010'), value: 'manual' },
|
||||
]),
|
||||
...formItems,
|
||||
]
|
||||
})
|
||||
|
||||
// 创建表单实例
|
||||
const {
|
||||
component: Form,
|
||||
data,
|
||||
example,
|
||||
} = useForm<StartNodeConfig>({
|
||||
defaultValue: param,
|
||||
config: formRender,
|
||||
rules,
|
||||
})
|
||||
|
||||
// 更新参数的函数
|
||||
const updateParamValue = (updates: StartNodeConfig) => {
|
||||
param.value = { ...updates }
|
||||
}
|
||||
|
||||
// 监听执行类型变化
|
||||
watch(
|
||||
() => param.value.exec_type,
|
||||
(newVal) => {
|
||||
if (newVal === 'auto') {
|
||||
updateParamValue(DEFAULT_AUTO_SETTINGS.day as StartNodeConfig)
|
||||
} else if (newVal === 'manual') {
|
||||
updateParamValue({ exec_type: 'manual' })
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听类型变化
|
||||
watch(
|
||||
() => param.value.type,
|
||||
(newVal) => {
|
||||
if (newVal && param.value.exec_type === 'auto') {
|
||||
updateParamValue(DEFAULT_AUTO_SETTINGS[newVal] as StartNodeConfig)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 确认事件触发
|
||||
confirm(async (close) => {
|
||||
try {
|
||||
await example.value?.validate()
|
||||
updateNodeConfig(props.node.id, data.value) // 更新节点配置
|
||||
isRefreshNode.value = props.node.id // 刷新节点
|
||||
close()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class="apply-node-drawer">
|
||||
<Form labelPlacement="top" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
// import { defineComponent, inject } from 'vue'
|
||||
// import { KEY_PROCESS_DATA, KEY_VALIDATOR } from '../../config/keys'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { useNodeValidator } from '@components/flowChart/lib/verify'
|
||||
import { useThemeCssVar } from '@baota/naive-ui/theme'
|
||||
import { $t } from '@locales/index'
|
||||
import rules from './verify'
|
||||
import type { StartNodeConfig } from '@components/flowChart/types'
|
||||
|
||||
interface NodeProps {
|
||||
node: {
|
||||
id: string
|
||||
config: StartNodeConfig
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StartNode',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; config: StartNodeConfig }>,
|
||||
default: () => ({ id: '', config: {} }),
|
||||
},
|
||||
},
|
||||
setup(props: NodeProps) {
|
||||
// 注册验证器
|
||||
const { isRefreshNode } = useStore()
|
||||
// 验证器
|
||||
const { validate, validationResult, registerCompatValidator, unregisterValidator } = useNodeValidator()
|
||||
// 主题色
|
||||
const cssVar = useThemeCssVar(['warningColor', 'primaryColor'])
|
||||
|
||||
// 是否有效
|
||||
const validColor = computed(() => {
|
||||
return validationResult.value.valid ? 'var(--n-primary-color)' : 'var(--n-warning-color)'
|
||||
})
|
||||
|
||||
// 提示
|
||||
const verificationPrompt = computed(() => {
|
||||
if (validationResult.value.valid) {
|
||||
return props.node.config.exec_type === 'auto' ? $t('t_4_1744875940750') : $t('t_5_1744875940010')
|
||||
}
|
||||
return '未配置'
|
||||
})
|
||||
|
||||
// 监听是否刷新节点
|
||||
// 监听是否刷新节点
|
||||
watch(
|
||||
() => isRefreshNode.value,
|
||||
(newVal) => {
|
||||
useTimeoutFn(() => {
|
||||
registerCompatValidator(props.node.id, rules, props.node.config)
|
||||
validate(props.node.id)
|
||||
isRefreshNode.value = null
|
||||
}, 500)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(() => unregisterValidator(props.node.id))
|
||||
|
||||
return () => (
|
||||
<div style={cssVar.value} class="text-[12px]">
|
||||
<div style={{ color: validColor.value }}>{verificationPrompt.value}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { FormRules } from 'naive-ui'
|
||||
import { $t } from '@locales/index'
|
||||
|
||||
export default {
|
||||
exec_type: { required: true, message: $t('t_31_1745735767891'), trigger: 'change' },
|
||||
type: { required: true, message: $t('t_32_1745735767156'), trigger: 'change' },
|
||||
week: { required: true, message: $t('t_33_1745735766532'), trigger: 'input', type: 'number' },
|
||||
month: { required: true, message: $t('t_33_1745735766532'), trigger: 'input', type: 'number' },
|
||||
hour: { required: true, message: $t('t_33_1745735766532'), trigger: 'input', type: 'number' },
|
||||
minute: { required: true, message: $t('t_33_1745735766532'), trigger: 'input', type: 'number' },
|
||||
} as FormRules
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useForm, useFormHooks, useModalHooks } from '@baota/naive-ui/hooks'
|
||||
import { FormConfig } from '@baota/naive-ui/types/form'
|
||||
import { $t } from '@locales/index'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import verifyRules from './verify'
|
||||
import { UploadNodeConfig } from '@components/flowChart/types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UploadNodeDrawer',
|
||||
props: {
|
||||
// 节点配置数据
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; config: UploadNodeConfig }>,
|
||||
default: () => ({
|
||||
id: '',
|
||||
config: {
|
||||
cert: '',
|
||||
key: '',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// 获取store
|
||||
const { updateNodeConfig, isRefreshNode } = useStore()
|
||||
// 获取表单助手函数
|
||||
const { useFormTextarea } = useFormHooks()
|
||||
// 节点配置数据
|
||||
const { config } = toRefs(props.node)
|
||||
// 弹窗辅助
|
||||
const { confirm } = useModalHooks()
|
||||
// 错误处理
|
||||
const { handleError } = useError()
|
||||
|
||||
// 表单渲染配置
|
||||
const formConfig: FormConfig = [
|
||||
useFormTextarea($t('t_34_1745735771147'), 'cert', {
|
||||
placeholder: $t('t_35_1745735781545'),
|
||||
rows: 6,
|
||||
}),
|
||||
useFormTextarea($t('t_36_1745735769443'), 'key', {
|
||||
placeholder: $t('t_37_1745735779980'),
|
||||
rows: 6,
|
||||
}),
|
||||
]
|
||||
|
||||
// 创建表单实例
|
||||
const {
|
||||
component: Form,
|
||||
data,
|
||||
example,
|
||||
} = useForm<UploadNodeConfig>({
|
||||
defaultValue: config,
|
||||
config: formConfig,
|
||||
rules: verifyRules,
|
||||
})
|
||||
|
||||
// 确认事件触发
|
||||
confirm(async (close) => {
|
||||
try {
|
||||
await example.value?.validate()
|
||||
updateNodeConfig(props.node.id, data.value) // 更新节点配置
|
||||
console.log(data.value, props.node.id)
|
||||
isRefreshNode.value = props.node.id // 刷新节点
|
||||
close()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
})
|
||||
return () => (
|
||||
<div class="upload-node-drawer">
|
||||
<Form labelPlacement="top" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useNodeValidator } from '@components/flowChart/lib/verify'
|
||||
import rules from './verify'
|
||||
import { useThemeCssVar } from '@baota/naive-ui/theme'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { UploadNodeConfig } from '@components/flowChart/types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UploadNode',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<{ id: string; config: UploadNodeConfig }>,
|
||||
default: () => ({ id: '', config: {} }),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// 注册验证器
|
||||
const { isRefreshNode } = useStore()
|
||||
const { validate, validationResult, registerCompatValidator, unregisterValidator } = useNodeValidator()
|
||||
// 主题色
|
||||
const cssVar = useThemeCssVar(['warningColor', 'primaryColor'])
|
||||
|
||||
// 是否有效
|
||||
const validColor = computed(() => {
|
||||
return validationResult.value.valid ? 'var(--n-primary-color)' : 'var(--n-warning-color)'
|
||||
})
|
||||
|
||||
// 提示内容
|
||||
const verificationPrompt = computed(() => {
|
||||
if (validationResult.value.valid) return '已配置'
|
||||
return '未配置'
|
||||
})
|
||||
|
||||
// 监听是否刷新节点
|
||||
watch(
|
||||
() => isRefreshNode.value,
|
||||
(newVal) => {
|
||||
useTimeoutFn(() => {
|
||||
registerCompatValidator(props.node.id, rules, props.node.config)
|
||||
validate(props.node.id)
|
||||
isRefreshNode.value = null
|
||||
}, 500)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(() => unregisterValidator(props.node.id))
|
||||
|
||||
return () => (
|
||||
<div style={cssVar.value} class="text-[12px]">
|
||||
<div style={{ color: validColor.value }}>{verificationPrompt.value}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { $t } from '@locales/index'
|
||||
import type { FormItemRule, FormRules } from 'naive-ui'
|
||||
export default {
|
||||
key: {
|
||||
required: true,
|
||||
trigger: 'input',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!value) {
|
||||
reject(new Error($t('t_38_1745735769521')))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
cert: {
|
||||
required: true,
|
||||
trigger: 'input',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!value) {
|
||||
reject(new Error($t('t_40_1745735815317')))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
} as FormRules
|
||||
@@ -0,0 +1,49 @@
|
||||
:root {
|
||||
--bg-color: #f5f5f7;
|
||||
--border-color: #5a5e66;
|
||||
}
|
||||
|
||||
.flowContainer {
|
||||
@apply flex relative box-border w-full h-[calc(100vh-19rem)] overflow-x-auto overflow-y-auto bg-slate-50 dark:bg-gray-900 ;
|
||||
}
|
||||
|
||||
.flowProcess {
|
||||
@apply relative h-full w-full;
|
||||
}
|
||||
|
||||
.flowZoom {
|
||||
@apply flex fixed items-center justify-between h-[4rem] w-[12.5rem] bottom-[4rem] z-[99];
|
||||
}
|
||||
|
||||
.flowZoomIcon {
|
||||
@apply w-[2.5rem] h-[2.5rem] cursor-pointer border flex items-center justify-center;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* 嵌套节点包装器样式 */
|
||||
.nested-node-wrap {
|
||||
@apply flex flex-col items-center relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.deep-nested-node-wrap {
|
||||
@apply flex flex-col items-center relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 右侧配置区样式 */
|
||||
.configPanel {
|
||||
@apply flex flex-col w-[360px] min-w-[360px] bg-white dark:bg-gray-800 dark:border-gray-700 z-10 rounded-lg;
|
||||
}
|
||||
|
||||
.configHeader {
|
||||
@apply flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.configContent {
|
||||
@apply flex-1 overflow-y-auto ;
|
||||
}
|
||||
|
||||
.emptyTip {
|
||||
@apply flex items-center justify-center h-full text-gray-400 dark:text-gray-500;
|
||||
}
|
||||
102
frontend/apps/allin-ssl/src/components/flowChart/index.tsx
Normal file
102
frontend/apps/allin-ssl/src/components/flowChart/index.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NButton, NIcon, NInput } from 'naive-ui'
|
||||
import { SaveOutlined, ArrowLeftOutlined } from '@vicons/antd'
|
||||
import { $t } from '@locales/index'
|
||||
import SvgIcon from '@components/svgIcon'
|
||||
|
||||
import { useController } from './useController'
|
||||
import { useStore } from './useStore'
|
||||
|
||||
import EndNode from './components/base/endNode'
|
||||
import NodeWrap from './components/render/nodeWrap'
|
||||
|
||||
import styles from './index.module.css'
|
||||
import type { FlowNode, FlowNodeProps } from './types'
|
||||
import { useThemeCssVar } from '@baota/naive-ui/theme'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FlowChart',
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'quick' | 'advanced'>,
|
||||
default: 'quick',
|
||||
},
|
||||
node: {
|
||||
type: Object as PropType<FlowNode>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props: FlowNodeProps) {
|
||||
const cssVars = useThemeCssVar([
|
||||
'borderColor',
|
||||
'dividerColor',
|
||||
'textColor1',
|
||||
'textColor2',
|
||||
'primaryColor',
|
||||
'primaryColorHover',
|
||||
'bodyColor',
|
||||
])
|
||||
const { flowData, selectedNodeId, flowZoom, resetFlowData } = useStore()
|
||||
const { initData, handleSaveConfig, handleZoom, handleSelectNode, goBack } = useController({
|
||||
type: props?.type,
|
||||
node: props?.node,
|
||||
isEdit: props?.isEdit,
|
||||
})
|
||||
onMounted(initData)
|
||||
onUnmounted(resetFlowData)
|
||||
return () => (
|
||||
<div class="flex flex-col w-full h-full" style={cssVars.value}>
|
||||
<div class="w-full h-[6rem] px-[2rem] mb-[2rem] bg-white rounded-lg flex items-center gap-2 justify-between">
|
||||
<div class="flex items-center">
|
||||
<NButton onClick={goBack}>
|
||||
<NIcon class="mr-1">
|
||||
<ArrowLeftOutlined />
|
||||
</NIcon>
|
||||
{$t('t_0_1744861190562')}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center ml-[.5rem]">
|
||||
<NInput
|
||||
v-model:value={flowData.value.name}
|
||||
placeholder={$t('t_0_1745490735213')}
|
||||
class="!w-[30rem] !border-none "
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<NButton type="primary" onClick={handleSaveConfig} disabled={!selectedNodeId}>
|
||||
<NIcon class="mr-1">
|
||||
<SaveOutlined />
|
||||
</NIcon>
|
||||
{$t('t_2_1744861190040')}
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex">
|
||||
{/* 左侧流程容器 */}
|
||||
<div class={styles.flowContainer}>
|
||||
{/* 流程容器*/}
|
||||
<div class={styles.flowProcess} style={{ transform: `scale(${flowZoom.value / 100})` }}>
|
||||
{/* 渲染流程节点 */}
|
||||
<NodeWrap node={flowData.value.childNode} onSelect={handleSelectNode} />
|
||||
{/* 流程结束节点 */}
|
||||
<EndNode />
|
||||
</div>
|
||||
{/* 缩放控制区 */}
|
||||
<div class={styles.flowZoom}>
|
||||
<div class={styles.flowZoomIcon} onClick={() => handleZoom(1)}>
|
||||
<SvgIcon icon="subtract" class={`${flowZoom.value === 50 ? styles.disabled : ''}`} color="#5a5e66" />
|
||||
</div>
|
||||
<span>{flowZoom.value}%</span>
|
||||
<div class={styles.flowZoomIcon} onClick={() => handleZoom(2)}>
|
||||
<SvgIcon icon="plus" class={`${flowZoom.value === 300 ? styles.disabled : ''}`} color="#5a5e66" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
// 基础节点
|
||||
export const END = 'end' // 结束节点
|
||||
export const DEFAULT = 'default' // 默认节点
|
||||
|
||||
|
||||
export const START = 'start' // 开始节点
|
||||
export const BRANCH = 'branch' // 分支节点(并行分支和条件分支)
|
||||
export const CONDITION = 'condition' // 条件子节点
|
||||
export const EXECUTE_RESULT_BRANCH = 'execute_result_branch' // 执行结果节点
|
||||
export const EXECUTE_RESULT_CONDITION = 'execute_result_condition' // 执行结果条件节点
|
||||
export const UPLOAD = 'upload' // 上传节点
|
||||
export const NOTIFY = 'notify' // 通知节点
|
||||
export const APPLY = 'apply' // 申请节点
|
||||
export const DEPLOY = 'deploy' // 部署节点
|
||||
266
frontend/apps/allin-ssl/src/components/flowChart/lib/config.tsx
Normal file
266
frontend/apps/allin-ssl/src/components/flowChart/lib/config.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import { deepMerge } from '@baota/utils/data'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
APPLY,
|
||||
BRANCH,
|
||||
CONDITION,
|
||||
DEPLOY,
|
||||
NOTIFY,
|
||||
UPLOAD,
|
||||
EXECUTE_RESULT_BRANCH,
|
||||
EXECUTE_RESULT_CONDITION,
|
||||
START,
|
||||
} from './alias'
|
||||
import {
|
||||
BaseRenderNodeOptions,
|
||||
NodeOptions,
|
||||
BaseNodeData,
|
||||
ExecuteResultConditionNodeData,
|
||||
ExecuteResultBranchNodeData,
|
||||
} from '../types'
|
||||
|
||||
const nodeOptions = {} as NodeOptions
|
||||
|
||||
// 基础节点配置
|
||||
const baseOptions = <T extends BaseNodeData>(
|
||||
options: Partial<BaseRenderNodeOptions<T>> & { defaultNode?: T },
|
||||
): BaseRenderNodeOptions<T> => {
|
||||
const defaultOptions: BaseRenderNodeOptions<T> = {
|
||||
title: {
|
||||
name: '', // 节点标题
|
||||
color: '#FFFFFF', // 节点标题颜色
|
||||
bgColor: '#3CB371', // 节点标题背景颜色
|
||||
},
|
||||
icon: {
|
||||
name: '', // 节点图标
|
||||
color: '#3CB371', // 节点图标颜色
|
||||
},
|
||||
operateNode: {
|
||||
add: true, // 节点是否可以追加
|
||||
sort: 1, // 节点排序,用于排序节点的显示优先级,主要用于配置节点的操作
|
||||
addBranch: false, // 节点是否可以添加分支
|
||||
edit: true, // 节点是否可以编辑
|
||||
remove: true, // 节点是否可以删除
|
||||
onSupportNode: [], // 节点不支持添加的节点类型
|
||||
},
|
||||
isHasDrawer: false, // 节点是否可以进行配置
|
||||
defaultNode: {} as T,
|
||||
}
|
||||
return deepMerge(defaultOptions, options) as BaseRenderNodeOptions<T>
|
||||
}
|
||||
|
||||
// ------------------------------ 基础节点配置 ------------------------------
|
||||
|
||||
// // 结束节点
|
||||
// nodeOptions[END] = baseOptions({
|
||||
// title: { name: '结束' },
|
||||
// icon: { name: END },
|
||||
// addNode: false,
|
||||
// removedNode: false,
|
||||
// hasDrawer: false,
|
||||
// hiddenNode: true,
|
||||
// })
|
||||
|
||||
// // 默认节点
|
||||
// nodeOptions[DEFAULT] = baseOptions({
|
||||
// title: { name: '默认' },
|
||||
// icon: { name: DEFAULT },
|
||||
// addNode: true,
|
||||
// hasDrawer: true,
|
||||
// defaultNode: {
|
||||
// name: '默认',
|
||||
// type: DEFAULT,
|
||||
// config: {},
|
||||
// childNode: null,
|
||||
// },
|
||||
// })
|
||||
|
||||
// ------------------------------ 执行业务节点配置 ------------------------------
|
||||
|
||||
// 开始节点
|
||||
nodeOptions[START] = () =>
|
||||
baseOptions({
|
||||
title: { name: '开始' },
|
||||
operateNode: { onSupportNode: [EXECUTE_RESULT_BRANCH], remove: false, edit: false, add: false },
|
||||
defaultNode: {
|
||||
id: uuidv4(),
|
||||
name: '开始',
|
||||
type: START,
|
||||
config: {
|
||||
exec_type: 'manual',
|
||||
},
|
||||
childNode: null,
|
||||
},
|
||||
})
|
||||
|
||||
// 申请节点
|
||||
nodeOptions[APPLY] = () =>
|
||||
baseOptions({
|
||||
title: { name: '申请' },
|
||||
icon: { name: APPLY },
|
||||
operateNode: { sort: 1 },
|
||||
defaultNode: {
|
||||
id: uuidv4(),
|
||||
name: '申请',
|
||||
type: APPLY,
|
||||
config: {
|
||||
domains: '',
|
||||
email: '',
|
||||
provider: '',
|
||||
provider_id: '',
|
||||
end_day: 30,
|
||||
},
|
||||
childNode: null,
|
||||
},
|
||||
})
|
||||
|
||||
// 上传节点
|
||||
nodeOptions[UPLOAD] = () =>
|
||||
baseOptions({
|
||||
title: { name: '上传' },
|
||||
icon: { name: UPLOAD },
|
||||
operateNode: { sort: 2, onSupportNode: [EXECUTE_RESULT_BRANCH] },
|
||||
defaultNode: {
|
||||
id: uuidv4(),
|
||||
name: '上传',
|
||||
type: UPLOAD,
|
||||
config: {
|
||||
cert: '',
|
||||
key: '',
|
||||
},
|
||||
childNode: null,
|
||||
},
|
||||
})
|
||||
|
||||
// 部署节点
|
||||
nodeOptions[DEPLOY] = () =>
|
||||
baseOptions({
|
||||
title: { name: '部署' },
|
||||
icon: { name: DEPLOY },
|
||||
operateNode: { sort: 3 },
|
||||
defaultNode: {
|
||||
id: uuidv4(),
|
||||
name: '部署',
|
||||
type: DEPLOY,
|
||||
inputs: [],
|
||||
config: {
|
||||
provider: '',
|
||||
provider_id: '',
|
||||
},
|
||||
childNode: null,
|
||||
},
|
||||
})
|
||||
|
||||
// 通知节点
|
||||
nodeOptions[NOTIFY] = () =>
|
||||
baseOptions({
|
||||
title: { name: '通知' },
|
||||
icon: { name: NOTIFY },
|
||||
operateNode: { sort: 4 },
|
||||
defaultNode: {
|
||||
id: uuidv4(),
|
||||
name: '通知',
|
||||
type: NOTIFY,
|
||||
config: {
|
||||
provider: '',
|
||||
provider_id: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
childNode: null,
|
||||
},
|
||||
})
|
||||
|
||||
// 分支节点
|
||||
nodeOptions[BRANCH] = () =>
|
||||
baseOptions({
|
||||
title: { name: '并行分支' },
|
||||
icon: { name: BRANCH },
|
||||
operateNode: { sort: 5, addBranch: true },
|
||||
defaultNode: {
|
||||
id: uuidv4(),
|
||||
name: '并行分支',
|
||||
type: BRANCH,
|
||||
conditionNodes: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '分支1',
|
||||
type: CONDITION,
|
||||
config: {},
|
||||
childNode: null,
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '分支2',
|
||||
type: CONDITION,
|
||||
config: {},
|
||||
childNode: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// 条件节点
|
||||
nodeOptions[CONDITION] = () =>
|
||||
baseOptions({
|
||||
title: { name: '分支1' },
|
||||
icon: { name: CONDITION },
|
||||
operateNode: { add: false, onSupportNode: [EXECUTE_RESULT_BRANCH] },
|
||||
defaultNode: {
|
||||
id: uuidv4(),
|
||||
name: '分支1',
|
||||
type: CONDITION,
|
||||
icon: { name: CONDITION },
|
||||
config: {},
|
||||
childNode: null,
|
||||
},
|
||||
})
|
||||
|
||||
// 执行结构分支
|
||||
nodeOptions[EXECUTE_RESULT_BRANCH] = () =>
|
||||
baseOptions<ExecuteResultBranchNodeData>({
|
||||
title: { name: '执行结果分支' },
|
||||
icon: { name: BRANCH },
|
||||
operateNode: { sort: 7, onSupportNode: [EXECUTE_RESULT_BRANCH] },
|
||||
defaultNode: {
|
||||
id: uuidv4(),
|
||||
name: '执行结果分支',
|
||||
type: EXECUTE_RESULT_BRANCH,
|
||||
conditionNodes: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '若当前节点执行成功…',
|
||||
type: EXECUTE_RESULT_CONDITION,
|
||||
icon: { name: 'success' },
|
||||
config: { type: 'success' },
|
||||
childNode: null,
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
name: '若当前节点执行失败…',
|
||||
type: EXECUTE_RESULT_CONDITION,
|
||||
icon: { name: 'error' },
|
||||
config: { type: 'fail' },
|
||||
childNode: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// 执行结构条件
|
||||
nodeOptions[EXECUTE_RESULT_CONDITION] = () =>
|
||||
baseOptions<ExecuteResultConditionNodeData>({
|
||||
title: { name: '执行结构条件' },
|
||||
icon: { name: BRANCH },
|
||||
operateNode: { add: false, onSupportNode: [EXECUTE_RESULT_BRANCH] },
|
||||
defaultNode: {
|
||||
id: uuidv4(),
|
||||
name: '若前序节点执行失败…',
|
||||
type: EXECUTE_RESULT_CONDITION,
|
||||
icon: { name: 'SUCCESS' },
|
||||
config: { type: 'SUCCESS' },
|
||||
childNode: null,
|
||||
},
|
||||
})
|
||||
|
||||
export default nodeOptions
|
||||
411
frontend/apps/allin-ssl/src/components/flowChart/lib/verify.tsx
Normal file
411
frontend/apps/allin-ssl/src/components/flowChart/lib/verify.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { ValidatorResult, ValidatorFunction, RuleItem, ValidatorDescriptor } from '../types'
|
||||
|
||||
/**
|
||||
* 验证器类
|
||||
* 用于管理所有节点的验证函数和验证结果
|
||||
*/
|
||||
class Validator {
|
||||
// 存储所有节点的验证函数
|
||||
private validators: Map<string, ValidatorFunction> = new Map()
|
||||
// 存储所有节点的验证结果
|
||||
private validationResults: Map<string, ValidatorResult> = new Map()
|
||||
// 存储所有节点的数据
|
||||
private valuesMap: Map<string, any> = new Map()
|
||||
// 存储节点规则验证状态
|
||||
public rulesMap: Map<string, boolean> = new Map()
|
||||
|
||||
/**
|
||||
* 注册验证器
|
||||
* @param nodeId - 节点ID
|
||||
* @param validator - 验证函数
|
||||
*/
|
||||
register(nodeId: string, validator: ValidatorFunction) {
|
||||
this.validators.set(nodeId, validator)
|
||||
this.validate(nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销验证器
|
||||
* @param nodeId - 节点ID
|
||||
*/
|
||||
unregister(nodeId: string) {
|
||||
this.validators.delete(nodeId)
|
||||
this.validationResults.delete(nodeId)
|
||||
this.valuesMap.delete(nodeId)
|
||||
}
|
||||
|
||||
unregisterAll() {
|
||||
this.validators.clear()
|
||||
this.validationResults.clear()
|
||||
this.valuesMap.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册兼容async-validator的规则
|
||||
* @param nodeId - 节点ID
|
||||
* @param descriptor - 验证规则描述符
|
||||
* @param initialValues - 初始值
|
||||
*/
|
||||
registerCompatValidator(nodeId: string, descriptor: ValidatorDescriptor, initialValues?: Record<string, any>) {
|
||||
if (initialValues) {
|
||||
this.valuesMap.set(nodeId, { ...initialValues })
|
||||
} else {
|
||||
this.valuesMap.set(nodeId, {})
|
||||
}
|
||||
|
||||
const validator = () => {
|
||||
return this.validateWithRules(nodeId, descriptor)
|
||||
}
|
||||
this.validators.set(nodeId, validator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置节点的值
|
||||
* @param nodeId - 节点ID
|
||||
* @param key - 属性名
|
||||
* @param value - 属性值
|
||||
*/
|
||||
setValue(nodeId: string, key: string, value: any) {
|
||||
const values = this.valuesMap.get(nodeId) || {}
|
||||
values[key] = value
|
||||
this.valuesMap.set(nodeId, values)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置节点的值
|
||||
* @param nodeId - 节点ID
|
||||
* @param values - 属性值集合
|
||||
*/
|
||||
setValues(nodeId: string, values: Record<string, any>) {
|
||||
const currentValues = this.valuesMap.get(nodeId) || {}
|
||||
this.valuesMap.set(nodeId, { ...currentValues, ...values })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的值
|
||||
* @param nodeId - 节点ID
|
||||
* @param key - 属性名
|
||||
*/
|
||||
getValue(nodeId: string, key: string) {
|
||||
const values = this.valuesMap.get(nodeId) || {}
|
||||
return values[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的所有值
|
||||
* @param nodeId - 节点ID
|
||||
*/
|
||||
getValues(nodeId: string) {
|
||||
return this.valuesMap.get(nodeId) || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用兼容async-validator的规则验证数据
|
||||
* @param nodeId - 节点ID
|
||||
* @param descriptor - 验证规则描述符
|
||||
* @returns 验证结果
|
||||
*/
|
||||
validateWithRules(nodeId: string, descriptor: ValidatorDescriptor): ValidatorResult {
|
||||
const values = this.valuesMap.get(nodeId) || {}
|
||||
for (const field in descriptor) {
|
||||
const rules = Array.isArray(descriptor[field])
|
||||
? (descriptor[field] as RuleItem[])
|
||||
: [descriptor[field] as RuleItem]
|
||||
const value = values[field]
|
||||
if (field in values) {
|
||||
for (const rule of rules) {
|
||||
// 检查必填
|
||||
if (rule.required && (value === undefined || value === null || value === '')) {
|
||||
const message = rule.message || `${field}是必填项`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
// 如果值为空但不是必填,则跳过其他验证
|
||||
if ((value === undefined || value === null || value === '') && !rule.required) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查类型
|
||||
if (rule.type && !this.validateType(rule.type, value)) {
|
||||
const message = rule.message || `${field}的类型应为${rule.type}`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
// 检查格式
|
||||
if (rule.pattern && !rule.pattern.test(String(value))) {
|
||||
const message = rule.message || `${field}格式不正确`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
// 检查长度范围
|
||||
if (rule.type === 'string' || rule.type === 'array') {
|
||||
const length = value.length || 0
|
||||
|
||||
if (rule.len !== undefined && length !== rule.len) {
|
||||
const message = rule.message || `${field}的长度应为${rule.len}`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
if (rule.min !== undefined && length < rule.min) {
|
||||
const message = rule.message || `${field}的长度不应小于${rule.min}`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
if (rule.max !== undefined && length > rule.max) {
|
||||
const message = rule.message || `${field}的长度不应大于${rule.max}`
|
||||
return { valid: false, message }
|
||||
}
|
||||
}
|
||||
|
||||
// 检查数值范围
|
||||
if (rule.type === 'number') {
|
||||
if (rule.len !== undefined && value !== rule.len) {
|
||||
const message = rule.message || `${field}应等于${rule.len}`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
if (rule.min !== undefined && value < rule.min) {
|
||||
const message = rule.message || `${field}不应小于${rule.min}`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
if (rule.max !== undefined && value > rule.max) {
|
||||
const message = rule.message || `${field}不应大于${rule.max}`
|
||||
return { valid: false, message }
|
||||
}
|
||||
}
|
||||
|
||||
// 检查枚举值
|
||||
if (rule.enum && !rule.enum.includes(value)) {
|
||||
const message = rule.message || `${field}的值不在允许范围内`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
// 检查空白字符
|
||||
if (rule.whitespace && rule.type === 'string' && !value.trim()) {
|
||||
const message = rule.message || `${field}不能只包含空白字符`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
// 自定义验证器
|
||||
if (rule.validator) {
|
||||
try {
|
||||
const result = rule.validator(rule, value, undefined)
|
||||
|
||||
if (result === false) {
|
||||
const message = rule.message || `${field}验证失败`
|
||||
return { valid: false, message }
|
||||
}
|
||||
|
||||
if (result instanceof Error) {
|
||||
return { valid: false, message: result.message }
|
||||
}
|
||||
|
||||
if (Array.isArray(result) && result.length > 0 && result[0] instanceof Error) {
|
||||
return { valid: false, message: result[0].message }
|
||||
}
|
||||
} catch (error) {
|
||||
return { valid: false, message: error instanceof Error ? error.message : `${field}验证出错` }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { valid: true, message: '' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证值的类型
|
||||
* @param type - 类型
|
||||
* @param value - 值
|
||||
* @returns 是否通过验证
|
||||
*/
|
||||
private validateType(type: string, value: any): boolean {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return typeof value === 'string'
|
||||
case 'number':
|
||||
return typeof value === 'number' && !isNaN(value)
|
||||
case 'boolean':
|
||||
return typeof value === 'boolean'
|
||||
case 'method':
|
||||
return typeof value === 'function'
|
||||
case 'regexp':
|
||||
return value instanceof RegExp
|
||||
case 'integer':
|
||||
return typeof value === 'number' && Number.isInteger(value)
|
||||
case 'float':
|
||||
return typeof value === 'number' && !Number.isInteger(value)
|
||||
case 'array':
|
||||
return Array.isArray(value)
|
||||
case 'object':
|
||||
return typeof value === 'object' && !Array.isArray(value) && value !== null
|
||||
case 'enum':
|
||||
return true // 枚举类型在单独的规则中验证
|
||||
case 'date':
|
||||
return value instanceof Date
|
||||
case 'url':
|
||||
try {
|
||||
new URL(value)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
case 'email':
|
||||
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证指定节点
|
||||
* @param nodeId - 节点ID
|
||||
* @returns ValidatorResult - 验证结果
|
||||
*/
|
||||
validate(nodeId: string) {
|
||||
const validator = this.validators.get(nodeId)
|
||||
if (validator) {
|
||||
const result = validator()
|
||||
this.validationResults.set(nodeId, result)
|
||||
return result
|
||||
}
|
||||
return { valid: false, message: '' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证所有已注册的节点
|
||||
* @returns 包含所有节点验证结果的对象
|
||||
*/
|
||||
validateAll() {
|
||||
let allValid = true
|
||||
const results: Record<string, ValidatorResult> = {}
|
||||
this.validators.forEach((validator, nodeId) => {
|
||||
const result = this.validate(nodeId)
|
||||
results[nodeId] = result
|
||||
if (!result.valid) {
|
||||
allValid = false
|
||||
}
|
||||
})
|
||||
return {
|
||||
valid: allValid,
|
||||
results,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定节点的验证结果
|
||||
* @param nodeId - 节点ID
|
||||
* @returns ValidatorResult - 验证结果
|
||||
*/
|
||||
getValidationResult(nodeId: string): ValidatorResult {
|
||||
return this.validationResults.get(nodeId) || { valid: true, message: '' }
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局验证器实例
|
||||
const validator = new Validator()
|
||||
|
||||
/**
|
||||
* 节点验证器 Hook
|
||||
* 提供节点验证相关的功能
|
||||
* @returns 包含验证相关方法和状态的对象
|
||||
*/
|
||||
export function useNodeValidator() {
|
||||
// 响应式的验证结果
|
||||
const validationResult = ref<ValidatorResult>({ valid: false, message: '' })
|
||||
|
||||
/**
|
||||
* 注册验证器函数
|
||||
* @param nodeId - 节点ID
|
||||
* @param validateFn - 验证函数
|
||||
*/
|
||||
const registerValidator = (nodeId: string, validateFn: ValidatorFunction) => {
|
||||
validator.register(nodeId, validateFn)
|
||||
validationResult.value = validator.getValidationResult(nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册兼容async-validator的规则
|
||||
* @param nodeId - 节点ID
|
||||
* @param descriptor - 验证规则描述符
|
||||
* @param initialValues - 初始值
|
||||
*/
|
||||
const registerCompatValidator = (
|
||||
nodeId: string,
|
||||
descriptor: ValidatorDescriptor,
|
||||
initialValues?: Record<string, any>,
|
||||
) => {
|
||||
validator.registerCompatValidator(nodeId, descriptor, initialValues)
|
||||
validationResult.value = validator.getValidationResult(nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字段值
|
||||
* @param nodeId - 节点ID
|
||||
* @param key - 字段名
|
||||
* @param value - 字段值
|
||||
*/
|
||||
const setFieldValue = (nodeId: string, key: string, value: any) => {
|
||||
validator.setValue(nodeId, key, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置字段值
|
||||
* @param nodeId - 节点ID
|
||||
* @param values - 字段值集合
|
||||
*/
|
||||
const setFieldValues = (nodeId: string, values: Record<string, any>) => {
|
||||
validator.setValues(nodeId, values)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段值
|
||||
* @param nodeId - 节点ID
|
||||
* @param key - 字段名
|
||||
*/
|
||||
const getFieldValue = (nodeId: string, key: string) => {
|
||||
return validator.getValue(nodeId, key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字段值
|
||||
* @param nodeId - 节点ID
|
||||
*/
|
||||
const getFieldValues = (nodeId: string) => {
|
||||
return validator.getValues(nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行验证
|
||||
* @param nodeId - 节点ID
|
||||
* @returns ValidatorResult - 验证结果
|
||||
*/
|
||||
const validate = (nodeId: string) => {
|
||||
const result = validator.validate(nodeId)
|
||||
validationResult.value = result
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销验证器
|
||||
* @param nodeId - 节点ID
|
||||
*/
|
||||
const unregisterValidator = (nodeId: string) => {
|
||||
validator.unregister(nodeId)
|
||||
}
|
||||
|
||||
return {
|
||||
validationResult, // 验证结果(响应式)
|
||||
registerValidator, // 注册验证器方法
|
||||
registerCompatValidator, // 注册兼容async-validator的规则
|
||||
setFieldValue, // 设置字段值
|
||||
setFieldValues, // 批量设置字段值
|
||||
getFieldValue, // 获取字段值
|
||||
getFieldValues, // 获取所有字段值
|
||||
validate, // 执行验证方法
|
||||
unregisterValidator, // 注销验证器方法
|
||||
validator, // 验证器实例
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
export default {
|
||||
name: '',
|
||||
childNode: {
|
||||
id: 'start-1',
|
||||
name: '开始',
|
||||
type: 'start',
|
||||
config: {
|
||||
exec_type: 'auto',
|
||||
type: 'day',
|
||||
hour: 1,
|
||||
minute: 0,
|
||||
},
|
||||
childNode: {
|
||||
id: 'apply-1',
|
||||
name: '申请证书',
|
||||
type: 'apply',
|
||||
config: {
|
||||
domains: '',
|
||||
email: '',
|
||||
provider_id: '',
|
||||
provider: '',
|
||||
end_day: 30,
|
||||
},
|
||||
childNode: {
|
||||
id: 'deploy-1',
|
||||
name: '部署',
|
||||
type: 'deploy',
|
||||
inputs: {},
|
||||
config: {
|
||||
provider: '',
|
||||
provider_id: '',
|
||||
inputs: {
|
||||
fromNodeId: '',
|
||||
name: '',
|
||||
},
|
||||
},
|
||||
childNode: {
|
||||
id: 'execute',
|
||||
name: '执行结果',
|
||||
type: 'execute_result_branch',
|
||||
config: { fromNodeId: 'deploy-1' },
|
||||
conditionNodes: [
|
||||
{
|
||||
id: 'execute-success',
|
||||
name: '执行成功',
|
||||
type: 'execute_result_condition',
|
||||
config: {
|
||||
fromNodeId: '',
|
||||
type: 'success',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'execute-failure',
|
||||
name: '执行失败',
|
||||
type: 'execute_result_condition',
|
||||
config: {
|
||||
fromNodeId: '',
|
||||
type: 'fail',
|
||||
},
|
||||
},
|
||||
],
|
||||
childNode: {
|
||||
id: 'notify-1',
|
||||
name: '通知任务',
|
||||
type: 'notify',
|
||||
config: {
|
||||
provider: '',
|
||||
provider_id: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
316
frontend/apps/allin-ssl/src/components/flowChart/types.d.ts
vendored
Normal file
316
frontend/apps/allin-ssl/src/components/flowChart/types.d.ts
vendored
Normal file
@@ -0,0 +1,316 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
APPLY,
|
||||
BRANCH,
|
||||
CONDITION,
|
||||
DEPLOY,
|
||||
END,
|
||||
EXECUTE_RESULT_BRANCH,
|
||||
EXECUTE_RESULT_CONDITION,
|
||||
NOTIFY,
|
||||
START,
|
||||
UPLOAD,
|
||||
DEFAULT,
|
||||
} from './lib/alias'
|
||||
import type { FormRules } from 'naive-ui'
|
||||
|
||||
// 拖拽效果
|
||||
export interface FlowNodeProps {
|
||||
isEdit: boolean
|
||||
type: 'quick' | 'advanced'
|
||||
node: FlowNode
|
||||
}
|
||||
|
||||
|
||||
export interface FlowNode {
|
||||
id: string
|
||||
name: string
|
||||
childNode: BaseNodeData
|
||||
}
|
||||
|
||||
// 添加节点选项
|
||||
export interface NodeSelect {
|
||||
title: NodeTitle
|
||||
type: NodeNum
|
||||
icon: NodeIcon
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
// 节点标题配置
|
||||
export interface NodeTitle {
|
||||
name: string
|
||||
color?: string
|
||||
bgColor?: string
|
||||
}
|
||||
|
||||
// 节点图标配置
|
||||
export interface NodeIcon {
|
||||
name: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
// 操作节点配置,用于渲染节点的操作功能
|
||||
export interface operateNodeOptions {
|
||||
add?: boolean // 是否显示添加节点
|
||||
sort?: number // 节点排序,用于排序节点的显示优先级,主要用于配置节点的操作
|
||||
addBranch?: boolean // 是否显示添加分支节点,仅在节点类型为分支节点时有效
|
||||
addBranchTitle?: string // 添加分支节点标题,仅在节点类型为分支节点时有效
|
||||
edit?: boolean // 是否可编辑
|
||||
remove?: boolean // 是否可删除
|
||||
onSupportNode?: NodeNum[] // 不支持添加的节点类型
|
||||
}
|
||||
|
||||
// 基础节点渲染配置,用于渲染节点
|
||||
export interface BaseRenderNodeOptions<T extends BaseNodeData> {
|
||||
title: NodeTitle // 节点标题
|
||||
icon?: NodeIcon // 节点图标
|
||||
isAddNode?: boolean // 是否显示添加节点
|
||||
isHasDrawer?: boolean // 是否显示抽屉
|
||||
operateNode?: operateNodeOptions // 是否显示操作节点
|
||||
defaultNode?: T // 默认节点数据 -- 节点数据,用于组合相应结构
|
||||
}
|
||||
|
||||
// 基础节点数据
|
||||
export interface BaseNodeData<T = Record<string, unknown>> {
|
||||
type: NodeNum // 节点类型
|
||||
id?: string // 节点id,用于编辑
|
||||
name: string // 节点名称
|
||||
icon?: NodeIcon // 节点图标
|
||||
inputs?: Record<string, unknown>[] // 输入,用于连接其他节点的数据
|
||||
config?: T // 参数,用于配置当前的节点
|
||||
childNode?: BaseNodeData | BranchNodeData | null // 子节点
|
||||
}
|
||||
|
||||
// 分支节点数据
|
||||
export interface BranchNodeData<T extends Record<string, unknown> = Record<string, unknown>> extends BaseNodeData<T> {
|
||||
type: typeof BRANCH // 节点类型
|
||||
conditionNodes: ConditionNodeData[] // 子节点
|
||||
}
|
||||
|
||||
// 执行条件分支节点数据
|
||||
export interface ExecuteResultBranchNodeData extends BaseNodeData {
|
||||
type: typeof EXECUTE_RESULT_BRANCH // 节点类型
|
||||
conditionNodes: ExecuteResultConditionNodeData[] // 子节点
|
||||
}
|
||||
|
||||
// 执行结果条件节点数据
|
||||
interface ExecuteResultCondition {
|
||||
type: 'success' | 'fail'
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// 条件节点数据
|
||||
export interface ExecuteResultConditionNodeData extends BaseNodeData<ExecuteResultCondition> {
|
||||
type: typeof EXECUTE_RESULT_CONDITION // 节点类型
|
||||
config: ExecuteResultCondition
|
||||
}
|
||||
|
||||
// 节点类型
|
||||
export type NodeNum =
|
||||
| typeof START // 开始节点
|
||||
| typeof DEFAULT // 默认节点
|
||||
| typeof END // 结束节点
|
||||
| typeof BRANCH // 分支节点
|
||||
| typeof CONDITION // 条件节点
|
||||
| typeof EXECUTE_RESULT_BRANCH // 执行结果分支节点(if)
|
||||
| typeof EXECUTE_RESULT_CONDITION // 执行结果条件节点
|
||||
| typeof UPLOAD // 上传节点(业务)
|
||||
| typeof NOTIFY // 通知节点(业务)
|
||||
| typeof APPLY // 申请节点(业务)
|
||||
| typeof DEPLOY // 部署节点(业务)
|
||||
|
||||
// 节点配置映射
|
||||
export type NodeOptions = {
|
||||
[START]: () => BaseRenderNodeOptions<BaseNodeData & { type: typeof START }>
|
||||
[END]: () => BaseRenderNodeOptions<BaseNodeData & { type: typeof END }>
|
||||
[DEFAULT]: () => BaseRenderNodeOptions<BaseNodeData & { type: typeof DEFAULT }>
|
||||
[BRANCH]: () => BaseRenderNodeOptions<BranchNodeData>
|
||||
[CONDITION]: () => BaseRenderNodeOptions<BaseNodeData & { type: typeof CONDITION }>
|
||||
[EXECUTE_RESULT_BRANCH]: () => BaseRenderNodeOptions<ExecuteResultBranchNodeData>
|
||||
[EXECUTE_RESULT_CONDITION]: () => BaseRenderNodeOptions<ExecuteResultConditionNodeData>
|
||||
[UPLOAD]: () => BaseRenderNodeOptions<BaseNodeData & { type: typeof UPLOAD }>
|
||||
[NOTIFY]: () => BaseRenderNodeOptions<BaseNodeData & { type: typeof NOTIFY }>
|
||||
[APPLY]: () => BaseRenderNodeOptions<BaseNodeData & { type: typeof APPLY }>
|
||||
[DEPLOY]: () => BaseRenderNodeOptions<BaseNodeData & { type: typeof DEPLOY }>
|
||||
}
|
||||
|
||||
// 基础节点配置
|
||||
interface BaseNodeProps {
|
||||
node: BaseNodeData<Record<string, unknown>>
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证结果接口
|
||||
* @property valid - 验证是否通过
|
||||
* @property message - 验证失败时的提示信息
|
||||
*/
|
||||
export interface ValidatorResult {
|
||||
valid: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证函数类型定义
|
||||
* @returns ValidatorResult - 返回验证结果对象
|
||||
*/
|
||||
export type ValidatorFunction = () => ValidatorResult
|
||||
|
||||
/**
|
||||
* 兼容async-validator的类型定义
|
||||
*/
|
||||
export interface RuleItem {
|
||||
type?: string
|
||||
required?: boolean
|
||||
pattern?: RegExp
|
||||
min?: number
|
||||
max?: number
|
||||
len?: number
|
||||
enum?: Array<any>
|
||||
whitespace?: boolean
|
||||
message?: string
|
||||
validator?: (
|
||||
rule: RuleItem,
|
||||
value: any,
|
||||
callback?: (error?: Error) => void,
|
||||
) => boolean | Error | Error[] | Promise<void>
|
||||
asyncValidator?: (rule: RuleItem, value: any, callback?: (error?: Error) => void) => Promise<void>
|
||||
transform?: (value: any) => any
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容async-validator的描述符类型
|
||||
*/
|
||||
export type ValidatorDescriptor = FormRules
|
||||
|
||||
// 定义组件接收的参数类型(开始节点)
|
||||
export interface StartNodeConfig {
|
||||
// 执行模式:auto-自动,manual-手动
|
||||
exec_type: 'auto' | 'manual'
|
||||
// 执行周期类型
|
||||
type?: 'month' | 'day' | 'week'
|
||||
month?: number
|
||||
week?: number
|
||||
hour?: number
|
||||
minute?: number
|
||||
}
|
||||
|
||||
// 定义组件接收的参数类型(申请节点)
|
||||
export interface ApplyNodeConfig {
|
||||
// 基本选项
|
||||
domains: string // 域名
|
||||
email: string // 邮箱
|
||||
provider_id: string // DNS提供商授权ID
|
||||
provider: string // DNS提供商
|
||||
end_day: number // 续签间隔
|
||||
// 高级功能
|
||||
// algorithm: 'RSA2048' | 'RSA3072' | 'RSA4096' | 'RSA8192' | 'EC256' | 'EC384' // 数字证书算法
|
||||
// dnsServer?: string // 指定DNS解析服务器
|
||||
// dnsTimeout?: number // DNS超时时间
|
||||
// dnsTtl?: number // DNS解析TTL时间
|
||||
// disableCnameFollow: boolean // 关闭CNAME跟随
|
||||
// disableAutoRenew: boolean // 关闭ARI续期
|
||||
// renewInterval?: number // 续签间隔
|
||||
}
|
||||
|
||||
// 部署节点配置
|
||||
export interface DeployConfig<
|
||||
Z extends Record<string, unknown>,
|
||||
T extends
|
||||
| 'ssh'
|
||||
| 'btpanel'
|
||||
| '1panel'
|
||||
| 'btpanel-site'
|
||||
| '1panel-site'
|
||||
| 'tencentcloud-cdn'
|
||||
| 'tencentcloud-cos'
|
||||
| 'aliyun-cdn'
|
||||
| 'aliyun-oss',
|
||||
> {
|
||||
provider: T
|
||||
provider_id: string
|
||||
[key: string]: Z
|
||||
}
|
||||
|
||||
export interface DeployPanelConfig {
|
||||
}
|
||||
|
||||
// 部署节点配置(ssh)
|
||||
export interface DeploySSHConfig {
|
||||
certPath: string // 证书文件路径
|
||||
keyPath: string // 私钥文件路径
|
||||
beforeCmd: string // 前置命令
|
||||
afterCmd?: string // 后置命令
|
||||
}
|
||||
|
||||
// 部署节点配置(宝塔面板)
|
||||
export interface DeployBTPanelConfig {
|
||||
siteName: string
|
||||
}
|
||||
|
||||
// 部署节点配置(1Panel)
|
||||
export interface Deploy1PanelConfig {
|
||||
site_id: string
|
||||
}
|
||||
|
||||
// 部署腾讯云CDN/阿里云CDN
|
||||
export interface DeployCDNConfig {
|
||||
domain: string
|
||||
}
|
||||
|
||||
// 部署腾讯云COS/阿里云OSS
|
||||
export interface DeployStorageConfig {
|
||||
domain: string
|
||||
region: string
|
||||
bucket: string
|
||||
}
|
||||
|
||||
|
||||
// 部署节点配置
|
||||
export type DeployNodeConfig = DeployConfig<
|
||||
DeploySSHConfig | DeployBTPanelConfig | Deploy1PanelConfig | DeployCDNConfig | DeployStorageConfig
|
||||
>
|
||||
|
||||
|
||||
// 部署节点输入配置
|
||||
export interface DeployNodeInputsConfig {
|
||||
name: string
|
||||
fromNodeId: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 定义通知节点配置类型
|
||||
interface NotifyNodeConfig {
|
||||
provider: string
|
||||
provider_id: string
|
||||
subject: string
|
||||
body: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 定义上传节点配置类型
|
||||
interface UploadNodeConfig {
|
||||
cert: string
|
||||
key: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 部署节点配置(ssh)
|
||||
// export type DeployNodeConfigSSH = DeployNodeConfig<'ssh', DeploySSHConfig>
|
||||
|
||||
|
||||
|
||||
// 部署节点配置(宝塔面板)
|
||||
// export type DeployNodeConfigBTPanel = DeployNodeConfig<'btpanel', DeployBTPanelConfig>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useModal, useMessage } from '@baota/naive-ui/hooks'
|
||||
import { $t } from '@locales/index'
|
||||
import { useStore } from '@components/flowChart/useStore'
|
||||
import { useStore as useWorkflowViewStore } from '@autoDeploy/children/workflowView/useStore'
|
||||
import { CONDITION, EXECUTE_RESULT_CONDITION } from '@components/flowChart/lib/alias'
|
||||
import FlowChart from '@components/flowChart/components/other/drawer'
|
||||
|
||||
import { useNodeValidator } from '@components/flowChart/lib/verify'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
|
||||
import type { BaseNodeData, BranchNodeData, FlowNodeProps, NodeNum, StartNodeConfig } from '@components/flowChart/types'
|
||||
|
||||
const message = useMessage()
|
||||
const {
|
||||
flowData,
|
||||
selectedNodeId,
|
||||
setflowZoom,
|
||||
initFlowData,
|
||||
updateFlowData,
|
||||
setShowAddNodeSelect,
|
||||
addNode,
|
||||
getAddNodeSelect,
|
||||
resetFlowData,
|
||||
} = useStore()
|
||||
|
||||
const { workflowData, addNewWorkflow, updateWorkflowData, resetWorkflowData } = useWorkflowViewStore()
|
||||
const { handleError } = useError()
|
||||
/**
|
||||
* 流程图控制器
|
||||
* 用于处理流程图的业务逻辑和用户交互
|
||||
* @param {FlowNodeProps} props - 节点数据,可选
|
||||
*/
|
||||
export const useController = (props: FlowNodeProps = { type: 'quick', node: flowData.value, isEdit: false }) => {
|
||||
// 使用store获取所有需要的方法和状态
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
/**
|
||||
* 当前选中的节点数据
|
||||
* @type {ComputedRef<BaseNodeData | null>}
|
||||
*/
|
||||
const selectedNode = computed(() => {
|
||||
if (!selectedNodeId.value) return null
|
||||
// 使用findNodeRecursive查找节点
|
||||
return findNodeRecursive(flowData.value.childNode, selectedNodeId.value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 节点标题
|
||||
* @type {ComputedRef<string>}
|
||||
*/
|
||||
const nodeTitle = computed(() => {
|
||||
if (!selectedNode.value) return $t('t_6_1744861190121')
|
||||
return selectedNode.value.name
|
||||
})
|
||||
|
||||
/**
|
||||
* 递归查找节点
|
||||
* @param {BaseNodeData | BranchNodeData} node - 当前节点
|
||||
* @param {string} targetId - 目标节点ID
|
||||
* @returns {BaseNodeData | null} 找到的节点或null
|
||||
*/
|
||||
const findNodeRecursive = (node: BaseNodeData | BranchNodeData, targetId: string): BaseNodeData | null => {
|
||||
if (node.id === targetId) return node as BaseNodeData
|
||||
|
||||
// 优先检查子节点
|
||||
if (node.childNode) {
|
||||
const found = findNodeRecursive(node.childNode, targetId)
|
||||
if (found) return found
|
||||
}
|
||||
|
||||
// 检查条件节点
|
||||
if ((node as BranchNodeData).conditionNodes?.length) {
|
||||
for (const conditionNode of (node as BranchNodeData).conditionNodes) {
|
||||
const found = findNodeRecursive(conditionNode, targetId)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择节点
|
||||
* @param {string} nodeId 节点ID
|
||||
* @param {NodeNum} nodeType 节点类型
|
||||
*/
|
||||
const handleSelectNode = (nodeId: string, nodeType: NodeNum) => {
|
||||
if (nodeType === CONDITION || nodeType === EXECUTE_RESULT_CONDITION) {
|
||||
selectedNodeId.value = ''
|
||||
} else {
|
||||
selectedNodeId.value = nodeId
|
||||
useModal({
|
||||
title: `${selectedNode.value?.name}${$t('t_1_1745490731990')}`,
|
||||
area: '60rem',
|
||||
component: () => <FlowChart node={selectedNode.value} />,
|
||||
confirmText: $t('t_2_1744861190040'),
|
||||
footer: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存节点配置
|
||||
* 将当前选中节点的配置保存到流程图中
|
||||
*/
|
||||
const handleSaveConfig = () => {
|
||||
const { validator } = useNodeValidator()
|
||||
const res = validator.validateAll()
|
||||
try {
|
||||
if (res.valid && flowData.value.name) {
|
||||
const { active } = workflowData.value
|
||||
const { id, name, childNode } = flowData.value
|
||||
const { exec_type, ...exec_time } = childNode.config as unknown as StartNodeConfig
|
||||
const param = {
|
||||
name,
|
||||
active,
|
||||
content: JSON.stringify(childNode),
|
||||
exec_type,
|
||||
exec_time: JSON.stringify(exec_time || {}),
|
||||
}
|
||||
if (route.query.isEdit) {
|
||||
updateWorkflowData({ id, ...param })
|
||||
} else {
|
||||
addNewWorkflow(param)
|
||||
}
|
||||
router.push('/auto-deploy')
|
||||
} else if (!flowData.value.name) {
|
||||
message.error('保存失败,请输入工作流名称')
|
||||
}
|
||||
for (const key in res.results) {
|
||||
if (res.results.hasOwnProperty(key)) {
|
||||
const result = res.results[key] as { valid: boolean; message: string }
|
||||
if (!result.valid) {
|
||||
message.error(result.message)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error).default($t('t_12_1745457489076'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行流程图
|
||||
* 触发流程图的执行
|
||||
*/
|
||||
const handleRun = () => {
|
||||
message.info($t('t_8_1744861189821'))
|
||||
// 这里可以添加实际的运行逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* 流程图缩放控制
|
||||
* @param {number} type - 缩放类型 1:缩小,2:放大
|
||||
*/
|
||||
const handleZoom = (type: number) => {
|
||||
setflowZoom(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一级
|
||||
*/
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化流程图数据
|
||||
*/
|
||||
const initData = () => {
|
||||
console.log(props.node, 'init')
|
||||
resetFlowData()
|
||||
resetWorkflowData()
|
||||
// 如果传入了节点数据,使用传入的数据
|
||||
if (props.isEdit && props.node) {
|
||||
console.log(props.node, 'edit')
|
||||
updateFlowData(props.node)
|
||||
} else if (props.type === 'quick') {
|
||||
initFlowData() // 否则使用默认数据初始化
|
||||
} else if (props.type === 'advanced') {
|
||||
updateFlowData(props.node)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果传入了node,则当node变化时更新store中的flowData
|
||||
if (props.node) {
|
||||
watch(
|
||||
() => props.node,
|
||||
(newVal) => {
|
||||
updateFlowData(newVal)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
flowData,
|
||||
selectedNodeId,
|
||||
selectedNode,
|
||||
nodeTitle,
|
||||
handleSaveConfig,
|
||||
handleSelectNode,
|
||||
handleZoom,
|
||||
handleRun,
|
||||
goBack,
|
||||
initData,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加节点控制器
|
||||
* 用于处理添加节点的业务逻辑
|
||||
*/
|
||||
export function useAddNodeController() {
|
||||
// 使用store获取所有需要的方法和状态
|
||||
const store = useStore()
|
||||
|
||||
/**
|
||||
* 是否显示添加节点选择框
|
||||
* @type {Ref<boolean>}
|
||||
*/
|
||||
const isShowAddNodeSelect = ref(false)
|
||||
|
||||
/**
|
||||
* 定时器
|
||||
* @type {Ref<number | null>}
|
||||
*/
|
||||
const timer = ref<number | null>(null)
|
||||
|
||||
/**
|
||||
* 显示节点选择
|
||||
* @param {boolean} flag - 是否显示
|
||||
* @param {NodeNum} [nodeType] - 节点类型
|
||||
*/
|
||||
const showNodeSelect = (flag: boolean, nodeType?: NodeNum) => {
|
||||
if (!flag) {
|
||||
clearTimeout(timer.value as number)
|
||||
timer.value = window.setTimeout(() => {
|
||||
isShowAddNodeSelect.value = flag
|
||||
}, 200) as unknown as number
|
||||
} else {
|
||||
isShowAddNodeSelect.value = false
|
||||
isShowAddNodeSelect.value = flag
|
||||
}
|
||||
// 设置添加节点选择状态
|
||||
if (nodeType) {
|
||||
setShowAddNodeSelect(flag, nodeType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加节点数据
|
||||
* @param {BaseNodeData} parentNode - 父节点
|
||||
* @param {NodeNum} type - 生成节点类型
|
||||
*/
|
||||
const addNodeData = (parentNode: BaseNodeData, type: NodeNum) => {
|
||||
console.log(parentNode, type)
|
||||
// 设置添加节点选择状态
|
||||
isShowAddNodeSelect.value = false
|
||||
// 判断是否存储节点配置数据
|
||||
if (parentNode.id) addNode(parentNode.id, type, { id: uuidv4() })
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加节点选中状态
|
||||
*/
|
||||
const itemNodeSelected = () => {
|
||||
clearTimeout(timer.value as number)
|
||||
}
|
||||
|
||||
// 获取添加节点选择
|
||||
getAddNodeSelect()
|
||||
|
||||
return {
|
||||
...store,
|
||||
addNodeData,
|
||||
itemNodeSelected,
|
||||
isShowAddNodeSelect,
|
||||
showNodeSelect,
|
||||
}
|
||||
}
|
||||
579
frontend/apps/allin-ssl/src/components/flowChart/useStore.tsx
Normal file
579
frontend/apps/allin-ssl/src/components/flowChart/useStore.tsx
Normal file
@@ -0,0 +1,579 @@
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
import { deepMerge } from '@baota/utils/data'
|
||||
import nodeOptions from '@components/flowChart/lib/config'
|
||||
import MockData from '@components/flowChart/mock'
|
||||
import type {
|
||||
NodeIcon,
|
||||
NodeNum,
|
||||
NodeTitle,
|
||||
FlowNode,
|
||||
BaseNodeData,
|
||||
NodeSelect,
|
||||
BranchNodeData,
|
||||
ExecuteResultBranchNodeData,
|
||||
} from '@components/flowChart/types'
|
||||
import { BRANCH, CONDITION, EXECUTE_RESULT_BRANCH, EXECUTE_RESULT_CONDITION } from './lib/alias'
|
||||
|
||||
/**
|
||||
* 流程图数据存储
|
||||
* 用于管理流程图的状态、缩放等数据
|
||||
*/
|
||||
export const useFlowStore = defineStore('flow-store', () => {
|
||||
const flowData = ref<FlowNode>({
|
||||
id: '',
|
||||
name: '',
|
||||
childNode: {
|
||||
id: 'start-1',
|
||||
name: '开始',
|
||||
type: 'start',
|
||||
config: {
|
||||
exec_type: 'manual',
|
||||
},
|
||||
childNode: null,
|
||||
},
|
||||
}) // 流程图数据
|
||||
const flowZoom = ref(100) // 流程图缩放比例
|
||||
const addNodeSelectList = ref<NodeSelect[]>([]) // 添加节点选项列表
|
||||
const excludeNodeSelectList = ref<NodeNum[]>([]) // 排除的节点选项列表
|
||||
const addNodeBtnRef = ref<HTMLElement | null>(null) // 添加节点按钮
|
||||
const addNodeSelectRef = ref<HTMLElement | null>(null) // 添加节点选择框
|
||||
const addNodeSelectPostion = ref<number | null>(null) // 添加节点选择框位置
|
||||
const selectedNodeId = ref<string | null>(null) // 当前选中的节点ID
|
||||
const isRefreshNode = ref<string | null>(null) // 是否刷新节点
|
||||
|
||||
// 计算添加节点选项列表,排除的节点选项列表
|
||||
const nodeSelectList = computed(() => {
|
||||
return addNodeSelectList.value.filter((item) => !excludeNodeSelectList.value.includes(item.type))
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取添加节点选项列表
|
||||
* @type {NodeSelect[]}
|
||||
*/
|
||||
const getAddNodeSelect = () => {
|
||||
addNodeSelectList.value = []
|
||||
Object.keys(nodeOptions).forEach((key) => {
|
||||
const item = nodeOptions[key as NodeNum]()
|
||||
if (item.operateNode?.add) {
|
||||
addNodeSelectList.value.push({
|
||||
title: { name: item.title.name } as NodeTitle,
|
||||
type: key as NodeNum,
|
||||
icon: { ...(item.icon || {}) } as NodeIcon,
|
||||
selected: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加排除的节点选项列表
|
||||
* @param {NodeNum[]} nodeTypes - 节点类型
|
||||
*/
|
||||
const addExcludeNodeSelectList = (nodeTypes: NodeNum[]) => {
|
||||
excludeNodeSelectList.value = nodeTypes
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除排除的节点选项列表
|
||||
*/
|
||||
const clearExcludeNodeSelectList = () => {
|
||||
excludeNodeSelectList.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示添加节点选择框
|
||||
* @param {boolean} flag - 是否显示添加节点选择框
|
||||
*/
|
||||
const setShowAddNodeSelect = (flag: boolean, nodeType: NodeNum) => {
|
||||
// 设置排除的节点选项列表
|
||||
excludeNodeSelectList.value = nodeOptions[nodeType]().operateNode?.onSupportNode || []
|
||||
// 设置添加节点选择框位置
|
||||
if (flag && addNodeSelectRef.value && addNodeBtnRef.value) {
|
||||
const box = addNodeSelectRef.value.getBoundingClientRect() // 添加节点选择框
|
||||
const boxWidth = box.width // 添加节点选择框宽度
|
||||
const btn = addNodeBtnRef.value.getBoundingClientRect() // 添加节点按钮
|
||||
const btnRight = btn.right // 添加节点按钮右侧位置
|
||||
const windowWidth = window.innerWidth // 窗口宽度
|
||||
addNodeSelectPostion.value = btnRight + boxWidth > windowWidth ? 1 : 2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化流程图数据
|
||||
* 创建一个默认的开始节点作为流程图的起点
|
||||
*/
|
||||
const initFlowData = () => {
|
||||
const deepMockData = JSON.parse(JSON.stringify(MockData))
|
||||
deepMockData.name = '工作流(' + formatDate(new Date(), 'yyyy/MM/dd HH:mm:ss') + ')'
|
||||
flowData.value = deepMockData
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置流程图数据
|
||||
* 清空当前流程图的所有数据
|
||||
*/
|
||||
const resetFlowData = () => initFlowData()
|
||||
|
||||
/**
|
||||
* 递归查找节点
|
||||
* @param node 当前节点
|
||||
* @param targetId 目标节点ID
|
||||
* @returns 找到的节点或null
|
||||
*/
|
||||
const findNodeRecursive = (node: BaseNodeData | BranchNodeData, targetId: string): BaseNodeData | null => {
|
||||
if (node.id === targetId) return node
|
||||
|
||||
// 优先检查子节点
|
||||
if (node.childNode) {
|
||||
const found = findNodeRecursive(node.childNode, targetId)
|
||||
if (found) return found
|
||||
}
|
||||
// 再检查条件节点
|
||||
if ((node as BranchNodeData).conditionNodes?.length) {
|
||||
for (const conditionNode of (node as BranchNodeData).conditionNodes) {
|
||||
const found = findNodeRecursive(conditionNode, targetId)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过节点id查找节点数据
|
||||
* @param nodeId 节点id
|
||||
* @returns 节点数据或null
|
||||
*/
|
||||
const getFlowFindNodeData = (nodeId: string): BaseNodeData | null => {
|
||||
return findNodeRecursive(flowData.value.childNode, nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归更新节点
|
||||
* @param node 当前节点
|
||||
* @param targetId 目标节点ID
|
||||
* @param updateFn 更新函数
|
||||
* @param parent 父节点
|
||||
* @returns 是否更新成功
|
||||
*/
|
||||
const updateNodeRecursive = (
|
||||
node: BaseNodeData | BranchNodeData,
|
||||
targetId: string,
|
||||
updateFn: (node: BaseNodeData | BranchNodeData, parent: BaseNodeData | BranchNodeData | null) => void,
|
||||
parent: BaseNodeData | BranchNodeData | null = null,
|
||||
): boolean => {
|
||||
if (node.id === targetId) {
|
||||
updateFn(node, parent)
|
||||
return true
|
||||
}
|
||||
|
||||
if (node.childNode) {
|
||||
if (updateNodeRecursive(node.childNode, targetId, updateFn, node)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if ((node as BranchNodeData).conditionNodes?.length) {
|
||||
for (const conditionNode of (node as BranchNodeData).conditionNodes) {
|
||||
if (updateNodeRecursive(conditionNode, targetId, updateFn, node)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加节点
|
||||
* @param parentNodeId 父节点ID
|
||||
* @param nodeType 节点类型
|
||||
* @param nodeData 节点数据
|
||||
* @param position 插入位置(对于条件节点有效)
|
||||
*/
|
||||
const addNode = (
|
||||
parentNodeId: string, // 父节点ID
|
||||
nodeType: NodeNum, // 节点类型
|
||||
nodeData: Partial<BaseNodeData | BranchNodeData> = {}, // 节点数据
|
||||
) => {
|
||||
// 获取父节点
|
||||
const parentNode = getFlowFindNodeData(parentNodeId)
|
||||
if (!parentNode) {
|
||||
console.warn(`Parent node with id ${parentNodeId} not found`)
|
||||
return
|
||||
}
|
||||
// 获取支持的节点默认配置
|
||||
let newNodeData = deepMerge(nodeOptions[nodeType]().defaultNode as BaseNodeData, nodeData) as
|
||||
| BaseNodeData
|
||||
| BranchNodeData // 获取支持的节点默认配置
|
||||
|
||||
// 更新原始数据
|
||||
updateNodeRecursive(flowData.value.childNode, parentNodeId, (node, parent) => {
|
||||
switch (nodeType) {
|
||||
case CONDITION:
|
||||
// console.log('条件节点', node, parent)
|
||||
if ((node as BranchNodeData).conditionNodes) {
|
||||
newNodeData.name = `分支${(node as BranchNodeData).conditionNodes.length + 1}`
|
||||
;(node as BranchNodeData).conditionNodes.push(newNodeData)
|
||||
}
|
||||
break
|
||||
case BRANCH:
|
||||
case EXECUTE_RESULT_BRANCH:
|
||||
// 执行结果分支节点
|
||||
if (nodeType === EXECUTE_RESULT_BRANCH) {
|
||||
newNodeData = { ...newNodeData, config: { fromNodeId: parentNodeId } }
|
||||
}
|
||||
|
||||
;(newNodeData as BranchNodeData).conditionNodes[0].childNode = node.childNode
|
||||
node.childNode = newNodeData
|
||||
break
|
||||
default:
|
||||
// console.log('其他节点', node, parent)
|
||||
if (node.childNode) newNodeData.childNode = node.childNode // 组件嵌套到 childNode 中
|
||||
node.childNode = newNodeData
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上查找数据类型为 apply 或 upload 的节点
|
||||
* @param nodeId 起始节点ID
|
||||
* @returns 符合条件的节点数组 [{name: string, id: string}]
|
||||
*/
|
||||
const findApplyUploadNodesUp = (
|
||||
nodeId: string,
|
||||
scanNode: string[] = ['apply', 'upload'],
|
||||
): Array<{ name: string; id: string }> => {
|
||||
const result: Array<{ name: string; id: string }> = []
|
||||
|
||||
// 递归查找父节点的函数
|
||||
const findParentRecursive = (
|
||||
currentNode: BaseNodeData | BranchNodeData,
|
||||
targetId: string,
|
||||
path: Array<BaseNodeData | BranchNodeData> = [],
|
||||
): Array<BaseNodeData | BranchNodeData> | null => {
|
||||
// 检查当前节点是否为目标节点
|
||||
if (currentNode.id === targetId) {
|
||||
return path
|
||||
}
|
||||
|
||||
// 检查子节点
|
||||
if (currentNode.childNode) {
|
||||
const newPath = [...path, currentNode]
|
||||
const found = findParentRecursive(currentNode.childNode, targetId, newPath)
|
||||
if (found) return found
|
||||
}
|
||||
|
||||
// 检查条件节点
|
||||
if ((currentNode as BranchNodeData).conditionNodes?.length) {
|
||||
for (const conditionNode of (currentNode as BranchNodeData).conditionNodes) {
|
||||
const newPath = [...path, currentNode]
|
||||
const found = findParentRecursive(conditionNode, targetId, newPath)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 从根节点开始查找路径
|
||||
const path = findParentRecursive(flowData.value.childNode, nodeId)
|
||||
// 如果找到路径,筛选出 apply 和 upload 类型的节点
|
||||
if (path) {
|
||||
path.forEach((node) => {
|
||||
if (scanNode.includes(node.type)) {
|
||||
result.push({
|
||||
name: node.name,
|
||||
id: node.id as string,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除节点
|
||||
* @param nodeId 要删除的节点ID
|
||||
* @param deep 是否深度删除(默认false,即子节点上移)
|
||||
*/
|
||||
const removeNode = (nodeId: string, deep: boolean = false) => {
|
||||
const node = getFlowFindNodeData(nodeId)
|
||||
if (!node) {
|
||||
console.warn(`Node with id ${nodeId} not found`)
|
||||
return
|
||||
}
|
||||
console.log(nodeId, node)
|
||||
|
||||
// 更新原始数据
|
||||
updateNodeRecursive(flowData.value.childNode, nodeId, (node, parent) => {
|
||||
if (!parent) {
|
||||
console.warn('Cannot remove root node')
|
||||
return
|
||||
}
|
||||
const { type, conditionNodes } = parent as BranchNodeData | ExecuteResultBranchNodeData
|
||||
// 处理条件节点(分支节点、执行结果分支节点)
|
||||
// console.log(type, conditionNodes, node)
|
||||
|
||||
// 条件一:当前节点为普通节点
|
||||
const nodeTypeList = [CONDITION, EXECUTE_RESULT_CONDITION, BRANCH, EXECUTE_RESULT_BRANCH]
|
||||
if (!nodeTypeList.includes(node.type) && parent.childNode?.id === nodeId) {
|
||||
console.log(deep)
|
||||
// 处理普通节点
|
||||
if (deep) {
|
||||
// 深度删除,直接移除
|
||||
parent.childNode = undefined
|
||||
} else {
|
||||
console.log(parent.childNode, node.childNode)
|
||||
// 非深度删除,子节点上移
|
||||
if (node.childNode) {
|
||||
parent.childNode = node.childNode
|
||||
} else {
|
||||
parent.childNode = undefined
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 条件二:当前节点为条件节点
|
||||
if (nodeTypeList.includes(node.type)) {
|
||||
// 条件节点为分支节点或执行结果分支节点
|
||||
if (conditionNodes.length === 2) {
|
||||
// 条件节点为分支节点,则选定对立节点的子节点作为当前节点的子节点
|
||||
// console.log('条件节点为分支节点', parent)
|
||||
if (type === BRANCH) {
|
||||
updateNodeRecursive(flowData.value.childNode, parent.id as string, (nodes, parents) => {
|
||||
const index = conditionNodes.findIndex((n) => n.id === nodeId)
|
||||
const backNode = nodes.childNode
|
||||
if (index !== -1 && parents) {
|
||||
parents.childNode = conditionNodes[index === 0 ? 1 : 0].childNode // 将选定对立节点的子节点作为当前节点的子节
|
||||
const allChildNode = getNodePropertyToLast(parents, 'childNode') as BaseNodeData
|
||||
allChildNode.childNode = backNode
|
||||
}
|
||||
})
|
||||
} else {
|
||||
updateNodeRecursive(flowData.value.childNode, parent.id as string, (nodes, parents) => {
|
||||
if (parents) {
|
||||
if (parent?.childNode?.id) {
|
||||
parents.childNode = parent.childNode
|
||||
} else {
|
||||
parents.childNode = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const index = (parent as BranchNodeData).conditionNodes.findIndex((n) => n.id === nodeId)
|
||||
if (index !== -1) {
|
||||
if (deep) {
|
||||
// 深度删除,直接移除
|
||||
;(parent as BranchNodeData).conditionNodes.splice(index, 1)
|
||||
} else {
|
||||
// 非深度删除,子节点上移
|
||||
const targetNode = (parent as BranchNodeData).conditionNodes[index]
|
||||
if (targetNode?.childNode) {
|
||||
;(parent as BranchNodeData).conditionNodes[index] = targetNode.childNode
|
||||
} else {
|
||||
;(parent as BranchNodeData).conditionNodes.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return flowData.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查询节点属性直到最后一层
|
||||
* @param node 当前节点
|
||||
* @param property 要查询的属性名
|
||||
* @returns 最后一层的属性值
|
||||
*/
|
||||
const getNodePropertyToLast = (node: BaseNodeData, property: string) => {
|
||||
if (!node) return null
|
||||
const value = (node as any)[property]
|
||||
if (!value) return node
|
||||
// 如果属性值是一个对象,继续递归查询
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return getNodePropertyToLast(value, property)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点配置
|
||||
* @param nodeId 节点ID
|
||||
* @param config 新的配置数据
|
||||
*/
|
||||
const updateNodeConfig = (nodeId: string, config: Record<string, any>) => {
|
||||
const node = getFlowFindNodeData(nodeId)
|
||||
if (!node) {
|
||||
console.warn(`Node with id ${nodeId} not found`)
|
||||
return
|
||||
}
|
||||
// 更新原始数据
|
||||
updateNodeRecursive(flowData.value.childNode, nodeId, (node) => {
|
||||
node.config = config
|
||||
})
|
||||
return flowData.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点数据
|
||||
* @param nodeId 要更新的节点ID
|
||||
* @param newNodeData 新的节点数据
|
||||
*/
|
||||
const updateNode = (nodeId: string, newNodeData: Partial<FlowNode>, isMergeArray: boolean = true) => {
|
||||
const node = getFlowFindNodeData(nodeId)
|
||||
if (!node) {
|
||||
console.warn(`Node with id ${nodeId} not found`)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新原始数据
|
||||
updateNodeRecursive(flowData.value.childNode, nodeId, (node) => {
|
||||
const updatedNode = deepMerge(node, newNodeData, isMergeArray) as BaseNodeData
|
||||
Object.keys(updatedNode).forEach((key) => {
|
||||
if (key in node) {
|
||||
;(node as any)[key] = updatedNode[key as keyof typeof updatedNode]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return flowData.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点是否存在子节点
|
||||
* @param nodeId 节点id
|
||||
* @returns 是否存在子节点
|
||||
*/
|
||||
const checkFlowNodeChild = (nodeId: string): boolean => {
|
||||
const node = getFlowFindNodeData(nodeId)
|
||||
return node ? !!(node.childNode || (node as BranchNodeData).conditionNodes?.length) : false
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在行内节点
|
||||
* @param nodeId 节点id
|
||||
*/
|
||||
const checkFlowInlineNode = (nodeId: string) => {
|
||||
const node = getFlowFindNodeData(nodeId)
|
||||
if (!node || node.type !== 'condition') return
|
||||
|
||||
// 更新原始数据
|
||||
updateNodeRecursive(flowData.value.childNode, nodeId, (node) => {
|
||||
if ((node as BranchNodeData).conditionNodes) {
|
||||
;(node as BranchNodeData).conditionNodes = (node as BranchNodeData).conditionNodes.filter(
|
||||
(n) => n.id !== nodeId,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @description 显示节点选择
|
||||
// * @param {boolean} flag
|
||||
// * @param {NodeNum} nodeData
|
||||
// */
|
||||
// const showNodeSelect = (flag: boolean, nodeType?: NodeNum) => {
|
||||
// if (!flag) {
|
||||
// clearTimeout(timer.value as number)
|
||||
// timer.value = window.setTimeout(() => {
|
||||
// isShowAddNodeSelect.value = flag
|
||||
// }, 1000) as unknown as null
|
||||
// } else {
|
||||
// isShowAddNodeSelect.value = false
|
||||
// isShowAddNodeSelect.value = flag
|
||||
// }
|
||||
// // 设置添加节点选择状态
|
||||
// if (nodeType) {
|
||||
// flowStore.setShowAddNodeSelect(flag, nodeType)
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 获取流程图数据
|
||||
* 返回当前流程图数据的深拷贝,避免直接修改原始数据
|
||||
* @returns {Object} 流程图数据的副本
|
||||
*/
|
||||
const getResultData = () => {
|
||||
return deepMerge({}, flowData.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新流程图数据
|
||||
* 用新的数据替换当前的流程图数据
|
||||
* @param {Object} newData - 新的流程图数据
|
||||
*/
|
||||
const updateFlowData = (newData: FlowNode) => {
|
||||
flowData.value = newData
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置流程图缩放比例
|
||||
* 控制流程图的显示大小
|
||||
* @param {number} type - 缩放类型:1 表示缩小,2 表示放大
|
||||
*/
|
||||
const setflowZoom = (type: number) => {
|
||||
if (type === 1 && flowZoom.value > 50) {
|
||||
flowZoom.value -= 10
|
||||
} else if (type === 2 && flowZoom.value < 300) {
|
||||
flowZoom.value += 10
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 数据
|
||||
flowData, // 流程图数据
|
||||
flowZoom, // 流程图缩放比例
|
||||
selectedNodeId, // 当前选中的节点ID
|
||||
isRefreshNode, // 是否刷新节点
|
||||
|
||||
// 方法
|
||||
initFlowData, // 初始化流程图数据
|
||||
resetFlowData, // 重置流程图数据
|
||||
getResultData, // 获取流程图数据
|
||||
updateFlowData, // 更新流程图数据
|
||||
setflowZoom, // 设置流程图缩放比例
|
||||
|
||||
// 添加节点-数据
|
||||
addNodeSelectList, // 添加节点选项列表
|
||||
nodeSelectList, // 计算添加节点选项列表,排除的节点选项列表
|
||||
excludeNodeSelectList, // 排除的节点选项列表
|
||||
addNodeBtnRef, // 添加节点按钮
|
||||
addNodeSelectRef, // 添加节点选择框
|
||||
addNodeSelectPostion, // 添加节点选择框位置
|
||||
|
||||
// 添加节点-方法
|
||||
getAddNodeSelect, // 获取添加节点选项列表
|
||||
addExcludeNodeSelectList, // 添加排除的节点选项列表
|
||||
clearExcludeNodeSelectList, // 清除排除的节点选项列表
|
||||
setShowAddNodeSelect, // 设置显示添加节点选择框
|
||||
|
||||
// 节点操作
|
||||
addNode,
|
||||
removeNode,
|
||||
updateNodeConfig,
|
||||
updateNode,
|
||||
findApplyUploadNodesUp, // 向上查找 apply 和 upload 类型节点
|
||||
checkFlowNodeChild, // 检查节点是否存在子节点
|
||||
checkFlowInlineNode, // 检查是否存在行内节点
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 使用流程图数据存储
|
||||
* 提供流程图数据存储的引用和解构
|
||||
* @returns {Object} 包含流程图数据存储的引用和解构
|
||||
*/
|
||||
export const useStore = () => {
|
||||
const flowStore = useFlowStore()
|
||||
const storeRef = storeToRefs(flowStore)
|
||||
return {
|
||||
...flowStore,
|
||||
...storeRef,
|
||||
}
|
||||
}
|
||||
151
frontend/apps/allin-ssl/src/components/logViewer/index.tsx
Normal file
151
frontend/apps/allin-ssl/src/components/logViewer/index.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { NCard, NText, NSpin, NScrollbar, NButton, NSpace, NIcon } from 'naive-ui'
|
||||
import { DownloadOutline } from '@vicons/ionicons5'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LogViewer',
|
||||
props: {
|
||||
// 日志内容
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 是否加载中
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 是否允许下载
|
||||
enableDownload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 下载文件名
|
||||
downloadFileName: {
|
||||
type: String,
|
||||
default: 'logs.txt',
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: '日志详情',
|
||||
},
|
||||
// 获取日志方法
|
||||
fetchLogs: {
|
||||
type: Function as PropType<() => Promise<string>>,
|
||||
default: () => Promise.resolve(''),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const logs = ref(props.content || '')
|
||||
const isLoading = ref(props.loading)
|
||||
const logContainerRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
// 监听内容变化
|
||||
watch(
|
||||
() => props.content,
|
||||
(newValue) => {
|
||||
logs.value = newValue
|
||||
scrollToBottom()
|
||||
},
|
||||
)
|
||||
|
||||
// 监听加载状态变化
|
||||
watch(
|
||||
() => props.loading,
|
||||
(newValue) => {
|
||||
isLoading.value = newValue
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.fetchLogs,
|
||||
(newValue) => {
|
||||
console.log('fetchLogs', props.fetchLogs)
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
// 如果有传入获取日志的方法,则调用
|
||||
// 这里可以根据需要设置一个定时器来定时获取日志
|
||||
loadLogs()
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
// 加载日志
|
||||
const loadLogs = async () => {
|
||||
if (!props.fetchLogs) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
const result = await props.fetchLogs()
|
||||
console.log('获取日志:', result)
|
||||
logs.value = result
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
console.error('加载日志失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下载日志
|
||||
const downloadLogs = () => {
|
||||
if (!logs.value) return
|
||||
|
||||
const blob = new Blob([logs.value], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = props.downloadFileName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
setTimeout(() => {
|
||||
if (logContainerRef.value) {
|
||||
const scrollElement = logContainerRef.value.querySelector('.n-scrollbar-container')
|
||||
if (scrollElement) {
|
||||
scrollElement.scrollTop = scrollElement.scrollHeight
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 刷新日志
|
||||
const refreshLogs = () => {
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
return () => (
|
||||
<NCard bordered={false} class="w-full h-full" contentClass="!pb-0 !px-0">
|
||||
<NSpin show={isLoading.value}>
|
||||
<div class="mb-2.5 flex justify-start items-center">
|
||||
<NSpace>
|
||||
<NButton onClick={refreshLogs} size="small">
|
||||
刷新
|
||||
</NButton>
|
||||
{props.enableDownload && (
|
||||
<NButton onClick={downloadLogs} size="small">
|
||||
<NIcon>
|
||||
<DownloadOutline />
|
||||
</NIcon>
|
||||
<span>下载日志</span>
|
||||
</NButton>
|
||||
)}
|
||||
</NSpace>
|
||||
</div>
|
||||
<div class="border border-gray-200 rounded bg-gray-50" ref={logContainerRef}>
|
||||
<NScrollbar class="h-max-[500px]">
|
||||
<NText class="block p-3 h-[500px] font-mono whitespace-pre-wrap break-all text-xs leading-normal">
|
||||
{logs.value ? logs.value : '暂无日志信息'}
|
||||
</NText>
|
||||
</NScrollbar>
|
||||
</div>
|
||||
</NSpin>
|
||||
</NCard>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,178 @@
|
||||
import { defineComponent, VNode } from 'vue'
|
||||
import { NButton, NFlex, NFormItemGi, NGrid, NSelect, NText } from 'naive-ui'
|
||||
import { $t } from '@locales/index'
|
||||
import { useStore } from '@/views/layout/useStore'
|
||||
import SvgIcon from '@components/svgIcon'
|
||||
|
||||
interface NotifyProviderOption {
|
||||
label: string
|
||||
value: string
|
||||
type: string
|
||||
}
|
||||
|
||||
interface NotifyProviderSelectProps {
|
||||
path: string
|
||||
value: string
|
||||
valueType: 'value' | 'type'
|
||||
isAddMode: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NotifyProviderSelect',
|
||||
props: {
|
||||
// 表单,用于绑定表单的值
|
||||
path: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 表单的值
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 表单的值类型
|
||||
valueType: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
|
||||
// 是否为添加模式
|
||||
isAddMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:value'],
|
||||
setup(props: NotifyProviderSelectProps, { emit }) {
|
||||
// 获取消息通知提供商
|
||||
const { fetchNotifyProvider, notifyProvider } = useStore()
|
||||
// 表单的值
|
||||
const param = ref<NotifyProviderOption>({
|
||||
label: '',
|
||||
value: '',
|
||||
type: '',
|
||||
})
|
||||
const notifyProviderRef = ref<NotifyProviderOption[]>([])
|
||||
|
||||
/**
|
||||
* 打开通知渠道配置页面
|
||||
*/
|
||||
const goToAddNotifyProvider = () => {
|
||||
window.open('/settings?tab=notification', '_blank')
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染单选标签
|
||||
* @param option - 选项
|
||||
* @returns 渲染后的VNode
|
||||
*/
|
||||
const renderSingleSelectTag = ({ option }: Record<string, any>): VNode => {
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
{option.label ? (
|
||||
<NFlex>
|
||||
<SvgIcon icon={`notify-${props.valueType === 'value' ? option.type : option.value}`} size="2rem" />
|
||||
<NText>{option.label}</NText>
|
||||
</NFlex>
|
||||
) : (
|
||||
<NText>{$t('t_0_1745887835267')}</NText>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染标签
|
||||
* @param option - 选项
|
||||
* @returns 渲染后的VNode
|
||||
*/
|
||||
const renderLabel = (option: NotifyProviderOption): VNode => {
|
||||
return (
|
||||
<NFlex>
|
||||
<SvgIcon icon={`notify-${props.valueType === 'value' ? option.type : option.value}`} size="2rem" />
|
||||
<NText>{option.label}</NText>
|
||||
</NFlex>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 更新类型
|
||||
* @param option - 选项
|
||||
* @param value - 值
|
||||
*/
|
||||
const handleUpdateType = (value: string) => {
|
||||
if (!value) return
|
||||
const row = notifyProviderRef.value.find((item) => {
|
||||
return item.value === value
|
||||
})
|
||||
param.value = {
|
||||
label: row?.label || '',
|
||||
value: row?.value || '',
|
||||
type: row?.type || '',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新表单的值
|
||||
* @param value - 表单的值
|
||||
*/
|
||||
const handleUpdateValue = (value: string) => {
|
||||
handleUpdateType(value)
|
||||
emit('update:value', param.value)
|
||||
}
|
||||
|
||||
// 监听父组件的值
|
||||
watch(
|
||||
() => props.value,
|
||||
(newVal) => {
|
||||
fetchNotifyProvider()
|
||||
handleUpdateType(newVal)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 监听消息通知提供商
|
||||
watch(
|
||||
() => notifyProvider.value,
|
||||
(newVal) => {
|
||||
notifyProviderRef.value =
|
||||
newVal.map((item) => ({
|
||||
label: item.label,
|
||||
value: props.valueType === 'value' ? item.value : item.type,
|
||||
type: props.valueType === 'value' ? item.type : item.value,
|
||||
})) || []
|
||||
handleUpdateType(props.value)
|
||||
},
|
||||
)
|
||||
|
||||
return () => (
|
||||
<NGrid cols={24}>
|
||||
<NFormItemGi span={props.isAddMode ? 13 : 24} label={$t('t_1_1745887832941')} path={props.path}>
|
||||
<NSelect
|
||||
class="flex-1 w-full "
|
||||
options={notifyProviderRef.value}
|
||||
renderLabel={renderLabel}
|
||||
renderTag={renderSingleSelectTag}
|
||||
filterable
|
||||
placeholder={$t('t_0_1745887835267')}
|
||||
v-model:value={param.value.value}
|
||||
onUpdateValue={handleUpdateValue}
|
||||
v-slots={{
|
||||
empty: () => {
|
||||
return <span class="text-[1.4rem]">{$t('t_0_1745887835267')}</span>
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</NFormItemGi>
|
||||
{props.isAddMode && (
|
||||
<NFormItemGi span={11}>
|
||||
<NButton class="mx-[8px]" onClick={goToAddNotifyProvider}>
|
||||
{$t('t_2_1745887834248')}
|
||||
</NButton>
|
||||
<NButton onClick={fetchNotifyProvider}>{$t('t_0_1746497662220')}</NButton>
|
||||
</NFormItemGi>
|
||||
)}
|
||||
</NGrid>
|
||||
)
|
||||
},
|
||||
})
|
||||
40
frontend/apps/allin-ssl/src/components/svgIcon/index.tsx
Normal file
40
frontend/apps/allin-ssl/src/components/svgIcon/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { defineComponent, computed, PropType } from 'vue'
|
||||
|
||||
interface SvgIconProps {
|
||||
size: string
|
||||
icon: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SvgIcon',
|
||||
props: {
|
||||
// 图标
|
||||
icon: {
|
||||
type: String as PropType<string>,
|
||||
required: true,
|
||||
},
|
||||
// 颜色
|
||||
color: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
// 大小
|
||||
size: {
|
||||
type: String as PropType<string>,
|
||||
default: '1.8rem',
|
||||
},
|
||||
},
|
||||
setup(props: SvgIconProps) {
|
||||
const iconName = computed(() => `#icon-${props.icon}`)
|
||||
return () => (
|
||||
<svg
|
||||
class="relative inline-block align-[-0.2rem]"
|
||||
style={{ width: props.size, height: props.size }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlinkHref={iconName.value} fill={props.color} />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
})
|
||||
110
frontend/apps/allin-ssl/src/components/typeIcon/index.tsx
Normal file
110
frontend/apps/allin-ssl/src/components/typeIcon/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { NTag, NText } from 'naive-ui'
|
||||
import SvgIcon from '../svgIcon/index' // 注意修改引入路径以匹配实际位置
|
||||
|
||||
// 定义支持的访问类型
|
||||
const types = {
|
||||
ssh: 'SSH',
|
||||
aliyun: '阿里云',
|
||||
tencentcloud: '腾讯云',
|
||||
btpanel: '宝塔面板',
|
||||
'1panel': '1Panel',
|
||||
mail: '邮件',
|
||||
dingtalk: '钉钉',
|
||||
wecom: '企业微信',
|
||||
feishu: '飞书',
|
||||
webhook: 'WebHook',
|
||||
'tencentcloud-cdn': '腾讯云CDN',
|
||||
'tencentcloud-cos': '腾讯云COS',
|
||||
'aliyun-cdn': '阿里云CDN',
|
||||
'aliyun-oss': '阿里云OSS',
|
||||
'1panel-site': '1Panel网站',
|
||||
'btpanel-site': '宝塔面板网站',
|
||||
}
|
||||
|
||||
export const AuthApiTypeIcon = defineComponent({
|
||||
name: 'TypeIcon',
|
||||
props: {
|
||||
// 图标类型
|
||||
icon: {
|
||||
type: String as PropType<keyof typeof types | string>,
|
||||
required: true,
|
||||
},
|
||||
// tag类型
|
||||
type: {
|
||||
type: String as PropType<'default' | 'primary' | 'info' | 'success' | 'warning' | 'error'>,
|
||||
default: 'default',
|
||||
},
|
||||
// 对齐方式
|
||||
align: {
|
||||
type: String as PropType<'left' | 'right'>,
|
||||
default: 'left',
|
||||
},
|
||||
// 是否显示文本
|
||||
text: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// 获取图标路径的函数 - 进一步优化版
|
||||
const iconPath = computed(() => {
|
||||
const isNotify = ['mail', 'dingtalk', 'wecom', 'feishu', 'webhook'].includes(props.icon)
|
||||
const RESOURCE_PREFIX = isNotify ? 'notify-' : 'resources-'
|
||||
|
||||
// 所有支持的类型直接映射到对应的资源名称
|
||||
const iconMap: Record<string, string> = {
|
||||
ssh: 'ssh',
|
||||
aliyun: 'aliyun',
|
||||
tencentcloud: 'tencentcloud',
|
||||
btpanel: 'btpanel',
|
||||
'1panel': '1panel',
|
||||
mail: 'mail',
|
||||
dingtalk: 'dingtalk',
|
||||
wecom: 'wecom',
|
||||
feishu: 'feishu',
|
||||
webhook: 'webhook',
|
||||
'tencentcloud-cdn': 'tencentcloud',
|
||||
'tencentcloud-cos': 'tencentcloud',
|
||||
'aliyun-cdn': 'aliyun',
|
||||
'aliyun-oss': 'aliyun',
|
||||
'1panel-site': '1panel',
|
||||
'btpanel-site': 'btpanel',
|
||||
}
|
||||
|
||||
// 返回匹配的图标路径或默认图标
|
||||
return RESOURCE_PREFIX + (iconMap[props.icon] || 'default')
|
||||
})
|
||||
const typeName = computed(() => types[props.icon as keyof typeof types] || props.icon)
|
||||
|
||||
watch(
|
||||
() => props.icon,
|
||||
(newVal) => {
|
||||
console.log(newVal, 'newVal')
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.type,
|
||||
(newVal) => {
|
||||
console.log(newVal, 'newVal')
|
||||
},
|
||||
)
|
||||
|
||||
return () => (
|
||||
<NTag
|
||||
bordered={false}
|
||||
class="cursor-pointer"
|
||||
type={props.type}
|
||||
v-slots={{
|
||||
avatar: () => <SvgIcon icon={iconPath.value} size="1.4rem" />,
|
||||
}}
|
||||
>
|
||||
<NText class="text-[12px]">{props.text && <span>{typeName.value}</span>}</NText>
|
||||
</NTag>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// 默认导出组件,方便使用
|
||||
export default AuthApiTypeIcon
|
||||
Reference in New Issue
Block a user