添加历史图表

This commit is contained in:
ZaneYork 2024-09-19 16:53:25 +08:00
parent a7bd6b83a3
commit a48ab84358
6 changed files with 314 additions and 137 deletions

View File

@ -64,11 +64,12 @@ def set_user_menu(menu: dict[str, float], nickname: str, dislike: list[str] = No
db.close() db.close()
def fetch_all_user_today_menu() -> Generator[UserMenu, None, None]: def fetch_all_user_menu(datestr: str = None) -> Generator[UserMenu, None, None]:
""" """
获取所有用户今日的投票内容 获取所有用户今日的投票内容
""" """
datestr = datetime.now().strftime('%Y-%m-%d') if not datestr:
datestr = datetime.now().strftime('%Y-%m-%d')
db = sqlite3.connect(db_path) db = sqlite3.connect(db_path)
cursor = db.cursor() cursor = db.cursor()
try: try:

View File

@ -8,12 +8,16 @@ from dao import *
from utils import is_mobile_request from utils import is_mobile_request
def fetch_user_menu_summary() -> tuple[defaultdict[str, float], list[str], list[tuple[str, list[str]]]]: def fetch_user_menu_summary(datestr: str = None) -> tuple[
defaultdict[str, float],
list[str],
list[tuple[str, list[str]]]
]:
""" """
获取今天所有人的投票汇总 获取今天所有人的投票汇总
:return: 品类->数量 :return: 品类->数量
""" """
all_menu: list[UserMenu] = list(fetch_all_user_today_menu()) all_menu: list[UserMenu] = list(fetch_all_user_menu(datestr))
if len(all_menu) > 0: if len(all_menu) > 0:
menus = list(map(lambda x: x.menu, all_menu)) menus = list(map(lambda x: x.menu, all_menu))
users = list(map(lambda x: x.nickname, all_menu)) users = list(map(lambda x: x.nickname, all_menu))
@ -156,6 +160,19 @@ def compute_eat_users(dislikes, result, users):
return eat_users return eat_users
@app.route('/chart')
def chart():
datestr = request.args.get('date')
recent_results: list[RollResult] = list(fetch_roll_result_list(-1, 7))
menus, users, dislikes = fetch_user_menu_summary(datestr)
return render_template('chart.html',
date=datestr,
menus=menus,
users=users,
recent_results=recent_results
)
@app.route('/dinner') @app.route('/dinner')
def dinner(): def dinner():
""" """

45
static/echarts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

33
templates/base.html Normal file
View File

@ -0,0 +1,33 @@
<html lang="zh">
<head>
{% block head %}
<title>{% block title %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/bootstrap.min.css">
{% endblock %}
</head>
<body>
<div class="container text-center pt-5">
{% block content %}{% endblock %}
</div>
<footer class="footer mt-auto py-3 bg-light">
<div class="container">
<div class="row">
<div class="col-12">
<a class="link-secondary link-offset-2"
href="https://git.zaneyork.cn:8443/ZaneYork/dinner_vote">本项目抽签完全公开透明,源码开放欢迎随时审查</a>
</div>
</div>
<div class="row">
<div class="col-12">
<span style="font-size: 0.7rem">Copyright © <script>document.write(new Date().getFullYear().toString())</script> Zane York. All Rights Reserved.</span>
</div>
</div>
</div>
</footer>
{% block script %}
<script src="/static/jquery-3.2.1.min.js"></script>
<script src="/static/jquery.serializejson.js"></script>
<script src="/static/bootstrap.bundle.min.js"></script>
{% endblock %}
</body>

97
templates/chart.html Normal file
View File

@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block title %}晚餐吃什么鸭{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
<div class="pt-1 mb-3">
<div class="row">
<div class="col-10">
<select id="dateSelect" class="form-select" aria-label="日期选择">
{% for recent_result in recent_results %}
<option value="{{ recent_result.datestr }}" {{ 'selected' if date==recent_result.datestr else '' }}>
{{ recent_result.datestr }}
</option>
{% endfor %}
</select>
</div>
<div class="col-1">
<button type="button" class="btn btn-primary" onclick="back()">返回</button>
</div>
</div>
</div>
<div id="main" style="width: 100%;height:400px;"></div>
{% endblock %}
{% block script %}
{{ super() }}
<script src="/static/echarts.min.js"></script>
<!--suppress JSSuspiciousNameCombination -->
<script>
function back() {
window.location.href = '/dinner';
}
$("#dateSelect").change(function () {
window.location.href = 'chart?date=' + $("#dateSelect").val();
});
const chartEl = document.getElementById('main');
const menus = {{ menus | tojson }};
const legendData = [], seriesData = [];
for (let menu in menus) {
legendData.push(menu);
seriesData.push({value: menus[menu], name: menu});
}
const option = {
title: {
text: '{{date}}',
left: 'center',
top: 'center'
},
legend: {
orient: 'horizontal',
left: 20,
right: 20,
top: 10,
data: legendData
},
series: [
{
type: 'pie',
radius: ['30%', '35%'],
center: ['50%', '50%'],
data: seriesData,
label: {
show: true,
position: 'outside',
color: '#1B233E',
formatter: function (params) {
return params.name + ' ' + params.percent + '%'
}
},
}
]
};
function resizeChart() {
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
if (windowWidth > windowHeight) {
windowHeight = 600;
} else {
windowHeight = windowWidth * 110 / 100;
}
chartEl.style.width = windowWidth * 95 / 100 + "px";
chartEl.style.height = windowHeight + "px";
}
resizeChart();
const mainChart = echarts.init(chartEl);
window.addEventListener('resize', function () {
resizeChart()
mainChart.resize();
});
mainChart.setOption(option);
</script>
{% endblock %}

View File

@ -1,11 +1,9 @@
<html lang="zh"> {% extends "base.html" %}
<head> {% block title %}晚餐吃什么鸭{% endblock %}
<title>晚餐吃什么鸭</title> {% block head %}
<meta name="viewport" content="width=device-width, initial-scale=1"> {{ super() }}
<link rel="stylesheet" href="/static/bootstrap.min.css"> {% endblock %}
</head> {% block content %}
<body>
<div class="container text-center pt-5">
<p>每天8:00-17:30间开放匿名投票更新17:30以后允许发起抽签抽签结果确定后不可更改</p> <p>每天8:00-17:30间开放匿名投票更新17:30以后允许发起抽签抽签结果确定后不可更改</p>
<p>第一名与第二名得票数相差不超过10%时随机抽签,按其得票数决定中签概率,否则选择第一名</p> <p>第一名与第二名得票数相差不超过10%时随机抽签,按其得票数决定中签概率,否则选择第一名</p>
<p>投票结果仅供参考,最终解释权归部门总经理、副总经理所有</p> <p>投票结果仅供参考,最终解释权归部门总经理、副总经理所有</p>
@ -17,7 +15,8 @@
<div class="mb-3"> <div class="mb-3">
<div class="input-group"> <div class="input-group">
<label class="input-group-text" for="inputName" id="basic-addon1">姓名</label> <label class="input-group-text" for="inputName" id="basic-addon1">姓名</label>
<input type="text" class="form-control" {{ 'readonly="readonly"' if user_menu.nickname else '' | safe }} <input type="text"
class="form-control" {{ 'readonly="readonly"' if user_menu.nickname else '' | safe }}
id="inputName" name="nickname" value="{{ user_menu.nickname }}"> id="inputName" name="nickname" value="{{ user_menu.nickname }}">
</div> </div>
</div> </div>
@ -92,141 +91,126 @@
<label class="mb-2">最近点餐结果</label> <label class="mb-2">最近点餐结果</label>
<ul class="list-group"> <ul class="list-group">
{% for recent_result in recent_results %} {% for recent_result in recent_results %}
<li class="list-group-item d-flex justify-content-between align-items-center"> <li onclick="viewChart('{{ recent_result.datestr }}')" class="list-group-item d-flex justify-content-between align-items-center">
<span>{{ recent_result.value }}</span> <span>{{ recent_result.value }}</span>
<span>{{ recent_result.datestr }}</span> <span>{{ recent_result.datestr }}</span>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<footer class="footer mt-auto py-3 bg-light"> {% endblock %}
<div class="container"> {% block script %}
<div class="row"> {{ super() }}
<div class="col-12"> <!--suppress JSUnresolvedReference -->
<a class="link-secondary link-offset-2" <script>
href="https://git.zaneyork.cn:8443/ZaneYork/dinner_vote">本项目抽签完全公开透明,源码开放欢迎随时审查</a> document.addEventListener('keydown', function (event) {
</div> if (event.key === 'Enter' || event.keyCode === 13) {
</div> update();
<div class="row"> return false;
<div class="col-12">
<span style="font-size: 0.7rem">Copyright © <script>document.write(new Date().getFullYear().toString())</script> Zane York. All Rights Reserved.</span>
</div>
</div>
</div>
</footer>
</div>
<script src="/static/jquery-3.2.1.min.js"></script>
<script src="/static/jquery.serializejson.js"></script>
<script src="/static/bootstrap.bundle.min.js"></script>
<!--suppress JSUnresolvedReference -->
<script>
document.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.keyCode === 13) {
update();
return false;
}
});
const rangesInput = $("input[type=range]");
const checkBoxs = $("input[type=checkbox]")
rangesInput.change(function () {
const values = rangesInput.map(function () {
return $(this).val();
}).get();
const summary = values.reduce((a, b) => parseInt(a) + parseInt(b));
const spans = $(".percentage")
for (let i = 0; i < values.length; i++) {
spans[i].innerHTML = summary > 0 ? (parseFloat(values[i]) * 100 / summary).toFixed(2) : 0;
if (values[i] > 0 && checkBoxs[i].checked) {
checkBoxs[i].checked = false;
} }
} });
}); const rangesInput = $("input[type=range]");
checkBoxs.change(function () { const checkBoxs = $("input[type=checkbox]")
const data = $('#inputForm').serializeJSON(); rangesInput.change(function () {
const values = checkBoxs.map(function () { const values = rangesInput.map(function () {
return $(this).val(); return $(this).val();
}).get(); }).get();
for (let i = 0; i < values.length; i++) { const summary = values.reduce((a, b) => parseInt(a) + parseInt(b));
if (data.dislike.includes(values[i])) { const spans = $(".percentage")
$(rangesInput[i]).val(0); for (let i = 0; i < values.length; i++) {
} spans[i].innerHTML = summary > 0 ? (parseFloat(values[i]) * 100 / summary).toFixed(2) : 0;
} if (values[i] > 0 && checkBoxs[i].checked) {
$(rangesInput[0]).trigger('change'); checkBoxs[i].checked = false;
});
function update() {
const data = $('#inputForm').serializeJSON();
const nickname = data.nickname;
data.nickname = undefined;
$.ajax({
url: 'dinner/update?nickname=' + nickname + '&value=' + JSON.stringify(data),
dataType: 'json',
success: function (result) {
if (result.code === 0) {
window.location.reload();
} else {
alert('更新失败: ' + result.data)
} }
} }
}) });
} checkBoxs.change(function () {
const data = $('#inputForm').serializeJSON();
function clearValue() { const values = checkBoxs.map(function () {
const data = $('#inputForm').serializeJSON(); return $(this).val();
const nickname = data.nickname; }).get();
$.ajax({ for (let i = 0; i < values.length; i++) {
url: 'dinner/update?nickname=' + nickname + '&value=', if (data.dislike.includes(values[i])) {
dataType: 'json', $(rangesInput[i]).val(0);
success: function (result) {
if (result.code === 0) {
window.location.reload();
} else {
alert('更新失败: ' + result.data)
} }
} }
}) $(rangesInput[0]).trigger('change');
} });
function roll() { function update() {
$.ajax({ const data = $('#inputForm').serializeJSON();
url: 'dinner/roll', const nickname = data.nickname;
dataType: 'json', data.nickname = undefined;
success: function (result) { $.ajax({
if (result.code === 0) { url: 'dinner/update?nickname=' + nickname + '&value=' + JSON.stringify(data),
window.location.reload(); dataType: 'json',
} else { success: function (result) {
alert('更新失败: ' + result.data) if (result.code === 0) {
window.location.reload();
} else {
alert('更新失败: ' + result.data)
}
} }
} })
})
}
$(function () {
const counterId = window.setInterval(counter, 1000);
function counter() {
const now = new Date();
const targetTime = new Date(
now.getFullYear(), now.getMonth(), now.getDate(),
17, 30, 0, 0
);
const diff = targetTime - now;
const btn = $("#btnRoll");
if (diff <= 0) {
window.clearInterval(counterId);
btn.text('开始抽签');
btn.attr('disabled', null)
return;
}
// Convert diff to hours, minutes, and seconds
const hours = Math.floor(diff / (1000 * 60 * 60)).toString().padStart(2, '0');
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)).toString().padStart(2, '0');
const seconds = Math.floor((diff % (1000 * 60)) / 1000).toString().padStart(2, '0');
btn.text(hours + ':' + minutes + ':' + seconds)
} }
});
</script> function clearValue() {
</body> const data = $('#inputForm').serializeJSON();
</html> const nickname = data.nickname;
$.ajax({
url: 'dinner/update?nickname=' + nickname + '&value=',
dataType: 'json',
success: function (result) {
if (result.code === 0) {
window.location.reload();
} else {
alert('更新失败: ' + result.data)
}
}
})
}
function roll() {
$.ajax({
url: 'dinner/roll',
dataType: 'json',
success: function (result) {
if (result.code === 0) {
window.location.reload();
} else {
alert('更新失败: ' + result.data)
}
}
})
}
function viewChart(datestr) {
window.location.href = 'chart?date=' + datestr;
}
$(function () {
const counterId = window.setInterval(counter, 1000);
function counter() {
const now = new Date();
const targetTime = new Date(
now.getFullYear(), now.getMonth(), now.getDate(),
17, 30, 0, 0
);
const diff = targetTime - now;
const btn = $("#btnRoll");
if (diff <= 0) {
window.clearInterval(counterId);
btn.text('开始抽签');
btn.attr('disabled', null)
return;
}
// Convert diff to hours, minutes, and seconds
const hours = Math.floor(diff / (1000 * 60 * 60)).toString().padStart(2, '0');
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)).toString().padStart(2, '0');
const seconds = Math.floor((diff % (1000 * 60)) / 1000).toString().padStart(2, '0');
btn.text(hours + ':' + minutes + ':' + seconds)
}
});
</script>
{% endblock %}