SkillForm.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. <script setup>
  2. import { ref, computed, watch } from 'vue'
  3. import { NModal, NButton, NInput, NIcon, NTooltip, NSelect, NCheckbox } from 'naive-ui'
  4. import { CloseOutline, AddOutline, TrashOutline } from '@vicons/ionicons5'
  5. import { translateText } from '../../api/translation'
  6. import { IO_TYPES } from '../../utils/ioInference'
  7. const FIELD_TYPES = IO_TYPES
  8. const props = defineProps({
  9. show: Boolean,
  10. skill: Object
  11. })
  12. const emit = defineEmits(['update:show', 'save'])
  13. const form = ref({
  14. name: '',
  15. description: '',
  16. inputs: [],
  17. outputs: []
  18. })
  19. const isTranslating = ref(false)
  20. watch(() => props.skill, (val) => {
  21. if (val) {
  22. form.value = {
  23. name: val.name || '',
  24. description: val.description || '',
  25. inputs: (val.inputs || []).map(f => ({ ...f })),
  26. outputs: (val.outputs || []).map(f => ({ ...f }))
  27. }
  28. }
  29. }, { immediate: true })
  30. /** 检测文本是否为纯英文(不含中文字符) */
  31. function isEnglish(text) {
  32. if (!text || !text.trim()) return false
  33. for (let i = 0; i < text.length; i++) {
  34. const c = text.charCodeAt(i)
  35. if (c >= 0x4e00 && c <= 0x9fff) return false
  36. }
  37. return true
  38. }
  39. const nameIsEnglish = computed(() => isEnglish(form.value.name))
  40. const descIsEnglish = computed(() => isEnglish(form.value.description))
  41. const hasEnglishField = computed(() => nameIsEnglish.value || descIsEnglish.value)
  42. async function handleTranslate() {
  43. if (isTranslating.value) return
  44. isTranslating.value = true
  45. try {
  46. if (nameIsEnglish.value) {
  47. const res = await translateText(form.value.name)
  48. form.value.name = res.data || form.value.name
  49. }
  50. if (descIsEnglish.value) {
  51. const res = await translateText(form.value.description)
  52. form.value.description = res.data || form.value.description
  53. }
  54. } catch (e) {
  55. // 翻译失败静默处理
  56. } finally {
  57. isTranslating.value = false
  58. }
  59. }
  60. function addField(list) {
  61. list.push({ name: '', type: 'string', description: '', required: false })
  62. }
  63. function removeField(list, index) {
  64. list.splice(index, 1)
  65. }
  66. function handleSave() {
  67. if (!form.value.name.trim()) {
  68. alert('请输入技能名称')
  69. return
  70. }
  71. // 过滤掉 name 为空的字段
  72. const data = {
  73. name: form.value.name,
  74. description: form.value.description,
  75. inputs: form.value.inputs.filter(f => f.name.trim()),
  76. outputs: form.value.outputs.filter(f => f.name.trim())
  77. }
  78. emit('save', data)
  79. }
  80. function closeModal() {
  81. emit('update:show', false)
  82. }
  83. </script>
  84. <template>
  85. <n-modal
  86. :show="show"
  87. @update:show="closeModal"
  88. >
  89. <div class="edit-modal">
  90. <!-- 头部 -->
  91. <div class="modal-header">
  92. <h3 class="modal-title">编辑技能</h3>
  93. <button class="close-btn" @click="closeModal">
  94. <n-icon size="18"><CloseOutline /></n-icon>
  95. </button>
  96. </div>
  97. <!-- 表单 -->
  98. <div class="form-area">
  99. <div class="form-group">
  100. <label class="form-label">技能名称</label>
  101. <n-input
  102. v-model:value="form.name"
  103. placeholder="请输入技能名称"
  104. :maxlength="100"
  105. />
  106. </div>
  107. <div class="form-group">
  108. <label class="form-label">技能描述</label>
  109. <n-input
  110. v-model:value="form.description"
  111. type="textarea"
  112. placeholder="请输入技能描述"
  113. :rows="4"
  114. :maxlength="500"
  115. show-count
  116. />
  117. </div>
  118. <!-- 前置数据 -->
  119. <div class="form-group">
  120. <div class="section-header">
  121. <label class="form-label">前置数据</label>
  122. <n-button size="tiny" quaternary @click="addField(form.inputs)">
  123. <template #icon><n-icon size="14"><AddOutline /></n-icon></template>
  124. 添加
  125. </n-button>
  126. </div>
  127. <div v-if="form.inputs.length" class="io-list">
  128. <div v-for="(field, i) in form.inputs" :key="'in-'+i" class="io-row">
  129. <n-input v-model:value="field.name" placeholder="字段名" size="small" style="width: 100px;" />
  130. <n-select v-model:value="field.type" :options="FIELD_TYPES" size="small" style="width: 100px;" />
  131. <n-input v-model:value="field.description" placeholder="描述" size="small" style="flex: 1;" />
  132. <n-checkbox v-model:checked="field.required" size="small">必填</n-checkbox>
  133. <button class="remove-btn" @click="removeField(form.inputs, i)">
  134. <n-icon size="14"><TrashOutline /></n-icon>
  135. </button>
  136. </div>
  137. </div>
  138. <div v-else class="io-empty">暂无前置数据</div>
  139. </div>
  140. <!-- 输出字段 -->
  141. <div class="form-group">
  142. <div class="section-header">
  143. <label class="form-label">输出字段</label>
  144. <n-button size="tiny" quaternary @click="addField(form.outputs)">
  145. <template #icon><n-icon size="14"><AddOutline /></n-icon></template>
  146. 添加
  147. </n-button>
  148. </div>
  149. <div v-if="form.outputs.length" class="io-list">
  150. <div v-for="(field, i) in form.outputs" :key="'out-'+i" class="io-row">
  151. <n-input v-model:value="field.name" placeholder="字段名" size="small" style="width: 100px;" />
  152. <n-select v-model:value="field.type" :options="FIELD_TYPES" size="small" style="width: 100px;" />
  153. <n-input v-model:value="field.description" placeholder="描述" size="small" style="flex: 1;" />
  154. <n-checkbox v-model:checked="field.required" size="small">必填</n-checkbox>
  155. <button class="remove-btn" @click="removeField(form.outputs, i)">
  156. <n-icon size="14"><TrashOutline /></n-icon>
  157. </button>
  158. </div>
  159. </div>
  160. <div v-else class="io-empty">暂无输出字段</div>
  161. </div>
  162. </div>
  163. <!-- 底部按钮 -->
  164. <div class="modal-footer">
  165. <n-tooltip v-if="hasEnglishField" placement="top">
  166. <template #trigger>
  167. <n-button
  168. :loading="isTranslating"
  169. @click="handleTranslate"
  170. quaternary
  171. >
  172. <template #icon>
  173. <span style="font-size: 16px;">🌐</span>
  174. </template>
  175. 翻译为中文
  176. </n-button>
  177. </template>
  178. 检测到英文字段,点击翻译为中文
  179. </n-tooltip>
  180. <div style="flex: 1;"></div>
  181. <n-button @click="closeModal" quaternary>
  182. 取消
  183. </n-button>
  184. <n-button type="primary" @click="handleSave">
  185. 保存修改
  186. </n-button>
  187. </div>
  188. </div>
  189. </n-modal>
  190. </template>
  191. <style scoped>
  192. .edit-modal {
  193. width: 580px;
  194. max-height: 85vh;
  195. overflow-y: auto;
  196. background: var(--bg-secondary);
  197. border: 1px solid var(--border-color);
  198. border-radius: var(--radius-xl);
  199. padding: 28px;
  200. }
  201. .modal-header {
  202. display: flex;
  203. align-items: center;
  204. justify-content: space-between;
  205. margin-bottom: 24px;
  206. }
  207. .modal-title {
  208. font-size: 18px;
  209. font-weight: 600;
  210. color: var(--text-primary);
  211. }
  212. .close-btn {
  213. width: 32px;
  214. height: 32px;
  215. display: flex;
  216. align-items: center;
  217. justify-content: center;
  218. background: none;
  219. border: none;
  220. border-radius: var(--radius-sm);
  221. color: var(--text-secondary);
  222. cursor: pointer;
  223. transition: all var(--transition-fast);
  224. }
  225. .close-btn:hover {
  226. background: rgba(255, 255, 255, 0.05);
  227. color: var(--text-primary);
  228. }
  229. .form-area {
  230. display: flex;
  231. flex-direction: column;
  232. gap: 20px;
  233. }
  234. .form-group {
  235. display: flex;
  236. flex-direction: column;
  237. gap: 8px;
  238. }
  239. .form-label {
  240. font-size: 13px;
  241. font-weight: 500;
  242. color: var(--text-secondary);
  243. }
  244. .section-header {
  245. display: flex;
  246. align-items: center;
  247. justify-content: space-between;
  248. }
  249. .io-list {
  250. display: flex;
  251. flex-direction: column;
  252. gap: 8px;
  253. }
  254. .io-row {
  255. display: flex;
  256. align-items: center;
  257. gap: 8px;
  258. }
  259. .io-empty {
  260. font-size: 12px;
  261. color: var(--text-tertiary);
  262. padding: 8px 0;
  263. }
  264. .remove-btn {
  265. display: flex;
  266. align-items: center;
  267. justify-content: center;
  268. width: 28px;
  269. height: 28px;
  270. background: none;
  271. border: none;
  272. border-radius: var(--radius-sm);
  273. color: var(--text-secondary);
  274. cursor: pointer;
  275. flex-shrink: 0;
  276. transition: all var(--transition-fast);
  277. }
  278. .remove-btn:hover {
  279. background: rgba(255, 80, 80, 0.1);
  280. color: #ff5050;
  281. }
  282. .modal-footer {
  283. display: flex;
  284. justify-content: flex-end;
  285. align-items: center;
  286. gap: 12px;
  287. margin-top: 28px;
  288. }
  289. </style>