|
|
@@ -0,0 +1,1759 @@
|
|
|
+// 控制台交互逻辑
|
|
|
+class SymjaConsole {
|
|
|
+ constructor() {
|
|
|
+ this.output = document.getElementById('output');
|
|
|
+ this.input = document.getElementById('input');
|
|
|
+ this.inputPrompt = document.getElementById('inputPrompt');
|
|
|
+ this.completionList = document.getElementById('completionList');
|
|
|
+ this.helpBtn = document.getElementById('helpBtn');
|
|
|
+ this.pluginsBtn = document.getElementById('pluginsBtn');
|
|
|
+ this.settingsBtn = document.getElementById('settingsBtn');
|
|
|
+
|
|
|
+ // 历史记录
|
|
|
+ this.history = this.loadHistory();
|
|
|
+ this.historyIndex = this.history.length;
|
|
|
+ this.tempInput = '';
|
|
|
+
|
|
|
+ // 插件相关
|
|
|
+ this.plugins = [];
|
|
|
+ this.currentPlugin = null;
|
|
|
+
|
|
|
+ // 自动补全相关
|
|
|
+ this.completions = [];
|
|
|
+ this.completionIndex = -1;
|
|
|
+
|
|
|
+ // 变量追踪
|
|
|
+ this.variables = new Set();
|
|
|
+
|
|
|
+ // 执行计数器
|
|
|
+ this.executionCount = this.loadExecutionCount();
|
|
|
+
|
|
|
+ this.init();
|
|
|
+ this.loadPlugins();
|
|
|
+ this.refreshPluginList(); // 初始化时刷新插件列表
|
|
|
+ this.showWelcome();
|
|
|
+ this.updatePrompt();
|
|
|
+ }
|
|
|
+
|
|
|
+ init() {
|
|
|
+ // 输入框事件
|
|
|
+ this.input.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
|
|
+ this.input.addEventListener('input', () => this.handleInput());
|
|
|
+
|
|
|
+ // 快捷键工具栏
|
|
|
+ this.initShortcutsBar();
|
|
|
+
|
|
|
+ // 帮助页面关闭按钮
|
|
|
+ document.getElementById('closeHelp')?.addEventListener('click', () => {
|
|
|
+ this.hideHelp();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 插件面板事件
|
|
|
+ document.getElementById('closePluginPanel')?.addEventListener('click', () => {
|
|
|
+ this.hidePluginPanel();
|
|
|
+ });
|
|
|
+
|
|
|
+ document.getElementById('closePluginDialog')?.addEventListener('click', () => {
|
|
|
+ this.hidePluginDialog();
|
|
|
+ });
|
|
|
+
|
|
|
+ document.getElementById('cancelPlugin')?.addEventListener('click', () => {
|
|
|
+ this.hidePluginDialog();
|
|
|
+ });
|
|
|
+
|
|
|
+ document.getElementById('executePlugin')?.addEventListener('click', () => {
|
|
|
+ this.executeCurrentPlugin();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ initShortcutsBar() {
|
|
|
+ const buttons = document.querySelectorAll('.shortcut-btn');
|
|
|
+ buttons.forEach(btn => {
|
|
|
+ btn.addEventListener('click', (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ const action = btn.dataset.action;
|
|
|
+ const value = btn.dataset.value;
|
|
|
+
|
|
|
+ switch(action) {
|
|
|
+ case 'bracket':
|
|
|
+ case 'insert':
|
|
|
+ this.insertText(value);
|
|
|
+ break;
|
|
|
+ case 'history-up':
|
|
|
+ this.navigateHistory(-1);
|
|
|
+ break;
|
|
|
+ case 'history-down':
|
|
|
+ this.navigateHistory(1);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保持输入框焦点
|
|
|
+ this.input.focus();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 初始化插件标签页
|
|
|
+ this.initPluginTabs();
|
|
|
+
|
|
|
+ // 初始化插件分享功能
|
|
|
+ this.initPluginShare();
|
|
|
+ }
|
|
|
+
|
|
|
+ initPluginTabs() {
|
|
|
+ const tabs = document.querySelectorAll('.plugin-tab');
|
|
|
+ const contents = document.querySelectorAll('.tab-content');
|
|
|
+
|
|
|
+ tabs.forEach(tab => {
|
|
|
+ tab.addEventListener('click', () => {
|
|
|
+ const targetTab = tab.dataset.tab;
|
|
|
+
|
|
|
+ // 切换标签激活状态
|
|
|
+ tabs.forEach(t => t.classList.remove('active'));
|
|
|
+ tab.classList.add('active');
|
|
|
+
|
|
|
+ // 切换内容显示
|
|
|
+ contents.forEach(content => {
|
|
|
+ if (content.dataset.content === targetTab) {
|
|
|
+ content.classList.add('active');
|
|
|
+ } else {
|
|
|
+ content.classList.remove('active');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 如果切换到导入/分享标签,更新插件列表
|
|
|
+ if (targetTab === 'import') {
|
|
|
+ this.updatePluginSelects();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ initPluginShare() {
|
|
|
+ // 使用事件委托,在document上监听点击事件
|
|
|
+ document.addEventListener('click', (e) => {
|
|
|
+ const target = e.target;
|
|
|
+
|
|
|
+ // 生成分享码
|
|
|
+ if (target.id === 'generateShareCode') {
|
|
|
+ console.log('generateShareCode clicked');
|
|
|
+ e.preventDefault();
|
|
|
+ this.generateShareCode();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 复制分享码
|
|
|
+ if (target.id === 'copyShareCode') {
|
|
|
+ console.log('copyShareCode clicked');
|
|
|
+ e.preventDefault();
|
|
|
+ this.copyShareCode();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 导入插件
|
|
|
+ if (target.id === 'importPlugin') {
|
|
|
+ console.log('importPlugin clicked');
|
|
|
+ e.preventDefault();
|
|
|
+ this.importPlugin();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除插件
|
|
|
+ if (target.id === 'deletePlugin') {
|
|
|
+ console.log('deletePlugin clicked');
|
|
|
+ e.preventDefault();
|
|
|
+ this.deletePluginConfirm();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ updatePluginSelects() {
|
|
|
+ const shareSelect = document.getElementById('sharePluginSelect');
|
|
|
+ const deleteSelect = document.getElementById('deletePluginSelect');
|
|
|
+
|
|
|
+ if (shareSelect) {
|
|
|
+ shareSelect.innerHTML = '<option value="">选择插件...</option>';
|
|
|
+ this.plugins.forEach((plugin, index) => {
|
|
|
+ const option = document.createElement('option');
|
|
|
+ option.value = index;
|
|
|
+ option.textContent = `${plugin.icon} ${plugin.name}`;
|
|
|
+ shareSelect.appendChild(option);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (deleteSelect) {
|
|
|
+ deleteSelect.innerHTML = '<option value="">选择要删除的插件...</option>';
|
|
|
+ this.plugins.forEach((plugin, index) => {
|
|
|
+ const option = document.createElement('option');
|
|
|
+ option.value = index;
|
|
|
+ option.textContent = `${plugin.icon} ${plugin.name}`;
|
|
|
+ deleteSelect.appendChild(option);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ generateShareCode() {
|
|
|
+ const select = document.getElementById('sharePluginSelect');
|
|
|
+ const output = document.getElementById('shareCodeOutput');
|
|
|
+ const copyBtn = document.getElementById('copyShareCode');
|
|
|
+
|
|
|
+ if (!select || !output) return;
|
|
|
+
|
|
|
+ const index = parseInt(select.value);
|
|
|
+ if (isNaN(index) || index < 0 || index >= this.plugins.length) {
|
|
|
+ this.addOutput('❌ 请先选择一个插件', 'error');
|
|
|
+ this.scrollToBottom();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const plugin = this.plugins[index];
|
|
|
+ const shareCode = JSON.stringify(plugin, null, 2);
|
|
|
+
|
|
|
+ output.value = shareCode;
|
|
|
+ if (copyBtn) copyBtn.style.display = 'block';
|
|
|
+
|
|
|
+ this.addOutput(`✅ 已生成 "${plugin.name}" 的分享码`, 'help');
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ copyShareCode() {
|
|
|
+ const output = document.getElementById('shareCodeOutput');
|
|
|
+ if (!output || !output.value) return;
|
|
|
+
|
|
|
+ output.select();
|
|
|
+ document.execCommand('copy');
|
|
|
+
|
|
|
+ this.addOutput('📋 分享码已复制到剪贴板', 'help');
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ importPlugin() {
|
|
|
+ const input = document.getElementById('importCodeInput');
|
|
|
+ if (!input || !input.value.trim()) {
|
|
|
+ this.addOutput('❌ 请粘贴插件代码', 'error');
|
|
|
+ this.scrollToBottom();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const pluginData = JSON.parse(input.value);
|
|
|
+
|
|
|
+ // 验证插件格式
|
|
|
+ if (!pluginData.id || !pluginData.name || !pluginData.jsCode) {
|
|
|
+ throw new Error('插件格式不正确,缺少必要字段');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用 Android 接口保存插件
|
|
|
+ try {
|
|
|
+ const result = Android.savePlugin(JSON.stringify(pluginData));
|
|
|
+
|
|
|
+ // 重新加载插件
|
|
|
+ this.loadPlugins();
|
|
|
+ this.updatePluginSelects();
|
|
|
+
|
|
|
+ // 刷新插件列表显示
|
|
|
+ this.refreshPluginList();
|
|
|
+
|
|
|
+ // 显示成功消息
|
|
|
+ this.addOutput(`✅ ${result}`, 'help');
|
|
|
+ this.addOutput(`📦 插件 "${pluginData.name}" 已添加到列表`, 'help');
|
|
|
+
|
|
|
+ // 清空输入
|
|
|
+ input.value = '';
|
|
|
+ } catch (e) {
|
|
|
+ this.addOutput(`❌ 保存失败: ${e.message}`, 'error');
|
|
|
+ }
|
|
|
+
|
|
|
+ this.scrollToBottom();
|
|
|
+ } catch (e) {
|
|
|
+ this.addOutput(`❌ 解析失败: ${e.message}`, 'error');
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ deletePluginConfirm() {
|
|
|
+ const select = document.getElementById('deletePluginSelect');
|
|
|
+ if (!select) {
|
|
|
+ console.error('deletePluginSelect element not found');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('Select value:', select.value);
|
|
|
+ console.log('Plugins length:', this.plugins.length);
|
|
|
+
|
|
|
+ const index = parseInt(select.value);
|
|
|
+ if (isNaN(index) || index < 0 || index >= this.plugins.length) {
|
|
|
+ this.addOutput('❌ 请先选择要删除的插件', 'error');
|
|
|
+ this.scrollToBottom();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const plugin = this.plugins[index];
|
|
|
+ console.log('Deleting plugin:', plugin.name, 'ID:', plugin.id);
|
|
|
+
|
|
|
+ // 使用自定义确认对话框或直接删除
|
|
|
+ const confirmMessage = `确定要删除插件 "${plugin.name}" 吗?`;
|
|
|
+ console.log('Showing confirm dialog:', confirmMessage);
|
|
|
+
|
|
|
+ let userConfirmed = false;
|
|
|
+ try {
|
|
|
+ userConfirmed = confirm(confirmMessage);
|
|
|
+ console.log('User confirmed:', userConfirmed);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Confirm dialog error:', e);
|
|
|
+ // 如果confirm失败,直接删除
|
|
|
+ userConfirmed = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (userConfirmed) {
|
|
|
+ console.log('Proceeding with deletion...');
|
|
|
+ try {
|
|
|
+ const result = Android.deletePlugin(plugin.id);
|
|
|
+ console.log('Delete result:', result);
|
|
|
+ this.addOutput(`✅ ${result}`, 'help');
|
|
|
+
|
|
|
+ // 重新加载插件
|
|
|
+ this.loadPlugins();
|
|
|
+ this.updatePluginSelects();
|
|
|
+
|
|
|
+ // 刷新插件列表显示
|
|
|
+ this.refreshPluginList();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Delete error:', e);
|
|
|
+ this.addOutput(`❌ 删除失败: ${e.message}`, 'error');
|
|
|
+ }
|
|
|
+
|
|
|
+ this.scrollToBottom();
|
|
|
+ } else {
|
|
|
+ console.log('User cancelled deletion');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ refreshPluginList() {
|
|
|
+ const list = document.getElementById('pluginList');
|
|
|
+ console.log('refreshPluginList called, list element:', list);
|
|
|
+ console.log('Current plugins count:', this.plugins.length);
|
|
|
+
|
|
|
+ if (!list) return;
|
|
|
+
|
|
|
+ list.innerHTML = '';
|
|
|
+
|
|
|
+ if (this.plugins.length === 0) {
|
|
|
+ console.log('No plugins, showing empty message');
|
|
|
+ list.innerHTML = '<div class="no-plugins">暂无插件</div>';
|
|
|
+ } else {
|
|
|
+ console.log('Rendering', this.plugins.length, 'plugins');
|
|
|
+ this.plugins.forEach(plugin => {
|
|
|
+ const item = document.createElement('div');
|
|
|
+ item.className = 'plugin-item';
|
|
|
+ const commandInfo = plugin.command ? `<div class="plugin-command">命令: ${plugin.command}</div>` : '';
|
|
|
+ item.innerHTML = `
|
|
|
+ <div class="plugin-icon">${plugin.icon}</div>
|
|
|
+ <div class="plugin-info">
|
|
|
+ <div class="plugin-name">${plugin.name}</div>
|
|
|
+ <div class="plugin-desc">${plugin.description}</div>
|
|
|
+ ${commandInfo}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ item.addEventListener('click', () => this.openPlugin(plugin));
|
|
|
+ list.appendChild(item);
|
|
|
+ });
|
|
|
+ console.log('Finished rendering plugins, list children:', list.children.length);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ insertText(text) {
|
|
|
+ const start = this.input.selectionStart;
|
|
|
+ const end = this.input.selectionEnd;
|
|
|
+ const value = this.input.value;
|
|
|
+
|
|
|
+ // 插入文本
|
|
|
+ this.input.value = value.substring(0, start) + text + value.substring(end);
|
|
|
+
|
|
|
+ // 设置光标位置
|
|
|
+ const newPos = start + text.length;
|
|
|
+ this.input.setSelectionRange(newPos, newPos);
|
|
|
+
|
|
|
+ // 触发输入事件
|
|
|
+ this.handleInput();
|
|
|
+ }
|
|
|
+
|
|
|
+ handleKeyDown(e) {
|
|
|
+ if (e.key === 'Enter') {
|
|
|
+ e.preventDefault();
|
|
|
+ this.hideCompletionList();
|
|
|
+ this.evaluate();
|
|
|
+ } else if (e.key === 'ArrowUp') {
|
|
|
+ e.preventDefault();
|
|
|
+ this.hideCompletionList();
|
|
|
+ this.navigateHistory(-1);
|
|
|
+ } else if (e.key === 'ArrowDown') {
|
|
|
+ e.preventDefault();
|
|
|
+ this.hideCompletionList();
|
|
|
+ this.navigateHistory(1);
|
|
|
+ } else if (e.key === 'Escape') {
|
|
|
+ this.input.value = '';
|
|
|
+ this.completions = [];
|
|
|
+ this.completionIndex = -1;
|
|
|
+ this.hideCompletionList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ handleInput() {
|
|
|
+ // 更新自动补全列表
|
|
|
+ console.log('handleInput called, value:', this.input.value);
|
|
|
+ this.updateCompletionList();
|
|
|
+ }
|
|
|
+
|
|
|
+ showWelcome() {
|
|
|
+ // 不显示欢迎消息
|
|
|
+ }
|
|
|
+
|
|
|
+ evaluate() {
|
|
|
+ let expression = this.input.value.trim();
|
|
|
+
|
|
|
+ if (!expression) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理特殊命令
|
|
|
+ if (this.handleSpecialCommand(expression)) {
|
|
|
+ // 特殊命令也要添加到历史记录
|
|
|
+ this.addToHistory(expression);
|
|
|
+ this.input.value = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加到历史记录
|
|
|
+ this.addToHistory(expression);
|
|
|
+
|
|
|
+ // 显示输入(带计数器)
|
|
|
+ this.addOutput(expression, 'input', { count: this.executionCount });
|
|
|
+
|
|
|
+ // 调用Android接口进行计算
|
|
|
+ try {
|
|
|
+ const result = Android.evaluate(expression);
|
|
|
+
|
|
|
+ // 检查是否是特殊功能请求
|
|
|
+ if (result.startsWith('##TEXFORM##')) {
|
|
|
+ const latex = result.substring(11);
|
|
|
+ this.addOutput(latex, 'result', { renderLatex: true, count: this.executionCount });
|
|
|
+ this.scrollToBottom();
|
|
|
+ } else {
|
|
|
+ this.addOutput(result, 'result', { count: this.executionCount });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 追踪变量
|
|
|
+ this.trackVariables(expression);
|
|
|
+ } catch (e) {
|
|
|
+ this.addOutput('错误: 无法连接到计算引擎', 'error');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 增加计数器
|
|
|
+ this.executionCount++;
|
|
|
+ this.saveExecutionCount();
|
|
|
+ this.updatePrompt();
|
|
|
+
|
|
|
+ // 清空输入框
|
|
|
+ this.input.value = '';
|
|
|
+ this.completions = [];
|
|
|
+ this.completionIndex = -1;
|
|
|
+
|
|
|
+ // 滚动到底部
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ handleSpecialCommand(cmd) {
|
|
|
+ const lower = cmd.toLowerCase();
|
|
|
+
|
|
|
+ if (lower === 'help' || lower === '?') {
|
|
|
+ this.showHelp();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (lower === 'clear' || lower === 'cls') {
|
|
|
+ this.clear();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (lower === 'history') {
|
|
|
+ this.showHistory();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (lower === 'plugins') {
|
|
|
+ this.showPluginPanel();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (lower === 'feedback') {
|
|
|
+ this.showFeedback();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (lower.startsWith('clear ')) {
|
|
|
+ const varName = cmd.substring(6).trim();
|
|
|
+ this.clearVariable(varName);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否是插件命令
|
|
|
+ if (this.handlePluginCommand(lower)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ handlePluginCommand(cmd) {
|
|
|
+ // 检查是否是带参数的插件命令格式: command[arg1, arg2, ...]
|
|
|
+ const paramMatch = cmd.match(/^([a-z0-9_-]+)\[(.*)\]$/i);
|
|
|
+
|
|
|
+ if (paramMatch) {
|
|
|
+ const commandName = paramMatch[1].toLowerCase();
|
|
|
+ const argsString = paramMatch[2];
|
|
|
+
|
|
|
+ // 查找匹配的插件
|
|
|
+ const plugin = this.plugins.find(p => p.command && p.command.toLowerCase() === commandName);
|
|
|
+
|
|
|
+ if (plugin) {
|
|
|
+ // 解析参数
|
|
|
+ const args = this.parsePluginArgs(argsString);
|
|
|
+
|
|
|
+ // 验证参数数量
|
|
|
+ if (args.length !== plugin.inputs.length) {
|
|
|
+ this.addOutput(`❌ 参数数量不匹配。${plugin.name} 需要 ${plugin.inputs.length} 个参数,但提供了 ${args.length} 个`, 'error');
|
|
|
+ this.scrollToBottom();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行插件(直接模式)
|
|
|
+ this.executePluginDirect(plugin, args);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找匹配的插件命令(无参数,打开对话框)
|
|
|
+ for (const plugin of this.plugins) {
|
|
|
+ if (plugin.command && plugin.command.toLowerCase() === cmd) {
|
|
|
+ this.addOutput(`🔌 打开插件: ${plugin.name}`, 'help');
|
|
|
+ this.scrollToBottom();
|
|
|
+ this.openPlugin(plugin);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ parsePluginArgs(argsString) {
|
|
|
+ // 解析逗号分隔的参数,支持引号、大括号、方括号内的逗号
|
|
|
+ const args = [];
|
|
|
+ let current = '';
|
|
|
+ let inQuotes = false;
|
|
|
+ let quoteChar = '';
|
|
|
+ let bracketDepth = 0; // 跟踪 {} 的嵌套深度
|
|
|
+ let squareBracketDepth = 0; // 跟踪 [] 的嵌套深度
|
|
|
+
|
|
|
+ for (let i = 0; i < argsString.length; i++) {
|
|
|
+ const char = argsString[i];
|
|
|
+
|
|
|
+ // 处理引号
|
|
|
+ if ((char === '"' || char === "'") && (i === 0 || argsString[i-1] !== '\\')) {
|
|
|
+ if (!inQuotes) {
|
|
|
+ inQuotes = true;
|
|
|
+ quoteChar = char;
|
|
|
+ } else if (char === quoteChar) {
|
|
|
+ inQuotes = false;
|
|
|
+ quoteChar = '';
|
|
|
+ }
|
|
|
+ current += char;
|
|
|
+ }
|
|
|
+ // 处理大括号
|
|
|
+ else if (char === '{' && !inQuotes) {
|
|
|
+ bracketDepth++;
|
|
|
+ current += char;
|
|
|
+ }
|
|
|
+ else if (char === '}' && !inQuotes) {
|
|
|
+ bracketDepth--;
|
|
|
+ current += char;
|
|
|
+ }
|
|
|
+ // 处理方括号
|
|
|
+ else if (char === '[' && !inQuotes) {
|
|
|
+ squareBracketDepth++;
|
|
|
+ current += char;
|
|
|
+ }
|
|
|
+ else if (char === ']' && !inQuotes) {
|
|
|
+ squareBracketDepth--;
|
|
|
+ current += char;
|
|
|
+ }
|
|
|
+ // 处理逗号分隔符
|
|
|
+ else if (char === ',' && !inQuotes && bracketDepth === 0 && squareBracketDepth === 0) {
|
|
|
+ args.push(current.trim().replace(/^["']|["']$/g, ''));
|
|
|
+ current = '';
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ current += char;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (current.trim()) {
|
|
|
+ args.push(current.trim().replace(/^["']|["']$/g, ''));
|
|
|
+ }
|
|
|
+
|
|
|
+ return args;
|
|
|
+ }
|
|
|
+
|
|
|
+ executePluginDirect(plugin, args) {
|
|
|
+ try {
|
|
|
+ console.log('executePluginDirect called with args:', args);
|
|
|
+
|
|
|
+ // 构建输入对象,并转换为 Symja 可运算类型
|
|
|
+ const inputValues = {};
|
|
|
+ plugin.inputs.forEach((input, index) => {
|
|
|
+ const originalArg = args[index];
|
|
|
+ const convertedArg = this.convertInputToSymjaType(originalArg);
|
|
|
+ console.log(`Converting arg[${index}]: "${originalArg}" -> "${convertedArg}"`);
|
|
|
+ inputValues[input.name] = convertedArg;
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('Final inputValues:', inputValues);
|
|
|
+
|
|
|
+ // 用于存储插件的控制台输出
|
|
|
+ const pluginOutputs = [];
|
|
|
+
|
|
|
+ // 创建 Symja API 对象
|
|
|
+ const symjaAPI = this.createSymjaAPI(pluginOutputs);
|
|
|
+
|
|
|
+ // 执行插件代码
|
|
|
+ const func = new Function('inputs', 'Symja', plugin.jsCode + '\nreturn calculate(inputs, Symja);');
|
|
|
+ const result = func(inputValues, symjaAPI);
|
|
|
+
|
|
|
+ console.log('Plugin result:', result);
|
|
|
+
|
|
|
+ // 验证返回值
|
|
|
+ if (typeof result !== 'string' || result.trim() === '') {
|
|
|
+ throw new Error('插件必须返回非空字符串类型的 Symja 表达式');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证返回值是否是有效的 Symja 表达式
|
|
|
+ try {
|
|
|
+ Android.symjaEval(result);
|
|
|
+ } catch (e) {
|
|
|
+ throw new Error('插件返回值必须是有效的 Symja 表达式,当前返回: ' + result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示插件名称和参数
|
|
|
+ const argsDisplay = args.map((arg, i) => `${plugin.inputs[i].name}=${arg}`).join(', ');
|
|
|
+ this.addOutput(`${plugin.icon} ${plugin.name}[${argsDisplay}]`, 'input', { count: this.executionCount });
|
|
|
+
|
|
|
+ // 显示插件的控制台输出(如果有)
|
|
|
+ if (pluginOutputs.length > 0) {
|
|
|
+ pluginOutputs.forEach(output => {
|
|
|
+ this.addOutput(output, 'plugin-info');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示插件的返回值(可运算结果)
|
|
|
+ this.addOutput(result, 'result', { count: this.executionCount });
|
|
|
+
|
|
|
+ // 将插件结果保存到 Symja 引擎,使其可以通过 % 引用
|
|
|
+ try {
|
|
|
+ Android.evaluate(`Out = ${result}`);
|
|
|
+ } catch (e) {
|
|
|
+ console.log('Plugin result storage error:', e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 增加计数器
|
|
|
+ this.executionCount++;
|
|
|
+ this.saveExecutionCount();
|
|
|
+ this.updatePrompt();
|
|
|
+
|
|
|
+ this.scrollToBottom();
|
|
|
+ } catch (e) {
|
|
|
+ console.log('Plugin execution error:', e);
|
|
|
+ this.addOutput(`插件执行错误: ${e.message}`, 'error');
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将用户输入转换为 Symja 可运算的类型
|
|
|
+ convertInputToSymjaType(value) {
|
|
|
+ console.log(`convertInputToSymjaType called with: "${value}"`);
|
|
|
+
|
|
|
+ if (!value || typeof value !== 'string') {
|
|
|
+ console.log('Value is not a string, returning as-is');
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+
|
|
|
+ const trimmed = value.trim();
|
|
|
+
|
|
|
+ // 空值直接返回
|
|
|
+ if (trimmed === '') {
|
|
|
+ console.log('Value is empty, returning as-is');
|
|
|
+ return trimmed;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否是单个变量名(字母开头,后跟字母数字下划线)或 % 符号
|
|
|
+ const varNameMatch = trimmed.match(/^([a-zA-Z]\w*|%)$/);
|
|
|
+ console.log(`Variable name match result:`, varNameMatch);
|
|
|
+
|
|
|
+ if (varNameMatch) {
|
|
|
+ // 直接尝试求值,如果是已定义的变量会返回值,否则返回变量名本身
|
|
|
+ try {
|
|
|
+ const evalResult = Android.symjaEval(trimmed);
|
|
|
+ console.log(`Evaluated "${trimmed}" to: "${evalResult}"`);
|
|
|
+
|
|
|
+ // 如果求值结果与输入不同,说明变量已定义
|
|
|
+ if (evalResult !== trimmed) {
|
|
|
+ console.log(`Variable ${trimmed} is defined, using value: ${evalResult}`);
|
|
|
+ return evalResult;
|
|
|
+ } else {
|
|
|
+ console.log(`Variable ${trimmed} is not defined or equals itself`);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error(`Failed to evaluate ${trimmed}:`, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试通过 Symja 验证,如果能成功解析说明是有效的表达式
|
|
|
+ console.log(`Trying to validate as Symja expression: "${trimmed}"`);
|
|
|
+ try {
|
|
|
+ Android.symjaEval(trimmed);
|
|
|
+ console.log(`Valid Symja expression, returning: "${trimmed}"`);
|
|
|
+ return trimmed;
|
|
|
+ } catch (e) {
|
|
|
+ // 如果无法解析为 Symja 表达式,返回原值
|
|
|
+ // 插件内部可以根据需要进一步处理
|
|
|
+ console.log(`Not a valid Symja expression, returning as literal: "${trimmed}"`);
|
|
|
+ return trimmed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ createSymjaAPI(pluginOutputs) {
|
|
|
+ return {
|
|
|
+ // 控制台输出 API
|
|
|
+ print: function(message) {
|
|
|
+ pluginOutputs.push(String(message));
|
|
|
+ },
|
|
|
+
|
|
|
+ // 基础计算
|
|
|
+ eval: function(expression) {
|
|
|
+ try {
|
|
|
+ return Android.symjaEval(expression);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: 无法调用 Symja';
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 微积分
|
|
|
+ differentiate: function(expr, variable) {
|
|
|
+ return this.eval(`D[${expr}, ${variable}]`);
|
|
|
+ },
|
|
|
+ integrate: function(expr, variable) {
|
|
|
+ return this.eval(`Integrate[${expr}, ${variable}]`);
|
|
|
+ },
|
|
|
+ limit: function(expr, variable, point, direction) {
|
|
|
+ try {
|
|
|
+ return Android.limit(expr, variable, point, direction || '');
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: function(expr, variable, point, order) {
|
|
|
+ try {
|
|
|
+ return Android.series(expr, variable, point, order || 5);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 代数
|
|
|
+ solve: function(equation, variable) {
|
|
|
+ return this.eval(`Solve[${equation}, ${variable}]`);
|
|
|
+ },
|
|
|
+ simplify: function(expr) {
|
|
|
+ return this.eval(`Simplify[${expr}]`);
|
|
|
+ },
|
|
|
+ expand: function(expr) {
|
|
|
+ return this.eval(`Expand[${expr}]`);
|
|
|
+ },
|
|
|
+ factor: function(expr) {
|
|
|
+ return this.eval(`Factor[${expr}]`);
|
|
|
+ },
|
|
|
+ apart: function(expr, variable) {
|
|
|
+ try {
|
|
|
+ return Android.apart(expr, variable);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ together: function(expr) {
|
|
|
+ try {
|
|
|
+ return Android.together(expr);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigSimplify: function(expr) {
|
|
|
+ try {
|
|
|
+ return Android.trigSimplify(expr);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 求和与求积
|
|
|
+ sum: function(expr, variable, start, end) {
|
|
|
+ try {
|
|
|
+ return Android.sum(expr, variable, String(start), String(end));
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ product: function(expr, variable, start, end) {
|
|
|
+ try {
|
|
|
+ return Android.product(expr, variable, String(start), String(end));
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 矩阵运算
|
|
|
+ matrix: {
|
|
|
+ det: function(matrix) {
|
|
|
+ try {
|
|
|
+ return Android.matrixOp('Det', matrix);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ inverse: function(matrix) {
|
|
|
+ try {
|
|
|
+ return Android.matrixOp('Inverse', matrix);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ transpose: function(matrix) {
|
|
|
+ try {
|
|
|
+ return Android.matrixOp('Transpose', matrix);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ eigenvalues: function(matrix) {
|
|
|
+ try {
|
|
|
+ return Android.matrixOp('Eigenvalues', matrix);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ rank: function(matrix) {
|
|
|
+ try {
|
|
|
+ return Android.matrixOp('MatrixRank', matrix);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 数论
|
|
|
+ gcd: function(a, b) {
|
|
|
+ try {
|
|
|
+ return Android.gcd(String(a), String(b));
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ lcm: function(a, b) {
|
|
|
+ try {
|
|
|
+ return Android.lcm(String(a), String(b));
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ factorInteger: function(n) {
|
|
|
+ try {
|
|
|
+ return Android.factorInteger(String(n));
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ isPrime: function(n) {
|
|
|
+ try {
|
|
|
+ return Android.isPrime(String(n));
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 组合数学
|
|
|
+ fibonacci: function(n) {
|
|
|
+ try {
|
|
|
+ return Android.fibonacci(n);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ binomial: function(n, k) {
|
|
|
+ try {
|
|
|
+ return Android.binomial(String(n), String(k));
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 数值计算
|
|
|
+ numeric: function(expr, precision) {
|
|
|
+ try {
|
|
|
+ return Android.numericEval(expr, precision || 16);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // LaTeX 转换和渲染
|
|
|
+ toTeX: function(expr) {
|
|
|
+ try {
|
|
|
+ return Android.toTeX(expr);
|
|
|
+ } catch (e) {
|
|
|
+ return '错误: ' + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 渲染 LaTeX(供插件使用)
|
|
|
+ renderLatex: (latex) => {
|
|
|
+ symjaConsole.addOutput(latex, 'result', { renderLatex: true });
|
|
|
+ return '已渲染 LaTeX';
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ trackVariables(expression) {
|
|
|
+ // 简单的变量赋值检测
|
|
|
+ const match = expression.match(/^([a-zA-Z]\w*)\s*=/);
|
|
|
+ if (match) {
|
|
|
+ this.variables.add(match[1]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ clearVariable(varName) {
|
|
|
+ try {
|
|
|
+ const result = Android.clearVariable(varName);
|
|
|
+ this.addOutput(result, 'help');
|
|
|
+ this.variables.delete(varName);
|
|
|
+ } catch (e) {
|
|
|
+ this.addOutput(`❌ 清除失败: ${e.message}`, 'error');
|
|
|
+ }
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ showHistory() {
|
|
|
+ if (this.history.length === 0) {
|
|
|
+ this.addOutput('📜 历史记录为空', 'help');
|
|
|
+ } else {
|
|
|
+ const recent = this.history.slice(-10).reverse();
|
|
|
+ const historyText = recent.map((cmd, i) => `${recent.length - i}. ${cmd}`).join('\n');
|
|
|
+ this.addOutput(`📜 最近的命令:\n${historyText}`, 'help');
|
|
|
+ }
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ showFeedback() {
|
|
|
+ const email = 'nonvioce@qq.com';
|
|
|
+ const feedbackId = 'feedback-' + Date.now();
|
|
|
+
|
|
|
+ // 创建带复制按钮的反馈信息
|
|
|
+ const line = document.createElement('div');
|
|
|
+ line.className = 'terminal-line';
|
|
|
+ line.innerHTML = `
|
|
|
+ <div class="feedback-line">
|
|
|
+ <div class="feedback-icon">📧</div>
|
|
|
+ <div class="feedback-content">
|
|
|
+ <div class="feedback-title">反馈与建议</div>
|
|
|
+ <div class="feedback-text">
|
|
|
+ 欢迎通过邮件向我们反馈问题或提出建议!
|
|
|
+ </div>
|
|
|
+ <div class="feedback-email-container">
|
|
|
+ <span class="feedback-email">${email}</span>
|
|
|
+ <button class="feedback-copy-btn" onclick="symjaConsole.copyFeedbackEmail('${email}', this)">
|
|
|
+ 📋 复制邮箱
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ this.output.appendChild(line);
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ copyFeedbackEmail(email, button) {
|
|
|
+ // 创建临时文本区域
|
|
|
+ const textarea = document.createElement('textarea');
|
|
|
+ textarea.value = email;
|
|
|
+ textarea.style.position = 'fixed';
|
|
|
+ textarea.style.opacity = '0';
|
|
|
+ document.body.appendChild(textarea);
|
|
|
+ textarea.select();
|
|
|
+
|
|
|
+ try {
|
|
|
+ document.execCommand('copy');
|
|
|
+ button.textContent = '✅ 已复制';
|
|
|
+ button.style.background = '#4CAF50';
|
|
|
+ setTimeout(() => {
|
|
|
+ button.textContent = '📋 复制邮箱';
|
|
|
+ button.style.background = '';
|
|
|
+ }, 2000);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Copy failed:', e);
|
|
|
+ button.textContent = '❌ 复制失败';
|
|
|
+ setTimeout(() => {
|
|
|
+ button.textContent = '📋 复制邮箱';
|
|
|
+ }, 2000);
|
|
|
+ }
|
|
|
+
|
|
|
+ document.body.removeChild(textarea);
|
|
|
+ }
|
|
|
+
|
|
|
+ addToHistory(cmd) {
|
|
|
+ // 避免重复的连续命令
|
|
|
+ if (this.history.length === 0 || this.history[this.history.length - 1] !== cmd) {
|
|
|
+ this.history.push(cmd);
|
|
|
+ // 限制历史记录大小
|
|
|
+ if (this.history.length > 100) {
|
|
|
+ this.history.shift();
|
|
|
+ }
|
|
|
+ this.saveHistory();
|
|
|
+ }
|
|
|
+ this.historyIndex = this.history.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ loadHistory() {
|
|
|
+ try {
|
|
|
+ const saved = localStorage.getItem('symja_history');
|
|
|
+ return saved ? JSON.parse(saved) : [];
|
|
|
+ } catch (e) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ saveHistory() {
|
|
|
+ try {
|
|
|
+ localStorage.setItem('symja_history', JSON.stringify(this.history));
|
|
|
+ } catch (e) {
|
|
|
+ // 忽略存储错误
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ saveSession(name) {
|
|
|
+ try {
|
|
|
+ const session = {
|
|
|
+ history: this.history,
|
|
|
+ variables: Array.from(this.variables),
|
|
|
+ timestamp: new Date().toISOString()
|
|
|
+ };
|
|
|
+ localStorage.setItem(`symja_session_${name}`, JSON.stringify(session));
|
|
|
+ this.addOutput(`💾 会话已保存: ${name}`, 'help');
|
|
|
+ } catch (e) {
|
|
|
+ this.addOutput(`❌ 保存失败: ${e.message}`, 'error');
|
|
|
+ }
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ addOutput(text, type, options = {}) {
|
|
|
+ const line = document.createElement('div');
|
|
|
+ line.className = 'terminal-line';
|
|
|
+
|
|
|
+ if (type === 'input') {
|
|
|
+ // 命令输入显示(带计数器)
|
|
|
+ const count = options.count !== undefined ? options.count : '';
|
|
|
+ line.innerHTML = `
|
|
|
+ <div class="command-line">
|
|
|
+ <span class="command-prompt">[${count}]:</span>
|
|
|
+ <span class="command-text">${this.escapeHtml(text)}</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ } else if (type === 'result') {
|
|
|
+ const isError = text.startsWith('错误:');
|
|
|
+
|
|
|
+ // 检查是否需要 LaTeX 渲染
|
|
|
+ if (options.renderLatex && !isError) {
|
|
|
+ const count = options.count !== undefined ? options.count : '';
|
|
|
+ line.innerHTML = `
|
|
|
+ <div class="latex-line">
|
|
|
+ <div class="result-prompt">[${count}]:</div>
|
|
|
+ <div class="latex-content">
|
|
|
+ <div class="latex-rendered" id="latex-${Date.now()}"></div>
|
|
|
+ <div class="latex-source" style="display: none;">${this.escapeHtml(text)}</div>
|
|
|
+ <div class="latex-actions">
|
|
|
+ <button class="latex-btn latex-toggle" onclick="symjaConsole.toggleLatexSource(this)">显示源码</button>
|
|
|
+ <button class="latex-btn latex-copy" onclick="symjaConsole.copyLatex(this)">复制 LaTeX</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ this.output.appendChild(line);
|
|
|
+
|
|
|
+ // 渲染 LaTeX
|
|
|
+ const renderedDiv = line.querySelector('.latex-rendered');
|
|
|
+ this.renderLatex(text, renderedDiv);
|
|
|
+ } else {
|
|
|
+ const className = isError ? 'error-line' : 'result-line';
|
|
|
+ const count = options.count !== undefined ? options.count : '';
|
|
|
+ line.innerHTML = `
|
|
|
+ <div class="${className}">
|
|
|
+ <span class="result-prompt">[${count}]:</span>
|
|
|
+ <span class="result-text">${this.escapeHtml(text)}</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ } else if (type === 'help') {
|
|
|
+ line.innerHTML = `<div class="info-line">${this.escapeHtml(text)}</div>`;
|
|
|
+ } else if (type === 'error') {
|
|
|
+ line.innerHTML = `<div class="error-line">${this.escapeHtml(text)}</div>`;
|
|
|
+ } else if (type === 'plugin-info') {
|
|
|
+ // 插件的控制台输出(通过 Symja.print() 输出的内容)
|
|
|
+ line.innerHTML = `<div class="plugin-info-line">${this.escapeHtml(text)}</div>`;
|
|
|
+ } else if (type === 'plugin') {
|
|
|
+ // 插件的返回值(可运算结果)
|
|
|
+ line.innerHTML = `<div class="plugin-line">${this.escapeHtml(text)}</div>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.output.appendChild(line);
|
|
|
+ }
|
|
|
+
|
|
|
+ // LaTeX 渲染功能
|
|
|
+ renderLatex(latex, element) {
|
|
|
+ try {
|
|
|
+ // 等待 KaTeX 加载
|
|
|
+ if (typeof katex !== 'undefined') {
|
|
|
+ // 清理 LaTeX 字符串
|
|
|
+ let cleanLatex = latex.trim();
|
|
|
+
|
|
|
+ // 如果是 Symja 的 TeXForm 输出,可能需要一些转换
|
|
|
+ if (cleanLatex.startsWith('TeXForm:')) {
|
|
|
+ cleanLatex = cleanLatex.substring(8).trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用 KaTeX 渲染
|
|
|
+ katex.render(cleanLatex, element, {
|
|
|
+ throwOnError: false,
|
|
|
+ displayMode: true,
|
|
|
+ output: 'html',
|
|
|
+ trust: false,
|
|
|
+ strict: false
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // KaTeX 未加载,显示格式化的源码
|
|
|
+ element.style.fontFamily = 'var(--font-mono)';
|
|
|
+ element.style.fontSize = '14px';
|
|
|
+ element.style.padding = '12px';
|
|
|
+ element.style.background = '#F5F5F5';
|
|
|
+ element.style.borderRadius = '4px';
|
|
|
+ element.style.overflowX = 'auto';
|
|
|
+ element.textContent = latex;
|
|
|
+ console.warn('KaTeX not loaded, showing source code');
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 渲染失败,显示源码
|
|
|
+ element.style.fontFamily = 'var(--font-mono)';
|
|
|
+ element.style.fontSize = '14px';
|
|
|
+ element.style.padding = '12px';
|
|
|
+ element.style.background = '#F5F5F5';
|
|
|
+ element.style.borderRadius = '4px';
|
|
|
+ element.style.overflowX = 'auto';
|
|
|
+ element.textContent = latex;
|
|
|
+ console.error('LaTeX rendering error:', e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 切换显示 LaTeX 源码
|
|
|
+ toggleLatexSource(button) {
|
|
|
+ const parent = button.closest('.latex-line');
|
|
|
+ if (!parent) return;
|
|
|
+
|
|
|
+ const source = parent.querySelector('.latex-source');
|
|
|
+ if (!source) return;
|
|
|
+
|
|
|
+ if (source.style.display === 'none') {
|
|
|
+ source.style.display = 'block';
|
|
|
+ button.textContent = '隐藏源码';
|
|
|
+ } else {
|
|
|
+ source.style.display = 'none';
|
|
|
+ button.textContent = '显示源码';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 复制 LaTeX 代码
|
|
|
+ copyLatex(button) {
|
|
|
+ const parent = button.closest('.latex-line');
|
|
|
+ if (!parent) return;
|
|
|
+
|
|
|
+ const source = parent.querySelector('.latex-source');
|
|
|
+ if (!source) return;
|
|
|
+
|
|
|
+ const text = source.textContent;
|
|
|
+
|
|
|
+ // 创建临时文本区域
|
|
|
+ const textarea = document.createElement('textarea');
|
|
|
+ textarea.value = text;
|
|
|
+ textarea.style.position = 'fixed';
|
|
|
+ textarea.style.opacity = '0';
|
|
|
+ document.body.appendChild(textarea);
|
|
|
+ textarea.select();
|
|
|
+
|
|
|
+ try {
|
|
|
+ document.execCommand('copy');
|
|
|
+ button.textContent = '已复制!';
|
|
|
+ setTimeout(() => {
|
|
|
+ button.textContent = '复制 LaTeX';
|
|
|
+ }, 2000);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Copy failed:', e);
|
|
|
+ }
|
|
|
+
|
|
|
+ document.body.removeChild(textarea);
|
|
|
+ }
|
|
|
+
|
|
|
+ showHelp() {
|
|
|
+ const helpPage = document.getElementById('helpPage');
|
|
|
+ const helpContent = document.getElementById('helpContent');
|
|
|
+
|
|
|
+ if (!helpPage || !helpContent) return;
|
|
|
+
|
|
|
+ // 隐藏键盘
|
|
|
+ this.hideKeyboard();
|
|
|
+
|
|
|
+ helpContent.innerHTML = `
|
|
|
+ <table class="help-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>说明</th>
|
|
|
+ <th>命令</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr><td>求导</td><td>D[x^2, x]</td></tr>
|
|
|
+ <tr><td>复合函数求导</td><td>D[Sin[x]*Cos[x], x]</td></tr>
|
|
|
+ <tr><td>不定积分</td><td>Integrate[x^2, x]</td></tr>
|
|
|
+ <tr><td>定积分</td><td>Integrate[x^2, {x, 0, 1}]</td></tr>
|
|
|
+ <tr><td>求极限</td><td>Limit[Sin[x]/x, x -> 0]</td></tr>
|
|
|
+ <tr><td>泰勒展开</td><td>Series[Exp[x], {x, 0, 5}]</td></tr>
|
|
|
+ <tr><td>求和</td><td>Sum[i^2, {i, 1, 10}]</td></tr>
|
|
|
+ <tr><td>求积(阶乘)</td><td>Product[i, {i, 1, 5}]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>展开</td><td>Expand[(x+1)^3]</td></tr>
|
|
|
+ <tr><td>因式分解</td><td>Factor[x^2-4]</td></tr>
|
|
|
+ <tr><td>化简</td><td>Simplify[Sin[x]^2+Cos[x]^2]</td></tr>
|
|
|
+ <tr><td>通分</td><td>Together[(1/x)+(1/y)]</td></tr>
|
|
|
+ <tr><td>部分分式</td><td>Apart[(x+1)/(x^2-1), x]</td></tr>
|
|
|
+ <tr><td>约分</td><td>Cancel[(x^2-1)/(x-1)]</td></tr>
|
|
|
+ <tr><td>合并同类项</td><td>Collect[x^2+3*x+2*x^2, x]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>解方程</td><td>Solve[x^2-5*x+6==0, x]</td></tr>
|
|
|
+ <tr><td>方程组</td><td>Solve[{x+y==1, x-y==3}, {x,y}]</td></tr>
|
|
|
+ <tr><td>数值求解</td><td>NSolve[x^3-2*x+1==0, x]</td></tr>
|
|
|
+ <tr><td>求根</td><td>Roots[x^2-4==0, x]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>行列式</td><td>Det[{{1,2},{3,4}}]</td></tr>
|
|
|
+ <tr><td>逆矩阵</td><td>Inverse[{{1,2},{3,4}}]</td></tr>
|
|
|
+ <tr><td>转置</td><td>Transpose[{{1,2},{3,4}}]</td></tr>
|
|
|
+ <tr><td>特征值</td><td>Eigenvalues[{{1,2},{3,4}}]</td></tr>
|
|
|
+ <tr><td>特征向量</td><td>Eigenvectors[{{1,2},{3,4}}]</td></tr>
|
|
|
+ <tr><td>矩阵秩</td><td>MatrixRank[{{1,2},{2,4}}]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>最大公约数</td><td>GCD[12, 18]</td></tr>
|
|
|
+ <tr><td>最小公倍数</td><td>LCM[12, 18]</td></tr>
|
|
|
+ <tr><td>质因数分解</td><td>FactorInteger[60]</td></tr>
|
|
|
+ <tr><td>判断素数</td><td>PrimeQ[17]</td></tr>
|
|
|
+ <tr><td>第 n 个素数</td><td>Prime[10]</td></tr>
|
|
|
+ <tr><td>斐波那契数</td><td>Fibonacci[10]</td></tr>
|
|
|
+ <tr><td>组合数 C(n,k)</td><td>Binomial[5, 2]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>高精度计算(50位)</td><td>N[Pi, 50]</td></tr>
|
|
|
+ <tr><td>数值化</td><td>N[Sqrt[2]]</td></tr>
|
|
|
+ <tr><td>四舍五入</td><td>Round[3.7]</td></tr>
|
|
|
+ <tr><td>向下取整</td><td>Floor[3.7]</td></tr>
|
|
|
+ <tr><td>向上取整</td><td>Ceiling[3.2]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>三角函数</td><td>Sin[x], Cos[x], Tan[x]</td></tr>
|
|
|
+ <tr><td>反三角函数</td><td>ArcSin[x], ArcCos[x], ArcTan[x]</td></tr>
|
|
|
+ <tr><td>双曲函数</td><td>Sinh[x], Cosh[x], Tanh[x]</td></tr>
|
|
|
+ <tr><td>三角展开</td><td>TrigExpand[Sin[2*x]]</td></tr>
|
|
|
+ <tr><td>三角化简</td><td>TrigReduce[Sin[x]^2]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>实部</td><td>Re[3+4*I]</td></tr>
|
|
|
+ <tr><td>虚部</td><td>Im[3+4*I]</td></tr>
|
|
|
+ <tr><td>模</td><td>Abs[3+4*I]</td></tr>
|
|
|
+ <tr><td>辐角</td><td>Arg[3+4*I]</td></tr>
|
|
|
+ <tr><td>共轭</td><td>Conjugate[3+4*I]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>生成表格</td><td>Table[i^2, {i, 1, 10}]</td></tr>
|
|
|
+ <tr><td>生成数列</td><td>Range[1, 10]</td></tr>
|
|
|
+ <tr><td>带步长的数列</td><td>Range[0, 1, 0.1]</td></tr>
|
|
|
+ <tr><td>列表长度</td><td>Length[{1,2,3,4}]</td></tr>
|
|
|
+ <tr><td>第一个元素</td><td>First[{1,2,3}]</td></tr>
|
|
|
+ <tr><td>最后一个元素</td><td>Last[{1,2,3}]</td></tr>
|
|
|
+ <tr><td>取前 n 个</td><td>Take[{1,2,3,4,5}, 3]</td></tr>
|
|
|
+ <tr><td>删除前 n 个</td><td>Drop[{1,2,3,4,5}, 2]</td></tr>
|
|
|
+ <tr><td>连接列表</td><td>Join[{1,2}, {3,4}]</td></tr>
|
|
|
+ <tr><td>反转列表</td><td>Reverse[{1,2,3}]</td></tr>
|
|
|
+ <tr><td>排序</td><td>Sort[{3,1,4,1,5}]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>替换规则</td><td>b+c /. b->1</td></tr>
|
|
|
+ <tr><td>变量替换</td><td>x^2+2*x+1 /. x->3</td></tr>
|
|
|
+ <tr><td>多重替换</td><td>expr /. {a->1, b->2}</td></tr>
|
|
|
+
|
|
|
+ <tr><td>微分方程</td><td>DSolve[y'[x]==y[x], y[x], x]</td></tr>
|
|
|
+ <tr><td>递推方程</td><td>RSolve[a[n]==2*a[n-1]+1, a[n], n]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>拉普拉斯变换</td><td>LaplaceTransform[Sin[t], t, s]</td></tr>
|
|
|
+ <tr><td>逆拉普拉斯变换</td><td>InverseLaplaceTransform[1/(s^2+1), s, t]</td></tr>
|
|
|
+ <tr><td>傅里叶变换</td><td>FourierTransform[Exp[-x^2], x, w]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>求最大值</td><td>Maximize[x^2-4*x+3, x]</td></tr>
|
|
|
+ <tr><td>求最小值</td><td>Minimize[x^2-4*x+3, x]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>平均值</td><td>Mean[{1,2,3,4,5}]</td></tr>
|
|
|
+ <tr><td>中位数</td><td>Median[{1,2,3,4,5}]</td></tr>
|
|
|
+ <tr><td>方差</td><td>Variance[{1,2,3,4,5}]</td></tr>
|
|
|
+ <tr><td>标准差</td><td>StandardDeviation[{1,2,3,4,5}]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>LaTeX 格式</td><td>TeXForm[x^2 + 2*x + 1]</td></tr>
|
|
|
+ <tr><td>完整形式</td><td>FullForm[x+y]</td></tr>
|
|
|
+ <tr><td>输入形式</td><td>InputForm[expr]</td></tr>
|
|
|
+
|
|
|
+ <tr><td>显示帮助</td><td>help 或 ?</td></tr>
|
|
|
+ <tr><td>清空输出</td><td>clear 或 cls</td></tr>
|
|
|
+ <tr><td>命令历史</td><td>history</td></tr>
|
|
|
+ <tr><td>清除变量</td><td>clear <变量></td></tr>
|
|
|
+ <tr><td>插件管理</td><td>plugins</td></tr>
|
|
|
+ <tr><td>显示反馈邮箱</td><td>feedback</td></tr>
|
|
|
+
|
|
|
+ <tr><td>圆周率 π</td><td>Pi</td></tr>
|
|
|
+ <tr><td>自然常数 e</td><td>E</td></tr>
|
|
|
+ <tr><td>虚数单位 i</td><td>I</td></tr>
|
|
|
+ <tr><td>无穷大 ∞</td><td>Infinity</td></tr>
|
|
|
+ <tr><td>布尔值</td><td>True, False</td></tr>
|
|
|
+ <tr><td>上一次的输出结果</td><td>%</td></tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ `;
|
|
|
+
|
|
|
+ helpPage.classList.add('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ hideHelp() {
|
|
|
+ const helpPage = document.getElementById('helpPage');
|
|
|
+ if (helpPage) {
|
|
|
+ helpPage.classList.remove('show');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ showSettings() {
|
|
|
+ // 设置功能待实现
|
|
|
+ this.addOutput('⚙️ 设置功能即将推出...', 'info');
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+
|
|
|
+ clear() {
|
|
|
+ this.output.innerHTML = '';
|
|
|
+ this.showWelcome();
|
|
|
+ // 重置计数器
|
|
|
+ this.executionCount = 1;
|
|
|
+ this.saveExecutionCount();
|
|
|
+ this.updatePrompt();
|
|
|
+ }
|
|
|
+
|
|
|
+ reset() {
|
|
|
+ try {
|
|
|
+ const result = Android.reset();
|
|
|
+ this.variables.clear();
|
|
|
+ this.addOutput('🔄 ' + result, 'help');
|
|
|
+ this.scrollToBottom();
|
|
|
+ // 重置计数器
|
|
|
+ this.executionCount = 1;
|
|
|
+ this.saveExecutionCount();
|
|
|
+ this.updatePrompt();
|
|
|
+ } catch (e) {
|
|
|
+ this.addOutput('错误: 无法重置计算器', 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ updatePrompt() {
|
|
|
+ if (this.inputPrompt) {
|
|
|
+ this.inputPrompt.textContent = `[${this.executionCount}]:`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ loadExecutionCount() {
|
|
|
+ // 每次打开应用都从 1 开始
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ saveExecutionCount() {
|
|
|
+ // 不再保存到 localStorage,每次打开应用都重置
|
|
|
+ }
|
|
|
+
|
|
|
+ navigateHistory(direction) {
|
|
|
+ if (this.history.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存当前输入
|
|
|
+ if (this.historyIndex === this.history.length) {
|
|
|
+ this.tempInput = this.input.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.historyIndex += direction;
|
|
|
+
|
|
|
+ if (this.historyIndex < 0) {
|
|
|
+ this.historyIndex = 0;
|
|
|
+ } else if (this.historyIndex >= this.history.length) {
|
|
|
+ this.historyIndex = this.history.length;
|
|
|
+ this.input.value = this.tempInput;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.input.value = this.history[this.historyIndex];
|
|
|
+ // 光标移到末尾
|
|
|
+ setTimeout(() => {
|
|
|
+ this.input.setSelectionRange(this.input.value.length, this.input.value.length);
|
|
|
+ }, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 自动补全功能
|
|
|
+ updateCompletionList() {
|
|
|
+ const value = this.input.value;
|
|
|
+ const cursorPos = this.input.selectionStart;
|
|
|
+ const textBeforeCursor = value.substring(0, cursorPos);
|
|
|
+ const match = textBeforeCursor.match(/[A-Za-z0-9_-]+$/);
|
|
|
+
|
|
|
+ console.log('updateCompletionList:', { value, textBeforeCursor, match });
|
|
|
+
|
|
|
+ if (match && match[0].length >= 2) {
|
|
|
+ const prefix = match[0];
|
|
|
+ console.log('Searching completions for prefix:', prefix);
|
|
|
+
|
|
|
+ // 特殊命令列表
|
|
|
+ const specialCommands = ['help', 'clear', 'cls', 'history', 'plugins', 'feedback'];
|
|
|
+
|
|
|
+ // 获取插件命令列表
|
|
|
+ const pluginCommands = this.plugins
|
|
|
+ .filter(p => p.command && p.command.length > 0)
|
|
|
+ .map(p => p.command);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const completionsJson = Android.getCompletions(prefix);
|
|
|
+ console.log('Raw completions JSON:', completionsJson);
|
|
|
+
|
|
|
+ // 解析补全结果 - 可能是逗号分隔的字符串或 JSON 数组
|
|
|
+ let symjaCompletions = [];
|
|
|
+ if (completionsJson.startsWith('[')) {
|
|
|
+ // JSON 数组格式
|
|
|
+ symjaCompletions = JSON.parse(completionsJson);
|
|
|
+ } else {
|
|
|
+ // 逗号分隔的字符串格式
|
|
|
+ symjaCompletions = completionsJson.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加匹配的特殊命令
|
|
|
+ const lowerPrefix = prefix.toLowerCase();
|
|
|
+ const matchingCommands = specialCommands.filter(cmd => cmd.startsWith(lowerPrefix));
|
|
|
+
|
|
|
+ // 添加匹配的插件命令
|
|
|
+ const matchingPluginCommands = pluginCommands.filter(cmd => cmd.toLowerCase().startsWith(lowerPrefix));
|
|
|
+
|
|
|
+ // 合并并去重(插件命令优先显示)
|
|
|
+ this.completions = [...new Set([...matchingPluginCommands, ...matchingCommands, ...symjaCompletions])];
|
|
|
+
|
|
|
+ console.log('Parsed completions:', this.completions);
|
|
|
+
|
|
|
+ if (this.completions.length > 0) {
|
|
|
+ this.showCompletionList();
|
|
|
+ } else {
|
|
|
+ this.hideCompletionList();
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Completion error:', e);
|
|
|
+ this.completions = [];
|
|
|
+ this.hideCompletionList();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.completions = [];
|
|
|
+ this.hideCompletionList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ showCompletionList() {
|
|
|
+ console.log('showCompletionList called');
|
|
|
+ console.log('completionList element:', this.completionList);
|
|
|
+ console.log('completions:', this.completions);
|
|
|
+
|
|
|
+ if (!this.completionList) {
|
|
|
+ console.error('completionList element not found');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.completions.length === 0) {
|
|
|
+ console.log('No completions to show');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.completionList.innerHTML = '';
|
|
|
+
|
|
|
+ // 最多显示 8 个补全项
|
|
|
+ const itemsToShow = this.completions.slice(0, 8);
|
|
|
+ console.log('Items to show:', itemsToShow);
|
|
|
+
|
|
|
+ itemsToShow.forEach(completion => {
|
|
|
+ const item = document.createElement('div');
|
|
|
+ item.className = 'completion-item';
|
|
|
+
|
|
|
+ // 检查是否是插件命令
|
|
|
+ const plugin = this.plugins.find(p => p.command === completion);
|
|
|
+ if (plugin) {
|
|
|
+ // 插件命令,显示图标和名称
|
|
|
+ item.innerHTML = `<span style="margin-right: 8px;">${plugin.icon}</span>${completion} <span style="color: #999; font-size: 12px; margin-left: 8px;">(${plugin.name})</span>`;
|
|
|
+ } else {
|
|
|
+ // 普通命令或函数
|
|
|
+ item.textContent = completion;
|
|
|
+ }
|
|
|
+
|
|
|
+ item.addEventListener('click', () => {
|
|
|
+ this.applyCompletion(completion);
|
|
|
+ });
|
|
|
+ this.completionList.appendChild(item);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.completionList.classList.add('show');
|
|
|
+ console.log('Completion list should now be visible');
|
|
|
+ }
|
|
|
+
|
|
|
+ hideCompletionList() {
|
|
|
+ if (this.completionList) {
|
|
|
+ this.completionList.classList.remove('show');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ applyCompletion(completion) {
|
|
|
+ const value = this.input.value;
|
|
|
+ const cursorPos = this.input.selectionStart;
|
|
|
+ const textBeforeCursor = value.substring(0, cursorPos);
|
|
|
+ const textAfterCursor = value.substring(cursorPos);
|
|
|
+
|
|
|
+ const match = textBeforeCursor.match(/[A-Za-z0-9_-]+$/);
|
|
|
+ if (match) {
|
|
|
+ const prefix = match[0];
|
|
|
+ const beforePrefix = textBeforeCursor.substring(0, textBeforeCursor.length - prefix.length);
|
|
|
+
|
|
|
+ // 检查是否是插件命令
|
|
|
+ const plugin = this.plugins.find(p => p.command === completion);
|
|
|
+ if (plugin && plugin.inputs && plugin.inputs.length > 0) {
|
|
|
+ // 插件命令,添加参数括号和提示
|
|
|
+ const paramHints = plugin.inputs.map(input => input.name).join(', ');
|
|
|
+ this.input.value = beforePrefix + completion + '[' + paramHints + ']' + textAfterCursor;
|
|
|
+
|
|
|
+ // 将光标放在第一个参数位置
|
|
|
+ const newCursorPos = beforePrefix.length + completion.length + 1;
|
|
|
+ this.input.setSelectionRange(newCursorPos, newCursorPos + plugin.inputs[0].name.length);
|
|
|
+ } else {
|
|
|
+ // 普通命令或函数
|
|
|
+ this.input.value = beforePrefix + completion + textAfterCursor;
|
|
|
+
|
|
|
+ const newCursorPos = beforePrefix.length + completion.length;
|
|
|
+ this.input.setSelectionRange(newCursorPos, newCursorPos);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.hideCompletionList();
|
|
|
+ this.input.focus();
|
|
|
+ }
|
|
|
+
|
|
|
+ handleAutoComplete() {
|
|
|
+ // 保留这个方法以防其他地方调用,但不再使用
|
|
|
+ this.updateCompletionList();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 插件相关功能
|
|
|
+ loadPlugins() {
|
|
|
+ try {
|
|
|
+ const pluginsJson = Android.getPlugins();
|
|
|
+ console.log('Loaded plugins JSON:', pluginsJson);
|
|
|
+ this.plugins = JSON.parse(pluginsJson);
|
|
|
+ console.log('Parsed plugins count:', this.plugins.length);
|
|
|
+ console.log('Plugins:', this.plugins);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Failed to load plugins:', e);
|
|
|
+ this.plugins = [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ showPluginPanel() {
|
|
|
+ this.loadPlugins();
|
|
|
+ const panel = document.getElementById('pluginPanel');
|
|
|
+
|
|
|
+ if (!panel) return;
|
|
|
+
|
|
|
+ // 隐藏键盘
|
|
|
+ this.hideKeyboard();
|
|
|
+
|
|
|
+ // 确保默认显示插件列表标签
|
|
|
+ const tabs = panel.querySelectorAll('.plugin-tab');
|
|
|
+ const contents = panel.querySelectorAll('.tab-content');
|
|
|
+
|
|
|
+ tabs.forEach(tab => {
|
|
|
+ if (tab.dataset.tab === 'list') {
|
|
|
+ tab.classList.add('active');
|
|
|
+ } else {
|
|
|
+ tab.classList.remove('active');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ contents.forEach(content => {
|
|
|
+ if (content.dataset.content === 'list') {
|
|
|
+ content.classList.add('active');
|
|
|
+ } else {
|
|
|
+ content.classList.remove('active');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新插件下拉列表(用于分享和删除)
|
|
|
+ this.updatePluginSelects();
|
|
|
+
|
|
|
+ // 使用统一的刷新方法
|
|
|
+ this.refreshPluginList();
|
|
|
+
|
|
|
+ panel.classList.add('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ hidePluginPanel() {
|
|
|
+ const panel = document.getElementById('pluginPanel');
|
|
|
+ if (panel) panel.classList.remove('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ openPlugin(plugin) {
|
|
|
+ this.currentPlugin = plugin;
|
|
|
+ this.hidePluginPanel();
|
|
|
+
|
|
|
+ const dialog = document.getElementById('pluginDialog');
|
|
|
+ const title = document.getElementById('pluginDialogTitle');
|
|
|
+ const desc = document.getElementById('pluginDialogDesc');
|
|
|
+ const inputs = document.getElementById('pluginInputs');
|
|
|
+
|
|
|
+ if (!dialog || !title || !desc || !inputs) return;
|
|
|
+
|
|
|
+ title.textContent = plugin.icon + ' ' + plugin.name;
|
|
|
+ desc.textContent = plugin.description;
|
|
|
+
|
|
|
+ inputs.innerHTML = '';
|
|
|
+ plugin.inputs.forEach(input => {
|
|
|
+ const inputGroup = document.createElement('div');
|
|
|
+ inputGroup.className = 'input-group';
|
|
|
+ inputGroup.innerHTML = `
|
|
|
+ <label>${input.label}</label>
|
|
|
+ <input type="text"
|
|
|
+ name="${input.name}"
|
|
|
+ value="${input.defaultValue || ''}"
|
|
|
+ placeholder="${input.placeholder || ''}">
|
|
|
+ `;
|
|
|
+ inputs.appendChild(inputGroup);
|
|
|
+ });
|
|
|
+
|
|
|
+ dialog.classList.add('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ hidePluginDialog() {
|
|
|
+ const dialog = document.getElementById('pluginDialog');
|
|
|
+ if (dialog) dialog.classList.remove('show');
|
|
|
+ this.currentPlugin = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ executeCurrentPlugin() {
|
|
|
+ if (!this.currentPlugin) return;
|
|
|
+
|
|
|
+ const inputsDiv = document.getElementById('pluginInputs');
|
|
|
+ if (!inputsDiv) return;
|
|
|
+
|
|
|
+ const inputElements = inputsDiv.querySelectorAll('input');
|
|
|
+ const inputValues = {};
|
|
|
+
|
|
|
+ // 收集输入值并转换为 Symja 可运算类型
|
|
|
+ inputElements.forEach(input => {
|
|
|
+ inputValues[input.name] = this.convertInputToSymjaType(input.value);
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 用于存储插件的控制台输出
|
|
|
+ const pluginOutputs = [];
|
|
|
+
|
|
|
+ // 创建 Symja API 对象供插件使用
|
|
|
+ const symjaAPI = this.createSymjaAPI(pluginOutputs);
|
|
|
+
|
|
|
+ // 执行插件代码,传入 inputs 和 Symja API
|
|
|
+ const func = new Function('inputs', 'Symja', this.currentPlugin.jsCode + '\nreturn calculate(inputs, Symja);');
|
|
|
+ const result = func(inputValues, symjaAPI);
|
|
|
+
|
|
|
+ // 验证返回值是否为有效的可运算类型
|
|
|
+ if (typeof result !== 'string' || result.trim() === '') {
|
|
|
+ throw new Error('插件必须返回非空字符串类型的 Symja 表达式');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证返回值是否是有效的 Symja 表达式(尝试计算)
|
|
|
+ try {
|
|
|
+ Android.symjaEval(result);
|
|
|
+ } catch (e) {
|
|
|
+ throw new Error('插件返回值必须是有效的 Symja 表达式,当前返回: ' + result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示插件名称
|
|
|
+ this.addOutput(`${this.currentPlugin.icon} ${this.currentPlugin.name}`, 'input');
|
|
|
+
|
|
|
+ // 显示插件的控制台输出(如果有)
|
|
|
+ if (pluginOutputs.length > 0) {
|
|
|
+ pluginOutputs.forEach(output => {
|
|
|
+ this.addOutput(output, 'plugin-info');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示插件的返回值(可运算结果)
|
|
|
+ this.addOutput(result, 'plugin');
|
|
|
+
|
|
|
+ // 将插件结果保存到 Symja 引擎,使其可以通过 % 引用
|
|
|
+ try {
|
|
|
+ Android.evaluate(`Out = ${result}`);
|
|
|
+ } catch (e) {
|
|
|
+ console.log('Plugin result storage error:', e);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.scrollToBottom();
|
|
|
+ this.hidePluginDialog();
|
|
|
+ } catch (e) {
|
|
|
+ this.addOutput(`插件执行错误: ${e.message}`, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ scrollToBottom() {
|
|
|
+ this.output.scrollTop = this.output.scrollHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ escapeHtml(text) {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.textContent = text;
|
|
|
+ return div.innerHTML;
|
|
|
+ }
|
|
|
+
|
|
|
+ hideKeyboard() {
|
|
|
+ // 移除输入框焦点
|
|
|
+ if (this.input) {
|
|
|
+ this.input.blur();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用 Android 接口隐藏键盘
|
|
|
+ try {
|
|
|
+ if (typeof Android !== 'undefined' && Android.hideKeyboard) {
|
|
|
+ Android.hideKeyboard();
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.log('hideKeyboard not available:', e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化控制台
|
|
|
+let symjaConsole;
|
|
|
+document.addEventListener('DOMContentLoaded', () => {
|
|
|
+ symjaConsole = new SymjaConsole();
|
|
|
+});
|