interact.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import json
  2. import logging
  3. from collections.abc import Mapping
  4. from base64 import b64decode
  5. from .framework import Global
  6. from .input_ctrl import send_msg, single_input, input_control
  7. logger = logging.getLogger(__name__)
  8. TEXT = 'text'
  9. NUMBER = "number"
  10. PASSWORD = "password"
  11. CHECKBOX = 'checkbox'
  12. RADIO = 'radio'
  13. SELECT = 'select'
  14. TEXTAREA = 'textarea'
  15. def _parse_args(kwargs):
  16. """处理传给各类input函数的原始参数,
  17. :return:(spec参数,valid_func)
  18. """
  19. # 对为None的参数忽律处理
  20. kwargs = {k: v for k, v in kwargs.items() if v is not None}
  21. kwargs.update(kwargs.get('other_html_attrs', {}))
  22. kwargs.pop('other_html_attrs', None)
  23. valid_func = kwargs.pop('valid_func', lambda _: None)
  24. return kwargs, valid_func
  25. def input(label, type=TEXT, *, valid_func=None, name='data', value=None, placeholder=None, required=None,
  26. readonly=None, disabled=None, help_text=None, **other_html_attrs):
  27. """可以通过datalist提供候选输入"""
  28. item_spec, valid_func = _parse_args(locals())
  29. # 参数检查
  30. allowed_type = {TEXT, NUMBER, PASSWORD, TEXTAREA}
  31. assert type in allowed_type, 'Input type not allowed.'
  32. def preprocess_func(d):
  33. if type == NUMBER:
  34. return int(d)
  35. return d
  36. return single_input(item_spec, valid_func, preprocess_func)
  37. def textarea(label, rows=6, *, code=None, valid_func=None, name='data', value=None, placeholder=None, required=None,
  38. maxlength=None, minlength=None, readonly=None, disabled=None, help_text=None, **other_html_attrs):
  39. """提供codemirror参数产生代码输入样式"""
  40. item_spec, valid_func = _parse_args(locals())
  41. item_spec['type'] = TEXTAREA
  42. return single_input(item_spec, valid_func, lambda d: d)
  43. def _parse_select_options(options):
  44. # option 可用形式:
  45. # {value:, label:, [selected:,] [disabled:]}
  46. # (value, label, [selected,] [disabled])
  47. # value 单值,label等于value
  48. opts_res = []
  49. for opt in options:
  50. if isinstance(opt, Mapping):
  51. assert 'value' in opt and 'label' in opt, 'options item must have value and label key'
  52. elif isinstance(opt, list):
  53. assert len(opt) > 1 and len(opt) <= 4, 'options item format error'
  54. opt = dict(zip(('value', 'label', 'selected', 'disabled'), opt))
  55. else:
  56. opt = dict(value=opt, label=opt)
  57. opts_res.append(opt)
  58. return opts_res
  59. def select(label, options, type=SELECT, *, multiple=None, valid_func=None, name='data', value=None,
  60. placeholder=None, required=None, readonly=None, disabled=None, inline=None, help_text=None,
  61. **other_html_attrs):
  62. """
  63. 参数值为None表示不指定,使用默认值
  64. :param label:
  65. :param options: option 列表
  66. option 可用形式:
  67. {value:, label:, [selected:,] [disabled:]}
  68. (value, label, [selected,] [disabled])
  69. value 单值,label等于value
  70. :param type:
  71. :param multiple:
  72. :param valid_func:
  73. :param name:
  74. :param value:
  75. :param placeholder:
  76. :param required:
  77. :param readonly:
  78. :param disabled:
  79. :param inline:
  80. :param other_html_attrs:
  81. :return:
  82. """
  83. item_spec, valid_func = _parse_args(locals())
  84. item_spec['options'] = _parse_select_options(options)
  85. allowed_type = {CHECKBOX, RADIO, SELECT}
  86. assert type in allowed_type, 'Input type not allowed.'
  87. if inline is not None and type not in {CHECKBOX, RADIO}:
  88. del item_spec['inline']
  89. logger.warning('inline 只能用于 CHECKBOX, RADIO type, now type:%s', type)
  90. if multiple is not None and type != SELECT:
  91. del item_spec['multiple']
  92. logger.warning('multiple 参数只能用于SELECT type, now type:%s', type)
  93. return single_input(item_spec, valid_func, lambda d: d)
  94. def _parse_action_buttons(buttons):
  95. """
  96. :param label:
  97. :param actions: action 列表
  98. action 可用形式:
  99. {value:, label:, [disabled:]}
  100. (value, label, [disabled])
  101. value 单值,label等于value
  102. :return:
  103. """
  104. act_res = []
  105. for act in buttons:
  106. if isinstance(act, Mapping):
  107. assert 'value' in act and 'label' in act, 'actions item must have value and label key'
  108. elif isinstance(act, list):
  109. assert len(act) in (2, 3), 'actions item format error'
  110. act = dict(zip(('value', 'label', 'disabled'), act))
  111. else:
  112. act = dict(value=act, label=act)
  113. act_res.append(act)
  114. return act_res
  115. def actions(label, buttons, name='data', help_text=None):
  116. """
  117. 选择一个动作。UI为多个按钮,点击后会将整个表单提交
  118. :param label:
  119. :param actions: action 列表
  120. action 可用形式:
  121. {value:, label:, [disabled:]}
  122. (value, label, [disabled])
  123. value 单值,label等于value
  124. 实现方式:
  125. 多个type=submit的input组成
  126. [
  127. <button data-name value>label</button>,
  128. ...
  129. ]
  130. """
  131. item_spec, valid_func = _parse_args(locals())
  132. item_spec['type'] = 'actions'
  133. item_spec['buttons'] = _parse_action_buttons(buttons)
  134. return single_input(item_spec, valid_func, lambda d: d)
  135. def file_upload(label, accept=None, name='data', placeholder='Choose file', help_text=None, **other_html_attrs):
  136. """
  137. :param label:
  138. :param accept: 表明服务器端可接受的文件类型;该属性的值必须为一个逗号分割的列表,包含了多个唯一的内容类型声明:
  139. 以 STOP 字符 (U+002E) 开始的文件扩展名。(例如:".jpg,.png,.doc")
  140. 一个有效的 MIME 类型,但没有扩展名
  141. audio/* 表示音频文件
  142. video/* 表示视频文件
  143. image/* 表示图片文件
  144. :param placeholder:
  145. :param help_text:
  146. :param other_html_attrs:
  147. :return:
  148. """
  149. item_spec, valid_func = _parse_args(locals())
  150. item_spec['type'] = 'file'
  151. def read_file(data): # data: {'filename':, 'dataurl'}
  152. header, encoded = data['dataurl'].split(",", 1)
  153. data['content'] = b64decode(encoded)
  154. del data['dataurl']
  155. return data
  156. return single_input(item_spec, valid_func, read_file)
  157. def confirm():
  158. pass
  159. def input_group(label, inputs, valid_func=None):
  160. """
  161. :param label:
  162. :param inputs: list of single_input coro
  163. :param valid_func: callback(data) -> (name, error_msg)
  164. :return:
  165. """
  166. spec_inputs = []
  167. preprocess_funcs = {}
  168. item_valid_funcs = {}
  169. for single_input_cr in inputs:
  170. input_kwargs = dict(single_input_cr.cr_frame.f_locals) # 拷贝一份,不可以对locals进行修改
  171. single_input_cr.close()
  172. input_name = input_kwargs['item_spec']['name']
  173. preprocess_funcs[input_name] = input_kwargs['preprocess_func']
  174. item_valid_funcs[input_name] = input_kwargs['valid_func']
  175. spec_inputs.append(input_kwargs['item_spec'])
  176. # def add_autofocus(spec_inputs):
  177. if all('autofocus' not in i for i in spec_inputs): # 每一个输入项都没有设置autofocus参数
  178. for i in spec_inputs:
  179. text_inputs = {TEXT, NUMBER, PASSWORD, SELECT} # todo update
  180. if i.get('type') in text_inputs:
  181. i['autofocus'] = True
  182. break
  183. spec = dict(label=label, inputs=spec_inputs)
  184. return input_control(spec, preprocess_funcs=preprocess_funcs, item_valid_funcs=item_valid_funcs,
  185. form_valid_funcs=valid_func)