wangweimin пре 4 година
родитељ
комит
d145168043

+ 15 - 5
docs/spec.rst

@@ -190,12 +190,13 @@ output:
 
   * callback_id:
   * buttons:[ {value:, label:, },...]
-  * small:
+  * small: bool,是否显示为小按钮样式
+  * link: bool,是否显示为链接样式
 
 * type: file
 
-  * name:
-  * content:
+  * name: 下载保存为的文件名
+  * content: 文件base64编码的内容
 
 * type: table
 
@@ -221,7 +222,7 @@ close_popup
 
 该命令字段 ``spec`` 为 ``null``
 
-output_ctl:
+output_ctl
 ^^^^^^^^^^^^^^^
 输入控制
 
@@ -248,12 +249,21 @@ output_ctl:
 * position: top/middle/bottom 与scroll_to一起出现, 表示滚动页面,让锚点位于屏幕可视区域顶部/中部/底部
 * remove: 将给定的scope连同scope处的内容移除
 
-run_script:
+run_script
 ^^^^^^^^^^^^^^^
 运行js代码
 
 命令 spec 字段为字符串格式的要运行的js代码
 
+download
+^^^^^^^^^^^^^^^
+下载文件
+
+命令 spec 字段:
+
+* name: 下载保存为的文件名
+* content: 文件base64编码的内容
+
 Event
 ------------
 

+ 14 - 9
pywebio/output.py

@@ -53,7 +53,7 @@ from functools import wraps
 from typing import Union
 
 from .io_ctrl import output_register_callback, send_msg, Output, safely_destruct_output_when_exp, OutputList
-from .session import get_current_session
+from .session import get_current_session, download
 from .utils import random_str, iscoroutinefunction
 
 try:
@@ -458,7 +458,7 @@ def table_cell_buttons(buttons, onclick, **callback_options) -> str:
     return ' '.join(btns_html)
 
 
-def put_buttons(buttons, onclick, small=None, scope=Scope.Current, position=OutputPosition.BOTTOM,
+def put_buttons(buttons, onclick, small=None, link_style=False, scope=Scope.Current, position=OutputPosition.BOTTOM,
                 **callback_options) -> Output:
     """
     输出一组按钮
@@ -479,6 +479,7 @@ def put_buttons(buttons, onclick, small=None, scope=Scope.Current, position=Outp
        | Tip: 可以使用 ``functools.partial`` 来在 ``onclick`` 中保存更多上下文信息.
        | Note: 当使用基于协程的会话实现时,回调函数可以使用协程函数.
     :param bool small: 是否显示小号按钮,默认为False
+    :param bool link_style: 是否将按钮显示为链接样式,默认为False
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
     :param callback_options: 回调函数的其他参数。根据选用的 session 实现有不同参数
 
@@ -520,7 +521,7 @@ def put_buttons(buttons, onclick, small=None, scope=Scope.Current, position=Outp
 
     callback_id = output_register_callback(click_callback, **callback_options)
     spec = _get_output_spec('buttons', callback_id=callback_id, buttons=btns, small=small,
-                            scope=scope, position=position)
+                            scope=scope, position=position, link=link_style)
 
     return Output(spec)
 
@@ -554,17 +555,21 @@ def put_image(src, format=None, title='', width=None, height=None,
     return put_html(html, scope=scope, position=position)
 
 
-def put_file(name, content, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
-    """输出文件
+def put_file(name, content, label=None, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
+    """显示一个文件下载链接
     在浏览器上的显示为一个以文件名为名的链接,点击链接后浏览器自动下载文件。
 
-    :param str name: 文件名
+    :param str name: 下载保存为的文件名
     :param content: 文件内容. 类型为 bytes-like object
+    :param str label: 下载链接的显示文本,默认和文件名相同
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
     """
-    content = b64encode(content).decode('ascii')
-    spec = _get_output_spec('file', name=name, content=content, scope=scope, position=position)
-    return Output(spec)
+    if label is None:
+        label = name
+    output = put_buttons(buttons=[label], link_style=True,
+                         onclick=[lambda: download(name, content)],
+                         scope=scope, position=position)
+    return output
 
 
 def put_link(name, url=None, app=None, new_window=False, scope=Scope.Current,

+ 16 - 2
pywebio/session/__init__.py

@@ -2,6 +2,7 @@ r"""
 
 .. autofunction:: run_async
 .. autofunction:: run_asyncio_coroutine
+.. autofunction:: download
 .. autofunction:: run_js
 .. autofunction:: eval_js
 .. autofunction:: register_thread
@@ -15,6 +16,7 @@ r"""
 """
 
 import threading
+from base64 import b64encode
 from functools import wraps
 
 from .base import Session
@@ -27,7 +29,7 @@ from ..utils import iscoroutinefunction, isgeneratorfunction, run_as_function, t
 _active_session_cls = []
 
 __all__ = ['run_async', 'run_asyncio_coroutine', 'register_thread', 'hold', 'defer_call', 'data', 'get_info',
-           'run_js', 'eval_js']
+           'run_js', 'eval_js', 'download']
 
 
 def register_session_implement_for_target(target_func):
@@ -135,6 +137,17 @@ def hold():
         yield next_client_event()
 
 
+def download(name, content):
+    """下载文件
+
+    :param str name: 下载保存为的文件名
+    :param content: 文件内容. 类型为 bytes-like object
+    """
+    from ..io_ctrl import send_msg
+    content = b64encode(content).decode('ascii')
+    send_msg('download', spec=dict(name=name, content=content))
+
+
 def run_js(code):
     """运行js代码.
 
@@ -182,7 +195,8 @@ def eval_js(expression):
     run_js(script)
 
     res = yield next_client_event()
-    assert res['event'] == 'js_yield', "Internal Error, please report this bug to us"
+    assert res[
+               'event'] == 'js_yield', "Internal Error, please report this bug on https://github.com/wang0618/PyWebIO/issues"
     return res['data']
 
 

+ 1 - 1
webiojs/src/handlers/base.ts

@@ -8,7 +8,7 @@ export interface CommandHandler {
 }
 
 export class CloseHandler implements CommandHandler {
-    accept_command: string[] = ['close_session']
+    accept_command: string[] = ['close_session'];
 
     constructor(readonly session: Session) {
     }

+ 14 - 0
webiojs/src/handlers/download.ts

@@ -0,0 +1,14 @@
+import {Command, Session} from "../session";
+import {CommandHandler} from "./base";
+import {b64toBlob} from "../utils";
+
+export class DownloadHandler implements CommandHandler {
+    accept_command: string[] = ['download'];
+
+    constructor() {}
+
+    handle_message(msg: Command) {
+        let blob = b64toBlob(msg.spec.content);
+        saveAs(blob, msg.spec.name, {}, false);
+    }
+}

+ 3 - 1
webiojs/src/main.ts

@@ -7,6 +7,7 @@ import {CloseHandler, CommandDispatcher} from "./handlers/base"
 import {PopupHandler} from "./handlers/popup";
 import {openApp} from "./utils";
 import {ScriptHandler} from "./handlers/script";
+import {DownloadHandler} from "./handlers/download";
 
 // 获取后端API地址
 function get_backend_addr() {
@@ -28,8 +29,9 @@ function set_up_session(webio_session: Session, output_container_elem: JQuery, i
     let popup_ctrl = new PopupHandler(webio_session);
     let close_ctrl = new CloseHandler(webio_session);
     let script_ctrl = new ScriptHandler(webio_session);
+    let download_ctrl = new DownloadHandler();
 
-    let dispatcher = new CommandDispatcher(output_ctrl, input_ctrl, popup_ctrl, close_ctrl, script_ctrl);
+    let dispatcher = new CommandDispatcher(output_ctrl, input_ctrl, popup_ctrl, close_ctrl, script_ctrl, download_ctrl);
 
     webio_session.on_server_message((msg: Command) => {
         try {

+ 3 - 1
webiojs/src/models/output.ts

@@ -56,8 +56,9 @@ let Buttons = {
     handle_type: 'buttons',
     get_element: function (spec: any) {
         const btns_tpl = `<div>{{#buttons}}
-                             <button value="{{value}}" onclick="WebIO.DisplayAreaButtonOnClick(this, '{{callback_id}}')" class="btn btn-primary {{#small}}btn-sm{{/small}}">{{label}}</button> 
+                             <button value="{{value}}" onclick="WebIO.DisplayAreaButtonOnClick(this, '{{callback_id}}')" class="btn {{btn_class}}{{#small}} btn-sm{{/small}}">{{label}}</button> 
                           {{/buttons}}</div>`;
+        spec.btn_class = spec.link ? "btn-link" : "btn-primary";
         let html = Mustache.render(btns_tpl, spec);
         return $(html);
     }
@@ -76,6 +77,7 @@ export function DisplayAreaButtonOnClick(this_ele: HTMLElement, callback_id: str
     });
 }
 
+// 已废弃。为了向下兼容而保留
 let File = {
     handle_type: 'file',
     get_element: function (spec: any) {