Bootstrap 3 How-To #3 布局

本文介绍如何使用Bootstrap框架创建响应式网页布局。通过实例演示了不同设备屏幕下的布局变化,包括列的分配、偏移及嵌套等特性。


对于 Web 开发来说,一个永远的话题是如何创建一个跨浏览器兼容的布局。许多年来,各种框架使用各种技术来解决这个问题。Bootstrap 使用了一个不同的方式来解决这个问题。基于 960 像素的布局 http://960.gs,bootstrap 提供了更为简单的语法,它还支持响应式布局,布局可以根据设备的不同尺寸进行调整,从桌面计算机到平板和手持设备。

以前版本的 bootstrap 在没有使用响应式布局的时候,使用了 940px 布局使用 span* 和 offset* 布局。在使用响应式布局的时候,网格系统使用 724px 或者 1170px 的宽度。

如果希望使用百分比的布局,可以将容器的类从 .row 替换为 row-fluid。

3.0 版本对这一部分有比较大的变动,可以区分特小型设备 xs ( Extra small devices, 小于 768px 比如手机 ), 小型设备 sm ( Small devices, 小于 992px, 比如平板 ), 中型设备 md ( Medium devices, 小于 1200px, 比如桌面计算机),  大型设备 lg ( Large devices, 比如宽屏显示器 )。

而 row-fluid 已经不存在了。

我们马上就基于这个系统创建网站的页面,首先,我们先熟悉 bootstrap 布局的基本特性。

 开始

 让我们从示例中的 jumbotron 页面开始。

 在我们自己创建的 study 文件夹中创建名为 3 的文件夹。

1. 将这个文件夹中的两个文件复制到 study 中的 3 文件夹中。

2. 在文本编辑器中打开 index.html 文件。

3. 将第 10 行的标题修改为

<title>Layout Playground | My Bootstrap site</title>

4. 找到第 71 行,将标题修改为

<h1>Layout Playground</h1>

5. 打开 index.html,你会看到如下的页面

如何使用

1. 找到页面的第 79 行,你会看到如下的内容

复制代码
<div class="row">
    <div class="col-lg-4">
        <h2>Heading</h2>
        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
        <p><a class="btn btn-default" href="#">View details &raquo;</a></p>
    </div>
    <div class="col-lg-4">
        <h2>Heading</h2>
        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
        <p><a class="btn btn-default" href="#">View details &raquo;</a></p>
    </div>
    <div class="col-lg-4">
        <h2>Heading</h2>
        <p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
        <p><a class="btn btn-default" href="#">View details &raquo;</a></p>
    </div>
</div>
复制代码

2. 你会看到在 class=”row” 的 div 内部,嵌入了三个 class=”col-lg-4” 的 div。
3. 将第一个 class=”col-lg-4” 修改为  “col-lg-6”,将第二个和第三个修改为 “col-lg-3”
4. 保存之后,刷新浏览器,你会看到如下的页面。

5. 我们在下面添加一个新的分栏。

复制代码
<div class="row">
    <div class="col-lg-2">
        <h2>Heading</h2>
        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
        <p><a class="btn btn-default" href="#">View details &raquo;</a></p>
    </div>
    <div class="col-lg-4">
        <h2>Heading</h2>
        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
        <p><a class="btn btn-default" href="#">View details &raquo;</a></p>
    </div>
    <div class="col-lg-6">
        <h2>Heading</h2>
        <p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
        <p><a class="btn btn-default" href="#">View details &raquo;</a></p>
    </div>
</div>
复制代码

6. 保存之后的页面

工作原理

Bootstrap 提供了一个 12 列的分栏系统,col-lg- 从 1 直到 12,col-lg-12 表示整个宽度,8 表示三分之二,6 表示一半,class=”row” 的 div 用来作为列的容器,每个行作为一个新的布局区域。详细的说明见: http://getbootstrap.com/css/#grid

单独使用 .col-md-* 的样式,可以在移动设备或者平板设备上创建基本的网格,在小型设备上是堆栈式的,在中型设备上将是横向的。

样式的内容如下所示:

复制代码
<div class="row">
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
  <div class="col-md-1">.col-md-1</div>
</div>
<div class="row">
  <div class="col-md-8">.col-md-8</div>
  <div class="col-md-4">.col-md-4</div>
</div>
<div class="row">
  <div class="col-md-4">.col-md-4</div>
  <div class="col-md-4">.col-md-4</div>
  <div class="col-md-4">.col-md-4</div>
</div>
<div class="row">
  <div class="col-md-6">.col-md-6</div>
  <div class="col-md-6">.col-md-6</div>
</div>
复制代码

同时支持移动设备和桌面设备

同时使用 .col-xs-* .col-md-* 样式可以取得更好的效果。

内容如下所示:

复制代码
<!-- 移动设备整个宽度,其它一般宽度 -->
<div class="row">
  <div class="col-xs-12 col-md-8">.col-xs-12 col-md-8</div>
  <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
</div>

<!-- 移动设备 50% 宽度,其它 33.3% 宽度 -->
<div class="row">
  <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
  <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
  <div class="col-xs-6 col-md-4">.col-xs-6 .col-md-4</div>
</div>

<!-- 移动和桌面都是 50% -->
<div class="row">
  <div class="col-xs-6">.col-xs-6</div>
  <div class="col-xs-6">.col-xs-6</div>
</div>
复制代码

同时支持移动,平板和桌面

复制代码
<div class="row">
  <div class="col-xs-12 col-sm-6 col-md-8">.col-xs-12 .col-sm-6 .col-md-8</div>
  <div class="col-xs-6 col-sm-6 col-md-4">.col-xs-6 .col-sm-6 .col-md-4</div>
</div>
<div class="row">
  <div class="col-xs-6 col-sm-4 col-md-4">.col-xs-6 .col-sm-4 .col-md-4</div>
  <div class="col-xs-6 col-sm-4 col-md-4">.col-xs-6 .col-sm-4 .col-md-4</div>
  <!-- Optional: clear the XS cols if their content doesn't match in height -->
  <div class="clearfix visible-xs"></div>
  <div class="col-xs-6 col-sm-4 col-md-4">.col-xs-6 .col-sm-4 .col-md-4</div>
</div>
复制代码

列定位

使用 .col-md-offset-* 可以进行列定位,比如 .col-md-offset-4 可以定位到第 4 个位置

复制代码
<div class="row">
  <div class="col-md-4">.col-md-4</div>
  <div class="col-md-4 col-md-offset-4">.col-md-4 .col-md-offset-4</div>
</div>
<div class="row">
  <div class="col-md-3 col-md-offset-3">.col-md-3 .col-md-offset-3</div>
  <div class="col-md-3 col-md-offset-3">.col-md-3 .col-md-offset-3</div>
</div>
<div class="row">
  <div class="col-md-6 col-md-offset-3">.col-md-6 .col-md-offset-3</div>
</div>
复制代码

列还可以嵌套

复制代码
<div class="row">
  <div class="col-md-9">
    Level 1: .col-md-9
    <div class="row">
      <div class="col-md-6">
        Level 2: .col-md-6
      </div>
      <div class="col-md-6">
        Level 2: .col-md-6
      </div>
    </div>
  </div>
</div>
复制代码

 

import os import pickle import pandas as pd import dash from dash import dcc, html, dash_table, callback_context from dash.dependencies import Input, Output, State import dash_bootstrap_components as dbc from datetime import datetime, timedelta import base64 import io # ------------------------------- # 🧩 1. 配置 Config # ------------------------------- CATEGORY_OPTIONS = [ {"label": "安装", "value": "安装"}, {"label": "勘测", "value": "勘测"}, {"label": "可选", "value": "可选"}, {"label": "网优", "value": "网优"}, {"label": "运输-正向", "value": "运输-正向"}, {"label": "运输-逆向", "value": "运输-逆向"} ] TABLE_STYLE = { &#39;maxHeight&#39;: &#39;600px&#39;, &#39;overflowY&#39;: &#39;auto&#39;, &#39;overflowX&#39;: &#39;auto&#39;, &#39;whiteSpace&#39;: &#39;nowrap&#39; } CELL_TOOLTIP_STYLE = { &#39;textAlign&#39;: &#39;left&#39;, &#39;overflow&#39;: &#39;hidden&#39;, &#39;textOverflow&#39;: &#39;ellipsis&#39;, &#39;maxWidth&#39;: 150, &#39;minWidth&#39;: 100 } CACHE_TTL = timedelta(minutes=10) # 全局缓存 DATA_CACHE = {} CLASSIFICATION_CACHE = {} RESULT_CACHE = None RESULT_CACHE_TIME = None # ------------------------------- # 🛠️ 2. 工具函数 Utils # ------------------------------- def get_time_str(): return datetime.now().strftime(&#39;%Y-%m-%d %H:%M:%S&#39;) def time_print(*args): print(f"{get_time_str()}", *args) def save_to_pickle(df_dict, path): with open(path, &#39;wb&#39;) as f: pickle.dump(df_dict, f) def load_from_pickle(path): with open(path, &#39;rb&#39;) as f: return pickle.load(f) def read_excel_with_sheets(file_path): try: xl = pd.ExcelFile(file_path) return {sheet: xl.parse(sheet) for sheet in xl.sheet_names}, None except Exception as e: return None, str(e) def get_file_size_mb(file_path): return round(os.path.getsize(file_path) / (1024 * 1024), 2) def validate_columns(df, required_cols): missing = [col for col in required_cols if col not in df.columns] return len(missing) == 0, missing def site_model(data_df, classification_df): """核心分析逻辑""" if data_df is None or classification_df is None: return pd.DataFrame({"错误": ["缺少数据"]}) key = "条目描述" if key not in data_df.columns or key not in classification_df.columns: return pd.DataFrame({"错误": [f"缺少 &#39;{key}&#39; 字段"]}) result = data_df.merge( classification_df[[key, "条目分类", "项目编码"]].drop_duplicates(), on=key, how="left" ).fillna({"条目分类": "未分类", "项目编码": "ALL"}) return result # ------------------------------- # 🎨 3. 布局 Layout # ------------------------------- app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUMEN], suppress_callback_exceptions=True) server = app.server app.layout = html.Div([ # html.H1("站点模型分类工具", style={&#39;textAlign&#39;: &#39;center&#39;, &#39;margin&#39;: &#39;30px&#39;}), # 文件上传区 html.Div([ html.H4("选择Excel文件"), dcc.Upload( id=&#39;upload-data&#39;, children=html.Div([&#39;拖拽文件到这里,或 &#39;, html.A(&#39;点击选择文件&#39;)]), style={ &#39;width&#39;: &#39;100%&#39;, &#39;height&#39;: &#39;60px&#39;, &#39;lineHeight&#39;: &#39;60px&#39;, &#39;borderWidth&#39;: &#39;1px&#39;, &#39;borderStyle&#39;: &#39;dashed&#39;, &#39;borderRadius&#39;: &#39;5px&#39;, &#39;textAlign&#39;: &#39;center&#39;, &#39;margin&#39;: &#39;10px&#39; }, multiple=False ), html.Div(id=&#39;file-info&#39;, style={&#39;margin&#39;: &#39;10px&#39;}), html.Div(id=&#39;cache-status&#39;, style={&#39;margin&#39;: &#39;10px&#39;}), ], style={&#39;padding&#39;: &#39;20px&#39;}), # 主 Tabs dcc.Tabs(id="main-tabs", value=&#39;tab-import&#39;, children=[ # 导入预览 dcc.Tab(label=&#39;导入数据及预览&#39;, value=&#39;tab-import&#39;, children=[ html.H5("选择Sheet"), dcc.Dropdown(id=&#39;sheet-dropdown&#39;), html.Div(id=&#39;validation-alert&#39;, style={&#39;margin&#39;: &#39;10px&#39;}), html.Div(id=&#39;data-preview-table&#39;, style=TABLE_STYLE), ]), # 条目分类 dcc.Tab(id=&#39;tab-classify&#39;, label=&#39;条目分类&#39;, value=&#39;tab-classify&#39;, disabled=True, children=[ html.Div([ html.H4("条目分类管理"), html.Div([ html.Button("批量修改分类", id="btn-batch-category", n_clicks=0, style={&#39;margin&#39;: &#39;5px&#39;}), html.Button("批量修改项目编码", id="btn-batch-code", n_clicks=0, style={&#39;margin&#39;: &#39;5px&#39;}) ], style={&#39;margin&#39;: &#39;10px&#39;}), html.Div(id=&#39;selection-count&#39;, style={&#39;margin&#39;: &#39;10px&#39;, &#39;color&#39;: &#39;#666&#39;}), # Modals dbc.Modal([ dbc.ModalHeader("批量修改 - 条目分类"), dbc.ModalBody([html.Label("选择新分类:"), dcc.Dropdown(id="modal-category-dropdown", options=CATEGORY_OPTIONS)]), dbc.ModalFooter([ html.Button("取消", id="close-category-modal", n_clicks=0), html.Button("确认修改", id="confirm-category-modal", n_clicks=0) ]) ], id="category-modal", is_open=False), dbc.Modal([ dbc.ModalHeader("批量修改 - 项目编码"), dbc.ModalBody([html.Label("输入新的项目编码(留空表示 ALL):"), dcc.Input(id="modal-code-input", type="text", placeholder="例如:PROJ001", style={&#39;width&#39;: &#39;100%&#39;})]), dbc.ModalFooter([ html.Button("取消", id="close-code-modal", n_clicks=0), html.Button("确认修改", id="confirm-code-modal", n_clicks=0) ]) ], id="code-modal", is_open=False), # 分类表格 dash_table.DataTable( id=&#39;classification-table&#39;, editable=True, # row_deletable=True, row_selectable="multi", sort_action="native", filter_action="native", page_size=15, fixed_rows={&#39;headers&#39;: True}, style_table=TABLE_STYLE, style_cell=CELL_TOOLTIP_STYLE, tooltip_duration=None ) ], style={&#39;padding&#39;: &#39;20px&#39;}) ]), # 数据分析 dcc.Tab(id=&#39;tab-analyze&#39;, label=&#39;数据分析&#39;, value=&#39;tab-analyze&#39;, disabled=True, children=[ html.Div([ html.H4("分析结果"), html.Button("刷新结果", id="btn-refresh-result", n_clicks=0, style={&#39;margin&#39;: &#39;10px&#39;}), html.Div(id=&#39;result-timestamp&#39;, style={&#39;margin&#39;: &#39;10px&#39;, &#39;color&#39;: &#39;#555&#39;}), dash_table.DataTable( id=&#39;analysis-result-table&#39;, page_size=20, style_table=TABLE_STYLE, style_cell=CELL_TOOLTIP_STYLE, tooltip_duration=None ), html.Div([ html.Button("导出全部数据", id="btn-export", n_clicks=0, style={&#39;margin&#39;: &#39;20px&#39;}), dcc.Download(id="download-data") ]) ], style={&#39;padding&#39;: &#39;20px&#39;}) ]) ]), # 存储组件 dcc.Store(id=&#39;stored-data-path&#39;), dcc.Store(id=&#39;stored-sheet-name&#39;), ]) # ------------------------------- # 🔁 回调 Callbacks # ------------------------------- from dash.exceptions import PreventUpdate @app.callback( [Output(&#39;file-info&#39;, &#39;children&#39;), Output(&#39;cache-status&#39;, &#39;children&#39;), Output(&#39;stored-data-path&#39;, &#39;data&#39;), Output(&#39;sheet-dropdown&#39;, &#39;options&#39;), Output(&#39;sheet-dropdown&#39;, &#39;value&#39;)], Input(&#39;upload-data&#39;, &#39;contents&#39;), [State(&#39;upload-data&#39;, &#39;filename&#39;)] ) def handle_upload(contents, filename): if contents is None: raise PreventUpdate content_type, content_string = contents.split(&#39;,&#39;, 1) decoded = base64.b64decode(content_string) temp_path = f"./temp_{filename}" with open(temp_path, &#39;wb&#39;) as f: f.write(decoded) file_size = get_file_size_mb(temp_path) info_msg = f"📁 {filename} | 💾 {file_size} MB" pickle_path = temp_path + ".pickle" if os.path.exists(pickle_path): sheets_dict = load_from_pickle(pickle_path) cache_msg = "✅ 从缓存加载" else: sheets_dict, err = read_excel_with_sheets(temp_path) if err: return f"❌ 失败: {err}", "", None, [], None save_to_pickle(sheets_dict, pickle_path) cache_msg = "🆕 已缓存" sheet_options = [{&#39;label&#39;: s, &#39;value&#39;: s} for s in sheets_dict.keys()] return info_msg, cache_msg, temp_path, sheet_options, sheet_options[0][&#39;value&#39;] @app.callback( [Output(&#39;data-preview-table&#39;, &#39;children&#39;), Output(&#39;validation-alert&#39;, &#39;children&#39;), Output(&#39;tab-classify&#39;, &#39;disabled&#39;), Output(&#39;tab-analyze&#39;, &#39;disabled&#39;)], [Input(&#39;sheet-dropdown&#39;, &#39;value&#39;), State(&#39;stored-data-path&#39;, &#39;data&#39;)] ) def update_preview(sheet, file_path): time_print("update preview...") if not file_path or not sheet: raise PreventUpdate try: df = load_from_pickle(file_path + ".pickle")[sheet] required = ["条目描述", "项目编码", "项目名称"] valid, missing = validate_columns(df, required) if valid: alert = "" can_proceed = True table = dash_table.DataTable( data=df.to_dict(&#39;records&#39;), columns=[{"name": c, "id": c} for c in df.columns], page_size=20, style_table=TABLE_STYLE, style_cell=CELL_TOOLTIP_STYLE, tooltip_data=[ {c: {&#39;value&#39;: str(v), &#39;type&#39;: &#39;markdown&#39;} for c, v in row.items()} for row in df.to_dict(&#39;records&#39;) ], tooltip_duration=None ) else: alert = dbc.Alert(f"❌ 缺少字段: {&#39;, &#39;.join(missing)}", color="danger") table = None can_proceed = False return table, alert, not can_proceed, not can_proceed except Exception as e: alert = dbc.Alert(f"❌ 预览失败: {e}", color="danger") return None, alert, True, True @app.callback( [Output(&#39;classification-table&#39;, &#39;data&#39;), Output(&#39;classification-table&#39;, &#39;columns&#39;), Output(&#39;classification-table&#39;, &#39;tooltip_data&#39;), Output(&#39;classification-table&#39;, &#39;dropdown&#39;)], Input(&#39;main-tabs&#39;, &#39;value&#39;), [State(&#39;stored-data-path&#39;, &#39;data&#39;), State(&#39;sheet-dropdown&#39;, &#39;value&#39;)], prevent_initial_call=True ) def load_classification_data(tab, file_path, selected_sheet, ): time_print("load classification data...", tab,file_path, selected_sheet) if tab != &#39;tab-classify&#39; or not file_path: # if not file_path or not selected_sheet: time_print("load_classification_data raise...") raise PreventUpdate try: class_file = os.path.join(os.path.dirname(file_path), "classification.xlsx") df_class = None # 尝试加载已有分类文件 if os.path.exists(class_file): df_class = pd.read_excel(class_file) required = ["条目描述", "条目分类", "项目编码"] valid, _ = validate_columns(df_class, required) if not valid: df_class = None # 若无有效分类文件,则从主表生成 if df_class is None: pickle_path = file_path + ".pickle" sheets_dict = load_from_pickle(pickle_path) main_df = sheets_dict[selected_sheet] if "条目描述" in main_df.columns: unique_desc = main_df["条目描述"].drop_duplicates().reset_index(drop=True) df_class = pd.DataFrame({ "条目描述": unique_desc, "条目分类": "", "项目编码": "" }) else: df_class = pd.DataFrame([{ "条目描述": "字段缺失", "条目分类": "", "项目编码": "" }]) # 确保 CATEGORY_OPTIONS 存在 options = CATEGORY_OPTIONS.copy() if CATEGORY_OPTIONS else [] # 构建列 columns = [ {"name": "条目描述", "id": "条目描述", "editable": False}, {"name": "条目分类", "id": "条目分类", "presentation": "dropdown", "editable": True}, {"name": "项目编码", "id": "项目编码", "editable": True, } ] # 构建数据 data = df_class.to_dict(&#39;records&#39;) if not data: data = [{"条目描述": "无数据", "条目分类": "", "项目编码": ""}] # 构建 tooltip tooltip_data = [ {k: {&#39;value&#39;: str(v), &#39;type&#39;: &#39;markdown&#39;} for k, v in row.items()} for row in data ] # ✅ 关键:确保 dropdown 结构正确 dropdown = { "条目分类": { "options": options or [] } } global CLASSIFICATION_CACHE CLASSIFICATION_CACHE = data.copy() if len(data) == 0: data = [{"条目描述": "", "条目分类": "", "项目编码": ""}] # time_print(dropdown) # columns, tooltip_data, # time_print(data.__len__()) # time_print(columns.__len__()) # time_print(tooltip_data.__len__()) # time_print(dropdown.__len__()) # time_print(data) # time_print(columns) # time_print(tooltip_data) # time_print(dropdown) time_print("return ...") return data, columns, tooltip_data, dropdown except Exception as e: time_print(f"[Error] 加载分类数据失败: {e}") # ✅ 返回兜底值,防止前端崩溃 fallback_cols = [ {"name": "条目描述", "id": "条目描述"}, {"name": "条目分类", "id": "条目分类", "presentation": "dropdown"}, {"name": "项目编码", "id": "项目编码"} ] empty_data = [{"条目描述": "加载失败", "条目分类": "", "项目编码": ""}] return ( empty_data, fallback_cols, [{}], {"条目分类": {"options": CATEGORY_OPTIONS or []}} ) @app.callback( [Output(&#39;category-modal&#39;, &#39;is_open&#39;), Output(&#39;code-modal&#39;, &#39;is_open&#39;)], [Input(&#39;btn-batch-category&#39;, &#39;n_clicks&#39;), Input(&#39;btn-batch-code&#39;, &#39;n_clicks&#39;), Input(&#39;close-category-modal&#39;, &#39;n_clicks&#39;), Input(&#39;close-code-modal&#39;, &#39;n_clicks&#39;), Input(&#39;confirm-category-modal&#39;, &#39;n_clicks&#39;), Input(&#39;confirm-code-modal&#39;, &#39;n_clicks&#39;)], [State(&#39;category-modal&#39;, &#39;is_open&#39;), State(&#39;code-modal&#39;, &#39;is_open&#39;)], prevent_initial_call=True ) def toggle_modals(*args): time_print("toggle modals...") ctx = callback_context if not ctx.triggered: return False, False btn = ctx.triggered[0][&#39;prop_id&#39;].split(&#39;.&#39;)[0] if btn == &#39;btn-batch-category&#39;: return True, False elif btn == &#39;btn-batch-code&#39;: return False, True elif btn in [&#39;close-category-modal&#39;, &#39;confirm-category-modal&#39;]: return False, args[-2] elif btn in [&#39;close-code-modal&#39;, &#39;confirm-code-modal&#39;]: return args[-3], False return False, False # ============================================= # ✅ 修复 Bug:支持单行 inline 编辑 # ============================================= # @app.callback( # Output(&#39;classification-table&#39;, &#39;data&#39;, allow_duplicate=True), # Input(&#39;classification-table&#39;, &#39;data_timestamp&#39;), # State(&#39;classification-table&#39;, &#39;data&#39;), # prevent_initial_call=True # ) def capture_inline_edit(data_timestamp, current_data): time_print("capture inline edit...") if not current_data: raise PreventUpdate return current_data @app.callback( Output(&#39;classification-table&#39;, &#39;data&#39;, allow_duplicate=True), [Input(&#39;confirm-category-modal&#39;, &#39;n_clicks&#39;), Input(&#39;confirm-code-modal&#39;, &#39;n_clicks&#39;)], [State(&#39;classification-table&#39;, &#39;data&#39;), State(&#39;classification-table&#39;, &#39;selected_rows&#39;), State(&#39;modal-category-dropdown&#39;, &#39;value&#39;), State(&#39;modal-code-input&#39;, &#39;value&#39;)], prevent_initial_call=True ) def apply_batch_changes(nc1, nc2, data, selected_rows, category, code): time_print("apply_batch_changes...") if not data: raise PreventUpdate indices = selected_rows if selected_rows else range(len(data)) # 全选时应用所有行 if &#39;confirm-category-modal&#39; in callback_context.triggered[0][&#39;prop_id&#39;] and category: # 分类修改 for i in indices: data[i][&#39;条目分类&#39;] = category # elif &#39;confirm-code-modal&#39; in callback_context.triggered[0][&#39;prop_id&#39;]: # final_code = code or "ALL" # for i in indices: # data[i][&#39;项目编码&#39;] = final_code return data @app.callback( Output(&#39;selection-count&#39;, &#39;children&#39;), Input(&#39;classification-table&#39;, &#39;derived_viewport_selected_row_ids&#39;) ) def show_selection_count(selected): return f"✅ 已选中 {len(selected or [])} 行" if selected else "未选中任何行" @app.callback( [Output(&#39;analysis-result-table&#39;, &#39;data&#39;), Output(&#39;analysis-result-table&#39;, &#39;columns&#39;), Output(&#39;analysis-result-table&#39;, &#39;tooltip_data&#39;), Output(&#39;result-timestamp&#39;, &#39;children&#39;)], # [Input(&#39;btn-refresh-result&#39;, &#39;n_clicks&#39;), Input(&#39;main-tabs&#39;, &#39;value&#39;), [State(&#39;stored-data-path&#39;, &#39;data&#39;), State(&#39;sheet-dropdown&#39;, &#39;value&#39;), State(&#39;classification-table&#39;, &#39;data&#39;)], prevent_initial_call=True ) def run_analysis(tab, file_path, sheet, class_data): # time_print("run analysis...") time_print("run analysis data...", tab,file_path, sheet) global RESULT_CACHE, RESULT_CACHE_TIME if tab != &#39;tab-analyze&#39; or not file_path: time_print("run analysis raise...") raise PreventUpdate now = datetime.now() do_refresh = callback_context.triggered[0][&#39;prop_id&#39;].startswith(&#39;btn-refresh&#39;) expired = RESULT_CACHE_TIME is None or (now - RESULT_CACHE_TIME) > CACHE_TTL if do_refresh or expired: try: df = load_from_pickle(file_path + ".pickle")[sheet] class_df = pd.DataFrame(class_data) RESULT_CACHE = site_model(df, class_df) RESULT_CACHE_TIME = now except Exception as e: RESULT_CACHE = pd.DataFrame({"错误": [str(e)]}) data = RESULT_CACHE.to_dict(&#39;records&#39;) cols = [{"name": c, "id": c} for c in RESULT_CACHE.columns] tooltips = [{k: {&#39;value&#39;: str(v), &#39;type&#39;: &#39;markdown&#39;} for k, v in row.items()} for row in data] ts = RESULT_CACHE_TIME.strftime("%Y-%m-%d %H:%M:%S") if RESULT_CACHE_TIME else "无" return data, cols, tooltips, f"⏱️ 上次刷新: {ts}" @app.callback( Output("download-data", "data"), Input("btn-export", "n_clicks"), [State(&#39;stored-data-path&#39;, &#39;data&#39;), State(&#39;sheet-dropdown&#39;, &#39;value&#39;), State(&#39;classification-table&#39;, &#39;data&#39;), State(&#39;analysis-result-table&#39;, &#39;data&#39;), State(&#39;analysis-result-table&#39;, &#39;columns&#39;)], prevent_initial_call=True ) def export_data(n, file_path, sheet, class_data, result_data, result_cols): if not file_path: return None buffer = io.BytesIO() with pd.ExcelWriter(buffer, engine=&#39;openpyxl&#39;) as writer: original = load_from_pickle(file_path + ".pickle")[sheet] original.to_excel(writer, index=False, sheet_name="原始数据") pd.DataFrame(class_data).to_excel(writer, index=False, sheet_name="条目分类") pd.DataFrame(result_data).to_excel(writer, index=False, sheet_name="分析结果") buffer.seek(0) filename = f"站点模型分类结果_{datetime.now().strftime(&#39;%Y%m%d_%H%M%S&#39;)}.xlsx" return dcc.send_bytes(buffer.getvalue(), filename) # @app.callback( # Input(&#39;tab-classify&#39;, &#39;nclicks&#39;), # ) # def test_tag_click(nclick): # time_print(f"click {nclick} times....") if __name__ == &#39;__main__&#39;: app.run(debug=True, port=8050) 为什么不能跳转到tab-calssify,对比analysy分析
最新发布
09-25
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值