feat: 支持按键分离事件与键盘捕获,并优化UI

- 后端与前端新增 keyAction 参数,支持按键按下/抬起独立事件,保持向下兼容
- 前端控制栏支持 pointerdown/pointerup 长按,新增键盘捕获输入框,映射浏览器按键到 Android KeyEvent
- 重构顶栏布局:用户区右置并添加在线指示器、状态文本省略,优化窄屏响应式表现
- 控制栏录制按钮组防拆行,分辨率/帧率选择器自适应收缩,全局样式增强
- 修复 AdminAuthFilter 中 doFilter 被异常捕获导致误返回 401 的问题
- 移除管理后台中用户 admin 角色及相关 UI 与 API
- 更新信令前端环境代理地址,修正路由名称大小写
This commit is contained in:
2026-08-03 11:09:37 +08:00
parent e6ed7c8e3e
commit 78aa3d3c71
16 changed files with 374 additions and 79 deletions

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, watch } from 'vue';
import { store, sendKey, sendResolutionChange, toggleRecording } from '../store/controllerStore';
import { store, sendKeyDown, sendKeyUp, sendResolutionChange, toggleRecording } from '../store/controllerStore';
// Android KeyEvent 键值(与被控端 SystemInputUtils 注入一致)
const keys = [
@@ -29,9 +29,69 @@ const selectedResolution = ref(0);
const fpsOptions = ref([15, 24, 30, 60]);
const selectedFps = ref(0); // 0 表示尚未同步到被控端当前帧率
function press(code) {
function pressDown(code) {
if (!store.dataChannelOpen) return;
sendKey(code);
sendKeyDown(code);
}
function pressUp(code) {
if (!store.dataChannelOpen) return;
sendKeyUp(code);
}
// ----- 键盘捕获输入(真实键盘打字) -----
// 将浏览器 KeyboardEvent.key / .code 映射为 Android KeyEvent 键值。
// 覆盖字母、数字、空格、回车、退格、制表、方向键、修饰键与常用符号。
const KEY_MAP = {
// 字母
a: 29, b: 30, c: 31, d: 32, e: 33, f: 34, g: 35, h: 36, i: 37, j: 38, k: 39,
l: 40, m: 41, n: 42, o: 43, p: 44, q: 45, r: 46, s: 47, t: 48, u: 49, v: 50,
w: 51, x: 52, y: 53, z: 54,
// 数字(主键盘)
'0': 7, '1': 8, '2': 9, '3': 10, '4': 11, '5': 12, '6': 13, '7': 14, '8': 15, '9': 16,
// 控制键
Enter: 66, Backspace: 67, Tab: 61, Space: 62, Delete: 112, Escape: 111,
ArrowLeft: 21, ArrowUp: 19, ArrowRight: 22, ArrowDown: 20,
Home: 3, End: 123, PageUp: 92, PageDown: 93, Insert: 124,
'`': 68, '-': 69, '=': 70, '[': 71, ']': 72, '\\': 73, ';': 74, '\'': 75, ',': 55, '.': 56, '/': 76,
};
// 修饰键(按下保持,直到抬起)
const MODIFIER_MAP = {
Shift: 59, Control: 113, Alt: 57, Meta: 117,
};
const keyInputRef = ref(null);
const keyLog = ref('');
function lookupKeyCode(e) {
// 优先使用 e.code如物理键位置稳定再回退到 e.key。
if (MODIFIER_MAP[e.key]) return { code: MODIFIER_MAP[e.key], modifier: true };
if (KEY_MAP[e.key] !== undefined) return { code: KEY_MAP[e.key], modifier: false };
if (KEY_MAP[e.code]) return { code: KEY_MAP[e.code], modifier: false };
return null;
}
function onKeyInputDown(e) {
if (!store.dataChannelOpen) return;
const m = lookupKeyCode(e);
if (!m) return;
e.preventDefault();
sendKeyDown(m.code);
}
function onKeyInputUp(e) {
if (!store.dataChannelOpen) return;
const m = lookupKeyCode(e);
if (!m) return;
e.preventDefault();
sendKeyUp(m.code);
}
function focusKeyInput() {
keyInputRef.value && keyInputRef.value.focus();
}
// 点击区域任意位置自动聚焦输入框,方便直接用键盘打字
function onCaptureClick() {
focusKeyInput();
}
function onResolutionChange() {
@@ -100,15 +160,22 @@ watch(() => store.currentResolution, (res) => {
<span class="record-sep"></span>
<button
class="record-btn"
:class="{ recording: store.recording }"
:disabled="!store.rtcConnected"
@click="toggleRecording"
>
{{ store.recording ? '停止录制' : '录制' }}
</button>
<span class="record-status" v-if="store.recordStatus">{{ store.recordStatus }}</span>
<!-- 录制按钮与状态包成一组保证换行时二者始终同进同退不被拆散 -->
<div class="record-group">
<button
class="record-btn"
:class="{ recording: store.recording }"
:disabled="!store.rtcConnected"
@click="toggleRecording"
>
{{ store.recording ? '停止录制' : '录制' }}
</button>
<span
class="record-status"
v-if="store.recordStatus"
:title="store.recordStatus"
>{{ store.recordStatus }}</span>
</div>
</div>
<button
@@ -116,33 +183,71 @@ watch(() => store.currentResolution, (res) => {
:key="k.code"
class="key-btn"
:disabled="!store.dataChannelOpen"
@click="press(k.code)"
@pointerdown.prevent="pressDown(k.code)"
@pointerup.prevent="pressUp(k.code)"
@pointerleave.prevent="pressUp(k.code)"
@pointercancel.prevent="pressUp(k.code)"
>
{{ k.label }}
</button>
<div class="keyboard-capture" @click="onCaptureClick">
<label class="kb-label">键盘输入:</label>
<input
ref="keyInputRef"
class="kb-input"
type="text"
placeholder="点击此处后可直接用键盘打字"
:disabled="!store.dataChannelOpen"
@keydown="onKeyInputDown"
@keyup="onKeyInputUp"
@blur="keyLog = ''"
/>
<span class="kb-hint">支持字母/数字/符号/方向键/修饰键组合</span>
</div>
</div>
</template>
<style scoped>
.record-sep {
display: inline-block;
width: 1px;
height: 22px;
margin: 0 10px;
margin: 0 4px;
background: #3a3f4b;
vertical-align: middle;
flex-shrink: 0;
}
/* 录制按钮 + 状态作为一个不可拆分的整体参与换行 */
.record-group {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
min-width: 0;
max-width: 100%;
}
.record-btn {
padding: 6px 14px;
/* flex-shrink:0 + nowrap 是"永不换行"的关键:
作为 flex 子项默认可被压缩到小于文字宽度,从而把"停止录制"挤成两行。 */
flex: 0 0 auto;
flex-shrink: 0;
white-space: nowrap;
word-break: keep-all;
min-width: fit-content;
padding: 8px 16px;
border: 1px solid #3a3f4b;
border-radius: 6px;
background: #2b6cff;
color: #fff;
font-size: 14px;
line-height: 1.2;
cursor: pointer;
transition: background-color .15s ease, border-color .15s ease;
}
.record-btn:hover:not(:disabled) { background: #1f5ce0; }
.record-btn:disabled {
background: #3a3f4b;
color: #888;
@@ -151,12 +256,68 @@ watch(() => store.currentResolution, (res) => {
.record-btn.recording {
background: #e5484d;
border-color: #e5484d;
}
.record-btn.recording:hover:not(:disabled) { background: #cf3b40; }
.record-status {
margin-left: 8px;
/* 空间不足时由状态文字收缩并省略,保证按钮文字完整、二者同处一行 */
flex: 0 1 auto;
min-width: 0;
max-width: 180px;
font-size: 13px;
color: #ffb020;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 换行后竖线分隔符失去意义,反而造成视觉噪点 */
@media (max-width: 640px) {
.record-sep { display: none; }
}
/* ----- 键盘捕获输入 ----- */
.keyboard-capture {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
flex-wrap: wrap;
}
.kb-label {
font-size: 14px;
color: #cfd3dc;
white-space: nowrap;
}
.kb-input {
flex: 1 1 240px;
min-width: 200px;
padding: 7px 10px;
border: 1px solid #3a3f4b;
border-radius: 6px;
background: #1b1e24;
color: #fff;
font-size: 14px;
outline: none;
}
.kb-input:focus {
border-color: #2b6cff;
}
.kb-input:disabled {
background: #23262d;
color: #888;
cursor: not-allowed;
}
.kb-hint {
font-size: 12px;
color: #888;
white-space: nowrap;
}
</style>