news 2026/10/24 19:21:05

Odoo17自定义仪表盘开发:OWL与Chart.js实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Odoo17自定义仪表盘开发:OWL与Chart.js实战

简介:Odoo17 自定义仪表盘开发示例源码面向具备 Python 与 Odoo 基础、希望学习后台仪表盘扩展方式的开发者,也可供二次开发人员参考模块组织与前后端协作思路。压缩包共二十五个文件,以九个脚本、六个 XML 视图、四个 Python 后端文件及三个编译缓存为主,另含样式表、说明文档和图标图片,整包约十六 KB,体量轻便,便于查阅与改造。代码围绕仪表盘模块展开,涵盖模块清单、控制器、静态资源与视图定义等关键环节,可帮助读者理解 Odoo17 仪表盘从后端数据接口、路由控制到前端展示的完整链路。已有二百五十四人学习或下载,适合对照搭建自己的仪表盘插件原型,并借鉴目录划分、清单配置和 XML 视图写法,减少从零摸索的成本,也能为课程演示或内部工具开发提供可复用骨架,目录清晰的特点也方便按功能定位代码。

1. 从一张空白仪表盘说起:Odoo17 看板真正要动的只有四个文件

把 Odoo17 装好,翻遍设置也找不到「自定义仪表盘」这个开关——原生只给到 graph 和 pivot 两种分析视图,想在企业首页摆一张带 KPI 卡片加趋势图的看板,只能自己写模块。这份 Odoo17 自定义仪表盘开发示例源码走的正是官方推荐的那条路:一个独立模块、一个ir.actions.client动作、一个 OWL 组件,再加一个基于_read_group的后端聚合方法,全程不侵入后端核心代码。它适合两类人:一类是 Odoo 实施顾问,客户张口就要「首页一眼看到这半年销售额」;另一类是刚接手二开任务的 Python 后端,第一次被扔进 OWL 和 assets 打包这两个新坑里。下面按文件拆,从骨架一路拆到图表实例。

2. 模块骨架与资产打包:把看板挂进 Odoo17 最小可跑结构

2.1 先定结构:一个独立模块,五个必需文件

自定义仪表盘最稳的做法是开新模块,而不是往web里塞代码。理由很直接:升级 Odoo 大版本时,改过核心的模块基本等于给自己埋雷;而独立模块最差也就是卸载重装。这套示例源码的目录结构不大,但每个文件都承担固定职责,少一个页面就转不起来。

custom_dashboard/ ├── __init__.py ├── __manifest__.py ├── models/ │ ├── __init__.py │ └── sale_order.py ├── views/ │ ├── dashboard_action.xml │ └── dashboard_menus.xml └── static/ └── src/ ├── js/dashboard.js ├── xml/dashboard.xml └── scss/dashboard.scss

这段结构里models/负责数据聚合,views/负责动作与菜单,static/src/是前端的全部家当,三个子目录分别对应脚本、QWeb 模板和样式,Odoo 的资产打包器按这三类文件分别处理,分开写排错时一眼就能定位是哪一层挂了。

manifest 是整份源码的入口,也是新手最先翻车的地方:

# custom_dashboard/__manifest__.py { "name": "Custom Dashboard", "version": "17.0.1.0.0", "category": "Productivity", "summary": "自定义仪表盘示例", "depends": ["base", "web", "sale"], # web 必带:OWL 运行时、assets_backend 都在里面 "data": [ "views/dashboard_action.xml", "views/dashboard_menus.xml", ], "assets": { "web.assets_backend": [ "web.chartjs_lib", # 复用 Odoo 自带的 Chart.js,别重复引一份 "custom_dashboard/static/src/js/dashboard.js", "custom_dashboard/static/src/xml/dashboard.xml", "custom_dashboard/static/src/scss/dashboard.scss", ], }, "installable": True, "application": True, "license": "LGPL-3", }

depends里的web不能省,它提供 OWL 运行时和web.assets_backend这个 bundle 名;sale是因为示例拿销售订单当数据源,换成别的业务就把这里换掉。assets那段是整个模块能不能在浏览器里跑起来的关键,四个路径缺一个就对应一类报错,后面第 5 章会逐条对照。"application": True让模块在应用列表里单独成项,安装和演示都方便。

2.2 客户端动作与菜单:tag 必须和注册表 key 一模一样

Odoo 里前端自定义页面不是挂一个 URL,而是注册一个「客户端动作」(client action),再用tag把它和 JS 注册表里的 key 对上。这两个字符串一旦不一致,点菜单就是一句Unknown action tag,而且不会告诉你差在哪。

<?xml version="1.0" encoding="utf-8"?> <odoo> <!-- tag 的值 = JS 侧 registry.category("actions").add() 的第一个参数 --> <record id="action_dashboard_sales" model="ir.actions.client"> <field name="name">销售仪表盘</field> <field name="tag">custom_dashboard.sales</field> <field name="params">{}</field> </record> <menuitem id="menu_dashboard_root" name="经营看板" sequence="5"/> <menuitem id="menu_dashboard_sales" name="销售仪表盘" parent="menu_dashboard_root" action="action_dashboard_sales" sequence="10"/> </odoo>

model="ir.actions.client"说明这是一个纯前端动作,服务端只负责把tag和params传下去,不返回业务数据。params字段后面第 6 章会拿来做区间配置,现在留空即可。菜单这边用parent挂了个根节点,是为了以后加第二个、第三个看板时不用再重复建一级菜单;sequence控制排序,数值小的靠前。

动作 XML 和菜单 XML 都写在data里,意味着模块安装或升级时会自动加载,不需要手动去「技术 → 动作」里建记录。

2.3 资产打包:web.assets_backend里少一行,页面就白给你看

Odoo 的 assets 不是把文件一个个<script src>引进去,而是启动时把 bundle 里所有文件合并压缩成一个资源,浏览器按 hash 缓存。这个机制平时很省心,调试时就特别反直觉:明明文件改了、重启了服务,页面还是老样子。

开发期一定要带--dev启动:

# 本地开发:关闭 assets 合并缓存,QWeb 模板改动即时生效 odoo-bin -c /etc/odoo/odoo.conf -d demo_db --dev=all

--dev=all实际上是--dev=assets,qwerkzeug,xml,reload的合集,其中对我们最有用的是assets和xml:前者让每次请求都重新走打包,后者让 QWeb 模板不用重启就生效。生产环境千万别开,性能会掉一大截;上线前用普通模式再跑一遍,确认合并后的 bundle 也没问题。如果不想重启服务,也可以开开发者模式后访问/web/assets/debug,手动触发生成。

还有一点容易忽略:web.assets_backend决定了这些资产只在后台(/web)加载,如果哪天把看板嵌到门户页,得改用web.assets_frontend,两套 bundle 是分开的。

3. 后端数据源:用 ORM 聚合把指标算成前端能吃的 JSON

3.1 为什么不直接写 SQL:权限、多公司与字段变更

看板上的数字,最省事的写法是self.env.cr.execute("SELECT ..."),跑起来也没问题,但一旦进了客户环境就会出三类事故。第一类是权限:原生 SQL 绕过ir.rule(记录规则),销售员能看到全部公司的订单金额,这在多公司场景下是硬伤。第二类是多公司:手写 SQL 很容易忘了company_id in (...)这一条,切公司数字不变。第三类是字段变更:哪天财务把amount_total的存储字段改了名,SQL 不会报错,只会返回空值,排查起来极其难受。

_read_group是 Odoo 官方提供的高层聚合接口,它在翻译成 SQL 之前已经套好了记录规则和公司过滤,返回值也是 Python 对象,字段改名时 IDE 和单测都能提前发现。代价是语法稍微绕一点,需要专门记一下分组后缀。

3.2_read_group的分组后缀:从date_order到date_order:month

Odoo17 的_read_group签名是_read_group(domain, groupby, aggregates),返回一个元组列表,每个元组的第一项是分组键,后面依次是聚合结果。

# custom_dashboard/models/sale_order.py from dateutil.relativedelta import relativedelta from odoo import api, fields, models class SaleOrder(models.Model): _inherit = "sale.order" @api.model def get_dashboard_data(self, months=12): """看板取数入口:一次调用返回标签、数值、总额、订单数。""" months = max(1, min(int(months or 12), 24)) # 参数兜底,防前端乱传 today = fields.Date.context_today(self) date_from = (today - relativedelta(months=months - 1)).replace(day=1) domain = [ ("state", "in", ("sale", "done")), ("date_order", ">=", date_from), ("company_id", "in", self.env.companies.ids), # 多公司必须显式带上 ] groups = self._read_group( domain=domain, groupby=["date_order:month"], # 按月分组 aggregates=["amount_total:sum", "__count"], # 求和 + 计数 ) # groups 形如 [(date(2024, 1, 1), 12000.0, 8), ...] groups.sort(key=lambda g: g[0]) return { "labels": [g[0].strftime("%Y-%m") for g in groups], "values": [round(g[1], 2) for g in groups], "count": sum(g[2] for g in groups), "total": round(sum(g[1] for g in groups), 2), }

逐项说清这几个参数。@api.model表示这是模型级方法,可以在前端通过orm.call("sale.order", "get_dashboard_data", [], {...})直接调到,不需要先有记录集。months做了双向夹逼,下限 1 个月、上限 24 个月,原因是前端参数一旦被篡改成999,date_from会算到 1999 年,数据库直接全表扫。date_from里.replace(day=1)是为了对齐到自然月,否则区间起点落在月中,第一个月的柱状图会短一截,客户第一眼就会问「这个月怎么掉这么多」。

groupby=["date_order:month"]里的:month是 Odoo 的日期粒度后缀,常见还有:day、:week、:quarter、:year。不写后缀默认按完整日期分组,一年的数据会拆成三百多根柱子,图表直接糊成一片。注意返回值里分组键是date对象不是字符串,前端要自己格式化,示例里用strftime("%Y-%m")转成2024-01这种形式。

aggregates=["amount_total:sum", "__count"]中,__count是 Odoo 内置的行数统计,不需要额外字段。如果改成"id:count"也能跑,但__count语义更明确。另外state过滤只取sale和done,草稿和已取消的订单不计入,这是业务口径问题,实施时一定要跟客户确认,别想当然。

3.3 参数兜底与自测:用 odoo shell 对着数据库核一遍

前端调试聚合结果很慢,正确姿势是直接开 shell 对着数据库核数:

# 进入模块所在数据库的交互式 shell,不启动 HTTP 服务 odoo-bin shell -c /etc/odoo/odoo.conf -d demo_db --no-http
# 在 shell 里手跑,和看板页面上的数字对一遍 >>> env["sale.order"].get_dashboard_data(months=6) {'labels': ['2024-07', '2024-08', ...], 'values': [12000.0, 15800.5, ...], 'count': 12, 'total': 34567.8} # 再用 search_count 交叉验证订单数,两个数对不上就说明 domain 写错了 >>> env["sale.order"].search_count([ ... ("state", "in", ("sale", "done")), ... ("date_order", ">=", "2024-07-01"), ... ("company_id", "in", env.companies.ids), ... ]) 12

--no-http很关键,不带的话 shell 会尝试占端口,本机已经有 Odoo 在跑就会报端口冲突。shell 里env是当前数据库的环境对象,env.companies和前端登录用户手上的公司集合一致,所以这里算出来的数就是那位用户在看板上该看到的数。如果两次数对不上,八成是domain里漏了company_id或者state口径不一致,逐个条件注释掉再跑一遍,几分钟就能定位。

提示:_read_group里不要放limit之类的参数,它不支持;真要限制返回行数,在 Python 侧对结果切片,别指望数据库层截断。

4. 前端渲染:OWL 组件 + Chart.js 把 JSON 画成看板

4.1 setup 里声明状态:onWillStart 取数、onMounted 画图、onWillUnmount 收尾

Odoo17 后端界面统一用 OWL 2,组件写法和 Vue/React 有几分像,但生命周期钩子的语义要单独记。取数放在onWillStart里,它会在首次渲染之前把异步任务跑完;画图放在onMounted里,这时 DOM 已经插好了,canvas拿得到;销毁放在onWillUnmount里,否则切换菜单之后图表实例还挂在内存里。

/** @odoo-module **/ import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { Component, onMounted, onWillStart, onWillUnmount, useRef, useState, } from "@odoo/owl"; /* global Chart */ export class SalesDashboard extends Component { static template = "custom_dashboard.SalesDashboard"; static props = { "*": true }; // 允许客户端动作透传任意 props setup() { this.orm = useService("orm"); this.chartRef = useRef("chart"); this.chart = null; this.state = useState({ loading: true, error: null, data: null }); onWillStart(async () => { try { // 第 4 个参数是 kwargs,会作为关键字参数传给后端方法 this.state.data = await this.orm.call( "sale.order", "get_dashboard_data", [], { months: this.props.action?.params?.months || 12 }, ); } catch (e) { this.state.error = e.message || String(e); } finally { this.state.loading = false; } }); onMounted(() => this.renderChart()); onWillUnmount(() => this.chart && this.chart.destroy()); } renderChart() { if (!this.state.data || !this.chartRef.el) { return; } this.chart && this.chart.destroy(); // 关键:先销毁旧实例再新建 this.chart = new Chart(this.chartRef.el, { type: "bar", data: { labels: this.state.data.labels, datasets: [{ label: "销售额", data: this.state.data.values, backgroundColor: "#875a7b", }], }, options: { responsive: true, maintainAspectRatio: false }, }); } } // key 必须与 XML 里 ir.actions.client 的 tag 完全一致 registry.category("actions").add("custom_dashboard.sales", SalesDashboard);

useService("orm")拿到的是 Odoo 封装好的 ORM 服务,call的第三个参数是位置参数数组,第四个是关键字参数字典,后端方法签名里的months正好对上。useRef("chart")和模板里的t-ref="chart"是一对,ref.el就是那个canvas元素。

static props = { "*": true }这行不能省:客户端动作会把整个 action 对象作为 props 传进组件,不声明的话 OWL 会直接抛Invalid props异常。this.props.action?.params?.months用了可选链,因为params可能是空对象,第 6 章会真正用上它。

renderChart第一行先destroy是血泪经验,后面第 5 章单独讲。maintainAspectRatio: false配合外层容器固定高度,图表才能随窗口宽度自适应而高度不塌。

4.2 QWeb 模板的三态:loading / error / empty 一个都不能少

OWL 组件的模板用 QWeb 语法,文件放在static/src/xml/下,顶层节点必须是<templates>。三态是底线:加载中、出错、正常渲染,少一个用户在弱网环境下就只能盯着白屏。

<?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-name="custom_dashboard.SalesDashboard"> <div class="o_sales_dashboard p-3"> <t t-if="state.loading"> <div class="text-muted py-5 text-center">数据加载中…</div> </t> <t t-elif="state.error"> <div class="alert alert-danger" t-esc="state.error"/> </t> <t t-else=""> <div class="d-flex gap-4 mb-3"> <div class="kpi-card"> <div class="kpi-label">区间销售额</div> <div class="kpi-value" t-esc="state.data.total"/> </div> <div class="kpi-card"> <div class="kpi-label">订单数</div> <div class="kpi-value" t-esc="state.data.count"/> </div> </div> <div class="chart-wrap" style="height: 320px;"> <canvas t-ref="chart"/> </div> </t> </div> </t> </templates>

t-name必须和 JS 里static template = "custom_dashboard.SalesDashboard"逐字符一致,这是第二个高频对不上的地方。t-esc会自动做 HTML 转义,数字和用户可读字符串都该用它,不要用t-out加| safe,除非你确定内容干净。t-ref="chart"挂在canvas上而不是外层div上,因为 Chart.js 要的是canvas的 2D context。

t-else=""这个写法有点怪,但 OWL 的指令要求不能省括号,写t-else不带等号在部分版本会报解析错误,照抄即可。

4.3 Chart.js 实例管理:destroy 写在 renderChart 的第一行

Chart.js 的实例和canvas是绑定的,同一个canvas上反复new Chart会叠加绘制层,表现是柱子颜色越来越深、鼠标悬浮提示出现多份、切几次菜单内存明显上涨。正确做法是在创建新实例之前先销毁旧实例,示例里把this.chart.destroy()放在renderChart第一行,就是防止任何一条重绘路径漏掉。onWillUnmount里再兜一次,保证离开页面时彻底释放。

如果后续要做「切换区间重新取数」,就不能只调renderChart,得先await新数据再画,顺序反了会画到上一次的数据上。建议把「取数 + 画图」封成reload()一个方法,所有触发点都走它,避免两处逻辑分叉。

5. 避坑与排查:六个真实翻车现场和它们的止损点

5.1 白屏 + 控制台 404:assets 没进包

现象:模块装上,菜单点得开,但页面一片空白,F12 里能看到dashboard.js请求返回 404,或者压根没发起请求。

原因:__manifest__.py的assets段路径写错,常见是漏了模块前缀custom_dashboard/,或者把文件放进了static/description/这种不在 bundle 里的目录。另一种是改完 manifest 没重启服务,bundle 还是老的。

解决:先确认路径和磁盘文件逐字符一致,再用--dev=assets重启,或者开发者模式访问/web/assets/debug强制重建。生产环境不想重启的话,升级模块(-u custom_dashboard)也能触发重建。

5.2 Unknown action tag:注册表 key 和 tag 差了一个字符

现象:点菜单弹出错误对话框,提示Unknown action tag: custom_dashboard.sales,但 JS 文件明明加载成功了。

原因:XML 里ir.actions.client的tag字段,和 JS 里registry.category("actions").add("...")的第一个参数不一致。大小写、下划线、点号都算,custom_dashboard.sales和custom_dashboard.Sales是两个不同的 key。

解决:把两处字符串复制到同一个文本文件里对比,别靠肉眼。更省事的做法是定义一个常量文件,XML 侧没法共享,但至少 JS 里只写一次,注册和模板引用都用同一个变量。

5.3 图表挤成一根柱:分组后缀写漏了

现象:看板能出来,但横轴只有一两个标签,柱子细得像根针,鼠标悬浮发现数值其实是全部订单的总和。

原因:groupby=["date_order"]没写:month,Odoo 按完整日期分组,但前端strftime("%Y-%m")又把同月的多个日期格式化成同一个字符串,Chart.js 把重复标签合并处理了。

解决:确认groupby里带粒度后缀,同时在 shell 里打印groups的原始长度,长度和页面标签数对不上就是这个问题。日期类字段的粒度后缀别靠猜,:day、:week、:month、:quarter、:year挨个试一遍,看哪个符合业务口径。

5.4 切了公司数字不变:domain 漏了 company_id

现象:多公司环境下,用 A 公司登录和用 B 公司登录,看板上的销售额一模一样,但列表视图里明明不一样。

原因:_read_group虽然会应用记录规则,但如果模型上没有配置跨公司的ir.rule,它不会自动按env.companies过滤。示例里那行("company_id", "in", self.env.companies.ids)就是补这个缺口。

解决:所有自定义聚合方法都显式带上这一条,别指望默认行为。验证方式是 shell 里切env.companies再跑一次,数字必须跟着变。

5.5 时区错位:月底订单跑到下个月

现象:客户反馈「12 月 31 号晚上的单子,看板上算到 1 月去了」,本地测试又复现不了。

原因:date_order在数据库里存的是 UTC,_read_group的:month按 UTC 切月,而用户在东八区,本地 12 月 31 日 23:00 对应 UTC 还是 12 月 31 日 15:00,理论上不该错;但如果用户在西半球时区,就会跨月。示例里用fields.Date.context_today(self)算起点是对的,分组这一层仍按 UTC 走。

解决:对时区敏感的业务,要么在前端把区间参数换成 UTC 边界传下去,要么在_read_group外层用pytz做一次时区转换再聚合。简单粗暴但有效的办法是:跟客户约定看板口径统一按公司时区,并在页面上标注「数据截至 UTC+8」,比事后解释省事得多。

5.6 切几次菜单图表变黑:实例没销毁

现象:反复在菜单间切换,看板柱子颜色越来越深,最后糊成一块,浏览器任务管理器里内存曲线一路上涨。

原因:每次挂载都new Chart,旧实例还挂在同一个canvas上,Chart.js 不会自动清理。onWillUnmount漏写或者写在renderChart之后都会导致这个问题。

解决:destroy放在renderChart第一行,onWillUnmount再兜一次。排查时可以在renderChart里打日志,看同一个页面生命周期内被调了几次,正常应该只有一次。

6. 进阶:把 months 变成可配置,三个动作验证这块看板没画错

硬编码12个月只能算 demo,真上线客户第一句就是「能不能自己选区间」。最小改动是把区间塞进客户端动作的params:

<field name="params">{"months": 6}</field>

前端this.props.action?.params?.months已经预留了读取逻辑,改 XML 不用动 JS。想做得再细一点,可以在res.users上加一个Integer字段存默认区间,登录时读一次写进params,这样每个用户看到的是自己习惯的窗口。

验证这块看板有没有画错,我一般固定跑三个动作:

动作做法能抓到的问题
单测对拍造两条已知金额的订单,断言total等于两者之和聚合口径、state 过滤写错
shell 交叉get_dashboard_data()与search_count结果对比domain 漏条件、多公司串数
浏览器 Network看call_kw的响应体字段是否齐全前端字段名拼错、后端返回结构变更

单测建议直接写在模块的tests/下,用TransactionCase,造数据时把date_order显式设成区间内和区间外各一条,跑--test-enable就能验证。Network 那一步最容易被跳过,但字段改名这类低级错误往往就是它第一个发现——后端把total改成amount_total,前端不报错,只是 KPI 卡片显示空白。

从那以后我每次交付自定义看板,都会先把「shell 对拍 + Network 看响应」走一遍再给客户演示,因为图表这东西太会骗人:柱子画出来了不代表数对,数对了不代表口径是客户要的。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 19:08:30

西北工业大学核心听力雨课堂答案文档:Python清洗与结构化复习指南

简介&#xff1a;这份文档面向西北工业大学修读核心听力课程的学生&#xff0c;整理了雨课堂平台上的听力练习参考答案&#xff0c;覆盖C1至C11各单元的选择、填空、判断及场景词汇等题型&#xff0c;适合用于课后核对、考前复习与听力薄弱环节的针对性训练。资源包内含1个docx…

作者头像 李华
网站建设 2026/10/24 19:07:47

机械基础课后题答案:受力分析与强度校核全攻略

简介&#xff1a;《机械基础&#xff08;第三版&#xff09;范思冲》课后题答案PDF&#xff0c;面向机械类专业学生、考研备考生及自学者&#xff0c;用于核对课后练习、梳理机械设计与机械原理中的计算与受力分析思路。整个资源只有1个PDF文件&#xff0c;大小约5.15MB&#x…

作者头像 李华
网站建设 2026/10/24 19:02:27

六类网线标准TIA/EIA-568-B.2-1深度解析

简介&#xff1a;本资源为美国通信工业协会&#xff08;TIA&#xff09;正式发布的ANSI/TIA/EIA-568-B.2-1六类&#xff08;CAT6&#xff09;布线系统标准原始PDF文档&#xff0c;是网络布线设计、施工、验收及认证测试的核心技术依据&#xff0c;面向综合布线工程师、弱电系统…

作者头像 李华
网站建设 2026/10/24 18:46:09

python的机器人基础原理工业场景模拟第九十八篇:搬运机器人,给定起始,目标关节角度,采样中间角度,查看末端空间运行轨迹。

《Python机器人基础原理》第九十八篇&#xff1a;离线仿真——搬运机器人&#xff08;关节空间轨迹规划与末端轨迹可视化&#xff09;声明&#xff1a;本篇全部为课程离线仿真代码&#xff0c;不能直接用于真实机器人控制器&#xff0c;规避工程误导风险。1. 实际工业应用场景周…

作者头像 李华
网站建设 2026/10/24 18:43:13

Kube-OVN部署排错指南:ContainerCreating卡死、LB不通与子网崩溃根因解析

简介&#xff1a;本资源是一份面向Kubernetes运维工程师、云原生网络架构师及容器平台建设者的Kube-OVN深度技术文档&#xff0c;系统解决企业级容器网络在功能丰富性、性能瓶颈与运维复杂度三方面的核心痛点。文档全面覆盖Kube-OVN作为CNCF沙盒项目的定位价值&#xff0c;详解…

作者头像 李华
网站建设 2026/10/24 18:19:19

Java零基础入门知识地图:从JVM运行机制到变量与数据类型核心梳理

简介&#xff1a;这份PDF笔记面向零基础Java学习者&#xff0c;系统整理了韩顺平循序渐进学Java课程第一阶段的完整内容&#xff0c;适合编程入门者、在校学生及转行人员搭建Java知识框架。笔记按课程三阶段展开&#xff1a;基础语法、数据类型、控制结构、类与对象&#xff0c…

作者头像 李华