在國(guó)企,很多領(lǐng)導(dǎo)是按部就班的晉升上來(lái)的,他們的年齡及經(jīng)歷使他們至今還沒(méi)有掌握電腦的基礎(chǔ)知識(shí),因此,紙質(zhì)報(bào)表是他們掌握全局的依據(jù),必備的工具。將報(bào)表下載下來(lái),然后能稍許進(jìn)行修飾,這是必須的。
項(xiàng)目描述
- 將頁(yè)面原始呈現(xiàn)的數(shù)據(jù)及搜索后的數(shù)據(jù)以 excel 報(bào)表的形式下載下來(lái)。
知識(shí)難點(diǎn)
單一的 url 頁(yè)面中會(huì)呈現(xiàn)出不同的數(shù)據(jù),需要能夠把不同的數(shù)據(jù)下載下來(lái),只需要用到 ajax 的知識(shí),在同一個(gè)頁(yè)面中以 json 的形式傳遞數(shù)據(jù)。
實(shí)現(xiàn)頁(yè)面模板
不再贅述,僅僅是在頁(yè)面上加載一個(gè)下載按鈕,詳情看源碼。
實(shí)現(xiàn)下載功能的試圖函數(shù)
views.py
的內(nèi)容如下:
...
...
import os
from collections import OrderedDict
from config import basedir
from pyexcel_xls import save_data, get_data
...
...
@show.route('/<path:filename>', methods=['GET', 'POST'])
def download_xls(filename):
data = OrderedDict()
data_path = os.path.join(excel_path, filename)
num = 0
if '01.xls' in filename:
header_data = ['序號(hào)', '部門(mén)', '角色', '員工', '電話', '客戶', '月份',
'本月資產(chǎn)余額 ', '上月資產(chǎn)余額', '新增余額']
body_data = [header_data]
for t in json.loads(request.get_data()):
num += 1
body_data.append([num, t["department"], t["role"], t["staff_name"], t["phone"], t["guest_name"],
t["month"], t["balance"], t["last_balance"],
float('%.2f' % t["balance"]) - float('%.2f' % t["last_balance"])])
data.update({'01報(bào)表': body_data})
if '02.xls' in filename:
header_data = ['序號(hào)', '部門(mén)', '角色', '員工', '電話', '月份', '管戶數(shù)'
'本月資產(chǎn)余額 ', '上月資產(chǎn)余額', '新增余額']
body_data = [header_data]
for t in json.loads(request.get_data()):
num += 1
body_data.append([num, t["department"], t["role"], t["staff_name"], t["staff_phone"],
t["month"], t["balance"], t["last_balance"],
float('%.2f' % t["balance"]) - float('%.2f' % t["last_balance"])])
data.update({'02報(bào)表': body_data})
save_data(data_path, data)
return jsonify({"data": "ok"})
很明顯,通過(guò) return 返回的 jsonify 數(shù)據(jù),大家應(yīng)該知道,我這是要用 json 了。
而其中的兩個(gè) filename 的判斷,我是因?yàn)橐鰞蓚€(gè)頁(yè)面的下載鏈接,所以對(duì) filename 進(jìn)行了一個(gè)判斷。
代碼中使用了 collections 的有序字典 OrderedDict, pyexcel_xls的相關(guān)模塊,同時(shí)也設(shè)置了要生成的 xls 文件的生成路徑及文件名——
data_path = os.path.join(excel_path, filename)
。
實(shí)現(xiàn) ajax 功能
既然是 ajax 那就是 javascript 咯,還得返回 html 模板。在 flask 項(xiàng)目中,模板的變量都是封裝在 jinja2 中,我們寫(xiě) javascript 的時(shí)候,不能單獨(dú)再建立一個(gè) js 文件,導(dǎo)入到模板中,因?yàn)椋瑹o(wú)法獲得 Jinja2 變量。還得直接在 html 中寫(xiě) javasript。
01.html
中實(shí)現(xiàn) ajax 下載
{% block js %}
{{ super() }} <!-- 繼承 block js 里的原始 js 文件 -->
<script>
$SCRIPT_ROOT = {{ request.script_root|tojson|safe }}; <!-- flask ajax 需要進(jìn)行該項(xiàng)設(shè)置 -->
function downloadxls() { <!-- 創(chuàng)建 downloadxls 函數(shù),頁(yè)面上的導(dǎo)出報(bào)表按鈕需要調(diào)用這個(gè)函數(shù) -->
var database = new Object();
database = {{ database|safe }}; <!-- 調(diào)用 jinja2 中的變量,使用 safe ,防止出現(xiàn)多重冒號(hào)。 -->
var data = JSON.stringify(database);
$.ajax({
type: "POST",
url: "{{ url_for('show.download_xls', filename='%s_01.xls' % current_user.phone) }}",<!-- 生成 xls 文件的 url 鏈接 -->
contentType: "application/json; charset=utf-8",
data: data, <!-- 返回到后臺(tái)的 json 數(shù)據(jù) -->
success: function (msg) {
if (msg.data === 'ok'){ <!-- 后臺(tái)送來(lái)的 'ok' 數(shù)據(jù) -->
//window.open("{{ url_for('show.static', filename='excel_files/%s_01.xls' % current_user.phone) }}");<!-- 用 js 打開(kāi) xls 文件,可以進(jìn)行下載,但是會(huì)出現(xiàn)頁(yè)面閃現(xiàn)的現(xiàn)象,界面不算友好。 -->
var $eleForm = $("<form method='get'></form>");<!-- 使用 form 表單的形式進(jìn)行文件下載,不會(huì)出現(xiàn)頁(yè)面閃現(xiàn)的現(xiàn)象。 -->
$eleForm.attr("action", "{{ url_for('show.static', filename='excel_files/%s_01.xls' % current_user.phone) }}");
$(document.body).append($eleForm);
//提交表單,實(shí)現(xiàn)下載
$eleForm.submit();
}
}
})
}
</script>
{% endblock %}
詳情請(qǐng)看注釋。
02.html
中實(shí)現(xiàn) ajax 下載
不在贅述,基本上是和 02.html
頁(yè)面一樣。
至此,你可以實(shí)驗(yàn)一下,你的項(xiàng)目是否可以實(shí)現(xiàn)報(bào)表下載了。
很不幸,我們忘了一個(gè)重要的事情,那就是 js 中 database 是繼承自 jinja2 變量,它是否是你所想的那種 json 數(shù)據(jù)呢。答案是否定的。結(jié)果是這樣的:
那么我們需要繼續(xù)工作咯。
使 sqlalchemy 數(shù)據(jù) json 化
models.py
中建立一個(gè)類:
class AlchemyJsonEncoder(json.JSONEncoder):
def default(self, obj):
# 判斷是否是Query
if isinstance(obj, Query):
# 定義一個(gè)字典數(shù)組
fields = []
# 檢索結(jié)果集的行記錄
for rec in obj:
# 定義一個(gè)字典對(duì)象
record = {}
# 檢索記錄中的成員
for field in [x for x in dir(rec) if
# 過(guò)濾屬性
not x.startswith('_')
# 過(guò)濾掉方法屬性
and hasattr(rec.__getattribute__(x), '__call__') == False
# 過(guò)濾掉不需要的屬性
and x != 'metadata']:
try:
record[field] = rec.__getattribute__(field)
except TypeError:
record[field] = None
fields.append(record)
# 返回字典數(shù)組
return fields
# 其他類型的數(shù)據(jù)按照默認(rèn)的方式序列化成JSON
return json.JSONEncoder.default(self, obj)
詳細(xì)功能請(qǐng)看注釋。
在 01 , 02 兩個(gè) url 鏈接的試圖函數(shù)中,加入如下內(nèi)容:
views.py
中增加
return render_template('show/01.html', data=data, searchForm=search_form, database=json.dumps(database, cls=AlchemyJsonEncoder))
此時(shí),在查看我們要傳輸?shù)?json 數(shù)據(jù):
至此,你可以使用下載功能了,不管你的頁(yè)面中的數(shù)據(jù)如何變化,你下載的報(bào)表都會(huì)顯示你當(dāng)前頁(yè)面的數(shù)據(jù)。
提示:
讓這么多的數(shù)據(jù)在頁(yè)面中使用明文傳輸,是一種極其不安全的行為,這是網(wǎng)絡(luò)爬蟲(chóng)的肥沃礦場(chǎng)。當(dāng)然,如果你使用的環(huán)境是我這樣的內(nèi)部 web 環(huán)境,那就另當(dāng)別論。不過(guò)如果你有更好的解決方法,那么可以告訴我,不勝感激。