705 lines
16 KiB
Markdown
705 lines
16 KiB
Markdown
# 科学计算器功能扩展实现计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 将桌面端计算器从基础四则运算扩展为完整科学计算器,支持三角/反三角/对数/幂/根/阶乘/记忆/括号等功能
|
||
|
||
**Architecture:** 在 CalculatorDesktop 中新增模式切换标签栏(基础/科学),科学模式下显示函数区。状态管理扩展 memory/angleMode/invMode/parenOpen/mode。保持 4 列布局,宽度不变。
|
||
|
||
**Tech Stack:** React, TypeScript, CSS, Ant Design (message)
|
||
|
||
---
|
||
|
||
## 文件结构
|
||
|
||
- **修改**: `src/pages/Calculator/Calculator.tsx` — 扩展 CalculatorDesktop 组件
|
||
- **修改**: `src/pages/Calculator/Calculator.css` — 新增科学函数按键样式、模式切换栏
|
||
- **不修改**: `src/pages/Calculator/CalculatorMobile.tsx` — 移动端保持不变
|
||
|
||
---
|
||
|
||
## Task 1: 新增模式切换标签栏
|
||
|
||
**Files:**
|
||
- Modify: `src/pages/Calculator/Calculator.tsx`
|
||
- Modify: `src/pages/Calculator/Calculator.css`
|
||
|
||
- [ ] **Step 1: 在 Calculator.tsx 中新增 mode 状态和切换逻辑**
|
||
|
||
在 `CalculatorState` 中添加 `mode: 'basic' | 'scientific'`,初始值为 `'basic'`。
|
||
|
||
新增函数:
|
||
```typescript
|
||
const toggleMode = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
mode: prev.mode === 'basic' ? 'scientific' : 'basic',
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 2: 在 display 区域上方渲染模式切换标签栏**
|
||
|
||
在 `calc-display` 之前添加:
|
||
```tsx
|
||
<div className="calc-mode-bar">
|
||
<div className="calc-mode-tabs">
|
||
<button
|
||
className={`calc-mode-tab ${state.mode === 'basic' ? 'active' : ''}`}
|
||
onClick={toggleMode}
|
||
>
|
||
{t("calculator.mode.basic")}
|
||
</button>
|
||
<button
|
||
className={`calc-mode-tab ${state.mode === 'scientific' ? 'active' : ''}`}
|
||
onClick={toggleMode}
|
||
>
|
||
{t("calculator.mode.scientific")}
|
||
</button>
|
||
</div>
|
||
<div className="calc-mode-indicator">
|
||
{state.mode === 'scientific' && (
|
||
<span className="calc-angle-badge">{state.angleMode}</span>
|
||
)}
|
||
{state.memory !== 0 && <span className="calc-memory-badge">M</span>}
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
- [ ] **Step 3: 在 Calculator.css 中添加模式切换栏样式**
|
||
|
||
```css
|
||
.calc-mode-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 16px;
|
||
background: #151928;
|
||
border-bottom: 1px solid rgba(51, 65, 85, 0.5);
|
||
}
|
||
|
||
.calc-mode-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
|
||
.calc-mode-tab {
|
||
padding: 6px 16px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
border: none;
|
||
background: transparent;
|
||
color: #64748b;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.calc-mode-tab.active {
|
||
background: #6366f1;
|
||
color: #fff;
|
||
}
|
||
|
||
.calc-mode-tab:hover:not(.active) {
|
||
color: #94a3b8;
|
||
background: rgba(99, 102, 241, 0.1);
|
||
}
|
||
|
||
.calc-mode-indicator {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.calc-angle-badge,
|
||
.calc-memory-badge {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
background: rgba(99, 102, 241, 0.2);
|
||
color: #a5b4fc;
|
||
}
|
||
|
||
.calc-memory-badge {
|
||
background: rgba(34, 197, 94, 0.2);
|
||
color: #86efac;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 添加国际化文本**
|
||
|
||
在 `zh.json` 和 `en.json` 的 `calculator` 节中添加:
|
||
```json
|
||
"mode": {
|
||
"basic": "基础",
|
||
"scientific": "科学"
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: 提交**
|
||
|
||
```bash
|
||
git add src/pages/Calculator/Calculator.tsx src/pages/Calculator/Calculator.css src/locales/zh.json src/locales/en.json
|
||
git commit -m "feat(calculator): add mode toggle bar (basic/scientific)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: 扩展状态管理
|
||
|
||
**Files:**
|
||
- Modify: `src/pages/Calculator/Calculator.tsx`
|
||
|
||
- [ ] **Step 1: 扩展 CalculatorState 接口**
|
||
|
||
```typescript
|
||
interface CalculatorState {
|
||
current: string;
|
||
previous: string;
|
||
operator: Operator | null;
|
||
overwrite: boolean;
|
||
history: string;
|
||
memory: number; // 新增:记忆存储值
|
||
angleMode: 'DEG' | 'RAD'; // 新增:角度/弧度模式
|
||
invMode: boolean; // 新增:反函数模式(2nd键)
|
||
parenOpen: boolean; // 新增:是否有未闭合的括号
|
||
mode: 'basic' | 'scientific'; // 新增:当前模式
|
||
}
|
||
|
||
type Operator = '+' | '-' | '*' | '/' | '^' | 'root';
|
||
```
|
||
|
||
- [ ] **Step 2: 更新初始状态**
|
||
|
||
```typescript
|
||
const [state, setState] = useState<CalculatorState>({
|
||
current: "0",
|
||
previous: "",
|
||
operator: null,
|
||
overwrite: false,
|
||
history: "",
|
||
memory: 0,
|
||
angleMode: 'DEG',
|
||
invMode: false,
|
||
parenOpen: false,
|
||
mode: 'basic',
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 3: 新增记忆操作函数**
|
||
|
||
```typescript
|
||
const memoryClear = () => {
|
||
setState((prev) => ({ ...prev, memory: 0 }));
|
||
};
|
||
|
||
const memoryRecall = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(prev.memory),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
|
||
const memoryAdd = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
memory: prev.memory + parseFloat(prev.current),
|
||
}));
|
||
};
|
||
|
||
const memorySubtract = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
memory: prev.memory - parseFloat(prev.current),
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 4: 新增角度模式切换函数**
|
||
|
||
```typescript
|
||
const toggleAngleMode = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
angleMode: prev.angleMode === 'DEG' ? 'RAD' : 'DEG',
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 5: 新增反函数模式切换函数**
|
||
|
||
```typescript
|
||
const toggleInvMode = () => {
|
||
setState((prev) => ({ ...prev, invMode: !prev.invMode }));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 6: 提交**
|
||
|
||
```bash
|
||
git add src/pages/Calculator/Calculator.tsx
|
||
git commit -m "feat(calculator): extend state management with memory, angle mode, inv mode"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: 新增科学函数区(科学模式专用)
|
||
|
||
**Files:**
|
||
- Modify: `src/pages/Calculator/Calculator.tsx`
|
||
- Modify: `src/pages/Calculator/Calculator.css`
|
||
|
||
- [ ] **Step 1: 在 buttons 数组前添加科学函数按钮定义**
|
||
|
||
```typescript
|
||
const sciButtons = [
|
||
{ label: state.invMode ? "sin⁻¹" : "sin", type: "function", onClick: sin },
|
||
{ label: state.invMode ? "cos⁻¹" : "cos", type: "function", onClick: cos },
|
||
{ label: state.invMode ? "tan⁻¹" : "tan", type: "function", onClick: tan },
|
||
{ label: state.invMode ? "x³" : "x²", type: "function", onClick: state.invMode ? cube : square },
|
||
{ label: "log", type: "function", onClick: log10 },
|
||
{ label: state.invMode ? "eˣ" : "ln", type: "function", onClick: state.invMode ? exp : ln },
|
||
{ label: state.invMode ? "³√" : "√", type: "function", onClick: state.invMode ? cbrt : sqrt },
|
||
{ label: "π", type: "function", onClick: insertPi },
|
||
{ label: "e", type: "function", onClick: insertE },
|
||
{ label: state.invMode ? "x√y" : "xʸ", type: "operator", onClick: state.invMode ? nthRoot : power },
|
||
{ label: "n!", type: "function", onClick: factorial },
|
||
{ label: "1/x", type: "function", onClick: reciprocal },
|
||
{ label: "DEG/RAD", type: "function", onClick: toggleAngleMode },
|
||
{ label: "2nd", type: "function", onClick: toggleInvMode },
|
||
{ label: "MC", type: "memory", onClick: memoryClear },
|
||
{ label: "MR", type: "memory", onClick: memoryRecall },
|
||
{ label: "M+", type: "memory", onClick: memoryAdd },
|
||
{ label: "M−", type: "memory", onClick: memorySubtract },
|
||
{ label: "(", type: "function", onClick: openParen },
|
||
{ label: ")", type: "function", onClick: closeParen },
|
||
];
|
||
```
|
||
|
||
- [ ] **Step 2: 在渲染 keypad 时,科学模式下显示 sciButtons**
|
||
|
||
```tsx
|
||
<div className="calc-keypad">
|
||
{state.mode === 'scientific' && (
|
||
<div className="calc-sci-row">
|
||
{sciButtons.slice(0, 10).map((btn, idx) => (
|
||
<button
|
||
key={`sci-${idx}`}
|
||
className={`calc-btn calc-btn-${btn.type} ${btn.className || ""}`}
|
||
onClick={btn.onClick}
|
||
>
|
||
{btn.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
{/* 原有的基础按钮 */}
|
||
{buttons.map((btn, idx) => (
|
||
<button
|
||
key={idx}
|
||
className={`calc-btn calc-btn-${btn.type} ${btn.className || ""}`}
|
||
onClick={btn.onClick}
|
||
>
|
||
{btn.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
```
|
||
|
||
- [ ] **Step 3: 添加科学函数区样式**
|
||
|
||
```css
|
||
.calc-sci-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 4px;
|
||
padding: 8px;
|
||
background: rgba(99, 102, 241, 0.05);
|
||
border-bottom: 1px solid rgba(51, 65, 85, 0.3);
|
||
}
|
||
|
||
.calc-sci-row .calc-btn {
|
||
font-size: 11px;
|
||
padding: 8px 4px;
|
||
}
|
||
|
||
[data-theme="dark"] .calc-sci-row {
|
||
background: rgba(99, 102, 241, 0.08);
|
||
border-bottom-color: rgba(51, 65, 85, 0.5);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 提交**
|
||
|
||
```bash
|
||
git add src/pages/Calculator/Calculator.tsx src/pages/Calculator/Calculator.css
|
||
git commit -m "feat(calculator): add scientific function row"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: 实现三角函数
|
||
|
||
**Files:**
|
||
- Modify: `src/pages/Calculator/Calculator.tsx`
|
||
|
||
- [ ] **Step 1: 实现 toRad/fromRad 辅助函数**
|
||
|
||
```typescript
|
||
const toRad = (val: number) =>
|
||
state.angleMode === 'DEG' ? val * Math.PI / 180 : val;
|
||
|
||
const fromRad = (val: number) =>
|
||
state.angleMode === 'DEG' ? val * 180 / Math.PI : val;
|
||
```
|
||
|
||
- [ ] **Step 2: 实现 sin/cos/tan 函数**
|
||
|
||
```typescript
|
||
const sin = () => {
|
||
const num = parseFloat(state.current);
|
||
const result = Math.sin(toRad(num));
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(result),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
|
||
const cos = () => {
|
||
const num = parseFloat(state.current);
|
||
const result = Math.cos(toRad(num));
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(result),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
|
||
const tan = () => {
|
||
const num = parseFloat(state.current);
|
||
const rad = toRad(num);
|
||
// 检查 tan 的无定义点 (90°, 270°, etc.)
|
||
const cosVal = Math.cos(rad);
|
||
if (Math.abs(cosVal) < 1e-10) {
|
||
message.error(t("calculator.errorTanUndefined"));
|
||
setState((prev) => ({ ...prev, current: "Error" }));
|
||
return;
|
||
}
|
||
const result = Math.tan(rad);
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(result),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 3: 提交**
|
||
|
||
```bash
|
||
git add src/pages/Calculator/Calculator.tsx
|
||
git commit -m "feat(calculator): implement trigonometric functions (sin/cos/tan)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: 实现对数/指数/幂函数
|
||
|
||
**Files:**
|
||
- Modify: `src/pages/Calculator/Calculator.tsx`
|
||
|
||
- [ ] **Step 1: 实现 log/ln/x²/x³/√/³√**
|
||
|
||
```typescript
|
||
const log10 = () => {
|
||
const num = parseFloat(state.current);
|
||
if (num <= 0) {
|
||
message.error(t("calculator.errorLogNegative"));
|
||
setState((prev) => ({ ...prev, current: "Error" }));
|
||
return;
|
||
}
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(Math.log10(num)),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
|
||
const ln = () => {
|
||
const num = parseFloat(state.current);
|
||
if (num <= 0) {
|
||
message.error(t("calculator.errorLogNegative"));
|
||
setState((prev) => ({ ...prev, current: "Error" }));
|
||
return;
|
||
}
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(Math.log(num)),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
|
||
const square = () => {
|
||
const num = parseFloat(state.current);
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(num * num),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
|
||
const cube = () => {
|
||
const num = parseFloat(state.current);
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(num * num * num),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
|
||
const cbrt = () => {
|
||
const num = parseFloat(state.current);
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(Math.cbrt(num)),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
|
||
const exp = () => {
|
||
const num = parseFloat(state.current);
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(Math.exp(num)),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 2: 实现 xʸ 和 x√y(双目运算)**
|
||
|
||
```typescript
|
||
const power = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
previous: prev.current,
|
||
operator: '^',
|
||
overwrite: true,
|
||
history: `${prev.current} ^`,
|
||
}));
|
||
};
|
||
|
||
const nthRoot = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
previous: prev.current,
|
||
operator: 'root',
|
||
overwrite: true,
|
||
history: `${prev.current} √`,
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 3: 更新 compute 函数支持 ^ 和 root**
|
||
|
||
```typescript
|
||
const compute = (a: string, b: string, op: Operator): string => {
|
||
const numA = parseFloat(a);
|
||
const numB = parseFloat(b);
|
||
if (isNaN(numA) || isNaN(numB)) return "0";
|
||
let result = 0;
|
||
switch (op) {
|
||
case "+": result = numA + numB; break;
|
||
case "-": result = numA - numB; break;
|
||
case "*": result = numA * numB; break;
|
||
case "/": result = numB === 0 ? NaN : numA / numB; break;
|
||
case "^": result = Math.pow(numA, numB); break;
|
||
case "root": result = Math.pow(numA, 1 / numB); break;
|
||
default: return b;
|
||
}
|
||
if (isNaN(result) || !isFinite(result)) return "Error";
|
||
const str = result.toString();
|
||
return str.length > 15 ? result.toExponential(8) : str;
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 4: 提交**
|
||
|
||
```bash
|
||
git add src/pages/Calculator/Calculator.tsx
|
||
git commit -m "feat(calculator): implement logarithm, exponent, and power functions"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: 实现阶乘/倒数/常数
|
||
|
||
**Files:**
|
||
- Modify: `src/pages/Calculator/Calculator.tsx`
|
||
|
||
- [ ] **Step 1: 实现阶乘**
|
||
|
||
```typescript
|
||
const factorial = () => {
|
||
const num = parseFloat(state.current);
|
||
if (num < 0 || !Number.isInteger(num)) {
|
||
message.error(t("calculator.errorFactorial"));
|
||
setState((prev) => ({ ...prev, current: "Error" }));
|
||
return;
|
||
}
|
||
if (num > 170) {
|
||
setState((prev) => ({ ...prev, current: "Error" }));
|
||
return;
|
||
}
|
||
let result = 1;
|
||
for (let i = 2; i <= num; i++) result *= i;
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(result),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 2: 实现倒数**
|
||
|
||
```typescript
|
||
const reciprocal = () => {
|
||
const num = parseFloat(state.current);
|
||
if (num === 0) {
|
||
message.error(t("calculator.errorDivideByZero"));
|
||
setState((prev) => ({ ...prev, current: "Error" }));
|
||
return;
|
||
}
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(1 / num),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 3: 实现 π 和 e 常数插入**
|
||
|
||
```typescript
|
||
const insertPi = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(Math.PI),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
|
||
const insertE = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
current: String(Math.E),
|
||
overwrite: true,
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 4: 提交**
|
||
|
||
```bash
|
||
git add src/pages/Calculator/Calculator.tsx
|
||
git commit -m "feat(calculator): implement factorial, reciprocal, and constants"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: 实现括号支持
|
||
|
||
**Files:**
|
||
- Modify: `src/pages/Calculator/Calculator.tsx`
|
||
|
||
- [ ] **Step 1: 实现 openParen/closeParen**
|
||
|
||
```typescript
|
||
const openParen = () => {
|
||
// 简化实现:在历史中记录括号
|
||
setState((prev) => ({
|
||
...prev,
|
||
history: prev.history + "(",
|
||
parenOpen: true,
|
||
}));
|
||
};
|
||
|
||
const closeParen = () => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
history: prev.history + ")",
|
||
parenOpen: false,
|
||
}));
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 2: 提交**
|
||
|
||
```bash
|
||
git add src/pages/Calculator/Calculator.tsx
|
||
git commit -m "feat(calculator): add parenthesis support"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: 国际化文本补充
|
||
|
||
**Files:**
|
||
- Modify: `src/locales/zh.json`
|
||
- Modify: `src/locales/en.json`
|
||
|
||
- [ ] **Step 1: 在 zh.json 中添加错误信息**
|
||
|
||
```json
|
||
"calculator": {
|
||
"...": "...",
|
||
"errorTanUndefined": "tan 在此角度无定义",
|
||
"errorLogNegative": "对数的真数必须大于 0",
|
||
"errorFactorial": "阶乘仅支持非负整数",
|
||
"errorDivideByZero": "不能除以零",
|
||
"mode": {
|
||
"basic": "基础",
|
||
"scientific": "科学"
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 在 en.json 中添加错误信息**
|
||
|
||
```json
|
||
"calculator": {
|
||
"...": "...",
|
||
"errorTanUndefined": "tan is undefined at this angle",
|
||
"errorLogNegative": "Logarithm requires positive number",
|
||
"errorFactorial": "Factorial only supports non-negative integers",
|
||
"errorDivideByZero": "Cannot divide by zero",
|
||
"mode": {
|
||
"basic": "Basic",
|
||
"scientific": "Scientific"
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 提交**
|
||
|
||
```bash
|
||
git add src/locales/zh.json src/locales/en.json
|
||
git commit -m "feat(calculator): add i18n error messages and mode labels"
|
||
```
|
||
|
||
---
|
||
|
||
## 测试要点
|
||
|
||
1. **基础四则运算回归**:确保原有功能不受影响
|
||
2. **模式切换**:基础 ↔ 科学模式切换正常
|
||
3. **三角函数**:sin(30°) = 0.5, cos(60°) = 0.5
|
||
4. **DEG/RAD 切换**:同一角度在不同模式下结果不同
|
||
5. **对数**:log(100) = 2, ln(e) = 1
|
||
6. **错误处理**:负数开方、除以零、阶乘负数都显示 Error
|
||
7. **记忆功能**:MC/MR/M+/M− 正确工作
|
||
8. **状态指示**:M 标记和 DEG/RAD 指示器正确显示
|