终于有时间继续学习技术了!开发了一个简易的用于记录日常工作内容的小软件,权当学习和练手。功能如下:用户登录、日志内容的查、增、删、改以及导出。
开发环境:
windows 10,mysql 8,Hbuilder X(最近vs code总是崩溃)
生产环境:
windows,mysql, Nginx,Waitress
1 数据库设计
数据库名:mywork
users表结构:
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`un` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pwd` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`truename` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`uid`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
works表结构:
DROP TABLE IF EXISTS `works`;
CREATE TABLE `works` (
`workid` int(11) NOT NULL AUTO_INCREMENT,
`date` date NULL DEFAULT NULL,
`weekday` varchar(9) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
`worktype` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`supervisetype` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`remark` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
PRIMARY KEY (`workid`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2 前端实现
21.1 创建vue3 项目
npm init vue@3
cd frontend[这是我的前端项目名称]
npm install
npm install axios --save
npm install element-plus --save
npm install xlsx --save
2.2 修改 index.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工作日志本</title>
<style>
#app{
background-image: url('/src/assets/meetingroom.jpg');
background-size: cover;
background-repeat: no-repeat;
height: 98vh;
width: 99vw;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
2.3 修改main.js文件
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import axios from 'axios'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn' //设置前端时区
axios.defaults.withCredentials = true //允许跨域
axios.defaults.baseURL = 'http://127.0.0.1:8080/' //设置后端api前缀
const app = createApp(App)
app.use(ElementPlus, {
locale: zhCn,
})
app.use(router)
app.mount('#app')
2.4 修改App.vue文件
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style scoped>
</style>
2.5 修改router/index.js路由配置文件
import { createRouter, createWebHistory } from 'vue-router'
import login from '../components/login.vue'
import index from '../components/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: login
},
{
path: '/',
name: 'index',
component: index
},{
path: '/index',
name: 'index',
component: index
}
]
})
export default router
2.6 login.vue文件(登录组件)
<template>
<div>
<div style="padding-top: 20rem;"></div>
<div class="loginBox" @keypress.enter="submithandle">
<div style="text-align: center;">
<h1>工作日志本</h1>
</div>
<div>
<el-form-item label="用户名">
<el-input v-model="un" />
</el-form-item>
</div>
<div>
<el-form-item label="密 码">
<el-input v-model="pwd" type="password" autocomplete="off" />
</el-form-item>
</div>
<div style="text-align: center;">
<el-button type="primary" round @click="submithandle">提交</el-button>
<el-button type="info" round>重置</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import axios from "axios";
import { ElMessage } from 'element-plus'
const un = ref('')
const pwd = ref('')
const curRouter = useRouter()
function submithandle() {
if (un.value == "" || pwd.value == "") {
ElMessage({
message: '请填完整再提交。',
type: 'error',
})
}
else {
axios.post("/login", { "un": un.value, "pwd": pwd.value }).then(rs => {
if (rs.data.code == 200) {
ElMessage({
message: '登录成功!',
type: 'success',
})
curRouter.push('/')
}
else {
ElMessage({
message: '用户名或密码不正确,请重试',
type: 'error',
})
}
})
}
}
</script>
<style>
.loginBox{
width: 30rem;
margin: 0 auto;
border: 1px solid gray;
border-radius: 5px;
padding: 1rem;
background-color: rgba(255, 255, 255, 0.8);
}
</style>
2.7 index.vue文件(核心功能组件)
<template>
<div>
<el-container>
<!-- 菜单栏 -->
<el-header
style="background: linear-gradient(90deg, #409eff 50%, #a12cc1 100%);border-bottom: 2px solid white;">
<el-row>
<el-col :span="8"></el-col>
<el-col :span="8" style="text-align: center;color: white;">
<h1>工作日志本</h1>
</el-col>
<el-col :span="8" style="text-align: right;">
<el-dropdown style="line-height: 70px;">
<span class="el-dropdown-link" style="color: white;">
{{ truename }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
</el-row>
</el-header>
<!-- 工作区 -->
<el-main>
<div>
<el-button type="success" @click="showDraw('添加')">添加</el-button>  
<el-date-picker v-model="queryformdata.querydate" type="daterange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" size="default" value-format="YYYY-MM-DD" format="YYYY-MM-DD" clearable /> 
<el-select v-model="queryformdata.worktype" placeholder="--工作类型--" style="width: 150px" clearable><el-option
v-for="item in worktypelist" :key="item.value" :label="item.label" :value="item.value" />
</el-select> 
<el-select v-model="queryformdata.supervisetype" placeholder="--监督方式--" style="width: 150px" clearable><el-option
v-for="item in supervisetypelist" :key="item.value" :label="item.label"
:value="item.value" />
</el-select> 
<el-input v-model="queryformdata.content" style="width: 240px" placeholder="关键词"
:prefix-icon="Search" clearable/> 
<el-button type="success" @click="handleQuery">查询</el-button>
<el-button type="success" @click="handleExport">导出</el-button>
<el-button type="success" @click="getTableData">全部</el-button>
</div>
<el-divider />
<div>
<el-table
:data="tableData.slice((statePager.currentPage - 1) * pagesize, statePager.currentPage * pagesize)"
border style="width: 100%">
<el-table-column prop="workid" label="编号" width="80" />
<el-table-column prop="date" label="日期" width="150" />
<el-table-column prop="weekday" label="星期" width="100" />
<el-table-column prop="content" label="工作内容" width="800" />
<el-table-column prop="worktype" label="工作类型" width="150" />
<el-table-column prop="supervisetype" label="监督方式" width="150" />
<el-table-column prop="remark" label="备注" />
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button @click="handleEdit(scope.$index, scope.row, '修改')" type="primary">
修改
</el-button>
<el-button type="danger" @click="handleDelete(scope.$index, scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div style="background-color: white;padding-top: 1rem;">
<el-pagination @current-change="handleCurrentChange" layout="total, prev, pager, next"
:page-size="pagesize" background :total="tableData.length"></el-pagination>
</div>
</el-main>
</el-container>
</div>
<!-- 编辑数据抽屉 -->
<el-drawer v-model="isshowdrawer" direction="rtl" size="500px">
<template #header>
<h4>{{ acttype }}</h4>
</template>
<template #default>
<div>
<el-form :model="formData" label-width="auto" style="max-width: 600px">
<el-form-item label="  日期">
<el-col>
<el-date-picker v-model="formData.date" type="date" placeholder="选择日期" style="width: 100%"
clearable="" value-format="YYYY-MM-DD" format="YYYY-MM-DD" />
</el-col>
</el-form-item>
<el-form-item label="工作内容">
<el-input v-model="formData.content" type="textarea" :rows="8" />
</el-form-item>
<el-form-item label="工作类型">
<el-select v-model="formData.worktype" placeholder="--请选择--" clearable><el-option
v-for="item in worktypelist" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="监督方式">
<el-select v-model="formData.supervisetype" placeholder="--请选择--" clearable><el-option
v-for="item in supervisetypelist" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="  备注">
<el-input v-model="formData.remark" type="text" />
</el-form-item>
</el-form>
</div>
</template>
<template #footer>
<div style="flex: auto">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">确定</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup>
// 引入功能
import {
ref,
onMounted,
reactive
} from "vue";
import {
useRouter
} from "vue-router";
import axios from "axios";
import {
ElMessage,
ElMessageBox
} from 'element-plus'
import {
Search
} from '@element-plus/icons-vue'
import * as XLSX from "xlsx";
// 以下为定义组件所需变量
const curRouter = useRouter() //用于路由跳转的变量
//接收后端传来的登录用户信息
const uid = ref('')
const un = ref('')
const truename = ref('')
//查询功能相关变量
const queryformdata=reactive({
querydate:'',
querydatefrom:'',
querydateto:'',
worktype:'',
supervisetype:'',
content:''
})
// 工作类型选项
const worktypelist = [{
value: '111',
label: '111',
}, {
value: '222',
label: '222',
}]
// 工作方式选项
const supervisetypelist = [{
value: '111',
label: '111',
}, {
value: '222',
label: '222',
}
]
const querykeywords = ref('')
//数据表呈现
const tableData = ref([]) //表格数据
const statePager = reactive({
currentPage: 1
}); //用于保存当前表格页的变量
const pagesize = ref(10) //表格中每页数据条数
// 表格翻页按钮
const handleCurrentChange = (e) => {
statePager.currentPage = e;
};
//抽屉相关
const isshowdrawer = ref(false) //控制抽屉是否展现,默认不展现
const acttype = ref('') //记录用户打开抽屉的行为类型,拟为“添加”和“修改”
const formData = reactive({
workid: '',
date: '',
content: '',
worktype: '',
supervisetype: '',
remark: ''
}) //保存抽屉中表单数据的变量
//组件加载时的动作,作用:一是检测是否登录,二是获取用户信息并拉取表格数据
onMounted(() => {
// 检测登录状态
axios.post("login/islog").then((rs) => {
// console.log(rs.data.code)
if (rs.data.code != 200) {
ElMessage({
message: '您尚未登录,即将跳转至登录页面。',
type: 'error',
})
curRouter.push('/login');
} else {
uid.value = rs.data.uid;
un.value = rs.data.un;
truename.value = rs.data.truename;
getTableData()
}
})
})
// 退出登录
function logout() {
ElMessageBox.confirm(
'确定要退出登录吗?',
'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
axios.post("login/logout").then((rs) => {
if (rs.data.code == 200) {
curRouter.push('/login');
}
})
})
.catch(() => {
})
}
// 拉取表格数据
function getTableData() {
axios.get("works").then((rs) => {
tableData.value = rs.data.data;
})
}
//查询函数
function handleQuery(){
if(queryformdata.querydate=='' && queryformdata.worktype=='' && queryformdata.supervisetype=='')
{
getTableData()
}
else
{
axios.post("works/filter",{data: queryformdata}).then((rs)=>{
tableData.value = rs.data.data;
})
}
}
function handleExport(){
const titleArr = ['编号','日期','星期','工作内容','工作类型','工作方式','备注','操作']//表头中文名
exportExcel(tableData.value, '我的工作日志', titleArr, 'sheetName');
}
/*
把表格数据导出到excel中的函数
* @description:
* @param {Object} json 服务端发过来的数据
* @param {String} name 导出Excel文件名字
* @param {String} titleArr 导出Excel表头
* @param {String} sheetName 导出sheetName名字
* @return:
*/
function exportExcel(json, name, titleArr, sheetName) {
/* convert state to workbook */
var data = new Array();
var keyArray = new Array();
const getLength = function (obj) {
var count = 0;
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
count++;
}
}
return count;
};
for (const key1 in json) {
if (json.hasOwnProperty(key1)) {
const element = json[key1];
var rowDataArray = new Array();
for (const key2 in element) {
if (element.hasOwnProperty(key2)) {
const element2 = element[key2];
rowDataArray.push(element2);
if (keyArray.length < getLength(element)) {
keyArray.push(key2);
}
}
}
data.push(rowDataArray);
}
}
// keyArray为英文字段表头
data.splice(0, 0, keyArray, titleArr);
console.log('data', data);
const ws = XLSX.utils.aoa_to_sheet(data);
const wb = XLSX.utils.book_new();
// 此处隐藏英文字段表头
var wsrows = [{ hidden: true }];
ws['!rows'] = wsrows; // ws - worksheet
XLSX.utils.book_append_sheet(wb, ws, sheetName);
/* generate file and send to client */
XLSX.writeFile(wb, name + '.xlsx');
}
// 用户点击数据表中编辑按钮的事件函数,意图是把表格中的某行数据读取到表单中
function handleEdit(index, row, tempacttype) {
isshowdrawer.value = true
acttype.value = tempacttype
formData.workid = row.workid
formData.date = row.date
formData.content = row.content
formData.worktype = row.worktype
formData.supervisetype = row.supervisetype
formData.remark = row.remark
}
// 用户点击数据表中删除按钮的事件函数,
function handleDelete(index, row) {
ElMessageBox.confirm('确定要删除编号为【' + row.workid + '】的内容吗?', 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
axios.delete("works/" + row.workid).then((rs) => {
if (rs.data.code == 200) {
getTableData()
}
})
})
.catch(() => {})
}
//控制抽屉展现的函数
function showDraw(tempacttype) {
isshowdrawer.value = true
acttype.value = tempacttype
formData.workid = ''
}
//控制抽屉关闭的函数
function cancelClick() {
isshowdrawer.value = false
}
// 用户点击抽屉中“确定”按钮事件的函数,
function confirmClick() {
//如果意图是新增
if (acttype.value == '添加') {
axios.post('works', {
data: formData
}).then((rs) => {
if (rs.data.code == 200) {
getTableData()
ElMessage({
message: '添加成功,您可以继续添加。',
type: 'success',
})
} else {
ElMessage({
message: '添加失败,请重试或联系开发人员。',
type: 'error',
})
}
})
}
// 如果意图是编辑
else if (acttype.value == '修改') {
axios.put('works/' + formData.workid, {
data: formData
}).then((rs) => {
console.log(formData)
if (rs.data.code == 200) {
getTableData()
isshowdrawer.value = false
ElMessage({
message: '修改成功!',
type: 'success',
})
} else {
ElMessage({
message: '修改失败,请重试或联系开发人员。',
type: 'error',
})
}
})
}
}
</script>
<style scoped>
.el-header {
--el-header-height: 70px;
}
.el-divider--horizontal {
margin: 0.5rem 0;
}
.el-table {
--el-table-text-color: #000000;
font-size: 16px;
}
</style>
3 后端实现
后端使用Python Flask框架实现,使用Blueprint功能,开发了RESTfull风格的相关接口。
3.1 后端主入口文件app.py
from flask import Flask
from flask import request
import pymysql
import json
from flask_cors import CORS
from flask import session
import time
import os
from flask import Blueprint, jsonify
#导入两个Blueprint模块
from api.login import login
from api.works import works
#设置时区
os.environ['TZ'] = 'Asia/Shanghai'
app = Flask(__name__)
#json数据不根据key名进行排序
app.config['JSON_SORT_KEYS'] = False
# 以下代码设置程序代码更新后自动重启web服务,仅用于开发过程中。
app.debug = True
app.config.update(DEBUG=True)
CORS(app, supports_credentials=True) #允许跨域访问
#以下代码解决跨域时设置session值无效的问题
app.config['SESSION_COOKIE_SAMESITE'] = "None" # 设置samesite 为None
app.config['SESSION_COOKIE_SECURE'] = True # SECURE 为 true
app.secret_key="guoxiyue"
#注册两个Blueprint实例
app.register_blueprint(login,url_prefix='/')
app.register_blueprint(works,url_prefix='/')
if __name__ == '__main__':
app.run(debug=True)
3.2 api/login.py文件
from flask import Blueprint, jsonify, request,session
import pymysql
db = pymysql.connect(host="localhost", port=3306, user='root', password='123456', charset='utf8', database='mywork', cursorclass=pymysql.cursors.DictCursor) #连接数据库
mycursor = db.cursor() #创建游标对象
login=Blueprint('login',__name__)
# 用户登录
@login.route(rule="/login",methods=['post'])
def dologin():
data=request.json
un=data['un']
pwd=data['pwd']
#查询数据库
sql = "select * from users where un='"+un+"' and pwd=md5('"+pwd+"')"
mycursor.execute(sql)
rs = mycursor.fetchone()
#处理查询结果
if rs is not None: #如果查询结果不为空,则创建session
session['uid']=rs['uid']
session['un']=rs['un']
session['truename']=rs['truename']
return jsonify({'code':200,'uid':rs['uid'],'un':rs['un'],'truename':rs['truename']})
else:
return jsonify({'code':400})
# 查询登录状态
@login.route(rule="/login/islog",methods=['post'])
def islog():
un = session.get('un')
if un is None:
return jsonify({"code":404,"msg":"not logged"})
else:
return jsonify({'code':200,'uid':session.get('uid'),'un':session.get('un'),'truename':session.get('truename')})
# 退出登录
@login.route(rule="/login/logout",methods=['post'])
def logout():
session.clear()
return jsonify({"code":200,"msg":"logout"})
3.3 api/works.py文件
from flask import Blueprint, jsonify,request
import pymysql
db = pymysql.connect(host="localhost", port=3306, user='root', password='123456', charset='utf8', database='mywork', cursorclass=pymysql.cursors.DictCursor) #连接数据库
mycursor = db.cursor() #创建游标对象
mycursor.execute("SET @@lc_time_names = 'zh_CN';")
works=Blueprint('works',__name__)
# 查询所有数据(即无条件查询)
@works.route(rule="/works",methods=['GET'])
def getworksList():
sql = "select workid,date_format(`date`,'%Y-%m-%d') as `date`,DAYNAME(`date`) as `weekday`,content,worktype,supervisetype,remark from works order by `date` desc,workid desc"
mycursor.execute(sql)
rs=mycursor.fetchall()
return jsonify({"code":200,"data":rs})
# 查询部分数据(即有条件查询)
@works.route(rule="/works/filter",methods=['post'])
def getFilteWorksList():
filterfields=""
data=request.json
# print(data)
if data['data']['querydate'] is not None and data['data']['querydate']!="":
querydatefrom=data['data']['querydate'][0]
querydateto=data['data']['querydate'][1]
filterfields+=" `date` between '"+querydatefrom+"' and '"+querydateto+"' "
if 'worktype' in data['data'] and data['data']['worktype'] is not None and data['data']['worktype']!="":
filterfields+=" and `worktype` = '"+data['data']['worktype']+"' "
if 'supervisetype' in data['data'] and data['data']['supervisetype'] is not None and data['data']['supervisetype']!="":
filterfields+="and `supervisetype` = '"+data['data']['supervisetype']+"' "
if data['data']['content'] is not None and data['data']['content']!="":
filterfields+="and `content` like '%"+data['data']['content']+"%' "
filterfields=filterfields.strip(" ") #去掉首尾空格
if filterfields[0:3]=="and":
filterfields=filterfields[3:] #去掉打头的and
sql = "select workid,date_format(`date`,'%Y-%m-%d') as `date`,DAYNAME(`date`) as `weekday`,content,worktype,supervisetype,remark from works where "+filterfields+" order by `date` desc,workid desc"
mycursor.execute(sql)
rs=mycursor.fetchall()
# print(sql)
return jsonify({"code":200,'data':rs})
# 查询单条记录
@works.route(rule="/works/<workid>",methods=['GET'])
def getworksById(workid):
# tempid = request.args.get('worksid')
return jsonify({"code":200,"msg":"get workid: "+str(workid)+", OK!"})
# 新增记录
@works.route(rule="/works/",methods=['POST'])
def addUser():
data=request.json
date=data['data']['date'][0:10]
content=data['data']['content']
worktype=data['data']['worktype']
if "supervisetype" in data['data']:
tempsql=data['data']['supervisetype']
else:
tempsql=""
# supervisetype=data['data']['supervisetype']
remark=data['data']['remark']
sql="insert into works values(null,'"+date+"',null,'"+content+"','"+worktype+"','"+tempsql+"','"+remark+"')"
# print(sql)
mycursor.execute(sql)
db.commit()
return jsonify({"code":200,"msg":"insert works, OK!"})
# 修改记录
@works.route(rule="/works/<int:workid>",methods=['PUT'])
def updateUser(workid):
data=request.json
date=data['data']['date'][0:10]
content=data['data']['content']
worktype=data['data']['worktype']
supervisetype=data['data']['supervisetype']
remark=data['data']['remark']
sql="update works set `date`='"+date+"',content='"+content+"',worktype='"+worktype+"',supervisetype='"+supervisetype+"',remark='"+remark+"' where workid="+str(workid)
mycursor.execute(sql)
db.commit()
return jsonify({"code":200,"msg":"update worksid: "+str(workid)+", OK!"})
# 删除记录
@works.route(rule="/works/<int:workid>",methods=['DELETE'])
def deleteUser(workid):
sql="delete from works where workid="+str(workid)
mycursor.execute(sql)
db.commit()
return jsonify({"code":200,"msg":"delete worksid: "+str(workid)+", OK!"})
4 生产环境部署
这里采用Nginx+Waitress来实现。Nginx和Waitress的安装过程略。
4.1 编译前端项目
在前端项目根目录运行以下命令,得到编译后的项目文件,在dist目录中
npm run build
把dist目录内容复制到Nginx的www目录下即可完成前端程序的部署。
4.2 配置后端项目
在后端项目的根目录下,创建run.bat文件,内容如下:
start waitress-serve --listen=127.0.0.1:5000 app:app
双击run.bat即可运行Waitress服务。
打开nginx的配置文件,在http模块中添加以下内容:
server{
listen 8080;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
以上代码的意思是,通过nginx服务来反向代理Waitress运行的Flask程序。即:把http://127.0.0.1:5000 访问转为http://127.0.0.1:8080,这样就实现了由Nginx来接管Flask程序的运行,但是运行Flask的Waitressp进程窗口不能关闭。重启Nginx后生效。
4.3 生产环境所需服务
一是MySQL服务,二是Nginx服务,三是Waitress进程,以上三者全部运行以后,打开浏览器,访问 http://127.0.0.1 即可运行项目。
5 运行界面截图