新手进阶Python:办公看板升级交互式可视化+移动端适配+多终端同步

大家好!我是CSDN的Python新手博主~ 上一篇我们完成了看板的跨系统联动与可视化任务编排,解决了多系统数据互通与自动化办公需求,但甲方客户反馈两大核心痛点:① 可视化体验单一,现有图表多为静态展示,无法实现数据钻取、联动筛选、自定义查看,数据分析效率低;② 不支持移动端适配,员工外出时无法通过手机查看看板数据、处理待办任务,只能依赖电脑,灵活性不足;③ 多终端数据不同步,电脑端修改的看板配置、查看的历史数据,手机端无法同步,操作体验割裂。今天就带来超落地的新手实战项目------办公看板升级交互式可视化+移动端适配+多终端同步!

本次基于之前的"跨系统联动看板"代码,新增3大核心功能:① 交互式可视化升级(ECharts进阶,实现图表联动、数据钻取、自定义筛选、主题切换,支持拖拽调整图表位置);② 移动端全面适配(基于Bootstrap响应式布局,优化看板组件、适配触摸操作,支持手机/平板横屏/竖屏切换);③ 多终端数据同步(基于Redis缓存+Token身份校验,实现看板配置、查看记录、待办任务、操作日志多终端实时同步)。全程基于现有技术栈(Flask+MySQL+ECharts+Bootstrap+Redis),新增交互式组件、响应式布局、多终端同步引擎,代码注释详细,新手只需配置适配参数、复制代码,就能完成升级,让看板兼顾数据分析效率与移动办公灵活性~

一、本次学习目标

  1. 掌握ECharts交互式可视化技巧,实现图表联动、数据钻取、自定义筛选、主题切换,提升数据分析体验,新手也能快速上手进阶配置;

  2. 学会Bootstrap响应式布局开发,优化看板组件结构,适配不同终端(电脑、手机、平板),解决移动端显示错乱、操作不便的问题;

  3. 理解多终端同步原理,基于Redis缓存与Token校验,实现看板配置、查看记录、待办任务等数据的实时同步,确保多终端操作一致性;

  4. 实现交互式可视化、移动端适配、多终端同步的功能闭环,支持员工随时随地查看数据、处理任务,提升办公灵活性;

  5. 适配企业移动办公场景,支持移动端快捷操作(一键刷新、快速筛选、待办处理),兼顾电脑端数据分析与移动端便捷性。

二、前期准备

  1. 安装核心依赖库

安装核心依赖(Redis缓存、响应式组件、ECharts进阶适配)

pip3 install redis flask-cors flask-bootstrap5 flask-wtf -i https://pypi.tuna.tsinghua.edu.cn/simple

确保已有依赖正常(Flask、ECharts、跨系统联动等)

pip3 install --upgrade flask flask-login flask-sqlalchemy requests celery pymysql openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple

说明:多终端同步用Redis实现数据缓存(存储看板配置、查看记录等);移动端适配用Bootstrap5响应式框架,无需额外开发移动端页面;交互式可视化基于ECharts进阶API,复用现有图表基础,只需新增配置。

  1. 第三方服务与配置准备
  • 交互式可视化配置:定义图表联动规则(如点击柱状图某一项,折线图同步显示对应数据)、数据钻取层级(如从部门数据钻取到个人数据)、筛选条件(时间范围、数据类型、部门筛选)、可选主题(浅色/深色/自定义配色);

  • 移动端适配配置:设置不同终端的看板布局(电脑端多列布局、手机端单列布局)、组件适配参数(按钮大小、字体大小、图表尺寸)、触摸操作规则(滑动切换看板、长按筛选);

  • 多终端同步配置:确定同步数据范围(看板配置、查看记录、待办任务、操作日志)、缓存过期时间(默认24小时,关键数据永久缓存)、Token校验规则(同一账号多终端登录,Token同步生效);

  • 环境准备:确保Redis服务正常启动(复用之前Celery使用的Redis,无需新增),云服务器开放移动端访问端口,配置跨域(支持手机端访问后端接口)。

  1. 数据库表优化与创建

-- 连接MySQL数据库(替换为你的数据库信息)

mysql -u office_user -p -h 47.108.xxx.xxx office_data

-- 创建可视化配置表(visual_config)

CREATE TABLE visual_config (

id INT AUTO_INCREMENT PRIMARY KEY,

user_id INT NOT NULL COMMENT '用户ID(0表示全局配置)',

dashboard_id INT NOT NULL COMMENT '看板ID',

chart_config TEXT NOT NULL COMMENT '图表配置(JSON格式,含ECharts参数、位置、大小)',

filter_config TEXT NOT NULL COMMENT '筛选配置(JSON格式,含默认筛选条件、可选筛选项)',

theme VARCHAR(20) DEFAULT 'light' COMMENT '可视化主题(light/dark/custom)',

theme_config TEXT NULL COMMENT '自定义主题配置(JSON格式,配色方案)',

is_default TINYINT(1) DEFAULT 0 COMMENT '是否为默认配置(1-是,0-否)',

create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

KEY idx_user_dashboard (user_id, dashboard_id),

FOREIGN KEY (dashboard_id) REFERENCES dashboard_info(id) ON DELETE CASCADE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交互式可视化配置表';

-- 创建终端同步记录表(terminal_sync)

CREATE TABLE terminal_sync (

id BIGINT AUTO_INCREMENT PRIMARY KEY,

user_id INT NOT NULL COMMENT '用户ID',

token VARCHAR(100) NOT NULL COMMENT '用户登录Token',

terminal_type ENUM('pc', 'mobile', 'pad') NOT NULL COMMENT '终端类型',

sync_data TEXT NOT NULL COMMENT '同步数据(JSON格式,看板配置、查看记录等)',

sync_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '同步时间',

last_operate_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '最后操作时间',

KEY idx_user_token (user_id, token),

KEY idx_terminal_type (terminal_type)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='多终端同步记录表';

-- 创建用户看板偏好表(user_dashboard_prefer)

CREATE TABLE user_dashboard_prefer (

id INT AUTO_INCREMENT PRIMARY KEY,

user_id INT NOT NULL COMMENT '用户ID',

dashboard_id INT NOT NULL COMMENT '看板ID',

prefer_config TEXT NOT NULL COMMENT '偏好配置(JSON格式,默认查看页面、快捷操作、排序方式)',

recent_view TEXT NULL COMMENT '最近查看记录(JSON格式,图表ID、查看时间)',

mobile_layout TINYINT(1) DEFAULT 1 COMMENT '移动端布局(1-单列,2-双列)',

create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

UNIQUE KEY uk_user_dashboard (user_id, dashboard_id),

FOREIGN KEY (dashboard_id) REFERENCES dashboard_info(id) ON DELETE CASCADE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户看板偏好表(适配多终端)';

-- 初始化全局可视化配置示例

INSERT INTO visual_config (user_id, dashboard_id, chart_config, filter_config, theme, is_default)

VALUES (

0,

1, -- 假设看板ID为1(主看板)

'{"charts":[{"id":1,"type":"bar","position":{"x":0,"y":0,"width":50,"height":40},"echarts_config":{"title":{"text":"每日订单量"},"xAxis":{"type":"category"},"yAxis":{"type":"value"}}},{"id":2,"type":"line","position":{"x":50,"y":0,"width":50,"height":40},"echarts_config":{"title":{"text":"每日销售额"}}}]}',

'{"default_filter":{"start_date":"2026-01-01","end_date":"2026-01-31","dept":"all"},"optional_filters":["dept","product","status"]}',

'light',

1

);

-- 初始化用户看板偏好示例

INSERT INTO user_dashboard_prefer (user_id, dashboard_id, prefer_config, recent_view, mobile_layout)

VALUES (

1, -- 管理员用户ID

1,

'{"default_page":"data_overview","quick_operate":["refresh","export","filter"],"sort_type":"time_desc"}',

'[{"chart_id":1,"view_time":"2026-01-26 10:30:00"},{"chart_id":2,"view_time":"2026-01-26 10:35:00"}]',

1

);

三、实战:交互式可视化+移动端适配+多终端同步升级

  1. 第一步:交互式可视化升级,实现图表联动与数据钻取

-- coding: utf-8 --

visual_upgrade.py 交互式可视化升级模块

import json

from flask import Blueprint, request, jsonify, g

from flask_login import login_required, current_user

from models import db, VisualConfig, DashboardInfo, UserDashboardPrefer

from audit_engine import audit_log

from dotenv import load_dotenv

import os

import redis

加载环境变量

load_dotenv()

初始化Redis连接(用于缓存可视化配置,提升响应速度)

redis_client = redis.Redis(

host=os.getenv("REDIS_HOST", "localhost"),

port=int(os.getenv("REDIS_PORT", 6379)),

db=int(os.getenv("REDIS_DB", 0)),

decode_responses=True

)

visual_bp = Blueprint("visual", name)

====================== 核心功能:获取交互式可视化配置 ======================

def get_visual_config(user_id, dashboard_id):

"""

获取用户的看板可视化配置(优先用户自定义,无则用全局配置)

:param user_id: 用户ID

:param dashboard_id: 看板ID

:return: 可视化配置字典

"""

先从Redis缓存获取,提升速度

cache_key = f"visual_config:{user_id}:{dashboard_id}"

cache_config = redis_client.get(cache_key)

if cache_config:

return json.loads(cache_config)

复制代码
# 从数据库获取:优先用户自定义配置
visual_config = VisualConfig.query.filter_by(
    user_id=user_id, dashboard_id=dashboard_id
).order_by(VisualConfig.is_default.desc()).first()

# 无用户配置,获取全局配置(user_id=0)
if not visual_config:
    visual_config = VisualConfig.query.filter_by(
        user_id=0, dashboard_id=dashboard_id, is_default=1
    ).first()
    if not visual_config:
        raise Exception(f"未找到看板{dashboard_id}的可视化配置")

# 格式化配置数据
config_data = {
    "chart_config": json.loads(visual_config.chart_config),
    "filter_config": json.loads(visual_config.filter_config),
    "theme": visual_config.theme,
    "theme_config": json.loads(visual_config.theme_config) if visual_config.theme_config else None
}

# 存入Redis缓存(过期时间24小时)
redis_client.setex(cache_key, 86400, json.dumps(config_data))
return config_data

====================== 核心功能:图表联动与数据钻取逻辑 ======================

def get_linked_chart_data(chart_id, drill_down_params=None):

"""

获取联动图表数据、钻取数据

:param chart_id: 触发联动的图表ID

:param drill_down_params: 数据钻取参数(如部门ID、时间范围)

:return: 联动图表数据字典

"""

模拟数据(实际项目中从数据库/跨系统接口获取)

base_data = {

1: {"name": "每日订单量", "type": "bar", "data": [120, 180, 150, 200, 220, 250]},

2: {"name": "每日销售额", "type": "line", "data": [12000, 18000, 15000, 20000, 22000, 25000]},

3: {"name": "订单来源分布", "type": "pie", "data": [{"name": "线上", "value": 600}, {"name": "线下", "value": 300}]}

}

复制代码
# 图表联动:点击图表1(订单量),联动图表2(销售额)显示对应数据
if chart_id == 1 and not drill_down_params:
    return {
        "trigger_chart": base_data[1],
        "linked_charts": [base_data[2]]  # 联动图表2
    }

# 数据钻取:点击图表1的某一项(如200),钻取显示该日各部门订单量
if chart_id == 1 and drill_down_params:
    day = drill_down_params.get("day", 3)  # 假设点击第3天(索引3,数据200)
    drill_data = {
        "name": f"第{day+1}天各部门订单量",
        "type": "bar",
        "data": [{"name": "销售部", "value": 80}, {"name": "市场部", "value": 60}, {"name": "客服部", "value": 60}]
    }
    return {
        "trigger_chart": base_data[1],
        "drill_down_data": drill_data  # 钻取数据
    }

# 默认返回所有图表数据
return {"all_charts": base_data}

====================== 接口:交互式可视化配置管理 ======================

@visual_bp.route("/visual/config/int:dashboard_id", methods=["GET"])

@login_required

def get_visual_config_api(dashboard_id):

"""获取看板交互式可视化配置"""

try:

config_data = get_visual_config(current_user.id, dashboard_id)

return jsonify({"success": True, "data": config_data})

except Exception as e:

return jsonify({"success": False, "error": str(e)})

@visual_bp.route("/visual/config/save/int:dashboard_id", methods=["POST"])

@login_required

@audit_log("update")

def save_visual_config(dashboard_id):

"""保存用户自定义可视化配置(图表位置、筛选条件、主题)"""

data = request.get_json()

required_fields = ["chart_config", "filter_config"]

for field in required_fields:

if not data.get(field):

return jsonify({"success": False, "error": f"缺少必填字段:{field}"})

复制代码
try:
    # 验证JSON格式
    json.loads(data["chart_config"])
    json.loads(data["filter_config"])
    if data.get("theme_config"):
        json.loads(data["theme_config"])
except json.JSONDecodeError:
    return jsonify({"success": False, "error": "配置字段格式错误(需JSON)"})

# 检查是否已有配置,有则更新,无则新增
visual_config = VisualConfig.query.filter_by(
    user_id=current_user.id, dashboard_id=dashboard_id
).first()

if visual_config:
    visual_config.chart_config = data["chart_config"]
    visual_config.filter_config = data["filter_config"]
    visual_config.theme = data.get("theme", "light")
    visual_config.theme_config = data.get("theme_config")
else:
    visual_config = VisualConfig(
        user_id=current_user.id,
        dashboard_id=dashboard_id,
        chart_config=data["chart_config"],
        filter_config=data["filter_config"],
        theme=data.get("theme", "light"),
        theme_config=data.get("theme_config"),
        is_default=0
    )
    db.session.add(visual_config)

db.session.commit()
# 更新Redis缓存
cache_key = f"visual_config:{current_user.id}:{dashboard_id}"
config_data = {
    "chart_config": json.loads(data["chart_config"]),
    "filter_config": json.loads(data["filter_config"]),
    "theme": data.get("theme", "light"),
    "theme_config": json.loads(data["theme_config"]) if data.get("theme_config") else None
}
redis_client.setex(cache_key, 86400, json.dumps(config_data))
return jsonify({"success": True, "msg": "可视化配置保存成功"})

====================== 接口:图表联动与数据钻取 ======================

@visual_bp.route("/visual/chart/link", methods=["POST"])

@login_required

def chart_link_api():

"""图表联动与数据钻取接口"""

data = request.get_json()

chart_id = data.get("chart_id")

drill_down_params = data.get("drill_down_params")

复制代码
if not chart_id:
    return jsonify({"success": False, "error": "缺少图表ID"})

try:
    chart_data = get_linked_chart_data(chart_id, drill_down_params)
    return jsonify({"success": True, "data": chart_data})
except Exception as e:
    return jsonify({"success": False, "error": str(e)})

====================== 接口:主题切换 ======================

@visual_bp.route("/visual/theme/switch/int:dashboard_id", methods=["POST"])

@login_required

def switch_theme(dashboard_id):

"""切换看板可视化主题(浅色/深色/自定义)"""

data = request.get_json()

theme = data.get("theme")

if not theme or theme not in ["light", "dark", "custom"]:

return jsonify({"success": False, "error": "无效的主题类型(仅支持light/dark/custom)"})

复制代码
# 更新数据库配置
visual_config = VisualConfig.query.filter_by(
    user_id=current_user.id, dashboard_id=dashboard_id
).first()
if not visual_config:
    # 无用户自定义配置,新增配置
    visual_config = VisualConfig(
        user_id=current_user.id,
        dashboard_id=dashboard_id,
        chart_config=json.dumps({"charts": []}),
        filter_config=json.dumps({"default_filter": {}, "optional_filters": []}),
        theme=theme,
        theme_config=data.get("theme_config"),
        is_default=0
    )
    db.session.add(visual_config)
else:
    visual_config.theme = theme
    if theme == "custom":
        if not data.get("theme_config"):
            return jsonify({"success": False, "error": "自定义主题需传入theme_config配置"})
        visual_config.theme_config = data["theme_config"]
    else:
        visual_config.theme_config = None

db.session.commit()
# 更新Redis缓存
cache_key = f"visual_config:{current_user.id}:{dashboard_id}"
config_data = get_visual_config(current_user.id, dashboard_id)
redis_client.setex(cache_key, 86400, json.dumps(config_data))
return jsonify({"success": True, "msg": f"主题切换为{theme}成功"})

{% extends "base.html" %}

{% block content %}

交互式数据看板
复制代码
    <!-- 筛选条件 -->
    <div class="d-flex flex-wrap align-items-center gap-2" id="filterBar">
      <select class="form-select form-select-sm" id="deptFilter">
        <option value="all" selected>全部部门</option>
        <option value="sales">销售部</option>
        <option value="market">市场部</option>
        <option value="service">客服部</option>
      </select>
      <input type="date" class="form-control form-control-sm" id="startDate" value="2026-01-01">
      <span>至</span>
      <input type="date" class="form-control form-control-sm" id="endDate" value="2026-01-31">
      <button class="btn btn-sm btn-primary" id="filterBtn">筛选</button>
    </div>
    
    <!-- 主题切换与刷新 -->
    <div class="d-flex gap-2">
      <select class="form-select form-select-sm" id="themeSwitch">
        <option value="light" selected>浅色主题</option>
        <option value="dark">深色主题</option>
        <option value="custom">自定义主题</option>
      </select>
      <button class="btn btn-sm btn-secondary" id="refreshBtn"><i class="bi bi-arrow-clockwise"></i> 刷新</button>
      <button class="btn btn-sm btn-success" id="saveConfigBtn"><i class="bi bi-save"></i> 保存配置</button>
    </div>
  </div>
</div>
每日订单量

点击图表联动销售额,点击数据项钻取详情

复制代码
<!-- 图表2:每日销售额(折线图,支持联动) -->
<div class="col-12 col-md-6 chart-item" data-chart-id="2">
  <div class="card h-100">
    <div class="card-header">
      <h5 class="card-title mb-0">每日销售额</h5>
    </div>
    <div class="card-body p-0">
      <div id="chart2" class="chart-dom" style="width:100%;height:300px;"></div>
    </div>
  </div>
</div>

<!-- 图表3:订单来源分布(饼图) -->
<div class="col-12 chart-item" data-chart-id="3">
  <div class="card h-100">
    <div class="card-header">
      <h5 class="card-title mb-0">订单来源分布</h5>
    </div>
    <div class="card-body p-0">
      <div id="chart3" class="chart-dom" style="width:100%;height:300px;"></div>
    </div>
  </div>
</div>

<!-- 钻取数据显示区域(默认隐藏) -->
<div class="col-12" id="drillDownArea" style="display:none;">
  <div class="card">
    <div class="card-header d-flex justify-content-between align-items-center">
      <h5 class="card-title mb-0" id="drillDownTitle">数据钻取详情</h5>
      <button class="btn btn-sm btn-danger" id="closeDrillBtn">关闭</button>
    </div>
    <div class="card-body p-0">
      <div id="drillDownChart" style="width:100%;height:300px;"></div>
    </div>
  </div>
</div>

{% endblock %}

  1. 第二步:移动端响应式适配,优化移动办公体验

-- coding: utf-8 --

mobile_adapt.py 移动端适配模块

import json

from flask import Blueprint, request, jsonify, render_template

from flask_login import login_required, current_user

from models import UserDashboardPrefer, db

from audit_engine import audit_log

from dotenv import load_dotenv

import os

加载环境变量

load_dotenv()

mobile_bp = Blueprint("mobile", name)

====================== 核心功能:判断终端类型 ======================

def get_terminal_type():

"""

判断终端类型(pc/mobile/pad)

:return: 终端类型字符串

"""

user_agent = request.headers.get("User-Agent", "").lower()

判断移动端(手机)

if any(keyword in user_agent for keyword in ["mobile", "android", "iphone", "ios"]):

return "mobile"

判断平板

elif any(keyword in user_agent for keyword in ["ipad", "tablet"]):

return "pad"

默认PC端

else:

return "pc"

====================== 核心功能:移动端数据压缩(提升加载速度) ======================

def compress_mobile_data(data):

"""

移动端数据压缩:简化图表数据、减少字段,提升加载速度

:param data: 原始数据

:return: 压缩后的数据

"""

if not isinstance(data, dict):

return data

复制代码
# 简化图表数据(保留核心字段,减少数据点)
if "chart_config" in data and "charts" in data["chart_config"]:
    for chart in data["chart_config"]["charts"]:
        if "echarts_config" in chart and "series" in chart["echarts_config"]:
            for series in chart["echarts_config"]["series"]:
                # 数据点数量减半(移动端无需过多细节)
                if isinstance(series["data"], list) and len(series["data"]) > 10:
                    series["data"] = series["data"][::2]  # 每隔一个取一个数据点
                # 移除不必要的样式配置
                if "itemStyle" in series:
                    series["itemStyle"] = {"color": series["itemStyle"].get("color", "#165DFF")}
                if "lineStyle" in series:
                    series["lineStyle"] = {"width": series["lineStyle"].get("width", 2)}

# 简化筛选配置(移动端只保留常用筛选项)
if "filter_config" in data:
    filter_config = data["filter_config"]
    if "optional_filters" in filter_config:
        # 保留前3个常用筛选项
        filter_config["optional_filters"] = filter_config["optional_filters"][:3]

return data

====================== 接口:移动端看板页面

相关推荐
m0_561359672 小时前
Python面向对象编程(OOP)终极指南
jvm·数据库·python
zhangfeng11332 小时前
deepseek部署和训练的PyTorch CUDA Transformers Accelerate PEFT稳定版本呢推荐
人工智能·pytorch·python
Bruk.Liu2 小时前
(LangChain实战5):LangChain消息模版ChatPromptTemplate
人工智能·python·langchain·agent
Wiktok2 小时前
SQLAlchemy+PyMySQL的实用实战示例
python·mysql·sqlalchemy
1candobetter2 小时前
JAVA后端开发——Spring Boot 组件化自动配置机制
java·开发语言·spring boot
yufuu982 小时前
用Python批量处理Excel和CSV文件
jvm·数据库·python
深蓝电商API2 小时前
异步爬虫结合 MongoDB 异步驱动 pymongo:高效数据爬取与存储实践
爬虫·python·mongodb
一个网络学徒2 小时前
python练习3
开发语言·python
爱敲代码的TOM2 小时前
大模型应用开发-LangChain框架基础
python·langchain·大模型应用