【初始化】前端工程项目

This commit is contained in:
chudong
2025-05-09 15:11:21 +08:00
parent c012704c9a
commit d7c556c3b0
524 changed files with 55595 additions and 112 deletions

Binary file not shown.

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

Binary file not shown.

View File

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

View File

@@ -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>
)
},
})

View File

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

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

@@ -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>
},
})

View File

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

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

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

View File

@@ -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>
)
},
})

View File

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

View File

@@ -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>
)
},
})

View File

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

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

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

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

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

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

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

View File

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

View 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>
)
},
})

View File

@@ -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' // 部署节点

View 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

View 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, // 验证器实例
}
}

View File

@@ -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: '',
},
},
},
},
},
},
}

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

View File

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

View 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,
}
}

View 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>
)
},
})

View File

@@ -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>
)
},
})

View 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>
)
},
})

View 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