Merge branch 'main' of git.opencomputing.cn:yumoqing/bricks

This commit is contained in:
yumoqing 2025-12-28 13:36:04 +08:00
commit f5a58a77cb
393 changed files with 14028 additions and 3244 deletions

View File

@ -2,21 +2,11 @@
A new web application development framework to make web application development more easier like play bricks
## Documentation
We have English and Chinese versions documents,
Documents in English, please read from [docs](docs/README.md), 中文资料看[这里](docs/cn/README.md)
## Development base on components
We have documents in 3 language
We built web development components which use a options objects as API.
third party can develops their component suply the standard of components API
* [中文](docs/zh/brief.md)
* [日本語](docs/ja/brief.md)
* [English](docs/en/brief.md)
Most front-end development tools only help user to build the front-end UI, and use script to build the app's logic.
Bricks not only build the UI but also the front-end logic.
Bricks provide a new mathiciam to description the event fire, and event handler, Bricks use json data to descripts event and it handler, when event fire, according the json data, Bricks dynamicly constructs the event handler.
## Dependanance
[Marked](https://github.com/yumoqing/marked) is a tool for markdown text parser, extends from [MarkedJs marked](https://github.com/markedjs/marked), we extends audio and video link, user can directly use `!v[text](url)` pattern to show a video player, and `!a[text](url)` pattern to show a audio player
NOTE: all the documents was generated by Qwen3-max

BIN
bricks/.DS_Store vendored

Binary file not shown.

View File

@ -1,4 +1,13 @@
var bricks = window.bricks || {};
bricks.get_current_language=function(){
var lang = navigator.language.substring(0, 2);
if (bricks.app){
if (bricks.app.lang) return bricks.app.lang;
bricks.app.lang = lang;
return lang;
}
return;
}
bricks.app = null;
/*
all type of bind action's desc has the following attributes:
@ -231,19 +240,21 @@ bricks.universal_handler = async function(from_widget, widget, desc, event){
bricks.default_popup = function(opts){
var popts = bricks.get_popup_default_options();
bricks.extend(popts, opts);
popts.origin_event = event;
return new bricks.Popup(popts);
}
bricks.default_popupwindow = function(opts){
bricks.default_popupwindow = function(opts,event){
var popts = bricks.get_popupwindow_default_options();
bricks.extend(popts, opts);
popts.origin_event = event;
return new bricks.PopupWindow(popts);
}
bricks.buildEventHandler = async function(w, desc, event){
var target;
if (desc.target == 'Popup'){
target = bricks.default_popup(desc.popup_options || {});
target = bricks.default_popup(desc.popup_options || {}, event);
} else if (desc.target == 'PopupWindow') {
target = bricks.default_popupwindow(desc.popup_options || {});
target = bricks.default_popupwindow(desc.popup_options || {}, event);
} else if ( desc.target instanceof bricks.JsWidget ) {
target = desc.target;
} else {
@ -325,22 +336,11 @@ var _buildWidget = async function(from_widget, target, mode, options, desc){
bricks.debug('options=', options, 'widgetBuild() failed');
return;
}
if (w.parent) {
if (target instanceof bricks.Popup || target instanceof bricks.PopupWindow){
target.destroy();
}
if (w instanceof bricks.Popup) {
return;
}
if (target instanceof bricks.Popup || target instanceof bricks.PopupWindow) {
if (! target.parent){
bricks.app.add_widget(target);
}
if (desc.popup_options){
if (desc.popup_options.eventpos){
target.bind('opened', bricks.relocate_by_eventpos.bind(null, desc.event, target));
}
}
if (w instanceof bricks.NewWindow) {
return;
}
if (mode == 'replace'){
target.clear_widgets();
@ -569,14 +569,15 @@ bricks.App = class extends bricks.Layout {
this.lang = this.opts.language;
}
else {
this.lang = navigator.language;
this.lang = navigator.language.substring(0,2);
}
this.lang_x = this.observable('lang', this.lang);
this.zindex = 10000;
this.textList = [];
this.i18n = new bricks.I18n(objget(opts, 'i18n', {}));
this.session_id = null;
this.tooltip = new bricks.Tooltip({otext:'test',i18n:true, wrap:true});
this.tooltip = new bricks.Tooltip({otext:'',i18n:true, wrap:true});
this.tooltip.hide();
this.add_widget(this.tooltip);
this._Width = this.dom_element.offsetWidth;
this._Height = this.dom_element.offsetHeight;
@ -598,7 +599,10 @@ bricks.App = class extends bricks.Layout {
console.log('event=', event);
event.preventDefault();
event.stopPropagation()
this.wins_panel = new bricks.WindowsPanel({})
var opts = bricks.get_popup_default_options();
opts.auto_open = false;
this.wins_panel = new bricks.WindowsPanel(opts);
this.wins_panel.open();
}
get_color(){
return getComputedStyle(this.dom_element).color;
@ -716,7 +720,7 @@ bricks.App = class extends bricks.Layout {
return w;
}
async run(){
await (this.change_language(this));
await (this.change_language(this.lang));
var w = await this.build();
this.root = w;
if (!w){

View File

@ -11,7 +11,7 @@ SOURCES=" page_data_loader.js factory.js uitypesdef.js utils.js uitype.js \
llm.js websocket.js datarow.js tabular.js continueaudio.js \
line.js pie.js bar.js gobang.js period.js iconbarpage.js \
keypress.js asr.js webspeech.js countdown.js progressbar.js \
qaframe.js svg.js videoplayer.js "
qaframe.js svg.js videoplayer.js sctter.js radar.js kline.js heatmap.js map.js"
echo ${SOURCES}
cat ${SOURCES} > ../dist/bricks.js
# uglifyjs --compress --mangle -- ../dist/bricks.js > ../dist/bricks.min.js

View File

@ -605,3 +605,9 @@ hr {
color: #00aaff;
background: #f0faff;
}
.mini-window {
width: 50px;
height: 50px;
}

View File

@ -22,6 +22,10 @@ bricks.EchartsExt = class extends bricks.VBox {
if(!this.idField) this.idField = 'id';
if(!this.nameField) this.nameField = 'name';
if(!this.valueFields) this.valueFields = ['value'];
// === 新增:处理 refresh_period ===
this.refresh_period = opts.refresh_period; // 单位:秒
this._refresh_timer = null;
this.build_title_widget();
this.build_description_widget();
this.holder = new bricks.Filler({});
@ -33,7 +37,49 @@ bricks.EchartsExt = class extends bricks.VBox {
schedule_once(this.render_urldata.bind(this), 0.1);
}
this.bind('element_resize', this.chart.resize.bind(this.chart));
// === 启动定时刷新(如果配置了 refresh_period===
if (this.refresh_period && this.data_url) {
this.start_auto_refresh();
}
}
// === 启动自动刷新任务 ===
start_auto_refresh() {
if (this._refresh_timer) return; // 防止重复启动
const doRefresh = () => {
this.render_urldata().then(() => {
// 继续下一轮
if (this._refresh_timer) { // 检查是否已被取消
this._refresh_timer = setTimeout(doRefresh, this.refresh_period * 1000);
}
}).catch(err => {
console.error('Auto-refresh failed:', err);
this._refresh_timer = setTimeout(doRefresh, this.refresh_period * 1000); // 失败也重试
});
};
// 初始延迟后开始第一轮,之后循环
this._refresh_timer = setTimeout(doRefresh, this.refresh_period * 1000);
}
// === 停止自动刷新 ===
stop_auto_refresh() {
if (this._refresh_timer) {
clearTimeout(this._refresh_timer);
this._refresh_timer = null;
}
}
// === 覆盖 destroy 方法,清理定时器 ===
destroy() {
this.stop_auto_refresh(); // 清理资源
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
super.destroy();
}
pivotify(data){
var series = [];
data.forEach(x => {
@ -83,7 +129,6 @@ bricks.EchartsExt = class extends bricks.VBox {
this.valueFields = this.get_series(data);
data = this.pivotify(data);
}
this.chart = echarts.init(this.holder.dom_element);
var opts = this.setup_options(data);
opts.grid = {
left: '3%',

View File

@ -22,6 +22,11 @@ bricks.FieldGroup = class {
this.build_fields(form, dc, fields[i].fields);
parent.add_widget(dc);
dc = new bricks.DynamicColumn({mobile_cols:2});
dc.set_id(fields[i].name+'_box');
if (fields[i].nonuse){
dc.disabled(true);
dc.hide();
}
} else {
var box;
if (! form.opts.input_layout || form.opts.input_layout == 'VBox'){
@ -42,6 +47,11 @@ bricks.FieldGroup = class {
height:'auto',
i18n:true});
box.add_widget(txt);
box.set_id(fields[i].name + '_box')
if (fields[i].nonuse){
box.disabled(true);
box.hide();
}
var w = Input.factory(fields[i]);
if (w){
box.add_widget(w);
@ -72,6 +82,10 @@ bricks.FormBody = class extends bricks.VScrollPanel {
},
...
]
exclusionfields:[
[a,b,c], # a,b,c互斥a enabledb,c必须disabled
[x,y] # x,y互斥
]
}
*/
constructor(form, opts){
@ -234,7 +248,33 @@ bricks.FormBase = class extends bricks.Layout {
}
return this.get_formdata();
}
toggle_disable(field_name, flg){
var w = bricks.getWidgetById(field_name + '_box', this);
if (! w) return;
w.disabled(flg);
if (flg) w.hide();
else w.show();
if (flg) return;
this.exclusionfields.forEach(arr =>{
if (arr.include(field_name)){
arr.forEach(x => {
if (x!=field_name){
var w1 = bricks.getWidgetById(x + '_box', this);
if (w1) {
w1.disabled(true);
w1.hide();
}
}
});
}
});
}
enable_field(field_name){
this.toggle_disable(field_name, false);
}
disable_field(field_name){
this.toggle_disable(field_name, true);
}
get_formdata(){
var data = new FormData();
var changed = false;
@ -243,6 +283,7 @@ bricks.FormBase = class extends bricks.Layout {
continue;
}
var w = this.name_inputs[name];
if (w.parent.is_disabled()) continue;
var d = w.getValue();
if (w.required && ( d[name] == '' || d[name] === null)){
new bricks.Error({title:'Requirement', message:'required field must input"' + w.label + '"'})
@ -326,8 +367,19 @@ bricks.Form = class extends bricks.FormBase {
toolbar:
submit_url:
method:
exclussionfields:[
[a,b,c],
[x,y]
]
fields
}
field {
name:
label:
uitype:
nonuse: # 不使用
...
}
*/
constructor(opts){
opts.height = "100%";

View File

@ -6,7 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="{{entire_url('/bricks/3parties/xterm.css')}}" />
<link rel="stylesheet" href="{{entire_url('/bricks/css/bricks.css')}}">
<link rel="stylesheet", href="{{entire_url('/css/myapp.css')}}">
{% for mycss in cssfiles() %}
<link rel="stylesheet" href="{{entire_url(mycss)}}">
{% endfor %}
</head>
<body>
<!--
@ -27,12 +29,13 @@
<script src="{{entire_url('/bricks/3parties/xterm.js')}}"></script>
<script src="{{entire_url('/bricks/3parties/xterm-addon-fit.js')}}"></script>
<script src="{{entire_url('/bricks/3parties/video.min.js')}}"></script>
<script src="{{entire_url('/bricks/3parties/recorder.wav.min.js')}}"></script>
<script src="https://sage.opencomputing.cn/bricks/3parties/hls.js"></script>
<script src="https://sage.opencomputing.cn/bricks/3parties/dash.all.min.js"></script>
<script src="{{entire_url('/bricks/3parties/hls.js')}}"></script>
<script src="{{entire_url('/bricks/3parties/dash.all.min.js')}}"></script>
<script src="{{entire_url('/bricks/bricks.js')}}"></script>
<script src="{{entire_url('/js/myapp.js')}}"></script>
{% for myjs in jsfiles() %}
<script src="{{entire_url(myjs)}}"></script>
{% endfor %}
<script>
const opts = {
"widget":

73
bricks/heatmap.js Normal file
View File

@ -0,0 +1,73 @@
var bricks = window.bricks || {};
bricks.ChartHeatmap = class extends bricks.EchartsExt {
/*
数据格式:
[
{ x: '周一', y: '上午', value: 120 },
{ x: '周二', y: '下午', value: 90 }, ...
]
参数:
{
data_url,
xField: 'x', // X 轴字段
yField: 'y', // Y 轴字段
valueField: 'value'
}
*/
setup_options(data) {
const xField = this.xField || 'x';
const yField = this.yField || 'y';
const valueField = this.valueField || 'value';
const xs = [...new Set(data.map(d => d[xField]))];
const ys = [...new Set(data.map(d => d[yField]))];
const map = {};
ys.forEach(y => map[y] = {});
data.forEach(d => {
map[d[yField]][d[xField]] = d[valueField];
});
const heatmapData = [];
for (let j = 0; j < ys.length; j++) {
const y = ys[j];
for (let i = 0; i < xs.length; i++) {
const x = xs[i];
heatmapData.push([i, j, map[y][x] || 0]);
}
}
return {
tooltip: { position: 'top' },
grid: { height: '80%', top: '10%' },
xAxis: {
type: 'category',
data: xs,
splitArea: { show: true }
},
yAxis: {
type: 'category',
data: ys,
splitArea: { show: true }
},
visualMap: {
min: 0,
max: Math.max(...data.map(d => d[valueField])),
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '5%'
},
series: [{
type: 'heatmap',
data: heatmapData,
label: { show: false },
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.5)' } }
}]
};
}
};
bricks.Factory.register('ChartHeatmap', bricks.ChartHeatmap);

View File

@ -52,13 +52,17 @@ bricks.HttpText = class {
width = bricks.app.screenWidth();
height = bricks.app.screenHeight();
}
console.log('=====bricks.app=', bricks.app);
this.params = {
"_webbricks_":1,
"width":width,
"height":height,
"_width":width,
"_height":height,
"_is_mobile":is_mobile
}
if (bricks.app){
this.params['_lang'] = bricks.app.lang;
}
}
url_parse(url){
var a = url.split('?');
@ -90,6 +94,7 @@ bricks.HttpText = class {
if (! params)
params = {};
var p = bricks.extend({}, this.params);
var lang = bricks.get_current_language()
p = bricks.extend(p, params);
if (session){
bricks.extend(p,{session:session});

49
bricks/kline.js Normal file
View File

@ -0,0 +1,49 @@
var bricks = window.bricks || {};
bricks.ChartKLine = class extends bricks.EchartsExt {
/*
数据格式:
[
{ name: '2025-04-01', open: 2100, close: 2150, low: 2080, high: 2160 },
...
]
参数:
{
data_url,
nameField: 'name', // 时间轴字段
valueFields: ['open','close','low','high']
}
*/
setup_options(data) {
const { nameField } = this;
const categories = data.map(d => d[nameField]);
const kData = data.map(d => [d.open, d.close, d.low, d.high]);
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
xAxis: {
type: 'category',
data: categories,
scale: true
},
yAxis: {
type: 'value',
scale: true
},
series: [{
type: 'candlestick',
data: kData
}],
dataZoom: [
{ type: 'inside' },
{ type: 'slider' }
]
};
}
};
bricks.Factory.register('ChartKLine', bricks.ChartKLine);

View File

@ -67,7 +67,7 @@ bricks.ModelOutput = class extends bricks.VBox {
this.content = new bricks.HBox({width:'100%'});
this.add_widget(this.content);
this.logid = null;
this.llmusageid = null;
this.run = new bricks.BaseRunning({target:this, cheight:2, cwidth:2});
this.content.add_widget(this.run);
this.filler = new bricks.LlmOut({width: '100%', css: 'card'});
@ -102,7 +102,7 @@ bricks.ModelOutput = class extends bricks.VBox {
"widgettype":"urlwidget",
"options":{
"params":{
"logid":this.logid,
"id":this.llmusageid,
"value":val
},
"url":this.estimate_url
@ -120,6 +120,9 @@ bricks.ModelOutput = class extends bricks.VBox {
this.run = null;
}
this.filler.update(data);
if (data.llmusageid) {
this.llmusageid = data.llmusageid
}
return;
}
finish(){
@ -205,6 +208,7 @@ bricks.LlmModel = class extends bricks.JsWidget {
var d = this.inputdata2uploaddata(data);
var hr = new bricks.HttpResponseStream();
var resp = await hr.post(this.opts.url, {params:d});
if (! resp) return;
await hr.handle_chunk(resp, this.chunk_response.bind(this, mout));
this.chunk_ended();
} else {
@ -212,6 +216,7 @@ bricks.LlmModel = class extends bricks.JsWidget {
console.log('data_inouted=', data, 'upload_data=', d);
var hj = new bricks.HttpJson()
var resp = await hj.post(this.opts.url, {params:d});
if (! resp) return;
mout.update_data(resp);
}
mout.estimate_w.show();

65
bricks/map.js Normal file
View File

@ -0,0 +1,65 @@
var bricks = window.bricks || {};
bricks.ChartMap = class extends bricks.EchartsExt {
/*
数据格式:
[
{ name: '北京', value: 120 },
{ name: '上海', value: 90 }, ...
]
参数:
{
data_url,
nameField: 'name',
valueField: 'value',
map_name: 'china', // 对应 echarts.registerMap 注册的地图名
map_options: {} // 可选自定义样式
}
*/
async setup_options(data) {
const { nameField, valueField = 'value', map_name = 'china', map_options } = this;
const mapData = data.map(d => ({
name: d[nameField],
value: d[valueField]
}));
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}'
},
visualMap: {
min: 0,
max: Math.max(...mapData.map(d => d.value)),
left: 'left',
top: 'bottom',
text: ['高', '低'],
calculable: true
},
series: [{
name: '数据',
type: 'map',
map: map_name,
roam: false,
label: { show: true, color: '#fff' },
itemStyle: {
areaColor: '#eee',
borderColor: '#444'
},
emphasis: {
label: { color: '#fff' },
itemStyle: { areaColor: '#e09191' }
},
data: mapData
}],
...(map_options || {})
};
}
};
bricks.Factory.register('ChartMap', bricks.ChartMap);
/*
💡 使用前需通过 `echarts.registerMap('china', geoJson)` 注册地图数据
*/

View File

@ -1,13 +1,14 @@
var bricks = window.bricks || {};
/*
*/
bricks.Menu = class extends bricks.VBox {
bricks.Menu = class extends bricks.VScrollPanel {
/*
{
"items":
}
*/
constructor(options){
opts.height = '100%';
super(options);
this.dom_element.style.display = "";
this.dom_element.style.backgroundColor = options.bgcolor || "white";

View File

@ -1,7 +1,7 @@
var bricks = window.bricks || {};
bricks.get_popup_default_options = function(){
return {
ret = {
timeout:0,
archor:'cc',
auto_open:true,
@ -11,6 +11,14 @@ bricks.get_popup_default_options = function(){
resizable:false,
modal:true
}
if (bricks.is_mobile()) {
ret.width = '100%';
ret.height = '100%';
} else {
ret.width = '70%';
ret.height = '70%';
}
return ret
}
bricks.Popup = class extends bricks.VBox {
/*
@ -171,6 +179,10 @@ bricks.Popup = class extends bricks.VBox {
positify_tl(){
var rect, w, h, t, l;
if (this.opts.eventpos && this.opts.origin_event){
bricks.relocate_by_eventpos(this.opts.origin_event, this);
return;
}
if (this.top && this.left){
this.set_style('top', this.top);
this.set_style('left', this.left);
@ -309,7 +321,6 @@ bricks.Popup = class extends bricks.VBox {
this.target_w = w;
}
}
this.positify_tl()
}
this.no_opened = false;
this.set_style('display', 'block');
@ -321,6 +332,7 @@ bricks.Popup = class extends bricks.VBox {
this.target_w.disabled(true);
}
this.bring_to_top();
this.positify_tl();
}
dismiss(){
if (! this.opened) return;
@ -364,7 +376,7 @@ bricks.Popup = class extends bricks.VBox {
}
bricks.get_popupwindow_default_options = function(){
return {
ret = {
timeout:0,
archor:'cc',
auto_open:true,
@ -374,63 +386,45 @@ bricks.get_popupwindow_default_options = function(){
resizable:true,
modal:true
}
if (bricks.is_mobile()) {
ret.width = '100%';
ret.height = '100%';
} else {
ret.width = '70%';
ret.height = '70%';
}
return ret
}
bricks.WindowsPanel = class extends bricks.Popup {
constructor(opts){
opts.width = "80%";
opts.auto_open = false;
opts.auto_dismiss = true;
opts.auto_destroy = true;
opts.height = "80%";
super(opts);
this.content = new bricks.Cols({
width: "100%",
height: "100%",
record_view: {
widgettype: "VBox",
options: {
css: "app-icon"
},
subwidgets: [
{
widgettype: "Text",
options: {
otext: "${title}",
i18n: true,
wrap: true
}
},
{
widgettype: "Svg",
options: {
rate: 2,
url: "${url}"
}
}
]
}
});
this.content.bind('record_click', this.del_window.bind(this));
var data = {
total: bricks.app.mwins.length,
rows:[]
};
for (var i=0; i< bricks.app.mwins.length; i++){
data.rows.push({
title: bricks.app.mwins[i].title,
url: bricks.app.mwins[i].url,
pos: i
open(){
this.auto_open = false;
var dc = new bricks.DynamicColumn({});
bricks.app.mwins.forEach(x => {
var w = new bricks.VBox({
"css": "mini-window card"
});
}
this.content.dataHandle(data);
this.add_widget(this.content);
dc.add_widget(w);
w.bind('click', this.reopen_window.bind(this, x));
var tw = new bricks.Title6({
width:'100%',
wrap:true,
text:x.title
});
var iw = new bricks.Svg({url:x.icon, rate:1.5});
w.add_widget(iw);
w.add_widget(tw);
});
this.add_widget(dc);
super.open();
}
del_window(event){
var pos = event.params.pos;
var w = bricks.app.mwins[pos];
reopen_window(w){
var nws = [];
w.open();
bricks.app.mwins.splice(pos, 1);
bricks.app.mwins.forEach(x => {
if (x != w) nws.push(x);
});
bricks.app.mwins = nws;
this.dismiss();
}
}

View File

@ -15,8 +15,8 @@ bricks.ProgressBar = class extends bricks.HBox {
this.add_widget(this.text_w);
}
set_value(v){
var pzt = (current / total) * 100;
pzt = Math.max(0, Math.min(100, percentage));
var pzt = this.total_value ? (v / this.total_value) * 100 : 0;
pzt = Math.max(0, Math.min(100, pzt));
this.text_w.set_style('width', pzt + '%')
}
}

54
bricks/radar.js Normal file
View File

@ -0,0 +1,54 @@
var bricks = window.bricks || {};
bricks.ChartRadar = class extends bricks.EchartsExt {
/*
数据格式示例:
[
{ name: '张三', indicator1: 80, indicator2: 60, ... },
{ name: '李四', indicator1: 70, indicator2: 90, ... }
]
参数:
{
data_url,
data_params,
nameField: 'name',
valueFields: ['indicator1', 'indicator2', ...],
radar_options: { // 自定义雷达配置
indicator: [
{ name: '销售', max: 100 },
{ name: '管理', max: 100 }, ...
]
}
}
*/
setup_options(data) {
const { nameField, valueFields } = this;
const series = [];
const indicator = this.radar_options?.indicator || valueFields.map(f => ({ name: f, max: 100 }));
data.forEach(item => {
series.push({
name: item[nameField],
type: 'radar',
data: [
{
value: valueFields.map(f => item[f]),
name: item[nameField]
}
]
});
});
return {
tooltip: { trigger: 'item' },
legend: { data: data.map(d => d[nameField]) },
radar: { indicator },
series
};
}
};
bricks.Factory.register('ChartRadar', bricks.ChartRadar);
/*
*/

View File

@ -7,12 +7,15 @@ bricks.BaseRunning = class extends bricks.FHBox {
}
*/
constructor(opts){
if (! opts.cwidth) opts.cwidth = 2;
if (! opts.cheight) opts.cheight = 2;
super(opts);
this.icon_w = new bricks.Icon({
rate: opts.rate|| 2,
url:opts.icon || bricks_resource('imgs/running.gif')
});
this.time_w = new bricks.Text({
text:'test123',
text:'00:00:00',
color:'#222',
wrap:false,
i18n:false

98
bricks/scatter.js Normal file
View File

@ -0,0 +1,98 @@
var bricks = window.bricks || {};
bricks.ChartScatter = class extends bricks.EchartsExt {
/*
* opts:
* {
* data_url,
* data_params,
* method,
* user_data,
* nameField: 可选用于标签
* xField: X坐标字段
* yField: Y坐标字段
* sizeField?: 气泡大小字段可选
* categoryField?: 分组字段用于不同颜色
* }
*/
setup_options(data) {
let seriesMap = {};
if (this.categoryField) {
// 按 categoryField 分组
data.forEach(item => {
const cat = item[this.categoryField] || 'default';
if (!seriesMap[cat]) {
seriesMap[cat] = {
name: cat,
type: 'scatter',
data: []
};
}
seriesMap[cat].data.push([
item[this.xField],
item[this.yField],
item[this.nameField] || null,
item[this.sizeField] || 1
]);
});
} else {
// 单一系列
const seriesData = data.map(item => [
item[this.xField],
item[this.yField],
item[this.nameField] || null,
item[this.sizeField] || 1
]);
seriesMap['scatter'] = {
type: 'scatter',
data: seriesData
};
}
const series = Object.values(seriesMap);
const xAxisName = this.xField;
const yAxisName = this.yField;
return {
tooltip: {
trigger: 'axis',
formatter: function(params) {
const p = params[0];
return `${p.seriesName}<br/>${xAxisName}: ${p.value[0]}<br/>${yAxisName}: ${p.value[1]}`;
}
},
legend: {
data: Object.keys(seriesMap)
},
xAxis: {
type: 'value',
name: xAxisName
},
yAxis: {
type: 'value',
name: yAxisName
},
series: series
};
}
};
// 注册工厂
bricks.Factory.register('ChartScatter', bricks.ChartScatter);
/*
new bricks.ChartScatter({
user_data: [
{ x: 10, y: 20, size: 30, group: 'Group A', label: 'Point 1' },
{ x: 15, y: 25, size: 40, group: 'Group B', label: 'Point 2' }
],
xField: 'x',
yField: 'y',
sizeField: 'size',
categoryField: 'group',
nameField: 'label'
});
*/

View File

@ -94,6 +94,9 @@ bricks.JsWidget = class {
observable(name, value){
return new bricks.Observable(this, name, value);
}
is_disabled(){
return this.dom_element.disabled == true;
}
disabled(flag){
if(flag){
this.dom_element.disabled = true;

5
doc.sh Normal file
View File

@ -0,0 +1,5 @@
sources=$(find bricks -name "*.js" -exec grep -l '.register(' {} \; -print|sort -u)
for s in $sources
do
t2t -p prompt.md -o docs/ai $s
done

BIN
docs/.DS_Store vendored

Binary file not shown.

View File

@ -1,36 +0,0 @@
# HBox
Hbox是一个水平布局小部件它按水平方向布局子小部件
## 用法
```html
<html>
<head>
<link rel="stylesheet" href="/bricks/css/bricks.css">
</head>
<body>
<script src="/bricks/bricks.js"></script>
<script>
const opts =
{
"widget": {
"widgettype": "HBox",
"options":
{
}
}
};
const app = new BricksApp(opts);
app.run();
</script>
</body>
</html>
```
**注意:hbox控件是水平排列的控件,相当于一个容器,可以容纳其他的控件,如button,form等**

View File

@ -1,307 +0,0 @@
# OEPNAI帮助生成的文档
我已经解压并直接从你上传的仓库源码里抓取并解析了第一批核心文件uitypesdef.js、bricks.js、jsoncall.js、input.js、form.js、button.js并把关键发现整理成第一阶段交付1Bricks JSON 的基本结构与约定2从源码明确的首批控件/类型与字段解析3接下来我会继续批量解析剩余控件并把完整规范输出为 Bricks JSON 规范 v2。
下面是我现在能直接、可靠交付的结果 —— 基于仓库源码的准确提取(你可以直接拿去作为 SwiftBricks 设计依据)。
## 一、从源码得出的最重要全局约定JSON 树格式)
(这些约定在 bricks.widgetBuild / 示例文件 中被反复使用——是框架运行时的标准格式)
根:任意 widget 描述是一个对象,关键字段包括:
widgettype — 必需,字符串,指定要创建的 widget 类型(与 bricks.Factory.register('XXX', ...) 注册名一致)。
options — 对象,控件的属性/配置(控件会读取 opts = desc.options || {})。
subwidgets — 数组,可选,子 widget 列表(用于容器/布局类)。
binds — 数组,可选,事件绑定定义(见下)。
wid — 可选,用于给 widget 指定 id可在绑定中引用
actiontype / action 等:某些场景下顶层会直接包含 action 相关字段(详见具体控件)。
binds事件绑定结构示例
```
"binds": [
{
"wid":"self",
"event":"click",
"actiontype":"script",
"script":"alert('HBox clicked');"
}
]
```
常见字段有:
wid事件源 id"self" 表示当前)
event事件名如 "click", "change"
actiontype执行动作类型script, ajax, bricks 等)
script / url / method / params动作所需额外字段
数据/表达式处理:
bricks.apply_data(desc, data) 用来把 data 应用到描述里(框架支持在 desc 中使用数据表达式并替换)。
## 二、uitypesdef.js基础 UI 类型定义)
bricks/uitypesdef.js 中明确了基本的 简单数据类型 view/input 构造器(主要用于表单字段、简易数据渲染):
从文件中提取到的 已注册基础类型:
* View/Input builder 类型(通过 bricks.uitypesdef.setViewBuilder / setInputBuilder 注册):
1 str (string)
2 password
3 int
4 float
5 code
这说明:表单/字段层面,字段类型会使用这些 uitypesdef 注册的 builder 来渲染 view 或 input例如 password 会用 UiPasswordstr 用 UiText 等)。
## 三、解析的核心控件文件级别提取input.js, button.js, form.js
下面是我从源码实现中抽取出来的控件属性/行为(可直接用作规范文档条目):
1) Button bricks.Button — button.js
注册名:'Button' (bricks.Factory.register('Button', bricks.Button);)
JSON 示例位置examples/button.ui
主要读取位置this.opts来自 desc.options
常见 options 字段(在源码注释/实现中出现):
name控件名
label显示文本
icon图标路径
tooltip提示
bgcolor / color / css 等样式配置
orientation, nonepack, height, width, item_rate 等布局/尺寸相关
action动作对象格式可包含 target, datawidget 等)
事件:
内部会 dispatch('click', this.opts),并触发 binds 中注册的 click。
可通过 binds 或 action 指定脚本或 RPC。
children
Button 通常是单节点(不常用 subwidgets但可嵌于容器如 HBox 的 subwidgets 中为 Button
2) UiType / 基本 Inputinput.js
主要类bricks.UiTypeextends bricks.Layout
常见 opts / options 字段:
name字段名表单字段标识
required是否必填
ctype字段类别例如 'text'
value / defaultvalue初始值
公开方法:
getValue():返回 { name: value } 对象
resultValue():拿到实际值(子类通常覆盖)
getUserData():从表单/父 widget 获取额外数据
行为:
Input 节点在 form 内会被 Form 收集Form 里有 build_fields、getValue 整体收集逻辑)。
3) Formform.js
负责构建 form 字段、处理提交并与后端交互
与表单相关常量bricks.need_formdata_fields = ['file', 'video', 'audio'];(决定是否使用 multipart/form-data
主要职责:
build_fields(form, parent, fields):根据 fields 描述构造表单 field widget支持 miniform、FieldGroup 等抽象)
command_handle(event)处理表单工具栏命令如提交、reset
show_resp_message_or_error(resp):显示服务端返回作为 widget build 的 JSON可把服务器返回的 JSON 当作 widget 描述渲染)
表单的 options/items 格式在 examples 下的 miniform.ui / login_form.json 有具体样例(我会在后续批次把每个示例抽成规范片段)
## 四、jsoncall.jsHTTP/JSON / RPC 支持)
包含 bricks.HttpJson、httpcall、withLoginInfo 等函数,用于:
向服务器发送请求并解析流式响应(支持 chunked / SSE-like
自动处理 cookie/session示例里会把 Set-Cookie 存入 bricks.app.save_session
支持超时、重试、与 widgetBuild 配合把 JSON 响应直接渲染成 widget
在 bricks.widgetBuild 中,如果 widgettype == 'urlwidget' 或 desc.options.url会使用 jsoncall 发起请求并用返回数据继续构造 widget
## 五、仓库中注册的所有 widget 类型(已从所有 *.js 文件的 bricks.Factory.register 抽取)
```
Accordion
ASRClient
AudioPlayer
AudioRecorder
TextedAudioPlayer
ChartBar
Button
Camera
Cols
Conform
ContinueAudioPlayer
Countdown
TimePassed
DataGrid
DataRow
DataViewer
DOCXviewer
EXCELviewer
PDFviewer
DynamicAccordion
DynamicColumn
EChart
FilePlayer
FloatIconBar
Footer
Form
Filler
Goban
Grid
Header
HeaderBar
HeaderBar2
HeaderTitle
HBox
IconBarPage
IconTextBar
Image
ImageList
Input
IFrame
KeyPress
KeyEvent
Label
Layout
Li
List
ListTable
Login
MapView
MarkdownViewer
Menu
Message
MiniForm
Modal
MultiStateImage
MyOperator
Navigator
Notification
PageDataLoader
Paging
Panel
Pie
Popup
ProgressBar
QRCode
RadialChart
Range
Row
Scroll
Select
Splitter
Spinner
SQLConsole
SvgImage
Tab
Tabular
Table
TablePager
Text
TextArea
TextInput
TimeAxis
Toolbar
Tree
TreeChoose
Tooltip
Title1
Title2
Title3
Title4
Title5
Title6
VideoPlayer
Video
WebSocket
WebTTS
WebASR
Tooltip
Text
KeyinText
Title1
Title2
Title3
Title4
Title5
Title6
LlmIO
Wterm
```
## 六、示例(摘自 examples/button.ui — 典型的 widget JSON
```
{
"widgettype": "HBox",
"options": {
"height":"auto",
"width":"100%"
},
"subwidgets": [
{
"widgettype": "Button",
"options": {
"name": "test",
"icon": "/bricks/imgs/submit.png",
"label": "test once",
"tooltip": "This is a joke too",
"bgcolor": "#ff00ff",
"nonepack": true,
"orientation": "horizontal"
}
}
],
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"script": "alert('HBox clicked');"
}
]
}
```
这种结构是框架的标准单位widgettype + options + subwidgets + binds。

View File

@ -30,6 +30,50 @@ bricks采用控件这一概念来描述web GUI的显示部件每个控件均
控件的详细介绍请参看[控件说明](widgets.md)
### 控件扩展
如果现有的控件没法满足系统要求bricks支持控件扩展控件扩展需遵守
* 控件class继承自某一个控件的class
* 按照需求实现控件逻辑
* 在需要的地方用this.dispatch触发此控件的事件
假设需要扩展一个名字叫ExtContainer的控件
```
bricks.ExtContainer = class extends bricks.VBox {
constructor(opts){
super(opts);
/* 新控件的创建代码 */
}
......
/* 对象的其他方法在需要的时候在某个方法中使用this.dispatch('new_event', data)方法引发事件 */
}
bricks.register('ExtContainer', bricks.ExtContainer); /* 注册新控件 */
```
新控件的使用example.ui
```
{
"widgettype":"ExtContainer",
"options":{
....
},
"subwidgets":[
...
],
"binds":[
{
"wid":"self",
"event":"new_event",
"actiontype":"urlwidget",
"target":"some_container",
"options":{
"url":"{{entire_url('./some_ui.ui')}}"
}
}
]
}
```
### 事件以及事件处理
每个控件都能触发所映射dom元素的事件以及控件js类的成员函数以及祖先类的
成员函数中dispatch出的事件

Some files were not shown because too many files have changed in this diff Show More