Skip to content

Commit efce45e

Browse files
feat: message input
1 parent c844b9f commit efce45e

20 files changed

Lines changed: 234 additions & 234 deletions

File tree

‎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/business/generate-content/index.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ onBeforeUnmount(stopGenerate)
161161
@keydown.stop="handleKeydown"
162162
@paste.stop
163163
/>
164-
<div class="text-right">
164+
<div class="text-right mt-3">
165165
<!-- 停止生成内容 -->
166166
<el-button v-if="loading" circle type="primary" @click="stopGenerate">
167167
<MkIcon name="icon_square_filled" />

‎ui/src/components/mk-dynamics-form/items/upload/UploadInput.vue‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,7 @@ const uploadFile = async (file: DynamicFormValue, fileList: DynamicFormValue[])
148148
:min-scale="0.2"
149149
:preview-src-list="getAttrsArray(images, 'url')"
150150
:initial-index="index"
151-
alt=""
152151
fit="cover"
153-
style="width: 170px; height: 170px; display: block"
154-
class="border-r-6"
155152
/>
156153
</div>
157154
</template>

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,7 @@ onBeforeUnmount(() => {
131131
:show-file-list="false"
132132
:on-change="selectImage"
133133
>
134-
<el-avatar v-if="previewIcon" :size="80" shape="square">
135-
<img :src="previewIcon" class="object-cover" />
136-
</el-avatar>
137-
134+
<el-image v-if="previewIcon" :src="previewIcon" fit="cover" class="h-20 w-20" />
138135
<div v-else class="flex-center h-20 w-20 rounded-md border border-dashed bg-N200 hover:border-primary">
139136
<MkIcon name="icon_add_outlined" :size="24" />
140137
</div>
Lines changed: 51 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
<script setup lang="ts">
22
import { ref } from 'vue'
3-
import { CircleCloseFilled, Paperclip, Promotion, VideoPause } from '@element-plus/icons-vue'
3+
import { Headset } from '@element-plus/icons-vue'
4+
import { getFileExtension, getFileIconUrl } from '@/utils/icon'
5+
import { formatFileSize } from '@/utils/number'
46
import { inputShortcut } from '../../core/shortcuts'
57
import { useMessageInputStore } from './index'
68
79
const {
810
question,
911
fileList,
10-
showDelete,
1112
maxFiles,
1213
maxSizeMB,
1314
acceptList,
1415
loading,
1516
placeholder,
1617
canSend,
1718
imageFiles,
18-
documentFiles,
1919
audioFiles,
2020
videoFiles,
21-
getFileIcon,
2221
addFiles,
2322
removeFile,
2423
send,
@@ -50,91 +49,41 @@ const handleKeydown = (e: KeyboardEvent) => {
5049
</script>
5150

5251
<template>
53-
<div class="mk-input-box conversation-message-input w-full border shadow-lg transition-colors focus-within:border-primary">
54-
<!-- 文件预览区域 -->
55-
<div v-if="fileList.length" class="mb-3 space-y-2">
56-
<!-- 图片预览 -->
57-
<el-space v-if="imageFiles.length" wrap>
58-
<template v-for="(file, index) in imageFiles" :key="file.uid">
59-
<div class="relative" @mouseenter="showDelete = file.url || ''" @mouseleave="showDelete = ''">
60-
<!-- 移除图片附件 -->
61-
<button
62-
v-if="showDelete === file.url"
63-
type="button"
64-
class="attachment-remove absolute -top-[5px] -right-[5px] z-1"
65-
@click="removeFile(index)"
66-
>
67-
<MkIcon :icon="CircleCloseFilled" />
68-
</button>
69-
<el-image v-if="file.url" :src="file.url" fit="cover" class="block h-10 w-10 rounded-md" />
70-
<el-image v-else-if="file.previewUrl" :src="file.previewUrl" fit="cover" class="block h-10 w-10 rounded-md" />
71-
</div>
72-
</template>
73-
</el-space>
74-
75-
<!-- 文档预览 -->
76-
<el-row v-if="documentFiles.length" :gutter="8" class="gap-y-2">
77-
<template v-for="(file, index) in documentFiles" :key="file.uid">
78-
<el-col :xs="24" :sm="12">
79-
<el-card shadow="never" body-class="p-2!" class="cursor-pointer" @mouseenter="showDelete = file.url || ''" @mouseleave="showDelete = ''">
80-
<div class="flex-between gap-2">
81-
<div class="flex-align-center min-w-0 gap-1">
82-
<img :src="getFileIcon(file.name)" alt="" class="w-6 shrink-0" />
83-
<span class="truncate" :title="file.name">{{ file.name }}</span>
84-
</div>
85-
<!-- 移除文档附件 -->
86-
<button v-if="showDelete === file.url" type="button" class="attachment-remove" @click="removeFile(index)">
87-
<MkIcon :icon="CircleCloseFilled" />
88-
</button>
52+
<div class="mk-input-box mk-conversation-message-input w-full shadow-lg">
53+
<!-- 文件预览区域:所有类型按上传顺序横向排列。 // TODO 未来会改成横向有左右按钮控制 -->
54+
<el-scrollbar v-if="fileList.length" class="mb-3 h-auto!" view-class="flex-align-center gap-2">
55+
<template v-for="(file, index) in fileList" :key="file.uid">
56+
<div class="group relative shrink-0">
57+
<el-image
58+
v-if="imageFiles.includes(file) && (file.url || file.previewUrl)"
59+
:src="file.url || file.previewUrl"
60+
:alt="file.name"
61+
:title="file.name"
62+
fit="cover"
63+
/>
64+
<video v-else-if="videoFiles.includes(file) && file.url" :src="file.url" :title="file.name" controls autoplay class="object-cover" />
65+
<el-card shadow="never" v-else class="small w-60 rounded-lg!">
66+
<div class="flex-align-center gap-2">
67+
<MkIcon v-if="audioFiles.includes(file)" :icon="Headset" :size="24" class="shrink-0 text-N600" />
68+
<img v-else :src="getFileIconUrl(file.name)" alt="" class="w-8 shrink-0" />
69+
<div class="min-w-0 flex-1">
70+
<div class="truncate" :title="file.name">{{ file.name }}</div>
71+
<div class="text-sm text-N500">{{ getFileExtension(file.name).toUpperCase() || '文件' }} – {{ formatFileSize(file.size) }}</div>
8972
</div>
90-
</el-card>
91-
</el-col>
92-
</template>
93-
</el-row>
94-
95-
<!-- 音频预览 -->
96-
<el-row v-if="audioFiles.length" :gutter="8" class="gap-y-2">
97-
<template v-for="(file, index) in audioFiles" :key="file.uid">
98-
<el-col :xs="24" :sm="12">
99-
<el-card shadow="never" body-class="p-2!" class="cursor-pointer" @mouseenter="showDelete = file.url || ''" @mouseleave="showDelete = ''">
100-
<div class="flex-between gap-2">
101-
<div class="flex-align-center min-w-0 gap-1">
102-
<img :src="getFileIcon(file.name)" alt="" class="w-6 shrink-0" />
103-
<span class="truncate" :title="file.name">{{ file.name }}</span>
104-
</div>
105-
<!-- 移除音频附件 -->
106-
<button v-if="showDelete === file.url" type="button" class="attachment-remove" @click="removeFile(index)">
107-
<MkIcon :icon="CircleCloseFilled" />
108-
</button>
109-
</div>
110-
</el-card>
111-
</el-col>
112-
</template>
113-
</el-row>
114-
115-
<!-- 视频预览 -->
116-
<el-space v-if="videoFiles.length" wrap>
117-
<template v-for="(file, index) in videoFiles" :key="file.uid">
118-
<div class="relative" @mouseenter="showDelete = file.url || ''" @mouseleave="showDelete = ''">
119-
<!-- 移除视频附件 -->
120-
<button
121-
v-if="showDelete === file.url"
122-
type="button"
123-
class="attachment-remove absolute -top-[5px] -right-[5px] z-1"
124-
@click="removeFile(index)"
125-
>
126-
<MkIcon :icon="CircleCloseFilled" />
127-
</button>
128-
<video v-if="file.url" :src="file.url" controls class="block w-25 rounded-md" autoplay />
73+
</div>
74+
</el-card>
75+
<!-- 移除附件 -->
76+
<div class="group-hover-visible attachment-remove" @click="removeFile(index)">
77+
<MkIcon name="icon_close_bold_outlined" class="text-white!" :size="6" />
12978
</div>
130-
</template>
131-
</el-space>
132-
</div>
79+
</div>
80+
</template>
81+
</el-scrollbar>
13382

13483
<!-- 输入框 -->
13584
<el-input
13685
v-model="question"
137-
:autosize="{ minRows: 1, maxRows: 10 }"
86+
:autosize="{ minRows: 1, maxRows: 7 }"
13887
type="textarea"
13988
resize="none"
14089
:placeholder="placeholder"
@@ -145,45 +94,35 @@ const handleKeydown = (e: KeyboardEvent) => {
14594
/>
14695

14796
<!-- 操作栏 -->
148-
<div class="mt-2 flex-align-center justify-end">
97+
<div class="text-right mt-3">
14998
<input ref="fileInputRef" type="file" multiple :accept="acceptList" class="hidden" @change="handleFileSelect" />
150-
<MkTooltip placement="top" popper-class="max-w-75">
99+
<MkTooltip placement="top">
151100
<template #content>
152-
<div class="break-all whitespace-pre-wrap">支持上传图片、文档、音频、视频文件,最多{{ maxFiles }}个,单个文件最大{{ maxSizeMB }}MB</div>
101+
<div class="break-all whitespace-pre-wrap">
102+
可拖拽到输入框内上传文件
103+
<br />
104+
上传文件:最多{{ maxFiles }}个,每个文件限制{{ maxSizeMB }}MB
105+
<br />
106+
<!-- // TODO: 类型格式处理 -->
107+
文件类型:{{ acceptList }}
108+
</div>
153109
</template>
154110
<!-- 上传文件 -->
155111
<el-button text :disabled="loading || fileList.length >= maxFiles" @click="fileInputRef?.click()">
156-
<MkIcon :icon="Paperclip" :size="20" />
112+
<MkIcon name="icon_attachment_outlined" :size="18" class="text-N900!" />
157113
</el-button>
158114
</MkTooltip>
159-
<el-divider direction="vertical" />
160-
<!-- 发送 -->
161-
<el-button v-if="!loading" text :disabled="!canSend" @click="send">
162-
<MkIcon :icon="Promotion" :size="20" />
163-
</el-button>
115+
<el-divider direction="vertical" class="ml-3! mr-4!" />
164116
<!-- 停止回复 -->
165-
<el-button v-else text @click="stop">
166-
<MkIcon :icon="VideoPause" :size="20" />
117+
<el-button v-if="loading" circle type="primary" @click="stop">
118+
<MkIcon name="icon_square_filled" />
119+
</el-button>
120+
<!-- 发送 -->
121+
<el-button v-else circle type="primary" :disabled="!canSend" @click="send()">
122+
<MkIcon name="icon_arrow-up_outlined" />
167123
</el-button>
168124
</div>
169125
</div>
170126
</template>
171127

172-
<style scoped lang="scss">
173-
/* 附件删除按钮,供图片、文档、音频和视频预览复用。 */
174-
.attachment-remove {
175-
align-items: center;
176-
background: transparent;
177-
border: 0;
178-
color: var(--mk-N600);
179-
cursor: pointer;
180-
display: flex;
181-
flex-shrink: 0;
182-
justify-content: center;
183-
padding: 0;
184-
185-
&:hover {
186-
color: var(--mk-danger);
187-
}
188-
}
189-
</style>
128+
<style scoped lang="scss"></style>

‎ui/src/conversation-panel/core/layout/index.vue‎

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,3 @@
1-
<template>
2-
<!-- 布局壳(纯展示):左/主/右三区 + 挤压/抽屉 + 遮罩。状态由外层传入,自身不含业务。 -->
3-
<div class="relative flex h-full overflow-hidden">
4-
<aside class="side-panel left shrink-0 overflow-hidden bg-N100" :class="[leftMode, { open: leftOpen }]" :style="{ '--side-w': leftWidth + 'px' }">
5-
<div class="side-panel__inner"><slot name="left" /></div>
6-
</aside>
7-
8-
<div class="flex-column min-w-0 flex-1"><slot name="main" /></div>
9-
10-
<aside
11-
v-if="hasRight"
12-
class="side-panel right shrink-0 overflow-hidden bg-N100"
13-
:class="[rightMode, { open: rightOpen }]"
14-
:style="{ '--side-w': rightWidth + 'px' }"
15-
>
16-
<div class="side-panel__inner"><slot name="right" /></div>
17-
</aside>
18-
19-
<!-- 点击遮罩收起抽屉侧栏 -->
20-
<div v-if="showMask" class="absolute inset-0 z-30 bg-N900/40" @click="$emit('mask-click')" />
21-
</div>
22-
</template>
23-
241
<script setup lang="ts">
252
import { computed, useSlots } from 'vue'
263
import type { SidePanelMode } from './types'
@@ -50,6 +27,28 @@ const hasRight = computed(() => !!slots.right)
5027
const showMask = computed(() => (props.leftMode === 'drawer' && props.leftOpen) || (props.rightMode === 'drawer' && props.rightOpen))
5128
</script>
5229

30+
<template>
31+
<!-- 布局壳(纯展示):左/主/右三区 + 挤压/抽屉 + 遮罩。状态由外层传入,自身不含业务。 -->
32+
<div class="relative flex h-full overflow-hidden">
33+
<aside class="side-panel left shrink-0 overflow-hidden bg-N100" :class="[leftMode, { open: leftOpen }]" :style="{ '--side-w': leftWidth + 'px' }">
34+
<div class="side-panel__inner"><slot name="left" /></div>
35+
</aside>
36+
37+
<slot name="main" />
38+
39+
<aside
40+
v-if="hasRight"
41+
class="side-panel right shrink-0 overflow-hidden bg-N100"
42+
:class="[rightMode, { open: rightOpen }]"
43+
:style="{ '--side-w': rightWidth + 'px' }"
44+
>
45+
<div class="side-panel__inner"><slot name="right" /></div>
46+
</aside>
47+
48+
<!-- 点击遮罩收起抽屉侧栏 -->
49+
<div v-if="showMask" class="absolute inset-0 z-30 bg-N900/40" @click="$emit('mask-click')" />
50+
</div>
51+
</template>
5352
<style scoped lang="scss">
5453
.side-panel {
5554
/* 固定侧栏内容宽度,避免展开和收起时内容变形。 */
Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,41 @@
1-
/* 对话模块公共样式,由 view/chat 与 view/debug 的 scoped SCSS 通过 @use 引入。
2-
* 公共类名使用 conversation-panel- 前缀;标题栏使用 mk-conversation-header。
3-
* 样式受入口组件作用域限制,跨子组件内部的规则需使用 :deep()。
1+
/* 对话模块公共样式,由 view/chat 与 view/debug 的非 scoped SCSS 通过 @use 引入。
42
* 组件专属布局和交互样式保留在所属组件的 scoped 样式中。
53
*/
64

5+
/* 对话主面板:聊天与只读展示共用布局。 */
6+
.mk-conversation-main {
7+
background: var(--color-white);
8+
display: flex;
9+
flex: 1;
10+
flex-direction: column;
11+
overflow: hidden;
12+
position: relative;
13+
}
14+
715
/* 对话标题栏:在中性底色上叠加浅主题色。 */
816
.mk-conversation-header {
917
background: color-mix(in srgb, var(--mk-primary) 5%, var(--mk-N100));
1018
}
19+
20+
/* 对话输入区:边框颜色随内部焦点平滑切换。 */
21+
.mk-conversation-message-input {
22+
border: 1px solid rgb(var(--mk-N350-rgb) / 60%);
23+
transition: border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
24+
25+
&:focus-within {
26+
border-color: rgb(var(--mk-primary-rgb) / 60%);
27+
}
28+
/* 移除附件 */
29+
.attachment-remove {
30+
background: rgb(var(--mk-N900-rgb) / 60%);
31+
border-radius: 9999px;
32+
padding: var(--spacing);
33+
position: absolute;
34+
right: var(--spacing);
35+
top: var(--spacing);
36+
z-index: 1;
37+
display: flex;
38+
justify-content: center;
39+
cursor: pointer;
40+
}
41+
}
Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
1-
<template>
2-
<!-- 带输入框的主面板:header + message-list + message-input -->
3-
<main class="main-panel" @drop.prevent="handleDrop" @dragover.prevent>
4-
<!-- 头部可被替换(如 debug 用 debug header);默认普通 chat header -->
5-
<slot name="header"><ChatHeader /></slot>
6-
<MessageList :app-name="list.appInfo.value?.name || 'AI 助手'" />
7-
<footer class="panel-input"><MessageInput /></footer>
8-
</main>
9-
</template>
10-
111
<script setup lang="ts">
122
import ChatHeader from '../../components/header/chat/index.vue'
133
import MessageList from '../../components/message-list/index.vue'
@@ -21,25 +11,12 @@ const input = useMessageInputStore()
2111
// 拖拽到面板任意处 → 交给 message-input 接收文件
2212
const handleDrop = (e: DragEvent) => input.addFiles(e.dataTransfer?.files)
2313
</script>
24-
25-
<style scoped lang="scss">
26-
.main-panel {
27-
flex: 1;
28-
min-width: 0;
29-
display: flex;
30-
flex-direction: column;
31-
height: 100%;
32-
max-height: 100%;
33-
background: var(--bg, #fff);
34-
overflow: hidden;
35-
position: relative;
36-
}
37-
.panel-input {
38-
flex-shrink: 0;
39-
padding: 12px 16px 16px;
40-
background: var(--bg, #fff);
41-
display: flex;
42-
flex-direction: column;
43-
align-items: center;
44-
}
45-
</style>
14+
<template>
15+
<!-- 带输入框的主面板:header + message-list + message-input -->
16+
<main class="mk-conversation-main" @drop.prevent="handleDrop" @dragover.prevent>
17+
<!-- 头部可被替换(如 debug 用 debug header);默认普通 chat header -->
18+
<slot name="header"><ChatHeader /></slot>
19+
<MessageList :app-name="list.appInfo.value?.name || 'AI 助手'" />
20+
<footer class="flex-center shrink-0 p-4"><MessageInput /></footer>
21+
</main>
22+
</template>

0 commit comments

Comments
 (0)