fix: refine input stadium interactions

This commit is contained in:
JOJO 2025-11-21 00:38:22 +08:00
parent 2beaf6c702
commit cd59bbfa41
3 changed files with 122 additions and 64 deletions

View File

@ -204,6 +204,8 @@ async function bootstrapApp() {
settingsOpen: false, settingsOpen: false,
quickMenuOpen: false, quickMenuOpen: false,
inputLineCount: 1, inputLineCount: 1,
inputIsMultiline: false,
inputIsFocused: false,
// 思考块滚动锁 // 思考块滚动锁
thinkingScrollLocks: new Map(), thinkingScrollLocks: new Map(),
@ -1327,6 +1329,7 @@ async function bootstrapApp() {
this.toolMenuOpen = false; this.toolMenuOpen = false;
this.quickMenuOpen = false; this.quickMenuOpen = false;
this.inputLineCount = 1; this.inputLineCount = 1;
this.inputIsMultiline = false;
this.toolSettingsLoading = false; this.toolSettingsLoading = false;
this.toolSettings = []; this.toolSettings = [];
@ -2458,19 +2461,37 @@ async function bootstrapApp() {
this.autoResizeInput(); this.autoResizeInput();
}, },
handleInputFocus() {
this.inputIsFocused = true;
},
handleInputBlur() {
this.inputIsFocused = false;
},
autoResizeInput() { autoResizeInput() {
this.$nextTick(() => { this.$nextTick(() => {
const textarea = this.$refs.stadiumInput; const textarea = this.$refs.stadiumInput;
if (!textarea) { if (!textarea) {
return; return;
} }
const previousHeight = textarea.offsetHeight;
textarea.style.height = 'auto'; textarea.style.height = 'auto';
const computedStyle = window.getComputedStyle(textarea); const computedStyle = window.getComputedStyle(textarea);
const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20; const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20;
const maxHeight = lineHeight * 6; const maxHeight = lineHeight * 6;
const newHeight = Math.min(textarea.scrollHeight, maxHeight); const targetHeight = Math.min(textarea.scrollHeight, maxHeight);
textarea.style.height = `${newHeight}px`; this.inputLineCount = Math.max(1, Math.round(targetHeight / lineHeight));
this.inputLineCount = Math.max(1, Math.round(newHeight / lineHeight)); this.inputIsMultiline = targetHeight > lineHeight * 1.4;
if (Math.abs(targetHeight - previousHeight) <= 0.5) {
textarea.style.height = `${targetHeight}px`;
return;
}
textarea.style.height = `${previousHeight}px`;
void textarea.offsetHeight;
requestAnimationFrame(() => {
textarea.style.height = `${targetHeight}px`;
});
}); });
}, },
@ -2478,7 +2499,7 @@ async function bootstrapApp() {
if (!this.quickMenuOpen) { if (!this.quickMenuOpen) {
return; return;
} }
const shell = this.$refs.compactInputShell; const shell = this.$refs.stadiumShellOuter || this.$refs.compactInputShell;
if (shell && shell.contains(event.target)) { if (shell && shell.contains(event.target)) {
return; return;
} }

View File

@ -488,35 +488,45 @@
<!-- 输入区域 --> <!-- 输入区域 -->
<div class="input-area compact-input-area"> <div class="input-area compact-input-area">
<div class="stadium-shell" ref="compactInputShell" :class="{ expanded: inputLineCount > 1 }"> <div class="stadium-input-wrapper" ref="stadiumShellOuter">
<input type="file" <div
ref="fileUploadInput" class="stadium-shell"
class="file-input-hidden" ref="compactInputShell"
@change="handleFileSelected"> :class="{
<button type="button" 'is-multiline': inputIsMultiline,
class="stadium-btn add-btn" 'is-focused': inputIsFocused,
@click.stop="toggleQuickMenu" 'has-text': inputMessage.trim().length > 0
:disabled="!isConnected"> }">
+ <input type="file"
</button> ref="fileUploadInput"
<textarea class="file-input-hidden"
ref="stadiumInput" @change="handleFileSelected">
v-model="inputMessage" <button type="button"
@input="handleInputChange" class="stadium-btn add-btn"
@keydown.enter.ctrl="sendMessage" @click.stop="toggleQuickMenu"
placeholder="输入消息... (Ctrl+Enter 发送)" :disabled="!isConnected">
class="stadium-input" +
:disabled="!isConnected || streamingMessage" </button>
rows="1"> <textarea
</textarea> ref="stadiumInput"
<button type="button" v-model="inputMessage"
class="stadium-btn send-btn" @input="handleInputChange"
@click="handleSendOrStop" @focus="handleInputFocus"
:disabled="!isConnected || (!inputMessage.trim() && !streamingMessage)"> @blur="handleInputBlur"
<span v-if="streamingMessage"></span> @keydown.enter.ctrl="sendMessage"
<span v-else class="send-icon"></span> placeholder="输入消息... (Ctrl+Enter 发送)"
</button> class="stadium-input"
:disabled="!isConnected || streamingMessage"
rows="1">
</textarea>
<button type="button"
class="stadium-btn send-btn"
@click="handleSendOrStop"
:disabled="!isConnected || (!inputMessage.trim() && !streamingMessage)">
<span v-if="streamingMessage"></span>
<span v-else class="send-icon"></span>
</button>
</div>
<transition name="quick-menu"> <transition name="quick-menu">
<div class="quick-menu" v-if="quickMenuOpen" ref="quickMenu" @click.stop> <div class="quick-menu" v-if="quickMenuOpen" ref="quickMenu" @click.stop>
<button type="button" <button type="button"
@ -606,8 +616,6 @@
</transition> </transition>
</div> </div>
</div> </div>
</main>
<!-- 右侧拖拽手柄 --> <!-- 右侧拖拽手柄 -->
<div class="resize-handle" @mousedown="startResize('right', $event)"></div> <div class="resize-handle" @mousedown="startResize('right', $event)"></div>

View File

@ -1474,39 +1474,70 @@ o-conversations {
pointer-events: none; pointer-events: none;
} }
.stadium-shell { .stadium-input-wrapper {
position: relative; position: relative;
width: min(900px, 94%); width: min(900px, 94%);
border: 1px solid var(--claude-border);
border-radius: 999px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 18px 36px rgba(61, 57, 41, 0.12);
padding: 10px 70px;
min-height: 56px;
transition: padding 0.25s ease, box-shadow 0.25s ease, border-radius 0.25s ease;
pointer-events: auto; pointer-events: auto;
} }
.stadium-shell.expanded { .stadium-shell {
padding-top: 18px; --stadium-radius: 24px;
padding-bottom: 18px; position: relative;
border-radius: 28px; width: 100%;
min-height: calc(var(--stadium-radius) * 2.1);
padding: 12px 18px;
border-radius: var(--stadium-radius);
border: 1px solid rgba(15, 23, 42, 0.12);
background: #ffffff;
box-shadow: 0 18px 46px rgba(15, 23, 42, 0.16);
display: flex;
align-items: center;
gap: 12px;
transition: padding 0.2s ease, min-height 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.stadium-shell.is-multiline {
padding-top: 16px;
padding-bottom: 16px;
min-height: calc(var(--stadium-radius) * 2.7);
border-color: rgba(15, 23, 42, 0.2);
box-shadow: 0 26px 70px rgba(15, 23, 42, 0.22);
}
.stadium-shell.is-focused,
.stadium-shell.has-text {
border-color: rgba(218, 119, 86, 0.35);
box-shadow:
0 0 0 1px rgba(218, 119, 86, 0.18),
0 0 14px rgba(218, 119, 86, 0.18),
0 18px 40px rgba(15, 23, 42, 0.18);
}
.stadium-shell.is-multiline.is-focused,
.stadium-shell.is-multiline.has-text {
box-shadow:
0 0 0 1px rgba(218, 119, 86, 0.2),
0 0 18px rgba(218, 119, 86, 0.2),
0 26px 68px rgba(15, 23, 42, 0.24);
} }
.stadium-input { .stadium-input {
flex: 1 1 auto;
width: 100%; width: 100%;
border: none; border: none;
resize: none; resize: none;
background: transparent; background: transparent;
font-size: 15px; font-size: 14px;
line-height: 1.6; line-height: 1.4;
font-family: inherit; font-family: inherit;
color: var(--claude-text); color: var(--claude-text);
padding: 4px 0; padding: 0;
min-height: 24px; min-height: 20px;
outline: none; outline: none;
overflow-y: auto; overflow-y: auto;
scrollbar-width: none; scrollbar-width: none;
transition: height 0.28s cubic-bezier(0.4, 0, 0.2, 1);
will-change: height;
} }
.stadium-input:disabled { .stadium-input:disabled {
@ -1520,24 +1551,19 @@ o-conversations {
} }
.stadium-btn { .stadium-btn {
position: absolute; flex: 0 0 36px;
bottom: 10px; width: 36px;
width: 40px; height: 36px;
height: 40px;
border: none; border: none;
border-radius: 50%; border-radius: 50%;
background: transparent; background: transparent;
color: var(--claude-text); color: var(--claude-text);
font-size: 20px; font-size: 18px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background 0.2s ease, transform 0.2s ease; transition: background 0.2s ease, transform 0.2s ease, margin-top 0.2s ease;
}
.stadium-shell.expanded .stadium-btn {
bottom: 14px;
} }
.stadium-btn:disabled { .stadium-btn:disabled {
@ -1550,12 +1576,10 @@ o-conversations {
} }
.add-btn { .add-btn {
left: 12px; font-size: 22px;
font-size: 26px;
} }
.stadium-btn.send-btn { .stadium-btn.send-btn {
right: 12px;
background: var(--claude-accent); background: var(--claude-accent);
color: #fffaf0; color: #fffaf0;
box-shadow: 0 10px 20px rgba(189, 93, 58, 0.28); box-shadow: 0 10px 20px rgba(189, 93, 58, 0.28);
@ -1589,6 +1613,11 @@ o-conversations {
border-left-color: rgba(255, 255, 255, 0.4); border-left-color: rgba(255, 255, 255, 0.4);
} }
.stadium-shell.is-multiline .stadium-btn {
align-self: flex-end;
margin-top: 0;
}
.file-input-hidden { .file-input-hidden {
display: none; display: none;
} }