Skip to content

Commit e9b7f4f

Browse files
feat: tool icon
1 parent 60c6188 commit e9b7f4f

18 files changed

Lines changed: 293 additions & 103 deletions

File tree

‎ui/src/api/API_README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ API 对象和工作空间上下文,作为该抽屉的范围选择例外;用
453453

454454
`admin/file.ts` 与 `chat/file.ts` 分别提供 `postUploadFile(file, sourceId, sourceType, onProgress?)`,
455455
通过各自请求客户端向 `/oss/file` 提交 FormData 的 `file`、`source_id` 和 `source_type`。
456+
Admin 上传尚未创建的资源文件时,`sourceId` 可传 `undefined`,省略 `source_id` 并使用后端默认值。
456457
返回值统一为 `{ request, abort }`,`request` 解包得到文件地址,`abort()` 中断客户端请求。
457458
普通上传直接等待 `request`,需要进度时传入 `(percent, event)`;只有能获取上传总量时才回调
458459
0–100 的百分比,100 表示请求体已上传,不代表服务端处理成功,完成状态以 `request` 为准。

‎ui/src/api/admin/file.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import type { FileSourceType } from '@/api/types'
55
/** 上传资源文件,支持可选的进度回调与取消操作。 */
66
const postUploadFile = (
77
file: File,
8-
sourceId: string,
8+
sourceId: string | undefined,
99
sourceType: FileSourceType,
1010
onProgress?: (percent: number, event: AxiosProgressEvent) => void,
1111
) => {
1212
const formData = new FormData()
1313
formData.append('file', file)
14-
formData.append('source_id', sourceId)
14+
if (sourceId !== undefined) formData.append('source_id', sourceId)
1515
formData.append('source_type', sourceType)
1616
return postUpload<string>('/oss/file', formData, onProgress)
1717
}

‎ui/src/assets/iconfont.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ui/src/components/COMPONENT_README.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启
365365
手动导入 `@/components/mk-edit-avatar/index.vue`,不参与全局注册。字符串 `v-model` 为当前
366366
自定义头像 URL,空字符串表示使用默认图标;不再接收 `defaultIcon`。
367367
`size` 默认为 `32`(px),只设置 `referenceRef` 触发容器的宽高;弹层预览和上传区域固定为 80px。
368-
`editable` 默认为 `true`,设为 `false` 时仅展示头像。
368+
`disabled` 默认为 `false`,直接传给 `el-popover` 控制禁用,不额外维护禁用状态监听。
369369

370370
必填默认插槽同时渲染触发区和默认 Logo 预览,只透出 `{ icon }`。触发区传入当前头像
371371
(空值转为 `undefined`);默认预览始终传入 `icon: undefined`。两处容器通过样式让插槽的
@@ -376,10 +376,14 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启
376376
门户编辑通过此插槽渲染 `PortalIcon`,并将头像绑定到 `portalForm.logo`。
377377

378378
悬停头像打开 Logo 设置,打开后保持显示以便选择本地文件;取消、点击外部或弹层内按 Escape
379-
关闭并丢弃草稿,确定后才更新 `v-model` 并触发 `change(icon, file)`。
380-
自定义图片支持 JPG、PNG、GIF,大小不超过 10MB;确认时 `icon` 为本地 Data URL,`file`
381-
为所选 `File`,未重新选文件或使用默认 Logo 时为 `null`。组件不请求上传接口;调用方负责上传
382-
和持久化,并可将返回的图片 URL 写回 `v-model`。
379+
关闭并丢弃草稿。选择新图片后确定,先将预览地址写入 `v-model`,再触发 `change(file)`;保留已有自定义
380+
头像直接确定时不触发事件。恢复默认时先将 `v-model` 设为空,再触发 `change(null)`。
381+
自定义图片支持 JPG、PNG、GIF,大小不超过 10MB;组件通过 `URL.createObjectURL` 创建预览,
382+
统一释放不再被 `previewIcon` 或 `v-model` 使用的临时地址,卸载时释放剩余地址。
383+
调用方不重复创建或释放预览地址;确认后即可通过插槽展示新头像,关闭弹层不会释放仍在显示的地址。
384+
`blob:` 地址仅供当前组件存续期间预览,不能提交给后端作为持久图片地址。
385+
组件只通过 `change(file)` 通知调用方,不请求接口,也不规定文件何时上传。
386+
调用方按业务流程处理文件,通过 `v-model` 传入用于展示的头像地址。
383387

384388
```vue
385389
<script setup lang="ts">

‎ui/src/components/mk-edit-avatar/index.vue‎

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,37 @@ import type { UploadFile, UploadInstance } from 'element-plus'
55
66
defineOptions({ name: 'MkEditAvatar' })
77
8-
const props = withDefaults(defineProps<{ editable?: boolean; size?: number }>(), {
9-
editable: true,
8+
const props = withDefaults(defineProps<{ disabled?: boolean; size?: number }>(), {
9+
disabled: false,
1010
size: 32,
1111
})
1212
const avatar = defineModel<string>({ default: '' })
13-
const emit = defineEmits<{ change: [icon: string, file: File | null] }>()
13+
const emit = defineEmits<{ change: [file: File | null] }>()
1414
defineSlots<{ default(props: { icon: string | undefined }): unknown }>()
1515
1616
// 头像编辑草稿:确认前不修改外部值。
1717
const visible = ref(false)
1818
const logoMode = ref<'default' | 'custom'>('default')
19-
const draftIcon = ref('')
19+
const previewIcon = ref('')
2020
const draftFile = ref<File | null>(null)
21-
const reading = ref(false)
2221
const referenceRef = useTemplateRef<HTMLElement>('referenceRef')
2322
const uploadRef = useTemplateRef<UploadInstance>('uploadRef')
2423
25-
let fileReader: FileReader | undefined
26-
2724
function open() {
28-
if (!props.editable || visible.value) return
25+
if (visible.value) return
2926
logoMode.value = avatar.value ? 'custom' : 'default'
30-
draftIcon.value = logoMode.value === 'custom' ? avatar.value : ''
27+
previewIcon.value = logoMode.value === 'custom' ? avatar.value : ''
3128
draftFile.value = null
3229
visible.value = true
3330
}
3431
3532
function close() {
3633
visible.value = false
37-
fileReader?.abort()
38-
reading.value = false
3934
}
4035
4136
function clearDraft() {
4237
if (visible.value) return
43-
draftIcon.value = ''
38+
previewIcon.value = ''
4439
draftFile.value = null
4540
uploadRef.value?.clearFiles()
4641
}
@@ -51,11 +46,14 @@ function closeOutside(event: MouseEvent) {
5146
}
5247
5348
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)
49+
if (logoMode.value === 'custom' && !previewIcon.value) return
50+
if (logoMode.value === 'default') {
51+
avatar.value = ''
52+
emit('change', null)
53+
} else if (draftFile.value) {
54+
avatar.value = previewIcon.value
55+
emit('change', draftFile.value)
56+
}
5957
close()
6058
}
6159
@@ -72,39 +70,40 @@ function selectImage(uploadFile: UploadFile) {
7270
ElMessage.warning('文件大小不能超过 10MB')
7371
return
7472
}
75-
fileReader?.abort()
76-
const reader = new FileReader()
77-
fileReader = reader
78-
reading.value = true
79-
reader.onload = () => {
80-
draftIcon.value = String(reader.result)
81-
draftFile.value = file
82-
reading.value = false
83-
}
84-
reader.onerror = () => {
85-
reading.value = false
86-
ElMessage.error('图片读取失败,请重新选择')
73+
previewIcon.value = URL.createObjectURL(file)
74+
draftFile.value = file
75+
}
76+
77+
// 临时地址由组件统一管理,仍用于头像或弹层预览时不释放。
78+
function releaseUnusedPreview(icon: string) {
79+
if (icon.startsWith('blob:') && icon !== avatar.value && icon !== previewIcon.value) {
80+
URL.revokeObjectURL(icon)
8781
}
88-
reader.readAsDataURL(file)
8982
}
9083
9184
watch(
92-
() => props.editable,
93-
(editable) => {
94-
if (!editable) close()
85+
[avatar, previewIcon],
86+
(_current, [previousAvatar, previousPreview]) => {
87+
releaseUnusedPreview(previousAvatar)
88+
if (previousPreview !== previousAvatar) releaseUnusedPreview(previousPreview)
9589
},
90+
{ flush: 'sync' },
9691
)
97-
onBeforeUnmount(() => fileReader?.abort())
92+
onBeforeUnmount(() => {
93+
if (previewIcon.value.startsWith('blob:') && previewIcon.value !== avatar.value) URL.revokeObjectURL(previewIcon.value)
94+
if (avatar.value.startsWith('blob:')) URL.revokeObjectURL(avatar.value)
95+
})
9896
</script>
9997

10098
<template>
101-
<el-popover :visible="visible" :width="414" placement="bottom-start" @after-leave="clearDraft">
99+
<!-- 单向控制显隐,避免选择文件时鼠标移出触发自动关闭。 -->
100+
<el-popover :visible="visible" :disabled="props.disabled" :width="414" placement="bottom-start" @after-leave="clearDraft">
102101
<template #reference>
103102
<!-- 悬停修改头像 -->
104103
<span
105104
ref="referenceRef"
106105
class="inline-flex shrink-0 [&>*]:h-full! [&>*]:w-full!"
107-
:class="{ 'cursor-pointer': props.editable }"
106+
:class="{ 'cursor-pointer': !props.disabled }"
108107
:style="{ width: `${props.size}px`, height: `${props.size}px` }"
109108
@mouseenter="open"
110109
>
@@ -114,7 +113,7 @@ onBeforeUnmount(() => fileReader?.abort())
114113

115114
<div v-click-outside="closeOutside" class="p-4" @keydown.esc.stop="close" @click.stop>
116115
<div class="mb-1">Logo 设置</div>
117-
<el-radio-group v-model="logoMode" :disabled="reading" class="mb-2">
116+
<el-radio-group v-model="logoMode" class="mb-2">
118117
<el-radio value="default">默认 Logo</el-radio>
119118
<el-radio value="custom">自定义上传</el-radio>
120119
</el-radio-group>
@@ -130,11 +129,10 @@ onBeforeUnmount(() => fileReader?.abort())
130129
accept="image/jpeg,image/png,image/gif"
131130
:auto-upload="false"
132131
:show-file-list="false"
133-
:disabled="reading"
134132
:on-change="selectImage"
135133
>
136-
<el-avatar v-if="draftIcon" :size="80" shape="square">
137-
<img :src="draftIcon" class="object-cover" />
134+
<el-avatar v-if="previewIcon" :size="80" shape="square">
135+
<img :src="previewIcon" class="object-cover" />
138136
</el-avatar>
139137

140138
<div v-else class="flex-center h-20 w-20 rounded-md border border-dashed bg-N200 hover:border-primary">
@@ -148,7 +146,7 @@ onBeforeUnmount(() => fileReader?.abort())
148146
<!-- 取消头像修改 -->
149147
<el-button plain @click="close">取消</el-button>
150148
<!-- 确认头像修改 -->
151-
<el-button type="primary" :loading="reading" :disabled="logoMode === 'custom' && !draftIcon" @click="confirm">确定</el-button>
149+
<el-button type="primary" :disabled="logoMode === 'custom' && !previewIcon" @click="confirm">确定</el-button>
152150
</div>
153151
</div>
154152
</el-popover>

‎ui/src/styles/STYLE_README.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
`tailwind.css` 内部按照 `@import`、`@theme`、`@custom-variant`、`@layer base`、
1616
`@layer components`、`@utility` 的顺序组织;不存在的部分直接省略。`@theme` 内部按照颜色、
17-
背景图片、间距、排版分组,每组通过注释标明职责,不将不同类型的 Token 混合排序。
17+
背景图片、间距、阴影、排版分组,每组通过注释标明职责,不将不同类型的 Token 混合排序。
1818

1919
`src/main.ts` 和 `src/chat.ts` 应按以下顺序加载样式:
2020

@@ -208,6 +208,21 @@ CSS 自定义属性同样按变量名排序。嵌套选择器、伪类和媒体
208208
白色内容卡片使用 `mk-white-card` 或 `mk-white-card-sm`,内边距和圆角与同尺寸灰色卡片一致。
209209
两种背景的尺寸类均可独立使用,包含对应背景色,无需叠加基础类。
210210

211+
## 阴影
212+
213+
大号阴影统一由 `variables.scss` 中的 `--mk-box-shadow-lg` 定义,值为
214+
`0 6px 24px 0 rgb(var(--mk-N900-rgb) / 8%)`。`tailwind.css` 将其映射为 `shadow-lg`,
215+
业务模板直接使用该工具类,悬停效果使用 `hover:shadow-lg`:
216+
217+
```html
218+
<div class="shadow-lg">常驻阴影</div>
219+
<div class="hover:shadow-lg">悬停阴影</div>
220+
```
221+
222+
普通 CSS/SCSS 使用 `box-shadow: var(--mk-box-shadow-lg)`。Element Plus 卡片的常驻、
223+
悬停和焦点阴影复用该变量。现有 `shadow-sm`、`shadow-md` 分别保持映射到
224+
`--el-box-shadow-lighter`、`--el-box-shadow-light`。
225+
211226
## 常用交互工具类
212227

213228
卡片内需要文字居左、单选圆点居右并垂直居中时,在 `el-radio` 上添加 `mk-card-radio`。

‎ui/src/styles/element-plus.scss‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@
274274
.el-card.is-always-shadow,
275275
.el-card.is-hover-shadow:hover,
276276
.el-card.is-hover-shadow:focus {
277-
box-shadow: 0px 6px 24px 0px rgb(var(--mk-N900-rgb) / 8%);
277+
box-shadow: var(--mk-box-shadow-lg);
278278
}
279279

280280
/* cascader */

‎ui/src/styles/tailwind.css‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
--spacing-chat-sidebar: var(--mk-chat-sidebar-width);
2929

3030
/* shadows */
31+
--shadow-lg: var(--mk-box-shadow-lg);
3132
--shadow-md: var(--el-box-shadow-light);
3233
--shadow-sm: var(--el-box-shadow-lighter);
3334

‎ui/src/styles/variables.scss‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
--mk-sidebar-expanded-width: 240px;
4848
--mk-sidebar-width: 68px;
4949

50+
/* shadows */
51+
--mk-box-shadow-lg: 0 6px 24px 0 rgb(var(--mk-N900-rgb) / 8%);
52+
5053
// 对话
5154
--mk-chat-sidebar-width: 280px;
5255
}

‎ui/src/views/VIEW_README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,11 @@ Workspace 的自定义工具和工作流工具菜单
306306
代码围栏,普通编辑区直接回写表单草稿,放大编辑器通过 `header-extra` 的 `replaceCode` 回调
307307
替换全屏草稿,点击“确定”后再同步表单。公共组件统一处理 `ConversationStream`、停止、重新生成和输入交互,
308308
不接收工具表单、不判断资源范围,不维护原始输入、暂停或继续状态。
309+
310+
`ToolFormDrawer` 通过 `MkEditAvatar` 暂存确认后的头像文件;保存校验通过后先调用 Admin
311+
`postUploadFile`,将返回地址写入 `toolForm.icon` 再提交工具。恢复默认时直接提交空字符串,
312+
未更换图片时不上传;上传失败保留文件供重试,上传成功后清空暂存文件,避免工具保存失败后重复上传。
313+
309314
工具列表页面通过 `ToolCard` 的 `actions` 和 `action-dropdown` 插槽组合
310315
操作;编辑 Action 负责按 `TOOL_TYPE` 打开对应类型表单,启动参数 Action 获取工具详情后打开
311316
`InitParamDialog`,MCP 配置 Action 获取 MCP 工具详情后打开 `McpConfigDialog`。工作流 Action 进入

0 commit comments

Comments
 (0)