| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- <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="选择变量"
- aria-label="选择变量"
- clearable
- @clear="handleClear"
- @remove-tag="handleClear"
- autocomplete
- v-bind="$attrs"
- >
- <template #tag>
- <div v-if="valueInfo.type === 'env'" class="flex gap-1 items-center truncate">
- <span class="var-select__item-prefix env text-6px">
- <span>ENV</span>
- </span>
- <span class="text-gray-600">{{ valueInfo.value }}</span>
- </div>
- <div v-else-if="valueInfo.type === 'sys'" class="flex gap-1 items-center truncate">
- <span class="var-select__item-prefix sys text-6px">
- <span>SYS</span>
- </span>
- <span class="text-gray-600">{{ valueInfo.value }}</span>
- </div>
- <div v-else class="truncate" :title="valueInfo.nodeName + ' / ' + valueInfo.value">
- <span
- class="var-select__item-prefix env text-10px"
- :style="{ background: nodeMap[valueInfo?.nodeType ?? '']?.iconColor ?? '' }"
- >
- <Icon :icon="nodeMap[valueInfo?.nodeType!]?.icon!" :size="18" />
- </span>
- <span>{{ valueInfo.nodeName }}</span>
- <span class="mx-2px text-gray-400">/</span>
- <span class="text-gray-600">{{ valueInfo.value }}</span>
- </div>
- </template>
- </el-input-tag>
- </div>
- </template>
- <div class="var-select__panel">
- <el-input
- v-model="keyword"
- clearable
- size="small"
- placeholder="搜索变量"
- 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>
- <Icon v-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">暂无匹配的变量</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 type { NodeVar, VarType } from '@/types/var'
- interface Props {
- /**
- * 选中的变量,格式 #{xxx.var}
- */
- modelValue: string
- /**
- * 过滤方法
- */
- filterFn?: (list: NodeVar[]) => NodeVar[]
- }
- const props = withDefaults(defineProps<Props>(), {
- modelValue: ''
- })
- const emit = defineEmits<{
- (e: 'update:modelValue', value: string): void
- (e: 'change', value: { value: string; type: string }): void
- }>()
- const nodeVars = inject<Ref<NodeVar[]>>('nodeVars')
- const visible = ref(false)
- const keyword = ref('')
- const innerValue = ref<string>(props.modelValue)
- watch(
- () => props.modelValue,
- (val) => {
- if (val !== innerValue.value) {
- innerValue.value = val
- }
- },
- { deep: true }
- )
- const handleClear = () => {
- innerValue.value = ''
- emit('update:modelValue', '')
- }
- watch(
- () => innerValue.value,
- (val) => {
- emit('update:modelValue', val)
- },
- { deep: true }
- )
- const valueInfo = computed(() => {
- // 根据#{xxx.var}解析变量
- // 解析格式 #{env.xxx} 或 #{nodeId.xxx}
- if (!innerValue.value?.startsWith('#{') || !innerValue.value?.endsWith('}')) {
- return {
- type: 'env',
- name: innerValue.value || '',
- value: innerValue.value || ''
- }
- }
- const expr = innerValue.value.slice(2, -1) // 去掉 #{ 和 }
- const [prefix, ...rest] = expr.split('.')
- const varName = rest.join('.')
- if (!prefix || !varName) {
- return {
- type: 'env',
- name: innerValue.value || '',
- value: innerValue.value || ''
- }
- }
- if (prefix === 'env') {
- // 环境变量
- return {
- type: 'env',
- name: 'env',
- value: varName
- }
- } else if (prefix.startsWith('sys')) {
- // 系统变量
- return {
- type: 'sys',
- name: 'sys',
- value: varName
- }
- } else {
- // 节点变量,需要解析节点类型名称
- if (nodeVars && Array.isArray(nodeVars.value)) {
- const node = nodeVars.value.find((item) => item.id === prefix)
- return {
- type: 'node',
- nodeType: node?.type || '',
- nodeName: node?.name || prefix,
- name: varName,
- value: varName
- }
- } else {
- return {
- type: 'node',
- nodeType: '',
- nodeName: prefix,
- name: varName,
- value: varName
- }
- }
- }
- })
- const normalizeTypeLabel = (type?: VarType) => {
- if (!type) return ''
- return VARIABLE_TYPE_OPTIONS.find((item) => item.value === type)?.label || ''
- }
- /**
- * 过滤变量列表
- */
- 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: string }) => {
- innerValue.value = variable.value
- 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>
|