不知名提交
This commit is contained in:
225
InfoGenie-frontend/public/toolbox/计算器/app.js
Normal file
225
InfoGenie-frontend/public/toolbox/计算器/app.js
Normal file
@@ -0,0 +1,225 @@
|
||||
(() => {
|
||||
const { createApp, ref, computed, watch } = Vue;
|
||||
|
||||
// 检测是否可用 Math.js
|
||||
const hasMath = typeof math !== 'undefined';
|
||||
if (hasMath) {
|
||||
math.config({ number: 'BigNumber', precision: 64 });
|
||||
}
|
||||
|
||||
// 保存原始三角函数以便覆盖时调用
|
||||
const originalSin = hasMath ? math.sin : null;
|
||||
const originalCos = hasMath ? math.cos : null;
|
||||
const originalTan = hasMath ? math.tan : null;
|
||||
|
||||
// 角度转换因子(deg -> rad)
|
||||
const RAD_FACTOR = hasMath ? math.divide(math.pi, math.bignumber(180)) : (Math.PI / 180);
|
||||
|
||||
// 动态角度模式变量供三角函数使用
|
||||
let angleModeVar = 'deg';
|
||||
|
||||
function sinWrapper(x) {
|
||||
try {
|
||||
if (angleModeVar === 'deg') {
|
||||
const xr = hasMath ? math.multiply(x, RAD_FACTOR) : (Number(x) * RAD_FACTOR);
|
||||
return hasMath ? originalSin(xr) : Math.sin(xr);
|
||||
}
|
||||
return hasMath ? originalSin(x) : Math.sin(Number(x));
|
||||
} catch (e) { throw e; }
|
||||
}
|
||||
function cosWrapper(x) {
|
||||
try {
|
||||
if (angleModeVar === 'deg') {
|
||||
const xr = hasMath ? math.multiply(x, RAD_FACTOR) : (Number(x) * RAD_FACTOR);
|
||||
return hasMath ? originalCos(xr) : Math.cos(xr);
|
||||
}
|
||||
return hasMath ? originalCos(x) : Math.cos(Number(x));
|
||||
} catch (e) { throw e; }
|
||||
}
|
||||
function tanWrapper(x) {
|
||||
try {
|
||||
if (angleModeVar === 'deg') {
|
||||
const xr = hasMath ? math.multiply(x, RAD_FACTOR) : (Number(x) * RAD_FACTOR);
|
||||
return hasMath ? originalTan(xr) : Math.tan(xr);
|
||||
}
|
||||
return hasMath ? originalTan(x) : Math.tan(Number(x));
|
||||
} catch (e) { throw e; }
|
||||
}
|
||||
|
||||
// 覆盖三角函数以支持角度模式(Math.js 可用时)
|
||||
if (hasMath) {
|
||||
math.import({ sin: sinWrapper, cos: cosWrapper, tan: tanWrapper }, { override: true });
|
||||
}
|
||||
|
||||
function formatBig(value) {
|
||||
try {
|
||||
if (value == null) return '';
|
||||
if (hasMath) {
|
||||
return math.format(value, {
|
||||
notation: 'auto',
|
||||
precision: 14,
|
||||
lowerExp: -6,
|
||||
upperExp: 15,
|
||||
});
|
||||
} else {
|
||||
const num = typeof value === 'number' ? value : Number(value);
|
||||
if (!isFinite(num)) return '错误';
|
||||
const str = num.toFixed(12);
|
||||
return str.replace(/\.0+$/, '').replace(/(\.[0-9]*?)0+$/, '$1');
|
||||
}
|
||||
} catch (e) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function normalize(exp) {
|
||||
// 将显示符号标准化为计算符号,保留原字符不做删除
|
||||
return exp
|
||||
.replace(/×/g, '*')
|
||||
.replace(/÷/g, '/')
|
||||
.replace(/√/g, 'sqrt');
|
||||
}
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const expression = ref('');
|
||||
const result = ref(hasMath ? math.bignumber(0) : 0);
|
||||
const errorMsg = ref('');
|
||||
const lastAns = ref(hasMath ? math.bignumber(0) : 0);
|
||||
const angleMode = ref('deg');
|
||||
|
||||
watch(angleMode, (val) => { angleModeVar = val; });
|
||||
|
||||
const formattedExpression = computed(() => expression.value || '0');
|
||||
const formattedResult = computed(() => errorMsg.value ? '' : formatBig(result.value));
|
||||
|
||||
function isParenthesesBalanced(s) {
|
||||
let count = 0;
|
||||
for (const ch of s) {
|
||||
if (ch === '(') count++;
|
||||
else if (ch === ')') count--;
|
||||
if (count < 0) return false;
|
||||
}
|
||||
return count === 0;
|
||||
}
|
||||
|
||||
function safeEvaluate(exp) {
|
||||
errorMsg.value = '';
|
||||
try {
|
||||
const s = normalize(exp);
|
||||
if (!s) { result.value = hasMath ? math.bignumber(0) : 0; return result.value; }
|
||||
// 检测非法字符:仅允许数字、运算符、括号、字母(用于函数和ANS),以及空白
|
||||
if (/[^0-9\.\+\-\*\/\^\(\)a-zA-Z\s]/.test(s)) { throw new Error('错误'); }
|
||||
if (!isParenthesesBalanced(s)) throw new Error('错误');
|
||||
if (hasMath) {
|
||||
const scope = { ANS: lastAns.value };
|
||||
const res = math.evaluate(s, scope);
|
||||
// 防止除以零等无效情况
|
||||
if (res && res.isFinite && !res.isFinite()) { throw new Error('错误'); }
|
||||
result.value = res;
|
||||
return res;
|
||||
} else {
|
||||
// 原生回退:将表达式映射到安全本地函数
|
||||
let expr = s
|
||||
.replace(/\^/g, '**')
|
||||
.replace(/sin\(/g, '__sin(')
|
||||
.replace(/cos\(/g, '__cos(')
|
||||
.replace(/tan\(/g, '__tan(')
|
||||
.replace(/sqrt\(/g, '__sqrt(')
|
||||
.replace(/\bANS\b/g, String(lastAns.value));
|
||||
// 严格校验(只允许安全字符)
|
||||
if (/[^0-9+\-*/()._^a-zA-Z\s]/.test(expr)) throw new Error('错误');
|
||||
// 定义本地安全函数
|
||||
const __sqrt = (x) => Math.sqrt(Number(x));
|
||||
const __sin = (x) => angleModeVar === 'deg' ? Math.sin(Number(x) * Math.PI / 180) : Math.sin(Number(x));
|
||||
const __cos = (x) => angleModeVar === 'deg' ? Math.cos(Number(x) * Math.PI / 180) : Math.cos(Number(x));
|
||||
const __tan = (x) => angleModeVar === 'deg' ? Math.tan(Number(x) * Math.PI / 180) : Math.tan(Number(x));
|
||||
const res = Function('__sqrt','__sin','__cos','__tan', `"use strict"; return (${expr});`)(__sqrt,__sin,__cos,__tan);
|
||||
if (!isFinite(res)) throw new Error('错误');
|
||||
result.value = res;
|
||||
return res;
|
||||
}
|
||||
} catch (err) {
|
||||
errorMsg.value = '错误';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(expression, (exp) => { safeEvaluate(exp); });
|
||||
|
||||
function press(token) {
|
||||
// 避免连续两个小数点
|
||||
if (token === '.' && expression.value.slice(-1) === '.') return;
|
||||
expression.value += token;
|
||||
}
|
||||
|
||||
function op(opSymbol) {
|
||||
const last = expression.value.slice(-1);
|
||||
if (/[\+\-×÷\*\/\^]/.test(last)) {
|
||||
expression.value = expression.value.slice(0, -1) + opSymbol;
|
||||
} else {
|
||||
expression.value += opSymbol;
|
||||
}
|
||||
}
|
||||
|
||||
function func(fn) {
|
||||
const map = { sqrt: 'sqrt', sin: 'sin', cos: 'cos', tan: 'tan' };
|
||||
const f = map[fn] || fn;
|
||||
expression.value += f + '(';
|
||||
}
|
||||
|
||||
function square() {
|
||||
expression.value += '^2';
|
||||
}
|
||||
|
||||
function backspace() {
|
||||
if (!expression.value) return;
|
||||
expression.value = expression.value.slice(0, -1);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
expression.value = '';
|
||||
result.value = math.bignumber(0);
|
||||
errorMsg.value = '';
|
||||
}
|
||||
|
||||
function equals() {
|
||||
const res = safeEvaluate(expression.value);
|
||||
if (res != null) {
|
||||
lastAns.value = res;
|
||||
expression.value = formatBig(res);
|
||||
result.value = res;
|
||||
}
|
||||
}
|
||||
|
||||
function ans() {
|
||||
expression.value += 'ANS';
|
||||
}
|
||||
|
||||
function setAngle(mode) {
|
||||
angleMode.value = mode;
|
||||
}
|
||||
|
||||
// 初始计算
|
||||
safeEvaluate(expression.value);
|
||||
|
||||
return {
|
||||
expression,
|
||||
result,
|
||||
errorMsg,
|
||||
formattedExpression,
|
||||
formattedResult,
|
||||
angleMode,
|
||||
setAngle,
|
||||
press,
|
||||
op,
|
||||
func,
|
||||
clear,
|
||||
backspace,
|
||||
equals,
|
||||
square,
|
||||
ans,
|
||||
};
|
||||
},
|
||||
}).mount('#app');
|
||||
})();
|
||||
63
InfoGenie-frontend/public/toolbox/计算器/index.html
Normal file
63
InfoGenie-frontend/public/toolbox/计算器/index.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
|
||||
<title>🖩网页计算器</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main id="app" class="calculator">
|
||||
<header class="topbar">
|
||||
<div class="brand">🖩网页计算器</div>
|
||||
</header>
|
||||
|
||||
<section class="display">
|
||||
<div class="expression" :title="expression">{{ formattedExpression }}</div>
|
||||
<div class="result" :class="{ error: !!errorMsg }">{{ errorMsg || formattedResult }}</div>
|
||||
</section>
|
||||
|
||||
<section class="keypad">
|
||||
<button @click="press('(')">(</button>
|
||||
<button @click="press(')')">)</button>
|
||||
<button @click="func('sqrt')">√</button>
|
||||
<button @click="clear()">AC</button>
|
||||
|
||||
<button @click="func('sin')">sin</button>
|
||||
<button @click="func('cos')">cos</button>
|
||||
<button @click="func('tan')">tan</button>
|
||||
<button @click="backspace()">⌫</button>
|
||||
|
||||
<button @click="press('7')">7</button>
|
||||
<button @click="press('8')">8</button>
|
||||
<button @click="press('9')">9</button>
|
||||
<button @click="op('÷')">÷</button>
|
||||
|
||||
<button @click="press('4')">4</button>
|
||||
<button @click="press('5')">5</button>
|
||||
<button @click="press('6')">6</button>
|
||||
<button @click="op('×')">×</button>
|
||||
|
||||
<button @click="press('1')">1</button>
|
||||
<button @click="press('2')">2</button>
|
||||
<button @click="press('3')">3</button>
|
||||
<button @click="op('-')">-</button>
|
||||
|
||||
<button @click="press('0')">0</button>
|
||||
<button @click="press('.')">.</button>
|
||||
<button @click="square()">x²</button>
|
||||
<button @click="op('+')">+</button>
|
||||
|
||||
<button class="span-2" @click="ans()">ANS</button>
|
||||
<button class="span-2 action" @click="equals()">=</button>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Frameworks -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjs@11/dist/math.min.js"></script>
|
||||
<!-- App -->
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
138
InfoGenie-frontend/public/toolbox/计算器/styles.css
Normal file
138
InfoGenie-frontend/public/toolbox/计算器/styles.css
Normal file
@@ -0,0 +1,138 @@
|
||||
:root {
|
||||
--bg-start: #d9f7d9;
|
||||
--bg-end: #e9fbd7;
|
||||
--btn-bg-1: #f7fff0;
|
||||
--btn-bg-2: #efffe6;
|
||||
--accent-1: #a6e3a1;
|
||||
--accent-2: #8fd68b;
|
||||
--text: #173b2b;
|
||||
--text-soft: #406a53;
|
||||
}
|
||||
|
||||
html, body { height: 100%; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Microsoft YaHei', 'Heiti SC', 'WenQuanYi Micro Hei', sans-serif;
|
||||
background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
|
||||
color: var(--text);
|
||||
overflow: auto; /* 保留滚动效果 */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条 */
|
||||
html::-webkit-scrollbar, body::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
||||
html, body { scrollbar-width: none; }
|
||||
|
||||
.calculator {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
padding: 12px env(safe-area-inset-right) 24px env(safe-area-inset-left);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.angle-toggle {
|
||||
display: inline-flex;
|
||||
background: rgba(255,255,255,0.35);
|
||||
border-radius: 999px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.angle-toggle button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
color: #24543a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.angle-toggle button.active {
|
||||
background: #b8e2b1;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.08) inset;
|
||||
}
|
||||
|
||||
.display {
|
||||
background: rgba(255,255,255,0.6);
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.expression {
|
||||
min-height: 28px;
|
||||
font-size: 18px;
|
||||
color: var(--text-soft);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.result {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
color: var(--text);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.result.error { color: #d35454; }
|
||||
|
||||
.keypad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.keypad button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 14px 0;
|
||||
min-height: 58px; /* 移动端友好触控 */
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
background: linear-gradient(180deg, var(--btn-bg-1), var(--btn-bg-2));
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.keypad button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.10);
|
||||
}
|
||||
|
||||
.keypad .action {
|
||||
background: linear-gradient(180deg, var(--accent-1), var(--accent-2));
|
||||
color: #0f2a1f;
|
||||
}
|
||||
|
||||
.keypad .span-2 { grid-column: span 2; }
|
||||
|
||||
.tips {
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
margin-top: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.keypad button { min-height: 52px; font-size: 16px; }
|
||||
.result { font-size: 28px; }
|
||||
}
|
||||
Reference in New Issue
Block a user