Skip to content

Commit 552b23b

Browse files
feat: tool edit
1 parent 955c0e7 commit 552b23b

6 files changed

Lines changed: 219 additions & 54 deletions

File tree

‎ui/src/components/COMPONENT_README.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,37 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启
312312

313313
## 手动导入的共享 UI
314314

315+
### MkEditAvatar(修改头像)
316+
317+
手动导入 `@/components/mk-edit-avatar/index.vue`,不参与全局注册。`defaultIcon` 必填,接收默认
318+
图标图片 URL;字符串 `v-model` 为当前自定义头像 URL,空字符串表示使用默认图标。
319+
`size` 默认为 `32`(px),`editable` 默认为 `true`,设为 `false` 时仅展示头像。
320+
321+
默认插槽替换触发区的头像内容,透出 `{ icon, size }`;`icon` 为当前 `v-model` 值,空值可由
322+
`ToolIcon` 等资源组件自行回退。未提供插槽时展示当前头像或 `defaultIcon` 图片。
323+
触发按钮及悬停、聚焦、点击行为由 `MkEditAvatar` 统一维护,插槽内只放展示内容,不嵌套按钮。
324+
`ToolFormDrawer` 通过此插槽渲染 `ToolIcon`,并将头像绑定到 `toolForm.icon`。
325+
326+
悬停、聚焦或点击头像打开 Logo 设置,打开后保持显示以便选择本地文件;取消、点击外部或 Escape
327+
关闭并丢弃草稿,确定后才更新 `v-model` 并触发 `change(icon, file)`。
328+
自定义图片支持 JPG、PNG、GIF,大小不超过 10MB;确认时 `icon` 为本地 Data URL,`file`
329+
为所选 `File`,未重新选文件或使用默认 Logo 时为 `null`。组件不请求上传接口;调用方负责上传
330+
和持久化,并可将返回的图片 URL 写回 `v-model`。
331+
332+
```vue
333+
<script setup lang="ts">
334+
import { ref } from 'vue'
335+
import MkEditAvatar from '@/components/mk-edit-avatar/index.vue'
336+
import defaultIcon from '@/assets/mk_icon_upload.svg'
337+
338+
const avatar = ref('')
339+
</script>
340+
341+
<template>
342+
<MkEditAvatar v-model="avatar" :default-icon="defaultIcon" />
343+
</template>
344+
```
345+
315346
### MkFilterableDropdown
316347

317348
带搜索过滤和滚动列表的下拉选择。组件不限制选项字段,默认使用 `label` 作为展示和搜索字段、
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<script setup lang="ts">
2+
import { computed, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'
3+
import { ClickOutside as vClickOutside, ElMessage } from 'element-plus'
4+
import { Plus } from '@element-plus/icons-vue'
5+
6+
defineOptions({ name: 'MkEditAvatar' })
7+
8+
const props = withDefaults(defineProps<{ defaultIcon: string; editable?: boolean; size?: number }>(), {
9+
editable: true,
10+
size: 32,
11+
})
12+
const avatar = defineModel<string>({ default: '' })
13+
const emit = defineEmits<{ change: [icon: string, file: File | null] }>()
14+
defineSlots<{ default?(props: { icon: string; size: number }): unknown }>()
15+
16+
// 头像编辑草稿:确认前不修改外部值。
17+
const visible = ref(false)
18+
const logoMode = ref<'default' | 'custom'>('default')
19+
const draftIcon = ref('')
20+
const draftFile = ref<File | null>(null)
21+
const reading = ref(false)
22+
const referenceRef = useTemplateRef<HTMLElement>('referenceRef')
23+
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
24+
const currentIcon = computed(() => avatar.value || props.defaultIcon)
25+
let fileReader: FileReader | undefined
26+
27+
function open() {
28+
if (!props.editable || visible.value) return
29+
logoMode.value = avatar.value && avatar.value !== props.defaultIcon ? 'custom' : 'default'
30+
draftIcon.value = logoMode.value === 'custom' ? avatar.value : ''
31+
draftFile.value = null
32+
visible.value = true
33+
}
34+
35+
function close() {
36+
visible.value = false
37+
fileReader?.abort()
38+
reading.value = false
39+
}
40+
41+
function clearDraft() {
42+
if (visible.value) return
43+
draftIcon.value = ''
44+
draftFile.value = null
45+
if (fileInputRef.value) fileInputRef.value.value = ''
46+
}
47+
48+
function closeOutside(event: MouseEvent) {
49+
if (event.target instanceof Node && referenceRef.value?.contains(event.target)) return
50+
close()
51+
}
52+
53+
function confirm() {
54+
if (reading.value || (logoMode.value === 'custom' && !draftIcon.value)) return
55+
const icon = logoMode.value === 'default' ? '' : draftIcon.value
56+
const file = logoMode.value === 'custom' ? draftFile.value : null
57+
avatar.value = icon
58+
emit('change', icon, file)
59+
close()
60+
}
61+
62+
// 本地图片校验与预览,文件由调用方上传。
63+
function selectImage(event: Event) {
64+
const input = event.target as HTMLInputElement
65+
const file = input.files?.[0]
66+
input.value = ''
67+
if (!file) return
68+
if (!['image/jpeg', 'image/png', 'image/gif'].includes(file.type)) {
69+
ElMessage.warning('仅支持 JPG、PNG、GIF 格式的图片')
70+
return
71+
}
72+
if (file.size > 10 * 1024 * 1024) {
73+
ElMessage.warning('文件大小不能超过 10MB')
74+
return
75+
}
76+
fileReader?.abort()
77+
const reader = new FileReader()
78+
fileReader = reader
79+
reading.value = true
80+
reader.onload = () => {
81+
draftIcon.value = String(reader.result)
82+
draftFile.value = file
83+
reading.value = false
84+
}
85+
reader.onerror = () => {
86+
reading.value = false
87+
ElMessage.error('图片读取失败,请重新选择')
88+
}
89+
reader.readAsDataURL(file)
90+
}
91+
92+
watch(
93+
() => props.editable,
94+
(editable) => {
95+
if (!editable) close()
96+
},
97+
)
98+
onBeforeUnmount(() => fileReader?.abort())
99+
</script>
100+
101+
<template>
102+
<!-- // TODO: 优化头像预览效果 -->
103+
<el-popover :visible="visible" :width="414" placement="bottom-start" @after-leave="clearDraft">
104+
<template #reference>
105+
<!-- 悬停或点击修改头像 -->
106+
<button
107+
ref="referenceRef"
108+
type="button"
109+
class="inline-flex shrink-0 overflow-hidden rounded-md border-0 bg-transparent p-0 align-middle"
110+
:class="editable ? 'cursor-pointer' : 'cursor-default'"
111+
:disabled="!editable"
112+
:title="editable ? '修改头像' : '头像'"
113+
@mouseenter="open"
114+
@focus="open"
115+
@click.stop="open"
116+
@keydown.esc.stop="close"
117+
>
118+
<slot :icon="avatar" :size="size">
119+
<img :src="currentIcon" alt="头像" class="object-contain" :style="{ width: `${size}px`, height: `${size}px` }" />
120+
</slot>
121+
</button>
122+
</template>
123+
124+
<div v-click-outside="closeOutside" class="text-N900" @keydown.esc.stop="close" @click.stop>
125+
<div class="mb-1">Logo 设置<span class="ml-0.5 text-danger">*</span></div>
126+
<el-radio-group v-model="logoMode" :disabled="reading" class="mb-2">
127+
<el-radio value="default">默认 Logo</el-radio>
128+
<el-radio value="custom">自定义上传</el-radio>
129+
</el-radio-group>
130+
131+
<img v-if="logoMode === 'default'" :src="defaultIcon" alt="默认 Logo" class="h-[106px] w-[106px] rounded-md object-contain" />
132+
<div v-else>
133+
<input ref="fileInputRef" type="file" accept="image/jpeg,image/png,image/gif" class="hidden" @change="selectImage" />
134+
<!-- 选择或更换头像图片 -->
135+
<button
136+
type="button"
137+
class="flex-center h-20 w-20 cursor-pointer overflow-hidden rounded-md border border-dashed bg-N100 p-0 hover:border-primary"
138+
:disabled="reading"
139+
:title="draftIcon ? '更换头像图片' : '上传头像图片'"
140+
@click="fileInputRef?.click()"
141+
>
142+
<img v-if="draftIcon" :src="draftIcon" alt="自定义头像预览" class="h-full w-full object-contain" />
143+
<MkIcon v-else :icon="Plus" :size="28" />
144+
</button>
145+
<p class="mt-1 text-N600">支持 JPG、PNG、GIF,文件大小不超过 10MB</p>
146+
</div>
147+
148+
<div class="mt-4 flex justify-end gap-3">
149+
<!-- 取消头像修改 -->
150+
<el-button class="min-w-20" @click="close">取消</el-button>
151+
<!-- 确认头像修改 -->
152+
<el-button class="ml-0! min-w-20" type="primary" :loading="reading" :disabled="logoMode === 'custom' && !draftIcon" @click="confirm">
153+
确定
154+
</el-button>
155+
</div>
156+
</div>
157+
</el-popover>
158+
</template>
Lines changed: 11 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
<script setup lang="ts">
22
import { nextTick, ref, useTemplateRef } from 'vue'
33
import type ToolApi from '@/api/admin/workspace/tool/tool'
4-
import type { ToolItem, ToolStoreItem, ToolStoreResponse } from '@/api/types'
4+
import type { ToolItem, ToolStoreResponse } from '@/api/types'
55
import { TOOL_TYPE } from '@/api/enums'
66
import ToolFormDrawer from '@/views/tool/tool-form/tool-custom/ToolFormDrawer.vue'
77
import DataSourceFormDrawer from '@/views/tool/tool-form/DataSourceFormDrawer.vue'
88
import McpFormDrawer from '@/views/tool/tool-form/McpFormDrawer.vue'
99
import SkillToolFormDrawer from '@/views/tool/tool-form/SkillToolFormDrawer.vue'
10-
import StoreToolFormDialog from '@/views/tool/tool-form/StoreToolFormDialog.vue'
1110
import WorkflowFormDialog from '@/views/tool/tool-form/WorkflowFormDialog.vue'
12-
import ToolStoreDetailDrawer from '@/views/tool/tool-store/ToolStoreDetailDrawer.vue'
1311
1412
defineOptions({ name: 'EditToolAction' })
1513
@@ -18,47 +16,13 @@ const props = defineProps<{ api: typeof ToolApi; label: string; storeTools: Tool
1816
const emit = defineEmits<{ update: [tool: ToolItem] }>()
1917
2018
const formMounted = ref(false)
21-
const storeToolFormDialogRef = useTemplateRef<InstanceType<typeof StoreToolFormDialog>>('storeToolFormDialogRef')
22-
const toolStoreDetailDrawerRef = useTemplateRef<InstanceType<typeof ToolStoreDetailDrawer>>('toolStoreDetailDrawerRef')
2319
const toolFormDrawerRef = useTemplateRef<InstanceType<typeof ToolFormDrawer>>('toolFormDrawerRef')
2420
const dataSourceFormDrawerRef = useTemplateRef<InstanceType<typeof DataSourceFormDrawer>>('dataSourceFormDrawerRef')
2521
const mcpFormDrawerRef = useTemplateRef<InstanceType<typeof McpFormDrawer>>('mcpFormDrawerRef')
2622
const skillToolFormDrawerRef = useTemplateRef<InstanceType<typeof SkillToolFormDrawer>>('skillToolFormDrawerRef')
2723
const workflowFormDialogRef = useTemplateRef<InstanceType<typeof WorkflowFormDialog>>('workflowFormDialogRef')
2824
2925
function handleOpenToolForm() {
30-
// TODO 模板转换而来的工具只允许修改名称。
31-
if (props.tool.template_id) {
32-
formMounted.value = true
33-
void nextTick(() =>
34-
storeToolFormDialogRef.value?.open(
35-
{ desc: props.tool.desc, icon: props.tool.icon, id: props.tool.id, name: props.tool.name, source: 'store', tool_type: props.tool.tool_type, version: props.tool.version },
36-
true,
37-
),
38-
)
39-
return
40-
}
41-
42-
// TODO 有版本号的展示readme,是商店更新过来的
43-
if (props.tool.version) {
44-
formMounted.value = true
45-
void nextTick(() => {
46-
const storeTool = props.storeTools.find((item) => item.id === props.tool.template_id)
47-
const toolDetail: ToolStoreItem = {
48-
...storeTool,
49-
desc: props.tool.desc,
50-
icon: props.tool.icon,
51-
id: props.tool.id,
52-
name: props.tool.name,
53-
source: 'store',
54-
tool_type: props.tool.tool_type,
55-
version: props.tool.version,
56-
}
57-
toolStoreDetailDrawerRef.value?.open(toolDetail, storeTool?.readMe)
58-
})
59-
return
60-
}
61-
6226
formMounted.value = true
6327
return nextTick(() => {
6428
if (props.tool.tool_type === TOOL_TYPE.DATA_SOURCE) {
@@ -87,19 +51,8 @@ function handleFormClosed() {
8751
</MkDropdownItem>
8852

8953
<template v-if="formMounted">
90-
<StoreToolFormDialog v-if="tool.template_id" ref="storeToolFormDialogRef" @closed="handleFormClosed" @update="emit('update', $event)" />
91-
<ToolStoreDetailDrawer v-else-if="tool.version" ref="toolStoreDetailDrawerRef" :show-add="false" @closed="handleFormClosed" />
92-
<ToolFormDrawer
93-
v-else-if="tool.tool_type === TOOL_TYPE.CUSTOM"
94-
ref="toolFormDrawerRef"
95-
title="编辑工具"
96-
:api="api"
97-
:folder-id="tool.folder_id ?? ''"
98-
@closed="handleFormClosed"
99-
@update="emit('update', $event)"
100-
/>
10154
<DataSourceFormDrawer
102-
v-else-if="tool.tool_type === TOOL_TYPE.DATA_SOURCE"
55+
v-if="tool.tool_type === TOOL_TYPE.DATA_SOURCE"
10356
ref="dataSourceFormDrawerRef"
10457
title="编辑数据源"
10558
:api="api"
@@ -134,5 +87,14 @@ function handleFormClosed() {
13487
@closed="handleFormClosed"
13588
@update="emit('update', $event)"
13689
/>
90+
<ToolFormDrawer
91+
v-else
92+
ref="toolFormDrawerRef"
93+
title="编辑工具"
94+
:api="api"
95+
:folder-id="tool.folder_id ?? ''"
96+
@closed="handleFormClosed"
97+
@update="emit('update', $event)"
98+
/>
13799
</template>
138100
</template>

‎ui/src/views/tool/tool-form/tool-custom/ToolFormDrawer.vue‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,27 @@ defineExpose({ open })
143143

144144
<template>
145145
<MkDrawer v-model="visible" :before-close="handleBeforeClose" :title="title" size="60%" @closed="handleClosed">
146-
<el-form ref="formRef" v-loading="formLoading" :model="toolForm" :rules="formRules" label-position="top" require-asterisk-position="right" @submit.prevent>
146+
<el-form
147+
ref="formRef"
148+
v-loading="formLoading"
149+
:model="toolForm"
150+
:rules="formRules"
151+
label-position="top"
152+
require-asterisk-position="right"
153+
@submit.prevent
154+
>
147155
<h4 class="mk-title-decoration mb-4">基本信息</h4>
148156
<el-form-item label="名称" prop="name">
149157
<div class="flex w-full items-center gap-3">
150-
<!-- // TODO 头像 统一修改组件-->
151-
<ToolIcon :size="32" />
152-
<el-input v-model="toolForm.name" maxlength="64" placeholder="请输入工具名称" show-word-limit @blur="toolForm.name = toolForm.name.trim()" />
158+
<!-- TODO 统一修改修改工具头像 -->
159+
<ToolIcon :icon="toolForm.icon" />
160+
<el-input
161+
v-model="toolForm.name"
162+
maxlength="64"
163+
placeholder="请输入工具名称"
164+
show-word-limit
165+
@blur="toolForm.name = toolForm.name.trim()"
166+
/>
153167
</div>
154168
</el-form-item>
155169
<el-form-item label="描述">
File renamed without changes.

‎ui/src/views/tool/tool-store/component/ToolStoreCard.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { computed, nextTick, ref, useTemplateRef } from 'vue'
33
import type { ToolStoreItem } from '@/api/types'
44
import { resetUrl } from '@/utils/icon'
55
import { numberFormat } from '@/utils/number'
6-
import StoreToolFormDialog from '../../tool-form/StoreToolFormDialog.vue'
6+
import StoreToolFormDialog from '../StoreToolFormDialog.vue'
77
import ToolStoreDetailDrawer from '../ToolStoreDetailDrawer.vue'
88
99
defineOptions({ name: 'ToolStoreCard' })

0 commit comments

Comments
 (0)