- 更新 shared_proto_db/metadata/000003.log 文件内容 - 更新 Site Characteristics Database/00003.log 文件内容 - 添加新的数据库条目和元数据记录 - 保持数据库文件格式的一致性 - 删除Vivid2D的内容 - 重写启动加载界面
481 lines
17 KiB
HTML
481 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>SQL Command Line</title>
|
||
<style>
|
||
:root {
|
||
--bg-color: #0c0c0c;
|
||
--text-color: #cccccc;
|
||
--caret-color: #ffffff;
|
||
--selection-bg: #264f78;
|
||
--menu-bg: #2b2d30;
|
||
--highlight-active: #0d293e;
|
||
}
|
||
|
||
body {
|
||
background-color: var(--bg-color);
|
||
color: var(--text-color);
|
||
font-family: 'Consolas', 'Courier New', monospace;
|
||
font-size: 16px;
|
||
margin: 0;
|
||
padding: 10px;
|
||
height: 100vh;
|
||
box-sizing: border-box;
|
||
overflow-y: auto;
|
||
word-break: break-all;
|
||
}
|
||
|
||
::selection { background: var(--selection-bg); color: white; }
|
||
|
||
#terminal { width: 100%; min-height: 100%; display: flex; flex-direction: column; padding-bottom: 50px; }
|
||
#output-area { white-space: pre-wrap; cursor: text; }
|
||
|
||
.input-line { display: flex; margin-top: 5px; position: relative; }
|
||
.prompt { color: #cccccc; margin-right: 8px; font-weight: bold; user-select: none; white-space: pre; }
|
||
.continuation-prompt { color: #cccccc; margin-right: 8px; user-select: none; white-space: pre; }
|
||
|
||
input[type="text"] {
|
||
background: transparent; border: none; color: #ffffff;
|
||
font-family: inherit; font-size: inherit; flex: 1;
|
||
outline: none; padding: 0; margin: 0; caret-color: var(--caret-color);
|
||
}
|
||
|
||
.welcome-msg { margin-bottom: 20px; color: #888; }
|
||
.error-text { color: #ff5555; }
|
||
|
||
/* --- 自动补全样式 --- */
|
||
#suggestion-box {
|
||
position: absolute; display: none; background-color: #2b2d30;
|
||
border: 1px solid #5f6165; box-shadow: 0 5px 15px rgba(0,0,0,0.6);
|
||
z-index: 999; max-height: 200px; overflow-y: auto; min-width: 250px;
|
||
border-radius: 3px;
|
||
}
|
||
.suggestion-item {
|
||
padding: 5px 10px; cursor: pointer; font-size: 14px;
|
||
color: #a9b7c6; display: flex; align-items: center;
|
||
}
|
||
.suggestion-item.active { background-color: var(--highlight-active); color: #ffffff; }
|
||
|
||
.icon {
|
||
display: inline-block; width: 16px; height: 16px; margin-right: 8px;
|
||
font-size: 10px; text-align: center; line-height: 16px; border-radius: 2px;
|
||
font-weight: bold; color: #000;
|
||
}
|
||
.icon-k { background-color: #ffc66d; } /* Keyword */
|
||
.icon-t { background-color: #4a88c7; color: #fff; } /* Table */
|
||
.icon-c { background-color: #9876aa; color: #fff; } /* Column */
|
||
|
||
.detail-text { margin-left: auto; color: #666; font-size: 12px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="terminal">
|
||
<div class="welcome-msg">
|
||
Axis SQL Client [Version 2.0.0]<br>
|
||
(c) 2024 Axis Innovators.<br>
|
||
Loaded local database mode.<br>
|
||
<br>
|
||
Commands end with ; <br>
|
||
Type 'help' for help. 'cls' to clear.<br>
|
||
</div>
|
||
<div id="output-area"></div>
|
||
<div class="input-line">
|
||
<span id="prompt-span" class="prompt">SQL></span>
|
||
<input type="text" id="cmd-input" autocomplete="off" spellcheck="false" autofocus>
|
||
<div id="suggestion-box"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// --- 核心状态 ---
|
||
const outputArea = document.getElementById('output-area');
|
||
const input = document.getElementById('cmd-input');
|
||
const promptSpan = document.getElementById('prompt-span');
|
||
const suggestionBox = document.getElementById('suggestion-box');
|
||
|
||
let history = [];
|
||
let historyIndex = -1;
|
||
|
||
// 多行命令缓冲区
|
||
let commandBuffer = "";
|
||
let isMultiLineMode = false;
|
||
|
||
// 数据库元数据 (由 Java 填充)
|
||
let dbSchema = {
|
||
tables: [], // ["student", "logs"]
|
||
columns: {}, // {"student": ["id", "name"], "logs": ["id", "msg"]}
|
||
allColumns: [] // flat list of all columns for quick search
|
||
};
|
||
|
||
// 静态关键字库
|
||
const keywords = [
|
||
"SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES", "UPDATE", "SET",
|
||
"DELETE", "CREATE", "TABLE", "DROP", "ALTER", "ORDER", "BY", "GROUP",
|
||
"LIMIT", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "ON", "AND", "OR",
|
||
"NOT", "NULL", "AS", "DISTINCT", "COUNT", "MAX", "MIN", "AVG", "SUM"
|
||
];
|
||
|
||
// --- 1. 初始化: 加载表结构 ---
|
||
window.onload = function() {
|
||
if (window.cefQuery) {
|
||
window.cefQuery({
|
||
request: JSON.stringify({ type: "initSchema" }),
|
||
onSuccess: function(response) {
|
||
const res = JSON.parse(response);
|
||
if (res.status === "success") {
|
||
dbSchema.tables = res.tables || [];
|
||
dbSchema.columns = res.columns || {};
|
||
// 扁平化所有列名,方便通用提示
|
||
let colSet = new Set();
|
||
for (let t in dbSchema.columns) {
|
||
dbSchema.columns[t].forEach(c => colSet.add(c));
|
||
}
|
||
dbSchema.allColumns = Array.from(colSet);
|
||
console.log("Schema loaded:", dbSchema);
|
||
}
|
||
},
|
||
onFailure: function() {}
|
||
});
|
||
} else {
|
||
// 测试模式数据
|
||
dbSchema.tables = ["student", "course"];
|
||
dbSchema.columns = { "student": ["id", "name", "age"], "course": ["id", "title"] };
|
||
dbSchema.allColumns = ["id", "name", "age", "title"];
|
||
}
|
||
};
|
||
|
||
// --- 2. 输入与键盘事件 ---
|
||
input.addEventListener('keydown', function(e) {
|
||
// 如果补全框打开,接管按键
|
||
if (suggestionBox.style.display === 'block') {
|
||
if (handleSuggestionKeys(e)) return;
|
||
}
|
||
|
||
if (e.key === 'Enter') {
|
||
handleEnterKey();
|
||
}
|
||
else if (e.key === 'ArrowUp') {
|
||
handleHistory(true, e);
|
||
}
|
||
else if (e.key === 'ArrowDown') {
|
||
handleHistory(false, e);
|
||
}
|
||
});
|
||
|
||
// --- 3. 多行命令处理逻辑 (核心升级) ---
|
||
function handleEnterKey() {
|
||
const rawLine = input.value;
|
||
|
||
// 1. 如果输入为空,只是换行
|
||
if (!rawLine.trim() && !commandBuffer.trim()) {
|
||
input.value = '';
|
||
return;
|
||
}
|
||
|
||
// 2. 显示当前行
|
||
const promptText = isMultiLineMode ? " -> " : "SQL> ";
|
||
appendLine(promptText + rawLine);
|
||
|
||
// 3. 处理历史记录 (每次回车都存,方便找回片段)
|
||
if (rawLine.trim()) {
|
||
history.push(rawLine);
|
||
historyIndex = history.length;
|
||
}
|
||
|
||
// 4. 判断特殊前端命令
|
||
const upperLine = rawLine.trim().toUpperCase();
|
||
if (upperLine === 'CLS' || upperLine === 'CLEAR') {
|
||
outputArea.innerHTML = '';
|
||
resetInput();
|
||
return;
|
||
}
|
||
if (upperLine === 'EXIT') {
|
||
// 这里可以调用关闭窗口
|
||
appendLine("Bye.");
|
||
return;
|
||
}
|
||
|
||
// 5. 拼接到缓冲区
|
||
commandBuffer += rawLine + " "; // 加空格防止粘连
|
||
|
||
// 6. 检查是否以分号结尾 (真正的执行触发器)
|
||
if (rawLine.trim().endsWith(';')) {
|
||
// 移除分号并执行
|
||
const finalCmd = commandBuffer.trim().replace(/;$/, '');
|
||
processCommand(finalCmd);
|
||
resetInput(); // 重置为单行模式
|
||
} else {
|
||
// 进入多行模式
|
||
isMultiLineMode = true;
|
||
promptSpan.textContent = " -> ";
|
||
input.value = '';
|
||
// 滚动到底部
|
||
window.scrollTo(0, document.body.scrollHeight);
|
||
}
|
||
}
|
||
|
||
function resetInput() {
|
||
commandBuffer = "";
|
||
isMultiLineMode = false;
|
||
promptSpan.textContent = "SQL> ";
|
||
input.value = '';
|
||
window.scrollTo(0, document.body.scrollHeight);
|
||
}
|
||
|
||
function handleHistory(isUp, e) {
|
||
e.preventDefault();
|
||
if (isUp && historyIndex > 0) {
|
||
historyIndex--;
|
||
input.value = history[historyIndex];
|
||
} else if (!isUp && historyIndex < history.length - 1) {
|
||
historyIndex++;
|
||
input.value = history[historyIndex];
|
||
} else if (!isUp) {
|
||
historyIndex = history.length;
|
||
input.value = '';
|
||
}
|
||
}
|
||
|
||
// --- 4. 智能感知自动补全 (Context Aware) ---
|
||
let currentSuggestions = [];
|
||
let activeIndex = -1;
|
||
|
||
input.addEventListener('input', function() {
|
||
const val = this.value;
|
||
const cursorPos = this.selectionStart;
|
||
|
||
// 获取正在输入的单词
|
||
const wordInfo = getCurrentWord(val, cursorPos);
|
||
if (!wordInfo) {
|
||
hideSuggestions();
|
||
return;
|
||
}
|
||
|
||
// 分析上下文 (Context Analysis)
|
||
const contextType = analyzeContext(val, wordInfo.startPos);
|
||
|
||
// 获取建议列表
|
||
const list = getFilteredSuggestions(wordInfo.word, contextType);
|
||
|
||
if (list.length > 0) {
|
||
showSuggestions(list, wordInfo.startPos);
|
||
} else {
|
||
hideSuggestions();
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 简单的上下文分析器
|
||
* 返回: 'TABLE' | 'COLUMN' | 'KEYWORD' (默认)
|
||
*/
|
||
function analyzeContext(text, cursorIndex) {
|
||
// 取光标前的文本,转大写,分割成 Token
|
||
const prefix = text.substring(0, cursorIndex).toUpperCase();
|
||
const tokens = prefix.split(/\s+/).filter(t => t.length > 0);
|
||
|
||
if (tokens.length === 0) return 'KEYWORD';
|
||
|
||
const lastToken = tokens[tokens.length - 1];
|
||
|
||
// 如果上一个词是这些,通常后面接表名
|
||
const tableTriggers = ["FROM", "JOIN", "UPDATE", "INTO", "TABLE"];
|
||
if (tableTriggers.includes(lastToken)) return 'TABLE';
|
||
|
||
// 如果上一个词是这些,通常后面接列名
|
||
const columnTriggers = ["SELECT", "WHERE", "ORDER", "BY", "GROUP", "HAVING", "SET", "ON", "AND", "OR"];
|
||
if (columnTriggers.includes(lastToken) || lastToken.endsWith(',')) return 'COLUMN';
|
||
|
||
return 'KEYWORD';
|
||
}
|
||
|
||
function getFilteredSuggestions(query, type) {
|
||
const q = query.toUpperCase();
|
||
let results = [];
|
||
|
||
// 1. 优先根据上下文添加
|
||
if (type === 'TABLE') {
|
||
dbSchema.tables.forEach(t => {
|
||
if (t.toUpperCase().startsWith(q)) results.push({ text: t, type: 't', detail: 'Table' });
|
||
});
|
||
} else if (type === 'COLUMN') {
|
||
dbSchema.allColumns.forEach(c => {
|
||
if (c.toUpperCase().startsWith(q)) results.push({ text: c, type: 'c', detail: 'Column' });
|
||
});
|
||
// 列名场景下也可能输入表名(例如 table.col)
|
||
dbSchema.tables.forEach(t => {
|
||
if (t.toUpperCase().startsWith(q)) results.push({ text: t, type: 't', detail: 'Table' });
|
||
});
|
||
}
|
||
|
||
// 2. 总是添加匹配的关键字 (但在特定上下文排在后面)
|
||
keywords.forEach(k => {
|
||
if (k.startsWith(q)) results.push({ text: k, type: 'k', detail: 'Keyword' });
|
||
});
|
||
|
||
return results.slice(0, 15); // 限制显示数量
|
||
}
|
||
|
||
// --- 补全 UI 渲染逻辑 --- (复用之前的基础,增加了类型判断)
|
||
function showSuggestions(list, wordStartPos) {
|
||
currentSuggestions = list;
|
||
activeIndex = 0;
|
||
|
||
// 计算 UI 位置
|
||
const promptWidth = getTextWidth(promptSpan.textContent, "bold 16px Consolas");
|
||
const inputBefore = input.value.substring(0, wordStartPos);
|
||
const inputWidth = getTextWidth(inputBefore, "16px Consolas");
|
||
|
||
suggestionBox.style.left = (promptWidth + inputWidth) + "px";
|
||
suggestionBox.style.top = "26px";
|
||
suggestionBox.style.display = "block";
|
||
|
||
renderList();
|
||
}
|
||
|
||
function renderList() {
|
||
suggestionBox.innerHTML = '';
|
||
currentSuggestions.forEach((item, idx) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'suggestion-item' + (idx === activeIndex ? ' active' : '');
|
||
|
||
let iconChar = 'K';
|
||
if (item.type === 't') iconChar = 'T';
|
||
if (item.type === 'c') iconChar = 'C';
|
||
|
||
div.innerHTML = `
|
||
<span class="icon icon-${item.type}">${iconChar}</span>
|
||
<span class="text">${item.text}</span>
|
||
<span class="detail-text">${item.detail}</span>
|
||
`;
|
||
|
||
div.onmousedown = (e) => { e.preventDefault(); applySuggestion(idx); }; // prevent blur
|
||
suggestionBox.appendChild(div);
|
||
});
|
||
|
||
// 自动滚动
|
||
const activeEl = suggestionBox.children[activeIndex];
|
||
if (activeEl) {
|
||
const boxTop = suggestionBox.scrollTop;
|
||
const boxBottom = boxTop + suggestionBox.clientHeight;
|
||
const elTop = activeEl.offsetTop;
|
||
const elBottom = elTop + activeEl.clientHeight;
|
||
|
||
if (elTop < boxTop) suggestionBox.scrollTop = elTop;
|
||
if (elBottom > boxBottom) suggestionBox.scrollTop = elBottom - suggestionBox.clientHeight;
|
||
}
|
||
}
|
||
|
||
function handleSuggestionKeys(e) {
|
||
if (e.key === 'ArrowDown') {
|
||
e.preventDefault();
|
||
activeIndex = (activeIndex + 1) % currentSuggestions.length;
|
||
renderList();
|
||
return true;
|
||
}
|
||
if (e.key === 'ArrowUp') {
|
||
e.preventDefault();
|
||
activeIndex = (activeIndex - 1 + currentSuggestions.length) % currentSuggestions.length;
|
||
renderList();
|
||
return true;
|
||
}
|
||
if (e.key === 'Tab' || e.key === 'Enter') {
|
||
e.preventDefault();
|
||
applySuggestion(activeIndex);
|
||
return true;
|
||
}
|
||
if (e.key === 'Escape') {
|
||
hideSuggestions();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function applySuggestion(index) {
|
||
const item = currentSuggestions[index];
|
||
const val = input.value;
|
||
const wordInfo = getCurrentWord(val, input.selectionStart);
|
||
|
||
if (wordInfo) {
|
||
const prefix = val.substring(0, wordInfo.startPos);
|
||
const suffix = val.substring(input.selectionStart);
|
||
const newText = prefix + item.text + " " + suffix; // 自动加个空格
|
||
input.value = newText;
|
||
const newPos = prefix.length + item.text.length + 1;
|
||
input.setSelectionRange(newPos, newPos);
|
||
}
|
||
hideSuggestions();
|
||
}
|
||
|
||
function hideSuggestions() {
|
||
suggestionBox.style.display = 'none';
|
||
activeIndex = -1;
|
||
}
|
||
|
||
// --- 工具函数 ---
|
||
function getCurrentWord(text, cursorIndex) {
|
||
const left = text.substring(0, cursorIndex);
|
||
const match = left.match(/([a-zA-Z0-9_]+)$/);
|
||
if (match) {
|
||
return { word: match[1], startPos: match.index };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function getTextWidth(text, font) {
|
||
const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
|
||
const ctx = canvas.getContext("2d");
|
||
ctx.font = font;
|
||
return ctx.measureText(text).width;
|
||
}
|
||
|
||
// --- 后端通信与显示 (复用) ---
|
||
function processCommand(cmd) {
|
||
if (window.cefQuery) {
|
||
window.cefQuery({
|
||
request: JSON.stringify({ type: "executeCommand", command: cmd }),
|
||
onSuccess: (res) => renderResponse(JSON.parse(res)),
|
||
onFailure: (c, msg) => appendLine("Error: " + msg, true)
|
||
});
|
||
}
|
||
}
|
||
|
||
function renderResponse(res) {
|
||
if (res.status === 'error') {
|
||
appendLine(res.output, true);
|
||
} else if (res.type === 'table') {
|
||
appendLine(generateAsciiTable(res.headers, res.rows));
|
||
if (res.info) appendLine(res.info);
|
||
} else {
|
||
appendLine(res.output);
|
||
}
|
||
appendLine(""); // 空行分割
|
||
}
|
||
|
||
function appendLine(text, isError = false) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
if (isError) div.className = 'error-text';
|
||
outputArea.appendChild(div);
|
||
}
|
||
|
||
function generateAsciiTable(headers, rows) {
|
||
if (!headers || !headers.length) return "";
|
||
const colWidths = headers.map(h => h.length);
|
||
rows.forEach(row => row.forEach((c, i) => colWidths[i] = Math.max(colWidths[i], String(c).length)));
|
||
|
||
const border = "+" + colWidths.map(w => "-".repeat(w + 2)).join("+") + "+";
|
||
const makeRow = (arr) => "|" + arr.map((c, i) => " " + String(c).padEnd(colWidths[i]) + " ").join("|") + "|";
|
||
|
||
return [border, makeRow(headers), border, ...rows.map(makeRow), border].join("\n");
|
||
}
|
||
|
||
// 点击聚焦 (不影响选中文本)
|
||
document.addEventListener('click', (e) => {
|
||
if (!window.getSelection().toString() && !suggestionBox.contains(e.target)) input.focus();
|
||
});
|
||
|
||
</script>
|
||
</body>
|
||
</html> |