fix: refine input stadium interactions
This commit is contained in:
parent
2beaf6c702
commit
cd59bbfa41
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user