diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8b5a742..aff5f20 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,5 +32,5 @@ jobs: - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: - github_token: ${{ secrets.GH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/dist \ No newline at end of file diff --git a/README.md b/README.md index 5f3b369..963065a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发 - **Clojure** - **Go** - **Java** +- **JavaScript (Browser)** +- **JavaScript (jQuery)** - **JavaScript (Node.js)** - **Kotlin** - **Node.js** diff --git a/public/icons/javascript-browser.svg b/public/icons/javascript-browser.svg new file mode 100644 index 0000000..1191bb8 --- /dev/null +++ b/public/icons/javascript-browser.svg @@ -0,0 +1,11 @@ + + + + + + Browser + + + + + \ No newline at end of file diff --git a/public/icons/javascript-jquery.svg b/public/icons/javascript-jquery.svg new file mode 100644 index 0000000..1fa295f --- /dev/null +++ b/public/icons/javascript-jquery.svg @@ -0,0 +1,11 @@ + + + + + + jQuery + + + + + \ No newline at end of file diff --git a/src-tauri/src/examples/javascript-browser.js b/src-tauri/src/examples/javascript-browser.js new file mode 100644 index 0000000..dd66904 --- /dev/null +++ b/src-tauri/src/examples/javascript-browser.js @@ -0,0 +1,264 @@ +// JavaScript (Browser) 示例代码 - CodeForge 代码执行环境 + +console.log('🎉 欢迎使用 CodeForge!'); +console.log('Welcome to CodeForge!'); +console.log(''); + +console.log('========================================='); +console.log(' CodeForge JavaScript '); +console.log('========================================='); +console.log(''); + +// 基本输出示例 +console.log('✅ JavaScript 运行成功! (JavaScript is working!)'); +console.log('🌟 这是 Node.js 脚本 (This is Node.js script)'); +console.log(''); + +// 变量操作 +const name = 'CodeForge'; +const version = 'JavaScript'; +const number1 = 10; +const number2 = 20; +const result = number1 + number2; + +console.log('🔢 简单计算 (Simple calculation):'); +console.log(`${number1} + ${number2} = ${result}`); +console.log(''); + +// 字符串操作 +console.log('📝 字符串操作 (String operations):'); +console.log(`平台名称 (Platform): ${name}`); +console.log(`语言版本 (Language): ${version}`); +console.log(`完整信息 (Full info): ${name} - ${version}`); +console.log(''); + +// 循环示例 +console.log('🔄 循环输出 (Loop output):'); +for (let i = 1; i <= 5; i++) { + console.log(`第 ${i} 次输出 (Output #${i}): Hello from CodeForge!`); +} +console.log(''); + +// 数组操作 +const fruits = ['苹果', '香蕉', '橙子', '葡萄']; +console.log('🍎 水果列表 (Fruit list):'); +fruits.forEach((fruit, index) => { + console.log(`${index + 1}. ${fruit}`); +}); +console.log(''); + +// 条件判断 +const score = 85; +console.log('📊 成绩评估 (Score evaluation):'); +if (score >= 90) { + console.log('优秀! (Excellent!)'); +} else if (score >= 80) { + console.log('良好! (Good!)'); +} else if (score >= 60) { + console.log('及格 (Pass)'); +} else { + console.log('需要努力 (Need improvement)'); +} +console.log(''); + +// undefined 和 null 示例 +console.log('🔍 undefined/null 示例 (undefined/null example):'); +let optionalValue = 42; +if (optionalValue !== undefined && optionalValue !== null) { + console.log(`可选值: ${optionalValue} (Optional value: ${optionalValue})`); +} else { + console.log('值为空 (Value is undefined/null)'); +} +console.log(''); + +// 函数示例 +function greetUser(username) { + return `Hello, ${username}! 👋`; +} + +console.log('🎭 函数示例 (Function example):'); +const greeting = greetUser('CodeForge用户'); +console.log(greeting); +console.log(''); + +// 箭头函数示例 +const addNumbers = (a, b) => a + b; +const multiplyNumbers = (a, b) => { + return a * b; +}; + +console.log('⚡ 箭头函数示例 (Arrow function example):'); +console.log(`5 + 3 = ${addNumbers(5, 3)}`); +console.log(`6 × 7 = ${multiplyNumbers(6, 7)}`); +console.log(''); + +// 数组高阶函数示例 +console.log('🔧 数组高阶函数示例 (Array higher-order functions):'); +const numbers = Array.from({length: 10}, (_, i) => i + 1); +const evenNumbers = numbers.filter(num => num % 2 === 0); +const doubled = numbers.map(num => num * 2); +const sum = numbers.reduce((acc, num) => acc + num, 0); + +console.log(`原始数字 (Original): ${numbers.join(', ')}`); +console.log(`偶数 (Even numbers): ${evenNumbers.join(', ')}`); +console.log(`翻倍 (Doubled): ${doubled.join(', ')}`); +console.log(`总和 (Sum): ${sum}`); +console.log(''); + +// 对象操作示例 +console.log('👤 对象示例 (Object example):'); +const person = { + name: '张三', + age: 25, + city: '北京', + introduce() { + return `我是${this.name},今年${this.age}岁,住在${this.city}`; + } +}; + +console.log(`姓名: ${person.name}, 年龄: ${person.age}, 城市: ${person.city}`); +console.log(person.introduce()); +console.log(''); + +// 解构赋值示例 +console.log('🔍 解构赋值示例 (Destructuring assignment):'); +const {name: personName, age: personAge} = person; +const [first, second, ...rest] = fruits; + +console.log(`解构对象 (Destructured object): ${personName}, ${personAge}`); +console.log(`解构数组 (Destructured array): ${first}, ${second}, 其他: [${rest.join(', ')}]`); +console.log(''); + +// Promise 示例 +console.log('🎯 Promise 示例 (Promise example):'); +const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + +// 使用 Promise +delay(100) + .then(() => { + console.log('Promise 已完成! (Promise completed!)'); + return '异步操作结果'; + }) + .then(result => { + console.log(`异步结果: ${result}`); + }) + .catch(error => { + console.error('Promise 错误:', error); + }); + +// async/await 示例 +async function asyncExample() { + try { + console.log('⏳ 开始异步操作...'); + await delay(50); + console.log('✅ async/await 完成!'); + return '异步函数结果'; + } catch (error) { + console.error('async/await 错误:', error); + } +} + +// 立即执行异步函数 +(async () => { + const result = await asyncExample(); + console.log(`异步函数返回: ${result}`); + console.log(''); +})(); + +// 类和继承示例 +console.log('🏗️ 类示例 (Class example):'); +class Animal { + constructor(name, type) { + this.name = name; + this.type = type; + } + + speak() { + return `${this.name} 发出声音`; + } +} + +class Dog extends Animal { + constructor(name) { + super(name, '狗'); + } + + speak() { + return `${this.name} 汪汪叫`; + } +} + +const dog = new Dog('小黄'); +console.log(dog.speak()); +console.log(''); + +// 模块和导出示例(注释版本,因为这是单文件) +console.log('📦 模块概念示例 (Module concept example):'); +// export const utilities = { +// formatDate: (date) => date.toLocaleDateString('zh-CN'), +// randomNumber: () => Math.floor(Math.random() * 100) +// }; + +const utilities = { + formatDate: (date) => date.toLocaleDateString('zh-CN'), + randomNumber: () => Math.floor(Math.random() * 100) +}; + +console.log(`当前日期: ${utilities.formatDate(new Date())}`); +console.log(`随机数: ${utilities.randomNumber()}`); +console.log(''); + +// JSON 操作示例 +console.log('📄 JSON 操作示例 (JSON operations):'); +const data = { + users: [ + {id: 1, name: 'Alice', active: true}, + {id: 2, name: 'Bob', active: false}, + {id: 3, name: 'Charlie', active: true} + ] +}; + +const jsonString = JSON.stringify(data, null, 2); +console.log('JSON 字符串:'); +console.log(jsonString); + +const parsedData = JSON.parse(jsonString); +const activeUsers = parsedData.users.filter(user => user.active); +console.log(`活跃用户: ${activeUsers.map(u => u.name).join(', ')}`); +console.log(''); + +// 错误处理示例 +console.log('🚨 错误处理示例 (Error handling):'); +try { + const riskyOperation = () => { + const random = Math.random(); + if (random < 0.5) { + throw new Error('随机错误发生了!'); + } + return '操作成功!'; + }; + + const result2 = riskyOperation(); + console.log(result2); +} catch (error) { + console.log(`捕获错误: ${error.message}`); +} finally { + console.log('错误处理完成'); +} +console.log(''); + +// 正则表达式示例 +console.log('🔤 正则表达式示例 (Regular expressions):'); +const text = 'CodeForge 是一个很棒的代码执行环境! Email: contact@codeforge.com'; +const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; +const emails = text.match(emailRegex); + +console.log(`文本: ${text}`); +console.log(`找到的邮箱: ${emails ? emails.join(', ') : '无'}`); +console.log(''); + +console.log('🎯 CodeForge JavaScript 代码执行完成!'); +console.log('🎯 CodeForge JavaScript execution completed!'); +console.log(''); +console.log('感谢使用 CodeForge 代码执行环境! 🚀'); +console.log('Thank you for using CodeForge! 🚀'); \ No newline at end of file diff --git a/src-tauri/src/examples/javascript-jquery.js b/src-tauri/src/examples/javascript-jquery.js new file mode 100644 index 0000000..47010aa --- /dev/null +++ b/src-tauri/src/examples/javascript-jquery.js @@ -0,0 +1,264 @@ +// JavaScript (jQuery) 示例代码 - CodeForge 代码执行环境 + +console.log('🎉 欢迎使用 CodeForge!'); +console.log('Welcome to CodeForge!'); +console.log(''); + +console.log('========================================='); +console.log(' CodeForge JavaScript '); +console.log('========================================='); +console.log(''); + +// 基本输出示例 +console.log('✅ JavaScript 运行成功! (JavaScript is working!)'); +console.log('🌟 这是 Node.js 脚本 (This is Node.js script)'); +console.log(''); + +// 变量操作 +const name = 'CodeForge'; +const version = 'JavaScript'; +const number1 = 10; +const number2 = 20; +const result = number1 + number2; + +console.log('🔢 简单计算 (Simple calculation):'); +console.log(`${number1} + ${number2} = ${result}`); +console.log(''); + +// 字符串操作 +console.log('📝 字符串操作 (String operations):'); +console.log(`平台名称 (Platform): ${name}`); +console.log(`语言版本 (Language): ${version}`); +console.log(`完整信息 (Full info): ${name} - ${version}`); +console.log(''); + +// 循环示例 +console.log('🔄 循环输出 (Loop output):'); +for (let i = 1; i <= 5; i++) { + console.log(`第 ${i} 次输出 (Output #${i}): Hello from CodeForge!`); +} +console.log(''); + +// 数组操作 +const fruits = ['苹果', '香蕉', '橙子', '葡萄']; +console.log('🍎 水果列表 (Fruit list):'); +fruits.forEach((fruit, index) => { + console.log(`${index + 1}. ${fruit}`); +}); +console.log(''); + +// 条件判断 +const score = 85; +console.log('📊 成绩评估 (Score evaluation):'); +if (score >= 90) { + console.log('优秀! (Excellent!)'); +} else if (score >= 80) { + console.log('良好! (Good!)'); +} else if (score >= 60) { + console.log('及格 (Pass)'); +} else { + console.log('需要努力 (Need improvement)'); +} +console.log(''); + +// undefined 和 null 示例 +console.log('🔍 undefined/null 示例 (undefined/null example):'); +let optionalValue = 42; +if (optionalValue !== undefined && optionalValue !== null) { + console.log(`可选值: ${optionalValue} (Optional value: ${optionalValue})`); +} else { + console.log('值为空 (Value is undefined/null)'); +} +console.log(''); + +// 函数示例 +function greetUser(username) { + return `Hello, ${username}! 👋`; +} + +console.log('🎭 函数示例 (Function example):'); +const greeting = greetUser('CodeForge用户'); +console.log(greeting); +console.log(''); + +// 箭头函数示例 +const addNumbers = (a, b) => a + b; +const multiplyNumbers = (a, b) => { + return a * b; +}; + +console.log('⚡ 箭头函数示例 (Arrow function example):'); +console.log(`5 + 3 = ${addNumbers(5, 3)}`); +console.log(`6 × 7 = ${multiplyNumbers(6, 7)}`); +console.log(''); + +// 数组高阶函数示例 +console.log('🔧 数组高阶函数示例 (Array higher-order functions):'); +const numbers = Array.from({length: 10}, (_, i) => i + 1); +const evenNumbers = numbers.filter(num => num % 2 === 0); +const doubled = numbers.map(num => num * 2); +const sum = numbers.reduce((acc, num) => acc + num, 0); + +console.log(`原始数字 (Original): ${numbers.join(', ')}`); +console.log(`偶数 (Even numbers): ${evenNumbers.join(', ')}`); +console.log(`翻倍 (Doubled): ${doubled.join(', ')}`); +console.log(`总和 (Sum): ${sum}`); +console.log(''); + +// 对象操作示例 +console.log('👤 对象示例 (Object example):'); +const person = { + name: '张三', + age: 25, + city: '北京', + introduce() { + return `我是${this.name},今年${this.age}岁,住在${this.city}`; + } +}; + +console.log(`姓名: ${person.name}, 年龄: ${person.age}, 城市: ${person.city}`); +console.log(person.introduce()); +console.log(''); + +// 解构赋值示例 +console.log('🔍 解构赋值示例 (Destructuring assignment):'); +const {name: personName, age: personAge} = person; +const [first, second, ...rest] = fruits; + +console.log(`解构对象 (Destructured object): ${personName}, ${personAge}`); +console.log(`解构数组 (Destructured array): ${first}, ${second}, 其他: [${rest.join(', ')}]`); +console.log(''); + +// Promise 示例 +console.log('🎯 Promise 示例 (Promise example):'); +const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + +// 使用 Promise +delay(100) + .then(() => { + console.log('Promise 已完成! (Promise completed!)'); + return '异步操作结果'; + }) + .then(result => { + console.log(`异步结果: ${result}`); + }) + .catch(error => { + console.error('Promise 错误:', error); + }); + +// async/await 示例 +async function asyncExample() { + try { + console.log('⏳ 开始异步操作...'); + await delay(50); + console.log('✅ async/await 完成!'); + return '异步函数结果'; + } catch (error) { + console.error('async/await 错误:', error); + } +} + +// 立即执行异步函数 +(async () => { + const result = await asyncExample(); + console.log(`异步函数返回: ${result}`); + console.log(''); +})(); + +// 类和继承示例 +console.log('🏗️ 类示例 (Class example):'); +class Animal { + constructor(name, type) { + this.name = name; + this.type = type; + } + + speak() { + return `${this.name} 发出声音`; + } +} + +class Dog extends Animal { + constructor(name) { + super(name, '狗'); + } + + speak() { + return `${this.name} 汪汪叫`; + } +} + +const dog = new Dog('小黄'); +console.log(dog.speak()); +console.log(''); + +// 模块和导出示例(注释版本,因为这是单文件) +console.log('📦 模块概念示例 (Module concept example):'); +// export const utilities = { +// formatDate: (date) => date.toLocaleDateString('zh-CN'), +// randomNumber: () => Math.floor(Math.random() * 100) +// }; + +const utilities = { + formatDate: (date) => date.toLocaleDateString('zh-CN'), + randomNumber: () => Math.floor(Math.random() * 100) +}; + +console.log(`当前日期: ${utilities.formatDate(new Date())}`); +console.log(`随机数: ${utilities.randomNumber()}`); +console.log(''); + +// JSON 操作示例 +console.log('📄 JSON 操作示例 (JSON operations):'); +const data = { + users: [ + {id: 1, name: 'Alice', active: true}, + {id: 2, name: 'Bob', active: false}, + {id: 3, name: 'Charlie', active: true} + ] +}; + +const jsonString = JSON.stringify(data, null, 2); +console.log('JSON 字符串:'); +console.log(jsonString); + +const parsedData = JSON.parse(jsonString); +const activeUsers = parsedData.users.filter(user => user.active); +console.log(`活跃用户: ${activeUsers.map(u => u.name).join(', ')}`); +console.log(''); + +// 错误处理示例 +console.log('🚨 错误处理示例 (Error handling):'); +try { + const riskyOperation = () => { + const random = Math.random(); + if (random < 0.5) { + throw new Error('随机错误发生了!'); + } + return '操作成功!'; + }; + + const result2 = riskyOperation(); + console.log(result2); +} catch (error) { + console.log(`捕获错误: ${error.message}`); +} finally { + console.log('错误处理完成'); +} +console.log(''); + +// 正则表达式示例 +console.log('🔤 正则表达式示例 (Regular expressions):'); +const text = 'CodeForge 是一个很棒的代码执行环境! Email: contact@codeforge.com'; +const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; +const emails = text.match(emailRegex); + +console.log(`文本: ${text}`); +console.log(`找到的邮箱: ${emails ? emails.join(', ') : '无'}`); +console.log(''); + +console.log('🎯 CodeForge JavaScript 代码执行完成!'); +console.log('🎯 CodeForge JavaScript execution completed!'); +console.log(''); +console.log('感谢使用 CodeForge 代码执行环境! 🚀'); +console.log('Thank you for using CodeForge! 🚀'); \ No newline at end of file diff --git a/src-tauri/src/plugins/javascript_browser.rs b/src-tauri/src/plugins/javascript_browser.rs new file mode 100644 index 0000000..f88cf96 --- /dev/null +++ b/src-tauri/src/plugins/javascript_browser.rs @@ -0,0 +1,54 @@ +use super::{LanguagePlugin, PluginConfig}; +use std::vec; + +pub struct JavaScriptBrowserPlugin; + +impl LanguagePlugin for JavaScriptBrowserPlugin { + fn get_order(&self) -> i32 { + 13 + } + + fn get_language_name(&self) -> &'static str { + "JavaScript (Browser)" + } + + fn get_language_key(&self) -> &'static str { + "javascript-browser" + } + + fn get_file_extension(&self) -> String { + self.get_config() + .map(|config| config.extension.clone()) + .unwrap_or_else(|| "js".to_string()) + } + + fn get_version_args(&self) -> Vec<&'static str> { + vec!["--"] + } + + fn get_path_command(&self) -> String { + "which node".to_string() + } + + fn get_default_config(&self) -> PluginConfig { + PluginConfig { + enabled: true, + language: self.get_language_key().to_string(), + before_compile: None, + extension: String::from("js"), + execute_home: None, + run_command: Some(String::from( + "echo ", + )), + after_compile: None, + template: Some(String::from("// 在这里输入 JavaScript (Browser) 代码")), + timeout: Some(30), + } + } + + fn get_default_command(&self) -> String { + self.get_config() + .and_then(|config| config.run_command.clone()) + .unwrap_or_else(|| "node".to_string()) + } +} diff --git a/src-tauri/src/plugins/javascript_jquery.rs b/src-tauri/src/plugins/javascript_jquery.rs new file mode 100644 index 0000000..4922df9 --- /dev/null +++ b/src-tauri/src/plugins/javascript_jquery.rs @@ -0,0 +1,54 @@ +use super::{LanguagePlugin, PluginConfig}; +use std::vec; + +pub struct JavaScriptJQueryPlugin; + +impl LanguagePlugin for JavaScriptJQueryPlugin { + fn get_order(&self) -> i32 { + 13 + } + + fn get_language_name(&self) -> &'static str { + "JavaScript (jQuery)" + } + + fn get_language_key(&self) -> &'static str { + "javascript-jquery" + } + + fn get_file_extension(&self) -> String { + self.get_config() + .map(|config| config.extension.clone()) + .unwrap_or_else(|| "js".to_string()) + } + + fn get_version_args(&self) -> Vec<&'static str> { + vec!["--"] + } + + fn get_path_command(&self) -> String { + "which node".to_string() + } + + fn get_default_config(&self) -> PluginConfig { + PluginConfig { + enabled: true, + language: self.get_language_key().to_string(), + before_compile: None, + extension: String::from("js"), + execute_home: None, + run_command: Some(String::from( + "echo \n", + )), + after_compile: None, + template: Some(String::from("// 在这里输入 JavaScript (jQuery) 代码")), + timeout: Some(30), + } + } + + fn get_default_command(&self) -> String { + self.get_config() + .and_then(|config| config.run_command.clone()) + .unwrap_or_else(|| "node".to_string()) + } +} diff --git a/src-tauri/src/plugins/manager.rs b/src-tauri/src/plugins/manager.rs index 8b66fbf..14cc9a3 100644 --- a/src-tauri/src/plugins/manager.rs +++ b/src-tauri/src/plugins/manager.rs @@ -4,6 +4,8 @@ use crate::plugins::c::CPlugin; use crate::plugins::clojure::ClojurePlugin; use crate::plugins::go::GoPlugin; use crate::plugins::java::JavaPlugin; +use crate::plugins::javascript_browser::JavaScriptBrowserPlugin; +use crate::plugins::javascript_jquery::JavaScriptJQueryPlugin; use crate::plugins::javascript_nodejs::JavaScriptNodeJsPlugin; use crate::plugins::kotlin::KotlinPlugin; use crate::plugins::nodejs::NodeJSPlugin; @@ -54,6 +56,14 @@ impl PluginManager { "typescript-browser".to_string(), Box::new(TypeScriptBrowserPlugin), ); + plugins.insert( + "javascript-browser".to_string(), + Box::new(JavaScriptBrowserPlugin), + ); + plugins.insert( + "javascript-jquery".to_string(), + Box::new(JavaScriptJQueryPlugin), + ); Self { plugins } } diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs index 082ec17..175c1f5 100644 --- a/src-tauri/src/plugins/mod.rs +++ b/src-tauri/src/plugins/mod.rs @@ -373,6 +373,8 @@ pub mod c; pub mod clojure; pub mod go; pub mod java; +pub mod javascript_browser; +pub mod javascript_jquery; pub mod javascript_nodejs; pub mod kotlin; pub mod manager; diff --git a/src-tauri/src/plugins/typescript_browser.rs b/src-tauri/src/plugins/typescript_browser.rs index 414cffb..5e1b12b 100644 --- a/src-tauri/src/plugins/typescript_browser.rs +++ b/src-tauri/src/plugins/typescript_browser.rs @@ -23,7 +23,7 @@ impl LanguagePlugin for TypeScriptBrowserPlugin { } fn get_version_args(&self) -> Vec<&'static str> { - vec!["--version"] + vec!["--"] } fn get_path_command(&self) -> String { diff --git a/src-tauri/src/setup/menus/app.rs b/src-tauri/src/setup/menus/app.rs index 8c3e046..f44b556 100644 --- a/src-tauri/src/setup/menus/app.rs +++ b/src-tauri/src/setup/menus/app.rs @@ -6,12 +6,31 @@ use tauri::{ menu::{MenuItemBuilder, Submenu, SubmenuBuilder}, }; +use crate::update::check_for_updates; + +fn check_update_sync() -> bool { + match tokio::runtime::Runtime::new() { + Ok(rt) => match rt.block_on(check_for_updates()) { + Ok(Some(_)) => true, + Ok(None) => false, + Err(_e) => false, + }, + Err(_e) => false, + } +} + pub fn create_app_submenu(app: &AppHandle) -> tauri::Result> { let about_item = MenuItemBuilder::new("关于 CodeForge") .id("about") .build(app)?; - let update_item = MenuItemBuilder::new("检查更新").id("update").build(app)?; + let update_text = if check_update_sync() { + "检查更新 (有新版本)" + } else { + "检查更新" + }; + + let update_item = MenuItemBuilder::new(update_text).id("update").build(app)?; let settings_item = MenuItemBuilder::new("设置") .id("settings") diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 7fcb290..e0fc2ef 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -31,6 +31,10 @@
+ +