Przeglądaj źródła

feat: put_table support nested put_xxx call

wangweimin 5 lat temu
rodzic
commit
3077b190a3

+ 5 - 0
pywebio/html/css/app.css

@@ -93,6 +93,11 @@ button {
     margin-bottom: 8px;
     margin-bottom: 8px;
 }
 }
 
 
+td blockquote, td dl, td ol, td p, td pre, td table, td ul, td button, td pre {
+    margin-bottom: 0 !important;
+}
+
+
 .input-container .form-group {
 .input-container .form-group {
     margin-bottom: 0;
     margin-bottom: 0;
 }
 }

Plik diff jest za duży
+ 0 - 0
pywebio/html/js/pywebio.min.js


Plik diff jest za duży
+ 0 - 0
pywebio/html/js/pywebio.min.js.map


+ 28 - 0
pywebio/io_ctrl.py

@@ -3,6 +3,7 @@
 
 
 
 
 """
 """
+import json
 import logging
 import logging
 
 
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
@@ -10,6 +11,33 @@ from .session import chose_impl, next_client_event, get_current_task_id, get_cur
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
+class OutputReturn:
+    """ ``output`` 消息的处理类 """
+
+    def __init__(self, spec, on_embed=None):
+        self.spec = spec
+        self.processed = False
+        self.on_embed = on_embed or (lambda d: d)
+
+    def embed_data(self):
+        """返回供嵌入到布局中的数据,可以设置一些默认值"""
+        self.processed = True
+        return self.on_embed(self.spec)
+
+    def __del__(self):
+        """未嵌入时的操作:直接输出消息"""
+        if not self.processed:
+            send_msg('output', self.spec)
+
+
+class OutputEncoder(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, OutputReturn):
+            return obj.embed_data()
+        # Let the base class default method raise the TypeError
+        return json.JSONEncoder.default(self, obj)
+
+
 def send_msg(cmd, spec=None):
 def send_msg(cmd, spec=None):
     msg = dict(command=cmd, spec=spec, task_id=get_current_task_id())
     msg = dict(command=cmd, spec=spec, task_id=get_current_task_id())
     get_current_session().send_task_command(msg)
     get_current_session().send_task_command(msg)

+ 42 - 25
pywebio/output.py

@@ -38,7 +38,7 @@ import io
 from base64 import b64encode
 from base64 import b64encode
 from collections.abc import Mapping
 from collections.abc import Mapping
 
 
-from .io_ctrl import output_register_callback, send_msg
+from .io_ctrl import output_register_callback, send_msg, OutputReturn
 
 
 try:
 try:
     from PIL.Image import Image as PILImage
     from PIL.Image import Image as PILImage
@@ -131,9 +131,9 @@ def scroll_to(anchor, position=TOP):
     send_msg('output_ctl', dict(scroll_to=inner_ancher_name, position=position))
     send_msg('output_ctl', dict(scroll_to=inner_ancher_name, position=position))
 
 
 
 
-def _put_content(type, anchor=None, before=None, after=None, **other_spec):
+def _get_output_spec(type, anchor=None, before=None, after=None, **other_spec):
     """
     """
-    向用户端发送 ``output`` 指令
+    获取 ``output`` 指令的spec字段
 
 
     :param str type: 输出类型
     :param str type: 输出类型
     :param content: 输出内容
     :param content: 输出内容
@@ -141,12 +141,14 @@ def _put_content(type, anchor=None, before=None, after=None, **other_spec):
     :param str before: 在给定的锚点之前输出内容。若给定的锚点不存在,则不输出任何内容
     :param str before: 在给定的锚点之前输出内容。若给定的锚点不存在,则不输出任何内容
     :param str after: 在给定的锚点之后输出内容。若给定的锚点不存在,则不输出任何内容。
     :param str after: 在给定的锚点之后输出内容。若给定的锚点不存在,则不输出任何内容。
         注意: ``before`` 和 ``after`` 参数不可以同时使用
         注意: ``before`` 和 ``after`` 参数不可以同时使用
-    :param other_spec: 额外的输出参数
+    :param other_spec: 额外的输出参数,值为None的参数不会包含到返回值中
+
+    :return dict:  ``output`` 指令的spec字段
     """
     """
     assert not (before and after), "Parameter 'before' and 'after' cannot be specified at the same time"
     assert not (before and after), "Parameter 'before' and 'after' cannot be specified at the same time"
 
 
     spec = dict(type=type)
     spec = dict(type=type)
-    spec.update(other_spec)
+    spec.update({k: v for k, v in other_spec.items() if v is not None})
     if anchor:
     if anchor:
         spec['anchor'] = _get_anchor_id(anchor)
         spec['anchor'] = _get_anchor_id(anchor)
     if before:
     if before:
@@ -154,10 +156,10 @@ def _put_content(type, anchor=None, before=None, after=None, **other_spec):
     elif after:
     elif after:
         spec['after'] = _get_anchor_id(after)
         spec['after'] = _get_anchor_id(after)
 
 
-    send_msg("output", spec)
+    return spec
 
 
 
 
-def put_text(text, inline=False, anchor=None, before=None, after=None):
+def put_text(text, inline=False, anchor=None, before=None, after=None) -> OutputReturn:
     """
     """
     输出文本内容
     输出文本内容
 
 
@@ -170,10 +172,11 @@ def put_text(text, inline=False, anchor=None, before=None, after=None):
     注意: ``before`` 和 ``after`` 参数不可以同时使用。
     注意: ``before`` 和 ``after`` 参数不可以同时使用。
     当 ``anchor`` 指定的锚点已经在页面上存在时,``before`` 和 ``after`` 参数将被忽略。
     当 ``anchor`` 指定的锚点已经在页面上存在时,``before`` 和 ``after`` 参数将被忽略。
     """
     """
-    _put_content('text', content=str(text), inline=inline, anchor=anchor, before=before, after=after)
+    spec = _get_output_spec('text', content=str(text), inline=inline, anchor=anchor, before=before, after=after)
+    return OutputReturn(spec)
 
 
 
 
-def put_html(html, anchor=None, before=None, after=None):
+def put_html(html, anchor=None, before=None, after=None) -> OutputReturn:
     """
     """
     输出Html内容。
     输出Html内容。
 
 
@@ -185,10 +188,11 @@ def put_html(html, anchor=None, before=None, after=None):
     if hasattr(html, '__html__'):
     if hasattr(html, '__html__'):
         html = html.__html__()
         html = html.__html__()
 
 
-    _put_content('html', content=html, anchor=anchor, before=before, after=after)
+    spec = _get_output_spec('html', content=html, anchor=anchor, before=before, after=after)
+    return OutputReturn(spec)
 
 
 
 
-def put_code(content, langage='', anchor=None, before=None, after=None):
+def put_code(content, langage='', anchor=None, before=None, after=None) -> OutputReturn:
     """
     """
     输出代码块
     输出代码块
 
 
@@ -197,10 +201,10 @@ def put_code(content, langage='', anchor=None, before=None, after=None):
     :param str anchor, before, after: 与 `put_text` 函数的同名参数含义一致
     :param str anchor, before, after: 与 `put_text` 函数的同名参数含义一致
     """
     """
     code = "```%s\n%s\n```" % (langage, content)
     code = "```%s\n%s\n```" % (langage, content)
-    put_markdown(code, anchor=anchor, before=before, after=after)
+    return put_markdown(code, anchor=anchor, before=before, after=after)
 
 
 
 
-def put_markdown(mdcontent, strip_indent=0, lstrip=False, anchor=None, before=None, after=None):
+def put_markdown(mdcontent, strip_indent=0, lstrip=False, anchor=None, before=None, after=None) -> OutputReturn:
     """
     """
     输出Markdown内容。
     输出Markdown内容。
 
 
@@ -240,14 +244,15 @@ def put_markdown(mdcontent, strip_indent=0, lstrip=False, anchor=None, before=No
         lines = (i.lstrip() for i in mdcontent.splitlines())
         lines = (i.lstrip() for i in mdcontent.splitlines())
         mdcontent = '\n'.join(lines)
         mdcontent = '\n'.join(lines)
 
 
-    _put_content('markdown', content=mdcontent, anchor=anchor, before=before, after=after)
+    spec = _get_output_spec('markdown', content=mdcontent, anchor=anchor, before=before, after=after)
+    return OutputReturn(spec)
 
 
 
 
-def put_table(tdata, header=None, span=None, anchor=None, before=None, after=None):
+def put_table(tdata, header=None, span=None, anchor=None, before=None, after=None) -> OutputReturn:
     """
     """
     输出表格
     输出表格
 
 
-    :param list tdata: 表格数据。列表项可以为 ``list`` 或者 ``dict``
+    :param list tdata: 表格数据。列表项可以为 ``list`` 或者 ``dict`` , 单元格的内容可以为字符串或其他输出函数的返回值,字符串内容显示时会被当作html。
     :param list header: 设定表头。
     :param list header: 设定表头。
        当 ``tdata`` 的列表项为 ``list`` 类型时,若省略 ``header`` 参数,则使用 ``tdata`` 的第一项作为表头。
        当 ``tdata`` 的列表项为 ``list`` 类型时,若省略 ``header`` 参数,则使用 ``tdata`` 的第一项作为表头。
 
 
@@ -298,7 +303,8 @@ def put_table(tdata, header=None, span=None, anchor=None, before=None, after=Non
     span = span or {}
     span = span or {}
     span = {('%s,%s' % row_col): val for row_col, val in span.items()}
     span = {('%s,%s' % row_col): val for row_col, val in span.items()}
 
 
-    _put_content('table', data=tdata, span=span, anchor=anchor, before=before, after=after)
+    spec = _get_output_spec('table', data=tdata, span=span, anchor=anchor, before=before, after=after)
+    return OutputReturn(spec)
 
 
 
 
 def _format_button(buttons):
 def _format_button(buttons):
@@ -325,7 +331,7 @@ def _format_button(buttons):
     return btns
     return btns
 
 
 
 
-def table_cell_buttons(buttons, onclick, **callback_options):
+def table_cell_buttons(buttons, onclick, **callback_options) -> str:
     """
     """
     在表格中显示一组按钮
     在表格中显示一组按钮
 
 
@@ -355,7 +361,8 @@ def table_cell_buttons(buttons, onclick, **callback_options):
     return ' '.join(btns_html)
     return ' '.join(btns_html)
 
 
 
 
-def put_buttons(buttons, onclick, small=False, anchor=None, before=None, after=None, **callback_options):
+def put_buttons(buttons, onclick, small=None, anchor=None, before=None, after=None,
+                **callback_options) -> OutputReturn:
     """
     """
     输出一组按钮
     输出一组按钮
 
 
@@ -370,6 +377,7 @@ def put_buttons(buttons, onclick, small=False, anchor=None, before=None, after=N
        函数签名为 ``onclick(btn_value)``.
        函数签名为 ``onclick(btn_value)``.
        当按钮组中的按钮被点击时,``onclick`` 被调用,并传入被点击的按钮的 ``value`` 值。
        当按钮组中的按钮被点击时,``onclick`` 被调用,并传入被点击的按钮的 ``value`` 值。
        可以使用 ``functools.partial`` 来在 ``onclick`` 中保存更多上下文信息,见 `table_cell_buttons` :ref:`代码示例 <table_cell_buttons-code-sample>` 。
        可以使用 ``functools.partial`` 来在 ``onclick`` 中保存更多上下文信息,见 `table_cell_buttons` :ref:`代码示例 <table_cell_buttons-code-sample>` 。
+    :param bool small: 是否显示小号按钮,默认为False
     :param str anchor, before, after: 与 `put_text` 函数的同名参数含义一致
     :param str anchor, before, after: 与 `put_text` 函数的同名参数含义一致
     :param callback_options: 回调函数的其他参数。根据选用的 session 实现有不同参数
     :param callback_options: 回调函数的其他参数。根据选用的 session 实现有不同参数
 
 
@@ -383,11 +391,18 @@ def put_buttons(buttons, onclick, small=False, anchor=None, before=None, after=N
     """
     """
     btns = _format_button(buttons)
     btns = _format_button(buttons)
     callback_id = output_register_callback(onclick, **callback_options)
     callback_id = output_register_callback(onclick, **callback_options)
-    _put_content('buttons', callback_id=callback_id, buttons=btns, small=small, anchor=anchor, before=before,
-                 after=after)
+    spec = _get_output_spec('buttons', callback_id=callback_id, buttons=btns, small=small, anchor=anchor, before=before,
+                            after=after)
+
+    def on_embed(spec):
+        spec.setdefault('small', True)
+        return spec
 
 
+    return OutputReturn(spec, on_embed=on_embed)
 
 
-def put_image(content, format=None, title='', width=None, height=None, anchor=None, before=None, after=None):
+
+def put_image(content, format=None, title='', width=None, height=None, anchor=None, before=None,
+              after=None) -> OutputReturn:
     """输出图片。
     """输出图片。
 
 
     :param content: 文件内容. 类型为 bytes-like object 或者为 ``PIL.Image.Image`` 实例
     :param content: 文件内容. 类型为 bytes-like object 或者为 ``PIL.Image.Image`` 实例
@@ -412,10 +427,10 @@ def put_image(content, format=None, title='', width=None, height=None, anchor=No
     html = r'<img src="data:{format};base64, {b64content}" ' \
     html = r'<img src="data:{format};base64, {b64content}" ' \
            r'alt="{title}" {width} {height}/>'.format(format=format, b64content=b64content,
            r'alt="{title}" {width} {height}/>'.format(format=format, b64content=b64content,
                                                       title=title, height=height, width=width)
                                                       title=title, height=height, width=width)
-    put_html(html, anchor=anchor, before=before, after=after)
+    return put_html(html, anchor=anchor, before=before, after=after)
 
 
 
 
-def put_file(name, content, anchor=None, before=None, after=None):
+def put_file(name, content, anchor=None, before=None, after=None) -> OutputReturn:
     """输出文件。
     """输出文件。
     在浏览器上的显示为一个以文件名为名的链接,点击链接后浏览器自动下载文件。
     在浏览器上的显示为一个以文件名为名的链接,点击链接后浏览器自动下载文件。
 
 
@@ -424,4 +439,6 @@ def put_file(name, content, anchor=None, before=None, after=None):
     :param str anchor, before, after: 与 `put_text` 函数的同名参数含义一致
     :param str anchor, before, after: 与 `put_text` 函数的同名参数含义一致
     """
     """
     content = b64encode(content).decode('ascii')
     content = b64encode(content).decode('ascii')
-    _put_content('file', name=name, content=content, anchor=anchor, before=before, after=after)
+    spec = _get_output_spec('file', name=name, content=content, anchor=anchor, before=before, after=after)
+    return OutputReturn(spec)
+

+ 4 - 1
pywebio/platform/aiohttp.py

@@ -1,5 +1,6 @@
 import asyncio
 import asyncio
 import fnmatch
 import fnmatch
+import json
 import logging
 import logging
 from functools import partial
 from functools import partial
 from os import path, listdir
 from os import path, listdir
@@ -8,6 +9,7 @@ from urllib.parse import urlparse
 from aiohttp import web
 from aiohttp import web
 
 
 from .tornado import open_webbrowser_on_server_started
 from .tornado import open_webbrowser_on_server_started
+from ..io_ctrl import OutputEncoder
 from ..session import CoroutineBasedSession, ThreadBasedSession, register_session_implement_for_target, AbstractSession
 from ..session import CoroutineBasedSession, ThreadBasedSession, register_session_implement_for_target, AbstractSession
 from ..session.base import get_session_info_from_headers
 from ..session.base import get_session_info_from_headers
 from ..utils import get_free_port, STATIC_PATH
 from ..utils import get_free_port, STATIC_PATH
@@ -57,7 +59,8 @@ def _webio_handler(target, session_cls, websocket_settings, check_origin_func=_i
 
 
         def send_msg_to_client(session: AbstractSession):
         def send_msg_to_client(session: AbstractSession):
             for msg in session.get_task_commands():
             for msg in session.get_task_commands():
-                ioloop.create_task(ws.send_json(msg))
+                msg_str = json.dumps(msg, cls=OutputEncoder)
+                ioloop.create_task(ws.send_str(msg_str))
 
 
         def close_from_session():
         def close_from_session():
             nonlocal close_from_session_tag
             nonlocal close_from_session_tag

+ 3 - 2
pywebio/platform/django.py

@@ -50,15 +50,16 @@ class DjangoHttpContext(HttpContext):
         """为当前响应设置http status"""
         """为当前响应设置http status"""
         self.response.status_code = status
         self.response.status_code = status
 
 
-    def set_content(self, content, json_type=False):
+    def set_content(self, content, json_type=False, json_cls=None):
         """设置相应的内容
         """设置相应的内容
 
 
         :param content:
         :param content:
         :param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
         :param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
+        :param json_cls: json.dumps 使用的JSONEncoder
         """
         """
         if json_type:
         if json_type:
             self.set_header('content-type', 'application/json')
             self.set_header('content-type', 'application/json')
-            self.response.content = json.dumps(content)
+            self.response.content = json.dumps(content, cls=json_cls)
         else:
         else:
             self.response.content = content
             self.response.content = content
 
 

+ 3 - 2
pywebio/platform/flask.py

@@ -55,15 +55,16 @@ class FlaskHttpContext(HttpContext):
         """为当前响应设置http status"""
         """为当前响应设置http status"""
         self.response.status_code = status
         self.response.status_code = status
 
 
-    def set_content(self, content, json_type=False):
+    def set_content(self, content, json_type=False, json_cls=None):
         """设置相应的内容
         """设置相应的内容
 
 
         :param content:
         :param content:
         :param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
         :param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
+        :param json_cls: json.dumps 使用的JSONEncoder
         """
         """
         if json_type:
         if json_type:
             self.set_header('content-type', 'application/json')
             self.set_header('content-type', 'application/json')
-            self.response.data = json.dumps(content)
+            self.response.data = json.dumps(content, cls=json_cls)
         else:
         else:
             self.response.data = content
             self.response.data = content
 
 

+ 5 - 3
pywebio/platform/httpbased.py

@@ -20,7 +20,8 @@ import threading
 from typing import Dict
 from typing import Dict
 
 
 import time
 import time
-
+import json
+from ..io_ctrl import OutputEncoder
 from ..session import CoroutineBasedSession, AbstractSession, register_session_implement_for_target
 from ..session import CoroutineBasedSession, AbstractSession, register_session_implement_for_target
 from ..session.base import get_session_info_from_headers
 from ..session.base import get_session_info_from_headers
 from ..utils import random_str, LRUDict
 from ..utils import random_str, LRUDict
@@ -59,11 +60,12 @@ class HttpContext:
         """为当前响应设置http status"""
         """为当前响应设置http status"""
         pass
         pass
 
 
-    def set_content(self, content, json_type=False):
+    def set_content(self, content, json_type=False, json_cls=None):
         """设置响应的内容。方法应该仅被调用一次
         """设置响应的内容。方法应该仅被调用一次
 
 
         :param content:
         :param content:
         :param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
         :param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
+        :param json_cls: json.dumps 使用的JSONEncoder
         """
         """
         pass
         pass
 
 
@@ -184,7 +186,7 @@ class HttpHandler:
             cls._last_check_session_expire_ts = time.time()
             cls._last_check_session_expire_ts = time.time()
             self._remove_expired_sessions(self.session_expire_seconds)
             self._remove_expired_sessions(self.session_expire_seconds)
 
 
-        context.set_content(webio_session.get_task_commands(), json_type=True)
+        context.set_content(webio_session.get_task_commands(), json_type=True, json_cls=OutputEncoder)
 
 
         if webio_session.closed():
         if webio_session.closed():
             self._remove_webio_session(webio_session_id)
             self._remove_webio_session(webio_session_id)

+ 3 - 2
pywebio/platform/tornado.py

@@ -14,6 +14,7 @@ import tornado.ioloop
 from tornado.web import StaticFileHandler
 from tornado.web import StaticFileHandler
 from tornado.websocket import WebSocketHandler
 from tornado.websocket import WebSocketHandler
 
 
+from ..io_ctrl import OutputEncoder
 from ..session import CoroutineBasedSession, ThreadBasedSession, ScriptModeSession, \
 from ..session import CoroutineBasedSession, ThreadBasedSession, ScriptModeSession, \
     register_session_implement_for_target, AbstractSession
     register_session_implement_for_target, AbstractSession
 from ..session.base import get_session_info_from_headers
 from ..session.base import get_session_info_from_headers
@@ -71,11 +72,11 @@ def _webio_handler(target, session_cls, check_origin_func=_is_same_site):
 
 
         def send_msg_to_client(self, session: AbstractSession):
         def send_msg_to_client(self, session: AbstractSession):
             for msg in session.get_task_commands():
             for msg in session.get_task_commands():
-                self.write_message(json.dumps(msg))
+                self.write_message(json.dumps(msg, cls=OutputEncoder))
 
 
         def open(self):
         def open(self):
             logger.debug("WebSocket opened")
             logger.debug("WebSocket opened")
-            self.set_nodelay(True)
+            # self.set_nodelay(True)
 
 
             self._close_from_session_tag = False  # 由session主动关闭连接
             self._close_from_session_tag = False  # 由session主动关闭连接
 
 

+ 11 - 1
test/template.py

@@ -73,6 +73,17 @@ def basic_output():
         {"Course": "DB", "Score": "93"},
         {"Course": "DB", "Score": "93"},
     ], header=["Course", "Score"], anchor='put_table')
     ], header=["Course", "Score"], anchor='put_table')
 
 
+    img_data = open(path.join(here_dir, 'assets', 'img.png'), 'rb').read()
+    put_table([
+        ['Type', 'Content'],
+        ['text', put_text('<hr/>', inline=True)],
+        ['html', 'X<sup>2</sup>'],
+        ['buttons', put_buttons(['A','B'], onclick=None)],
+        ['markdown', put_markdown('`awesome PyWebIO!`\n - 1\n - 2\n - 3')],
+        ['file', put_file('hello.text', b'')],
+        ['image', put_image(img_data)],
+    ])
+
     put_text('code:')
     put_text('code:')
     put_code(json.dumps(dict(name='pywebio', author='wangweimin'), indent=4), 'json', anchor='scroll_basis')
     put_code(json.dumps(dict(name='pywebio', author='wangweimin'), indent=4), 'json', anchor='scroll_basis')
 
 
@@ -95,7 +106,6 @@ def basic_output():
 
 
     put_buttons(['A', 'B', 'C'], onclick=partial(put_text, after='put_buttons'), anchor='put_buttons')
     put_buttons(['A', 'B', 'C'], onclick=partial(put_text, after='put_buttons'), anchor='put_buttons')
 
 
-    img_data = open(path.join(here_dir, 'assets', 'img.png'), 'rb').read()
     put_image(img_data, anchor='put_image1')
     put_image(img_data, anchor='put_image1')
     put_image(img_data, width="30px", anchor='put_image2')
     put_image(img_data, width="30px", anchor='put_image2')
     put_image(img_data, height="50px", anchor='put_image3')
     put_image(img_data, height="50px", anchor='put_image3')

+ 45 - 11
webiojs/src/models/output.ts

@@ -1,6 +1,11 @@
 import {state} from '../state'
 import {state} from '../state'
 import {b64toBlob} from "../utils";
 import {b64toBlob} from "../utils";
 
 
+/*
+* 当前限制
+* 若外层为layout类的Widget,则内层Widget在get_element中绑定的事件将会失效
+* */
+
 export interface Widget {
 export interface Widget {
     handle_type: string;
     handle_type: string;
 
 
@@ -33,9 +38,13 @@ let Html = {
     handle_type: 'html',
     handle_type: 'html',
     get_element: function (spec: any) {
     get_element: function (spec: any) {
         let nodes = $.parseHTML(spec.content, null, true);
         let nodes = $.parseHTML(spec.content, null, true);
-        let elem = $(nodes) as any;
+        let elem;
         if (nodes.length > 1)
         if (nodes.length > 1)
             elem = $('<div><div/>').append(nodes);
             elem = $('<div><div/>').append(nodes);
+        else if (nodes.length === 1)
+            elem = $(nodes[0]);
+        else
+            elem = $(nodes);
         return elem;
         return elem;
     }
     }
 };
 };
@@ -43,7 +52,7 @@ let Html = {
 let Buttons = {
 let Buttons = {
     handle_type: 'buttons',
     handle_type: 'buttons',
     get_element: function (spec: any) {
     get_element: function (spec: any) {
-        const btns_tpl = `<div class="form-group">{{#buttons}}
+        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-primary {{#small}}btn-sm{{/small}}">{{label}}</button> 
                           {{/buttons}}</div>`;
                           {{/buttons}}</div>`;
         let html = Mustache.render(btns_tpl, spec);
         let html = Mustache.render(btns_tpl, spec);
@@ -70,7 +79,7 @@ export function DisplayAreaButtonOnClick(this_ele: HTMLElement, callback_id: str
 let File = {
 let File = {
     handle_type: 'file',
     handle_type: 'file',
     get_element: function (spec: any) {
     get_element: function (spec: any) {
-        const html = `<div class="form-group"><button type="button" class="btn btn-link">${spec.name}</button></div>`;
+        const html = `<div><button type="button" class="btn btn-link">${spec.name}</button></div>`;
         let element = $(html);
         let element = $(html);
         let blob = b64toBlob(spec.content);
         let blob = b64toBlob(spec.content);
         element.on('click', 'button', function (e) {
         element.on('click', 'button', function (e) {
@@ -100,28 +109,53 @@ let Table = {
       {{/tdata}}
       {{/tdata}}
     
     
 </table>`;
 </table>`;
-        interface itemType  {
-            data:string,
-            col?: number, row?: number
+
+        interface itemType {
+            data: string,
+            col?: number,
+            row?: number
         }
         }
 
 
-        let table_data:itemType[][] = [];
+        // 将spec转化成模版引擎的输入
+        let table_data: itemType[][] = [];
         for (let row_id in spec.data) {
         for (let row_id in spec.data) {
             table_data.push([]);
             table_data.push([]);
             let row = spec.data[row_id];
             let row = spec.data[row_id];
             for (let col_id in row) {
             for (let col_id in row) {
+                let data = spec.data[row_id][col_id];
+
+                // 处理复合类型单元格,即单元格不是简单的html,而是一个output命令的spec
+                if (typeof data === 'object') {
+                    let html = '';
+                    try {
+                        // @ts-ignore
+                        let nodes = type2processor[data.type](data);
+                        for (let node of nodes)
+                            html += node.outerHTML || '';
+                    } catch (e) {
+                        console.error('Get sub widget html error,', e, data);
+                    }
+                    data = html;
+                }
+
                 table_data[row_id].push({
                 table_data[row_id].push({
-                    data: spec.data[row_id][col_id],
+                    data: data,
                     ...(spec.span[row_id + ',' + col_id] || {})
                     ...(spec.span[row_id + ',' + col_id] || {})
                 });
                 });
             }
             }
         }
         }
-        let header:itemType[], data:itemType[][];
+
+        let header: itemType[], data: itemType[][];
         [header, ...data] = table_data;
         [header, ...data] = table_data;
-        let html = Mustache.render(table_tpl, {header:header, tdata:data});
+        let html = Mustache.render(table_tpl, {header: header, tdata: data});
         return $(html);
         return $(html);
     }
     }
 };
 };
 
 
 
 
-export let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table];
+export let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table];
+
+let type2processor: { [i: string]: (spec: any) => JQuery } = {};
+for (let w of all_widgets)
+    type2processor[w.handle_type] = w.get_element;
+

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików