| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- <script setup>
- import { ref, computed, watch } from 'vue'
- import { NModal, NButton, NInput, NIcon, NTooltip, NSelect, NCheckbox } from 'naive-ui'
- import { CloseOutline, AddOutline, TrashOutline } from '@vicons/ionicons5'
- import { translateText } from '../../api/translation'
- import { IO_TYPES } from '../../utils/ioInference'
- const FIELD_TYPES = IO_TYPES
- const props = defineProps({
- show: Boolean,
- skill: Object
- })
- const emit = defineEmits(['update:show', 'save'])
- const form = ref({
- name: '',
- description: '',
- inputs: [],
- outputs: []
- })
- const isTranslating = ref(false)
- watch(() => props.skill, (val) => {
- if (val) {
- form.value = {
- name: val.name || '',
- description: val.description || '',
- inputs: (val.inputs || []).map(f => ({ ...f })),
- outputs: (val.outputs || []).map(f => ({ ...f }))
- }
- }
- }, { immediate: true })
- /** 检测文本是否为纯英文(不含中文字符) */
- function isEnglish(text) {
- if (!text || !text.trim()) return false
- for (let i = 0; i < text.length; i++) {
- const c = text.charCodeAt(i)
- if (c >= 0x4e00 && c <= 0x9fff) return false
- }
- return true
- }
- const nameIsEnglish = computed(() => isEnglish(form.value.name))
- const descIsEnglish = computed(() => isEnglish(form.value.description))
- const hasEnglishField = computed(() => nameIsEnglish.value || descIsEnglish.value)
- async function handleTranslate() {
- if (isTranslating.value) return
- isTranslating.value = true
- try {
- if (nameIsEnglish.value) {
- const res = await translateText(form.value.name)
- form.value.name = res.data || form.value.name
- }
- if (descIsEnglish.value) {
- const res = await translateText(form.value.description)
- form.value.description = res.data || form.value.description
- }
- } catch (e) {
- // 翻译失败静默处理
- } finally {
- isTranslating.value = false
- }
- }
- function addField(list) {
- list.push({ name: '', type: 'string', description: '', required: false })
- }
- function removeField(list, index) {
- list.splice(index, 1)
- }
- function handleSave() {
- if (!form.value.name.trim()) {
- alert('请输入技能名称')
- return
- }
- // 过滤掉 name 为空的字段
- const data = {
- name: form.value.name,
- description: form.value.description,
- inputs: form.value.inputs.filter(f => f.name.trim()),
- outputs: form.value.outputs.filter(f => f.name.trim())
- }
- emit('save', data)
- }
- function closeModal() {
- emit('update:show', false)
- }
- </script>
- <template>
- <n-modal
- :show="show"
- @update:show="closeModal"
- >
- <div class="edit-modal">
- <!-- 头部 -->
- <div class="modal-header">
- <h3 class="modal-title">编辑技能</h3>
- <button class="close-btn" @click="closeModal">
- <n-icon size="18"><CloseOutline /></n-icon>
- </button>
- </div>
- <!-- 表单 -->
- <div class="form-area">
- <div class="form-group">
- <label class="form-label">技能名称</label>
- <n-input
- v-model:value="form.name"
- placeholder="请输入技能名称"
- :maxlength="100"
- />
- </div>
- <div class="form-group">
- <label class="form-label">技能描述</label>
- <n-input
- v-model:value="form.description"
- type="textarea"
- placeholder="请输入技能描述"
- :rows="4"
- :maxlength="500"
- show-count
- />
- </div>
- <!-- 前置数据 -->
- <div class="form-group">
- <div class="section-header">
- <label class="form-label">前置数据</label>
- <n-button size="tiny" quaternary @click="addField(form.inputs)">
- <template #icon><n-icon size="14"><AddOutline /></n-icon></template>
- 添加
- </n-button>
- </div>
- <div v-if="form.inputs.length" class="io-list">
- <div v-for="(field, i) in form.inputs" :key="'in-'+i" class="io-row">
- <n-input v-model:value="field.name" placeholder="字段名" size="small" style="width: 100px;" />
- <n-select v-model:value="field.type" :options="FIELD_TYPES" size="small" style="width: 100px;" />
- <n-input v-model:value="field.description" placeholder="描述" size="small" style="flex: 1;" />
- <n-checkbox v-model:checked="field.required" size="small">必填</n-checkbox>
- <button class="remove-btn" @click="removeField(form.inputs, i)">
- <n-icon size="14"><TrashOutline /></n-icon>
- </button>
- </div>
- </div>
- <div v-else class="io-empty">暂无前置数据</div>
- </div>
- <!-- 输出字段 -->
- <div class="form-group">
- <div class="section-header">
- <label class="form-label">输出字段</label>
- <n-button size="tiny" quaternary @click="addField(form.outputs)">
- <template #icon><n-icon size="14"><AddOutline /></n-icon></template>
- 添加
- </n-button>
- </div>
- <div v-if="form.outputs.length" class="io-list">
- <div v-for="(field, i) in form.outputs" :key="'out-'+i" class="io-row">
- <n-input v-model:value="field.name" placeholder="字段名" size="small" style="width: 100px;" />
- <n-select v-model:value="field.type" :options="FIELD_TYPES" size="small" style="width: 100px;" />
- <n-input v-model:value="field.description" placeholder="描述" size="small" style="flex: 1;" />
- <n-checkbox v-model:checked="field.required" size="small">必填</n-checkbox>
- <button class="remove-btn" @click="removeField(form.outputs, i)">
- <n-icon size="14"><TrashOutline /></n-icon>
- </button>
- </div>
- </div>
- <div v-else class="io-empty">暂无输出字段</div>
- </div>
- </div>
- <!-- 底部按钮 -->
- <div class="modal-footer">
- <n-tooltip v-if="hasEnglishField" placement="top">
- <template #trigger>
- <n-button
- :loading="isTranslating"
- @click="handleTranslate"
- quaternary
- >
- <template #icon>
- <span style="font-size: 16px;">🌐</span>
- </template>
- 翻译为中文
- </n-button>
- </template>
- 检测到英文字段,点击翻译为中文
- </n-tooltip>
- <div style="flex: 1;"></div>
- <n-button @click="closeModal" quaternary>
- 取消
- </n-button>
- <n-button type="primary" @click="handleSave">
- 保存修改
- </n-button>
- </div>
- </div>
- </n-modal>
- </template>
- <style scoped>
- .edit-modal {
- width: 580px;
- max-height: 85vh;
- overflow-y: auto;
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-xl);
- padding: 28px;
- }
- .modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 24px;
- }
- .modal-title {
- font-size: 18px;
- font-weight: 600;
- color: var(--text-primary);
- }
- .close-btn {
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: none;
- border: none;
- border-radius: var(--radius-sm);
- color: var(--text-secondary);
- cursor: pointer;
- transition: all var(--transition-fast);
- }
- .close-btn:hover {
- background: rgba(255, 255, 255, 0.05);
- color: var(--text-primary);
- }
- .form-area {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .form-group {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .form-label {
- font-size: 13px;
- font-weight: 500;
- color: var(--text-secondary);
- }
- .section-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .io-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .io-row {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .io-empty {
- font-size: 12px;
- color: var(--text-tertiary);
- padding: 8px 0;
- }
- .remove-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- background: none;
- border: none;
- border-radius: var(--radius-sm);
- color: var(--text-secondary);
- cursor: pointer;
- flex-shrink: 0;
- transition: all var(--transition-fast);
- }
- .remove-btn:hover {
- background: rgba(255, 80, 80, 0.1);
- color: #ff5050;
- }
- .modal-footer {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 12px;
- margin-top: 28px;
- }
- </style>
|