VarSelect.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. <template>
  2. <div class="var-select">
  3. <el-popover
  4. v-model:visible="visible"
  5. trigger="click"
  6. width="320"
  7. popper-class="var-select__popover"
  8. placement="bottom-start"
  9. >
  10. <template #reference>
  11. <div class="var-select__trigger">
  12. <el-input-tag
  13. :model-value="innerValue ? [innerValue] : undefined"
  14. :placeholder="t('common.nodeBase.varSelect.selectVariable')"
  15. :aria-label="t('common.nodeBase.varSelect.selectVariable')"
  16. clearable
  17. @clear="handleClear"
  18. @remove-tag="handleClear"
  19. autocomplete
  20. v-bind="$attrs"
  21. >
  22. <template #tag>
  23. <VarLabel :label="innerValue" />
  24. </template>
  25. </el-input-tag>
  26. </div>
  27. </template>
  28. <div class="var-select__panel">
  29. <el-input
  30. v-model="keyword"
  31. clearable
  32. size="small"
  33. :placeholder="t('common.nodeBase.varSelect.searchVariable')"
  34. class="var-select__search"
  35. />
  36. <div class="var-select__list">
  37. <!-- 变量列表 -->
  38. <div v-for="group in filteredVars" class="var-select__group">
  39. <div class="var-select__group-title">{{ group.name }}</div>
  40. <div
  41. v-for="item in group.variableList"
  42. :key="item.expression"
  43. class="var-select__item"
  44. @click="
  45. handleSelect({
  46. value: item.expression,
  47. type: item.type
  48. })
  49. "
  50. >
  51. <span
  52. class="var-select__item-prefix env text-10px"
  53. :style="{ background: nodeMap[group?.type ?? '']?.iconColor ?? '' }"
  54. >
  55. <span v-if="group.id === 'env'" class="text-6px">ENV</span>
  56. <span v-if="group.id === 'sys'" class="text-6px">SYS</span>
  57. <img
  58. v-else-if="isImageIcon(nodeMap[group?.type ?? '']?.icon)"
  59. :src="nodeMap[group?.type ?? '']?.icon"
  60. alt="node icon"
  61. class="w-18px h-18px object-contain"
  62. />
  63. <Icon v-else-if="group?.type" :icon="nodeMap[group?.type]?.icon!" :size="18" />
  64. </span>
  65. <span class="var-select__item-name" :title="item.name">{{ item.name }}</span>
  66. <span class="var-select__item-type">{{ normalizeTypeLabel(item.type) }}</span>
  67. </div>
  68. </div>
  69. <div v-if="!filteredVars.length" class="var-select__empty">
  70. {{ t('common.nodeBase.varSelect.empty') }}
  71. </div>
  72. </div>
  73. </div>
  74. </el-popover>
  75. </div>
  76. </template>
  77. <script setup lang="ts">
  78. import { computed, ref, watch, inject, type Ref } from 'vue'
  79. import { Icon } from '@repo/ui'
  80. import { nodeMap } from '@/nodes'
  81. import { VARIABLE_TYPE_OPTIONS } from '@/constant'
  82. import { useI18n } from '@/composables/useI18n'
  83. import VarLabel from '@/components/VarLabel/index.vue'
  84. import type { NodeVar, VarType } from '@/types/var'
  85. interface Props {
  86. /**
  87. * 选中的变量,格式 #{xxx.var}
  88. */
  89. modelValue: string
  90. /**
  91. * 过滤方法
  92. */
  93. filterFn?: (list: NodeVar[]) => NodeVar[]
  94. /**
  95. * 绑定类型
  96. */
  97. varType?: VarType | ''
  98. }
  99. const props = withDefaults(defineProps<Props>(), {
  100. modelValue: ''
  101. })
  102. const emit = defineEmits<{
  103. (e: 'update:modelValue', value: string): void
  104. (e: 'update:varType', value?: VarType | ''): void
  105. (e: 'change', value: { value: string; type: VarType }): void
  106. (e: 'clear'): void
  107. }>()
  108. const { t } = useI18n()
  109. const nodeVars = inject<Ref<NodeVar[]>>('nodeVars')
  110. const visible = ref(false)
  111. const keyword = ref('')
  112. const innerValue = ref<string>(props.modelValue)
  113. const innerType = ref<VarType | '' | undefined>(props.varType)
  114. watch(
  115. () => props.modelValue,
  116. (val) => {
  117. if (val !== innerValue.value) {
  118. innerValue.value = val
  119. }
  120. },
  121. { deep: true }
  122. )
  123. const handleClear = () => {
  124. innerValue.value = ''
  125. innerType.value = ''
  126. emit('update:varType', '')
  127. emit('update:modelValue', '')
  128. emit('clear')
  129. }
  130. watch(
  131. () => innerValue.value,
  132. (val) => {
  133. emit('update:modelValue', val)
  134. }
  135. )
  136. watch(
  137. () => innerType.value,
  138. (val) => {
  139. emit('update:varType', val)
  140. }
  141. )
  142. const normalizeTypeLabel = (type?: VarType) => {
  143. if (!type) return ''
  144. const labelMap: Record<string, string> = {
  145. string: t('common.nodeBase.valueTypes.string'),
  146. number: t('common.nodeBase.valueTypes.number'),
  147. boolean: t('common.nodeBase.valueTypes.boolean'),
  148. object: t('common.nodeBase.valueTypes.object'),
  149. 'array[string]': t('common.nodeBase.valueTypes.arrayString'),
  150. 'array[number]': t('common.nodeBase.valueTypes.arrayNumber'),
  151. 'array[boolean]': t('common.nodeBase.valueTypes.arrayBoolean'),
  152. 'array[object]': t('common.nodeBase.valueTypes.arrayObject'),
  153. 'array[file]': t('common.nodeBase.valueTypes.arrayFile')
  154. }
  155. return labelMap[type] || VARIABLE_TYPE_OPTIONS.find((item) => item.value === type)?.label || ''
  156. }
  157. const isImageIcon = (icon?: string) => !!icon && icon.startsWith('data:image/')
  158. /**
  159. * 过滤变量列表
  160. */
  161. const filteredVars = computed(() => {
  162. const kw = keyword.value.trim().toLowerCase()
  163. let list = nodeVars?.value || []
  164. // 先根据 filterFn 过滤一遍
  165. if (props.filterFn) {
  166. list = props.filterFn(list)
  167. }
  168. const options = list?.filter((item) => item.variableList.find((i) => i.name.includes(kw)))
  169. return options || []
  170. })
  171. const handleSelect = (variable: { value: string; type: VarType }) => {
  172. innerValue.value = variable.value
  173. innerType.value = variable.type
  174. visible.value = false
  175. keyword.value = ''
  176. emit('change', variable)
  177. }
  178. </script>
  179. <style scoped lang="less">
  180. .var-select {
  181. width: 100%;
  182. &__trigger {
  183. width: 100%;
  184. cursor: pointer;
  185. }
  186. }
  187. .var-select__popover {
  188. padding: 8px !important;
  189. }
  190. .var-select__panel {
  191. display: flex;
  192. flex-direction: column;
  193. gap: 8px;
  194. }
  195. .var-select__search {
  196. :deep(.el-input__wrapper) {
  197. box-shadow: none;
  198. border-radius: 8px;
  199. background-color: #f5f5f5;
  200. }
  201. }
  202. .var-select__list {
  203. max-height: 260px;
  204. overflow-y: auto;
  205. }
  206. .var-select__group + .var-select__group {
  207. margin-top: 8px;
  208. border-top: 1px solid #f2f2f2;
  209. padding-top: 8px;
  210. }
  211. .var-select__group-title {
  212. font-size: 12px;
  213. color: #999;
  214. margin-bottom: 4px;
  215. }
  216. .var-select__item {
  217. display: flex;
  218. align-items: center;
  219. gap: 6px;
  220. padding: 4px 6px;
  221. border-radius: 6px;
  222. cursor: pointer;
  223. font-size: 12px;
  224. color: #333;
  225. &:hover {
  226. background-color: #f5f7ff;
  227. }
  228. }
  229. .var-select__item-prefix {
  230. display: inline-flex;
  231. align-items: center;
  232. justify-content: center;
  233. width: 20px;
  234. height: 20px;
  235. border-radius: 6px;
  236. color: #fff;
  237. flex-shrink: 0;
  238. &.env {
  239. background: #6366f1;
  240. }
  241. &.sys {
  242. background: #f97316;
  243. }
  244. }
  245. .var-select__item-name {
  246. flex: 1;
  247. overflow: hidden;
  248. text-overflow: ellipsis;
  249. white-space: nowrap;
  250. }
  251. .var-select__item-type {
  252. color: #999;
  253. flex-shrink: 0;
  254. }
  255. .var-select__empty {
  256. padding: 12px 4px;
  257. font-size: 12px;
  258. color: #999;
  259. text-align: center;
  260. }
  261. :deep(.el-input-tag__inner) {
  262. flex-wrap: nowrap;
  263. }
  264. </style>