Calculator
2301-计算机学院-软件工程 | https://bbs.csdn.net/forums/ssynkqtd-05 |
---|---|
这个作业要求在哪里 | https://bbs.csdn.net/topics/617377308 |
这个作业的目标 | 实现一个前后端分离计算器 |
其他参考文献 | 各大文档 |
PSP Stage | Description | Estimated Time (minutes) | Actual Time (minutes) |
---|---|---|---|
Planning | Plan the task | 30 | 20 |
•Estimate | Estimate how much time the task will take | 15 | 10 |
Development | Development phase | 630 | 610 |
Analysis | Requirement analysis (including learning new tech) | 30 | 30 |
Design Spec | Generate design document | 60 | 75 |
Design Review | Design review | 30 | 45 |
Coding Standard | Code standard (develop appropriate standards) | 30 | 20 |
Design | Detailed design | 30 | 30 |
Coding | Actual coding | 320 | 300 |
Code Review | Code review | 60 | 60 |
Test | Testing (self-testing, code modification, submit modification) | 70 | 50 |
Reporting | Reporting phase | 90 | 60 |
Test Report | Test report | 30 | 20 |
Size Measurement | Calculate work size | 15 | 10 |
Postmortem & Process Improvement Plan | Postmortem, and propose process improvement plan | 45 | 30 |
Total | null | 660 | 630 |
设计实现过程
- 项目规划与设计:
开始前,需要仔细规划项目的结构和功能。设计数据库模型、前端组件、API端点等。
考虑用户界面的设计和用户体验。TailwindCSS提供了很大的灵活性,可以轻松创建漂亮的界面。
- 学习新技术
掌握React组件开发、状态管理、路由、生命周期等方面的知识。
了解如何使用Vite构建现代的前端应用。
学习Gin框架的路由、中间件和API开发。
理解GORM作为对象关系映射(ORM)库的用法。
学习如何与MySQL数据库交互。
- 前后端通信:
需要编写API端点,使前端能够与后端进行数据交换。
掌握数据的CRUD操作(创建、读取、更新、删除)。
- 数据库设计与迁移:
设计数据库表结构,选择适当的数据类型和关系。
使用GORM的数据库迁移工具来确保数据库与代码的一致性。
- 状态管理:
学会使用React的状态管理工具,如useState和useEffect。
实现前端的状态管理,确保数据的一致性。
- 部署与优化:
部署前端和后端应用到生产环境,配置服务器、域名、SSL证书等。
进行性能优化,确保应用能够高效运行。
使用工具如Webpack来进行前端代码的优化。
- 错误处理与调试:
开发过程中,可能会遇到各种错误和问题。学会如何调试前端和后端代码。
实现友好的错误处理,向用户提供有用的错误信息。
成品展示
演示视频
1.错误提示
2.历史记录
2.科学计算器
3.利息计算
代码说明
1.前端
计算利息
const calculateInterest = () => {
// 根据term.Name 来计算存款时间
let depositTime = 0;
// term中分号前面是存款时间,后面是利率
const type = term.split(':')[0];
const rate = term.split(': ')[1].split('%')[0] / 100;
if (calculatorType === 'deposit') {
switch (type) {
case '活期存款':
depositTime = time;
break;
case '三个月':
depositTime = 0.25;
break;
case '半年':
depositTime = 0.5;
break;
case '一年':
depositTime = 1;
break;
case '二年':
depositTime = 2;
break;
case '三年':
depositTime = 3;
break;
case '五年"':
depositTime = 5;
break;
default:
depositTime = 0;
break;
}
} else {
switch (type) {
case '六个月':
depositTime = 0.5;
break;
case '一年':
depositTime = 1;
break;
case '一至三年':
depositTime = time;
break;
case '三至五年':
depositTime = time;
break;
case '五年以上':
depositTime = time;
break;
default:
depositTime = 0;
break;
}
}
// 计算利息
const interest = money * rate * depositTime;
console.log(money, rate, depositTime, interest, term);
setInterest(interest);
};
表达式计算
const equal = async () => {
if (areParenthesesMatching(text) === false) {
setText('ERR');
setStatus(1);
setValue('');
return;
}
try {
if (text !== '') {
const equalArr = text.split('');
equalArr[text.split('').length - 1];
if (sign === '^') {
setSign('');
setText(eval(mathPow(text)));
setResult(eval(mathPow(text)));
setStatus(1);
setValue('');
if ((text + '=' + eval(mathPow(text))).length < 20) {
axiosSave(text + '=' + eval(mathPow(text)));
}
let history = await axios
.get('http://203.15.1.138:8081/list')
.then((response) => {
return response.data.history;
});
setList(history);
return;
} else if (
(sign === '/' || sign === '%') &&
equalArr[equalArr.length - 1] === '0'
) {
setText('ERR');
setStatus(1);
setValue('');
return;
}
setText(eval(text));
setResult(eval(text));
setStatus(1);
setValue('');
if ((text + '=' + eval(text)).length < 20) {
axiosSave(text + '=' + eval(text));
}
let history = await axios
.get('http://203.15.1.138:8081/list')
.then((response) => {
return response.data.history;
});
setList(history);
}
} catch {
setText('ERR');
setStatus(1);
setValue('');
}
};
幂运算
const mathPow = (inputString) => {
let parts = inputString.split('^');
if (parts.length === 2) {
let x = parts[0];
let y = parts[1];
let resultString = `Math.pow(${x},${y})`;
return resultString;
} else {
console.log('输入字符串格式不正确');
return '';
}
};
回退
const del = () => {
if (text !== '') {
if (typeof text === 'number') {
// 计算结果后值为数字类型 强制转成字符串类型
setText(text.toString());
}
setText(text.substring(0, text.length - 1));
setStatus(0);
}
};
清零
const clear = () => {
setText('');
setStatus(0);
setValue('');
};
获取历史记录
const showAxios = async () => {
setShow(!show);
if (show) {
let history = await axios
.get('http://203.15.1.138:8081/list')
.then((response) => {
return response.data.history;
});
setList(history);
}
};
保存历史记录
function axiosSave(value) {
const postData = {
history: value,
};
axios
.post('http://203.15.1.138:8081/save', postData)
.then((response) => {
// 请求成功时的处理
console.log('成功响应:', response.data);
})
.catch((error) => {
// 请求失败时的处理
console.error('错误:', error);
});
}
获取利率表
useEffect(() => {
axios.get('http://203.15.1.138:8081/deposit').then((response) => {
setDeposit(response.data.deposit);
});
axios.get('http://203.15.1.138:8081/loan').then((response) => {
setLoan(response.data.loan);
});
}, []);
利率UI
<div className="content rounded-lg bg-[#c7eeff] fixed left-2 top-1/2 -translate-y-1/2">
<div className="overflow-x-auto">
<table className="table">
{/* head */}
<thead>
<tr className="text-sky-400">
<th>Deposit</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
{deposit.map((item, index) => {
return (
<tr key={index} className="hover:text-sky-400">
<td>{item.Name}</td>
<td>{item.Percentage}</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="overflow-x-auto">
<table className="table">
{/* head */}
<thead>
<tr className="text-sky-400">
<th>Loan</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
{loan.map((item, index) => {
return (
<tr key={index} className="hover:text-sky-400">
<td>{item.Name}</td>
<td>{item.Percentage}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
利息UI
<div className="content p-4 max-w-md mx-auto fixed left-52 top-1/2 -translate-y-1/2 bg-[#c7eeff] rounded-lg">
<h2 className="text-2xl font-semibold mb-4 text-sky-400">
Percentage Calculator
</h2>
<div className="mb-4 hover:text-sky-400">
<label className="block text-sm font-medium">Calculator Type:</label>
<select
value={calculatorType}
onChange={(e) => setCalculatorType(e.target.value)}
className="select select-bordered select-sm w-full max-w-xs">
<option value="loan">loan</option>
<option value="deposit">deposit</option>
</select>
</div>
<form>
<div className="mb-4 hover:text-sky-400">
<label htmlFor="term" className="block text-sm font-medium">
Time Term
</label>
<select
value={term}
onChange={(e) => setTerm(e.target.value)}
className="select select-bordered select-sm w-full max-w-xs">
{calculatorType === 'loan'
? loan.map((item, index) => (
<option key={index}>
{item.Name}: {item.Percentage}%
</option>
))
: deposit.map((item, index) => (
<option key={index}>
{item.Name}: {item.Percentage}%
</option>
))}
</select>
</div>
<div className="mb-4 hover:text-sky-400">
<label htmlFor="principal" className="block text-sm font-medium">
Money Amount:
</label>
<input
type="number"
id="principal"
className="input input-bordered input-accent input-sm w-full max-w-xs"
value={money}
onChange={(e) => setMoney(e.target.value)}
/>
</div>
<div className="mb-4 hover:text-sky-400">
<label htmlFor="principal" className="block text-sm font-medium">
Extra Time(year):
</label>
<input
type="number"
id="principal"
className="input input-bordered input-accent input-sm w-full max-w-xs"
value={time}
onChange={(e) => setTime(e.target.value)}
/>
</div>
<div
className="tooltip tooltip-right mt-2"
data-tip="如果Time Term是时间段, 需要额外填写时间">
<button
type="button"
className="btn btn-info text-white font-bold py-2 px-4 rounded-lg"
onClick={calculateInterest}>
Calculate
</button>
</div>
</form>
{interest !== 0 && (
<div className="mt-4">
<h3 className="text-lg font-semibold">Total Interest:</h3>
<p className="text-xl">${interest.toFixed(2)}</p>
</div>
)}
</div>
计算器UI
<div className="content rounded-lg p-6 pb-3 w-96 bg-[#c7eeff] fixed left-1/2 top-1/2 -translate-y-1/2 -translate-x-1/2">
{/* 显示区域 */}
<div className="display mb-4 border border-gray-200 border-solid rounded-lg bg-slate-100">
<div className="h-16 w-66 text-right leading-normal rounded-t-lg text-5xl mr-1 text-slate-500">
{text}
</div>
<div className="h-10 w-66 text-right leading-normal rounded-b-lg text-2xl mr-2 text-slate-500">
{value}
</div>
</div>
{/* 键盘区 */}
<div className="btn-cont pt-4">
<div className="grid gap-x-3 grid-cols-4 mb-5">
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl text-sky-600 hover:text-red-400"
onClick={del}>
Del
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl text-sky-600 hover:text-red-400"
onClick={clear}>
C
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
^
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
+
</button>
</div>
<div className="grid gap-x-3 grid-cols-4 mb-3">
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
7
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
8
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
9
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
-
</button>
</div>
<div className="grid gap-x-3 grid-cols-4 mb-3">
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
4
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
5
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
6
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
*
</button>
</div>
<div className="grid gap-x-3 grid-cols-4 mb-3">
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:fill-sky-400 fill-slate-400 flex items-center justify-center"
onClick={change}>
1
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
2
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
3
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
/
</button>
</div>
<div className="grid gap-x-3 grid-cols-4 mb-3">
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
0
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
.
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
%
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl text-sky-600 hover:text-sky-700"
onClick={equal}>
=
</button>
</div>
<div className="grid gap-x-3 grid-cols-4 mb-3">
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:fill-sky-400 fill-slate-400 flex items-center justify-center"
onClick={showAxios}>
<svg
className="w-6 h-6"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg">
<path
d="M511.998 64C264.574 64 64 264.574 64 511.998S264.574 960 511.998 960 960 759.422 960 511.998 759.422 64 511.998 64z m353.851 597.438c-82.215 194.648-306.657 285.794-501.306 203.579S78.749 558.36 160.964 363.711 467.621 77.917 662.27 160.132c168.009 70.963 262.57 250.652 225.926 429.313a383.995 383.995 0 0 1-22.347 71.993z"
className="fill-inherit"></path>
<path
d="M543.311 498.639V256.121c0-17.657-14.314-31.97-31.97-31.97s-31.97 14.314-31.97 31.97v269.005l201.481 201.481c12.485 12.485 32.728 12.485 45.213 0s12.485-32.728 0-45.213L543.311 498.639z"
className="fill-inherit"></path>
</svg>
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
(
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={change}>
)
</button>
<button
className="text-2xl m-2 w-14 h-14 rounded-3xl hover:text-sky-400"
onClick={() => {
setText(text + result.toString());
}}>
Ans
</button>
</div>
</div>
</div>
历史记录UI
{show && list.length != 0 && (
<div className="content rounded-lg p-6 pb-3 bg-[#c7eeff] fixed right-36 top-1/2 -translate-y-1/2 text-2xl">
<div className="text-2xl font-semibold mb-4 text-sky-400">
History
</div>
{list.map((item, index) => {
return (
<div
className="rounded-lg mb-3 display hover:text-sky-400"
key={index}>
{index + '=>' + item.History}
</div>
);
})}
</div>
)}
2.后端
类型包装
type CalculatorHistory struct {
Id uint `gorm:"primaryKey"`
History string
Date time.Time `gorm:"type:timestamp"`
}
type TempSource struct {
Name string
Percentage float64
}
后端跨域
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
连接数据库
dsn := "xxx:xxx@tcp(siau.top:3307)/homework?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println("连接数据库失败")
return
}
服务代码
r := gin.Default()
r.Use(corsMiddleware()) // 一定要有跨域
r.POST("/save", func(c *gin.Context) {
var requestData struct {
History string `json:"history"`
}
if err := c.BindJSON(&requestData); err != nil {
c.JSON(200, gin.H{
"message": "error",
})
return
}
var historys []CalculatorHistory
db.Debug().Limit(-1).Order("date").Find(&historys)
if len(historys) >= 10 {
db.Debug().Delete(&historys[0])
}
calculatorHistory := CalculatorHistory{History: requestData.History, Date: time.Now()}
db.Debug().Create(&calculatorHistory)
c.JSON(200, gin.H{
"message": "success",
})
})
r.GET("/list", func(c *gin.Context) {
var historys []CalculatorHistory
db.Debug().Limit(-1).Order("date").Find(&historys) // 用Find
c.JSON(200, gin.H{
"message": "success",
"history": historys,
})
})
r.GET("/deposit", func(c *gin.Context) {
var tempSources []TempSource
tempSources = append(tempSources, TempSource{Name: "活期存款", Percentage: 0.5})
tempSources = append(tempSources, TempSource{Name: "三个月", Percentage: 2.85})
tempSources = append(tempSources, TempSource{Name: "半年", Percentage: 3.05})
tempSources = append(tempSources, TempSource{Name: "一年", Percentage: 3.25})
tempSources = append(tempSources, TempSource{Name: "二年", Percentage: 4.15})
tempSources = append(tempSources, TempSource{Name: "三年", Percentage: 4.75})
tempSources = append(tempSources, TempSource{Name: "五年", Percentage: 5.25})
c.JSON(200, gin.H{
"message": "success",
"deposit": tempSources,
})
})
r.GET("/loan", func(c *gin.Context) {
var tempSources []TempSource
tempSources = append(tempSources, TempSource{Name: "六个月", Percentage: 5.85})
tempSources = append(tempSources, TempSource{Name: "一年", Percentage: 6.31})
tempSources = append(tempSources, TempSource{Name: "一至三年", Percentage: 6.40})
tempSources = append(tempSources, TempSource{Name: "三至五年", Percentage: 6.65})
tempSources = append(tempSources, TempSource{Name: "五年以上", Percentage: 6.80})
c.JSON(200, gin.H{
"message": "success",
"loan": tempSources,
})
})
err = r.Run(":8081")
if err != nil {
return
} // 监听并在 0.0.0.0:8080 上启动服务
3.数据库
心路历程与收获
在开发这个全栈Web应用的过程中,我获得了丰富的经验和收获。这个项目让我深入了解了前端和后端的工作流程,以及它们之间的协作。我学到了如何使用React构建交互式用户界面,如何创建和管理前端状态,以及如何处理用户输入。同时,我也掌握了使用Vite这一现代前端构建工具的能力,它为项目提供了快速的开发和热更新功能。
在后端方面,我学会了使用Gin框架和GORM库来创建API端点和管理数据库。我了解了RESTful API的设计原则,实现了用户认证和授权功能,以确保数据的安全性。此外,我还熟悉了数据库的设计和迁移,以及如何执行各种数据库操作。
总的来说,这个项目让我成为一个更全面的开发者,具备了前端和后端开发的技能。我学到了如何将不同的技术堆栈整合到一个完整的应用中,解决了许多挑战和问题。这个经验也使我更有信心地面对未来的全栈开发项目,同时也激发了我对不断学习和提高技能的渴望。