| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- <template>
- <div class="var-select">
- <el-popover
- v-model:visible="visible"
- trigger="click"
- width="320"
- popper-class="var-select__popover"
- placement="bottom-start"
- >
- <template #reference>
- <div class="var-select__trigger">
- <el-input-tag
- :model-value="innerValue ? [innerValue] : undefined"
- :placeholder="t('common.nodeBase.varSelect.selectVariable')"
- :aria-label="t('common.nodeBase.varSelect.selectVariable')"
- clearable
- @clear="handleClear"
- @remove-tag="handleClear"
- autocomplete
- v-bind="$attrs"
- >
- <template #tag>
- <VarLabel :label="innerValue" />
- </template>
- </el-input-tag>
- </div>
- </template>
- <div class="var-select__panel">
- <el-input
- v-model="keyword"
- clearable
- size="small"
- :placeholder="t('common.nodeBase.varSelect.searchVariable')"
- class="var-select__search"
- />
- <div class="var-select__list">
- <!-- 变量列表 -->
- <div v-for="group in filteredVars" class="var-select__group">
- <div class="var-select__group-title">{{ group.name }}</div>
- <div
- v-for="item in group.variableList"
- :key="item.expression"
- class="var-select__item"
- @click="
- handleSelect({
- value: item.expression,
- type: item.type
- })
- "
- >
- <span
- class="var-select__item-prefix env text-10px"
- :style="{ background: nodeMap[group?.type ?? '']?.iconColor ?? '' }"
- >
- <span v-if="group.id === 'env'" class="text-6px">ENV</span>
- <span v-if="group.id === 'sys'" class="text-6px">SYS</span>
- <img
- v-else-if="isImageIcon(nodeMap[group?.type ?? '']?.icon)"
- :src="nodeMap[group?.type ?? '']?.icon"
- alt="node icon"
- class="w-18px h-18px object-contain"
- />
- <Icon v-else-if="group?.type" :icon="nodeMap[group?.type]?.icon!" :size="18" />
- </span>
- <span class="var-select__item-name" :title="item.name">{{ item.name }}</span>
- <span class="var-select__item-type">{{ normalizeTypeLabel(item.type) }}</span>
- </div>
- </div>
- <div v-if="!filteredVars.length" class="var-select__empty">
- {{ t('common.nodeBase.varSelect.empty') }}
- </div>
- </div>
- </div>
- </el-popover>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, ref, watch, inject, type Ref } from 'vue'
- import { Icon } from '@repo/ui'
- import { nodeMap } from '@/nodes'
- import { VARIABLE_TYPE_OPTIONS } from '@/constant'
- import { useI18n } from '@/composables/useI18n'
- import VarLabel from '@/components/VarLabel/index.vue'
- import type { NodeVar, VarType } from '@/types/var'
- interface Props {
- /**
- * 选中的变量,格式 #{xxx.var}
- */
- modelValue: string
- /**
- * 过滤方法
- */
- filterFn?: (list: NodeVar[]) => NodeVar[]
- /**
- * 绑定类型
- */
- varType?: VarType | ''
- }
- const props = withDefaults(defineProps<Props>(), {
- modelValue: ''
- })
- const emit = defineEmits<{
- (e: 'update:modelValue', value: string): void
- (e: 'update:varType', value?: VarType | ''): void
- (e: 'change', value: { value: string; type: VarType }): void
- (e: 'clear'): void
- }>()
- const { t } = useI18n()
- const nodeVars = inject<Ref<NodeVar[]>>('nodeVars')
- const visible = ref(false)
- const keyword = ref('')
- const innerValue = ref<string>(props.modelValue)
- const innerType = ref<VarType | '' | undefined>(props.varType)
- watch(
- () => props.modelValue,
- (val) => {
- if (val !== innerValue.value) {
- innerValue.value = val
- }
- },
- { deep: true }
- )
- const handleClear = () => {
- innerValue.value = ''
- innerType.value = ''
- emit('update:varType', '')
- emit('update:modelValue', '')
- emit('clear')
- }
- watch(
- () => innerValue.value,
- (val) => {
- emit('update:modelValue', val)
- }
- )
- watch(
- () => innerType.value,
- (val) => {
- emit('update:varType', val)
- }
- )
- const normalizeTypeLabel = (type?: VarType) => {
- if (!type) return ''
- const labelMap: Record<string, string> = {
- string: t('common.nodeBase.valueTypes.string'),
- number: t('common.nodeBase.valueTypes.number'),
- boolean: t('common.nodeBase.valueTypes.boolean'),
- object: t('common.nodeBase.valueTypes.object'),
- 'array[string]': t('common.nodeBase.valueTypes.arrayString'),
- 'array[number]': t('common.nodeBase.valueTypes.arrayNumber'),
- 'array[boolean]': t('common.nodeBase.valueTypes.arrayBoolean'),
- 'array[object]': t('common.nodeBase.valueTypes.arrayObject'),
- 'array[file]': t('common.nodeBase.valueTypes.arrayFile')
- }
- return labelMap[type] || VARIABLE_TYPE_OPTIONS.find((item) => item.value === type)?.label || ''
- }
- const isImageIcon = (icon?: string) => !!icon && icon.startsWith('data:image/')
- /**
- * 过滤变量列表
- */
- const filteredVars = computed(() => {
- const kw = keyword.value.trim().toLowerCase()
- let list = nodeVars?.value || []
- // 先根据 filterFn 过滤一遍
- if (props.filterFn) {
- list = props.filterFn(list)
- }
- const options = list?.filter((item) => item.variableList.find((i) => i.name.includes(kw)))
- return options || []
- })
- const handleSelect = (variable: { value: string; type: VarType }) => {
- innerValue.value = variable.value
- innerType.value = variable.type
- visible.value = false
- keyword.value = ''
- emit('change', variable)
- }
- </script>
- <style scoped lang="less">
- .var-select {
- width: 100%;
- &__trigger {
- width: 100%;
- cursor: pointer;
- }
- }
- .var-select__popover {
- padding: 8px !important;
- }
- .var-select__panel {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .var-select__search {
- :deep(.el-input__wrapper) {
- box-shadow: none;
- border-radius: 8px;
- background-color: #f5f5f5;
- }
- }
- .var-select__list {
- max-height: 260px;
- overflow-y: auto;
- }
- .var-select__group + .var-select__group {
- margin-top: 8px;
- border-top: 1px solid #f2f2f2;
- padding-top: 8px;
- }
- .var-select__group-title {
- font-size: 12px;
- color: #999;
- margin-bottom: 4px;
- }
- .var-select__item {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 4px 6px;
- border-radius: 6px;
- cursor: pointer;
- font-size: 12px;
- color: #333;
- &:hover {
- background-color: #f5f7ff;
- }
- }
- .var-select__item-prefix {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- border-radius: 6px;
- color: #fff;
- flex-shrink: 0;
- &.env {
- background: #6366f1;
- }
- &.sys {
- background: #f97316;
- }
- }
- .var-select__item-name {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .var-select__item-type {
- color: #999;
- flex-shrink: 0;
- }
- .var-select__empty {
- padding: 12px 4px;
- font-size: 12px;
- color: #999;
- text-align: center;
- }
- :deep(.el-input-tag__inner) {
- flex-wrap: nowrap;
- }
- </style>
|