鸿蒙开发实例:用ArkTS实现二次函数最值计算与图像绘制
2026/9/8 17:01:37 网站建设 项目流程

开发鸿蒙应用做到第259个实例,我反而回头选了一道中学数学题:二次函数最值问题。有人可能会问,这不是用笔算两分钟就能出结果的东西吗,值得做成一个 HARMONYOS 应用实例?我的看法是,恰恰是这种“看起来简单”的题目,最能练一个应用的完整骨架。输入系数、判断二次函数开口方向、求解顶点、处理闭区间边界、再把函数曲线画出来,这一串动作下来,界面布局、类型处理、数学计算、Canvas 绘图、真机调试全部覆盖了,比那些一上来就堆十几个页面的示例有用得多。

如果你正在学 ArkTS 和 ArkUI,或者你打算做数学类的教育工具但不知道怎么落地,这篇实例应该能给你一个非常完整的参考。文章里写的不是我临时拼凑的 Demo,而是我实际在 DevEco Studio 里跑通、又在真机上验证过的一个小工具:输入 a、b、c,再输入一个区间,应用会告诉你这个二次函数在实数范围和在指定区间上的最大最小值,同时画出抛物线、标出顶点和对称轴。

1. 为什么第 259 个实例要选这道题:看似简单,链路却完整

先聊动机。HARMONYOS 应用实例我做了很多期,大部分示例要么是业务界面,要么是系统能力调用。但业务界面容易堆组件,系统能力又依赖特定硬件,两者都不太适合拿来练手。二次函数最值问题不一样,它是一道你能够在十分钟内厘清逻辑、花上一个晚上把界面和图像都做出来的题目。

1.1 这个工具最终给谁用

我做完之后的第一反应是,这东西确实能给三类人用。第一类是初中生或高中生,他们输入一个函数就能直接看到顶点、对称轴和区间最值,尤其是“区间不包含顶点时最值在端点取到”这种容易混淆的概念,用图像验证一遍会清晰很多。第二类是教数学的老师,做课件的时候需要现场生成函数图像,比起打开电脑里的专业数学软件,一个装在手机里的小工具更方便。第三类是刚接触鸿蒙开发的人,他们不需要理解复杂的硬件特性,只需要顺着一条清晰的主线把代码写完,就能得到成就感。

为了让三方都用得顺手,我没有把功能做得很宽,只保留了几个最核心的动作:输入参数、看结论、看图。

1.2 我最终定下来的交互闭环

整个应用的交互流程并不复杂,核心就是“一条线走完”:

  1. 在六个输入框中分别填入系数 a、b、c 和区间左端点 left、右端点 right。
  2. 点击“计算”按钮(或者输入结束后触发计算)。
  3. 结果卡片依次显示开口方向、对称轴、顶点坐标、实数范围内的最值、闭区间上的最大值和最小值,以及取得最值对应的 x 值。
  4. 下方 Canvas 自动绘制抛物线,并标出对称轴和顶点。
  5. 点击“随机示例”可以快速载入一组典型数值,方便检查图像显示是否正常。

这个闭环没有引入外部依赖,也没有复杂的状态管理,但它把常见的输入组件、业务类型封装、格式化输出、Canvas 自绘、异常输入处理都带出来了。后面的几个章节,我就按照代码的推进顺序来写,如果你是先写计算逻辑再拼界面的类型,读起来应该会很顺。

2. 先写清楚数学条件:求最值的逻辑骨架与边界处理

写 UI 之前,我先把最值计算单独拎出来封装好了。这样做不是因为数学难,而是因为如果直接把这些判断散落在页面文件里,后面增加随机示例或者换一个入口页面时会非常痛苦。

2.1 从顶点式到代码映射

二次函数的标准形式是:

f(x) = ax² + bx + c

其中 a 不能等于 0。所有关于最值的讨论,其实都围绕顶点式来展开:

f(x) = a(x - h)² + k

这里:

h = -b / (2a)

k = c - b² / (4a) = (4ac - b²) / 4a

在代码里,我不是直接用字符串去拼公式,而是把这些计算收敛到一个分析器中。下面是一段我在工程里实际使用的核心类型代码,以 ArkTS 的 interface 描述分析结果:

// common/quadratic.ts export interface QuadraticParams { a: number; b: number; c: number; } export interface QuadraticAnalysis { params: QuadraticParams; vertexX: number; vertexY: number; axisX: number; openUp: boolean; realMinX: number | null; realMinY: number | null; realMaxX: number | null; realMaxY: number | null; intervalMinX: number | null; intervalMinY: number | null; intervalMaxX: number | null; intervalMaxY: number | null; } export function analyzeQuadratic(params: QuadraticParams, left: number, right: number): QuadraticAnalysis { const { a, b, c } = params; const vertexX = -b / (2 * a); const vertexY = c - (b * b) / (4 * a); const openUp = a > 0; const analysis: QuadraticAnalysis = { params: params, vertexX: vertexX, vertexY: vertexY, axisX: vertexX, openUp: openUp, realMinX: null, realMinY: null, realMaxX: null, realMaxY: null, intervalMinX: null, intervalMinY: null, intervalMaxX: null, intervalMaxY: null }; // 整个实数范围内,a > 0 开口向上有最小值,a < 0 开口向下有最大值 if (openUp) { analysis.realMinX = vertexX; analysis.realMinY = vertexY; } else { analysis.realMaxX = vertexX; analysis.realMaxY = vertexY; } // 闭区间上的最值不能只靠开口方向判断,需要用候选点法 const candidatesX: number[] = [left, right]; if (left <= vertexX && vertexX <= right) { candidatesX.push(vertexX); } let minY = Number.MAX_VALUE; let maxY = -Number.MAX_VALUE; let minX = left; let maxX = left; for (let i = 0; i < candidatesX.length; i++) { const x = candidatesX[i]; const y = a * x * x + b * x + c; if (y < minY) { minY = y; minX = x; } if (y > maxY) { maxY = y; maxX = x; } } analysis.intervalMinX = minX; analysis.intervalMinY = minY; analysis.intervalMaxX = maxX; analysis.intervalMaxY = maxY; return analysis; }

这段代码的核心逻辑是候选点法。对于定义在闭区间[left, right]上的连续函数,最值只可能出现在三类点上:区间左端点、区间右端点、区间内部的极值点。二次函数的唯一极值点就是顶点,所以只要判断顶点横坐标是否落在区间内,再加上两个端点一起比较,就能得到区间最值。

2.2 区分实数范围和闭区间范围

这里有一个我见过很多初学者踩坑的地方:直接说“开口向上所以函数没有最大值”,但用户输入了一个闭区间后,函数在这个闭区间上一定有最大值和最小值。

如果没有指定区间,讨论的是整个实数域,那么结论确实只有一半:

  • a > 0:开口向上,整个实数范围内存在最小值vertexY,没有最大值。
  • a < 0:开口向下,整个实数范围内存在最大值vertexY,没有最小值。

一旦给定了区间,哪怕开口向上,区间端点也可能成为最大值;哪怕开口向下,区间端点也可能成为最小值。我在这版工具里把两类结果都展示出来,就是为了避免这种混淆。

上面的分析器也处理了“区间写反”的情况。我的界面上额外做了一次校验:如果right < left,就弹出提示并中止计算。这个判断不放在分析器里,因为分析器只负责算已知合理输入,而用户交互层的职责是保证参数合理。

2.3 输入校验:a 不能为 0,空输入不能参与计算

用户在输入框里填入的值,本质上都是字符串。通过parseFloat转换后可能出现NaN,也可能因为用户把 a 填成 0,让“二次函数”变成了“一次函数”。遇到这些情况,我会在入口处统一拦截:

function parseInput(text: string): number { if (text.trim() === '') { return Number.NaN; } return Number.parseFloat(text); } function validateParams(): boolean { const a = parseInput(this.aText); const b = parseInput(this.bText); const c = parseInput(this.cText); const left = parseInput(this.leftText); const right = parseInput(this.rightText); if (Number.isNaN(a) || Number.isNaN(b) || Number.isNaN(c) || Number.isNaN(left) || Number.isNaN(right)) { this.toastMessage('请完整填写所有数值'); return false; } if (Math.abs(a) < 1e-12) { this.toastMessage('二次项系数 a 不能为 0'); return false; } if (right < left) { this.toastMessage('区间右端点需要不小于左端点'); return false; } return true; }

很多人觉得输入校验不重要,但真机演示的时候,只要有人随手把 a 清空,应用很可能表现异常。后来我很庆幸一开始就处理了这些边界:解析输入后先判断NaN,再判断 a 是否近似为 0,最后判断左右边界逻辑。这个顺序不能倒过来,因为如果先比较right < left,遇到NaN时比较结果永远是 false,用户看到的是莫名其妙的异常而不是友好提示。

3. ArkUI 布局与交互:一道数学题如何变成一个顺手的小工具

计算逻辑准备好了,接下来就是界面。这个应用的界面复杂度不高,但我还是按照真实产品的思路把它拆成了几个区域:输入区、表达式预览区、结果卡片区和图像区。所有区域放在一个纵向滚动的容器中,避免小屏手机上内容被截断。

3.1 输入区用 Row 还是 Grid 更合适

ArkUI 中常用的布局是 Row、Column 和 Grid。系数输入需要显示“a =”“b =”“c =”这种标签,并且每个标签后面接一个 TextInput,所以最简单清晰的方式是一行一个输入项,或者用三到五个等宽列。

我最后选择了每一个系数占一个输入卡片的方式,因为参数之间存在严格的数学语义,让它们在同一行内整齐排列,比纵向罗列更直观。以三个系数为例,界面结构大致是:

Row({ space: 8 }) { this.buildParamInput('a =', this.aText, (value: string) => { this.aText = value; }, 1); this.buildParamInput('b =', this.bText, (value: string) => { this.bText = value; }, -3); this.buildParamInput('c =', this.cText, (value: string) => { this.cText = value; }, 2); } .width('100%');

为了不让页面重复代码太多,我把参数输入封装成了一个私有方法。这个方法里使用TextInput作为输入组件,并且设置了键盘类型为小数键盘:

@Builder buildParamInput(label: string, value: string, onChange: (value: string) => void, defaultValue: number) { Column({ space: 4 }) { Text(label) .fontSize(14) .fontColor('#666') .width('100%'); TextInput({ text: value, placeholder: defaultValue.toString() }) .type(InputType.NUMBER_DECIMAL) .onChange(onChange) .height(44) } .layoutWeight(1) }

这里有一个使用细节值得提一下:TextInputtext参数需要传入一个字符串。用户在输入过程中,ArkUI 会通过onChange把最新字符串带出来。如果你只是把字符串保存在临时变量里、没有绑定到@State,界面上的内容不会及时更新。为了让“随机示例”按钮能够改变输入框的值,必须保证aTextbText这些变量是@State

3.2 表达式的格式化输出

用户想看到的不只是系数输入框,而是像f(x) = x² - 3x + 2这样的标准表达式。如果直接把 a、b、c 拼起来,很容易得到f(x) = 1x^2+-3x+2这种很丑的结果。

我封装了一个专门负责格式化表达式的方法,处理了三类细节:

  • 系数为 1 时不显示“1x²”,直接显示“x²”。
  • 系数为 -1 时显示“-x²”而不是“-1x²”。
  • 正负项之间用空格和加减号隔开,避免出现两个符号连在一起。

参考代码片段如下:

function formatTerm(coef: number, exp: number): string { let head: string; if (coef === 1) { head = ''; } else if (coef === -1) { head = '-'; } else { head = `${coef}`; } if (exp === 2) { return `${head}x²`; } if (exp === 1) { return `${head}x`; } return `${coef}`; } export function buildExpression(a: number, b: number, c: number): string { let result = ''; const ax2 = formatTerm(a, 2); const bx = formatTerm(b, 1); if (ax2 !== '') { result += ax2; } if (b !== 0) { result += b > 0 && result.length > 0 ? ` + ${bx}` : ` ${bx}`; } if (c !== 0) { result += c > 0 && result.length > 0 ? ` + ${c}` : ` ${c}`; } return result === '' ? '0' : result; }

这个函数还有很多可以打磨的地方,比如a = 0.5时可以显示“0.5x²”,如果希望更接近数学排版,还可以把系数转换成带分数形式,不过目前用到的场景已经足够。对于负数项,formatTerm返回的字符串自带负号,所以拼接时不用额外处理。比如 b = -3,bx是“-3x”,最终表达式就是“x² - 3x + 2”。

3.3 结果卡片的文案策略

计算结果的展示不是把几个数字丢到屏幕上那么简单。我问了自己一个问题:用户看这个工具时,最想确认的第一眼信息是什么?答案是“顶点和区间最值”。所以在卡片设计里,我把两行关键信息做成大字号:

  • 开口方向:抛物线开口向上/向下。
  • 顶点坐标:(2.0, -1.0),同时显示对称轴x = 2.0

接下来才是闭区间上的最大值和最小值。为了让结论不产生歧义,我在文案上明确写清是在区间[left, right]上的取值:

在区间 [-10, 10] 上 最小值 y = -1.0,当 x = 2.0 最大值 y = 132.0,当 x = -10.0

这里其实藏着一个值得展开的细节:闭区间上最大最小值对应的 x 值不一定唯一。比如在对称区间上取常数的退化情形,或者在最大值落在一侧端点而顶点也在区间内时,严格说可能有多个 x 取得相同最值。但作为教学工具,我认为先给出一个取值点即可,界面下方画出的曲线也能让用户直观看到其他位置的情况。

4. Canvas 自绘抛物线:坐标系换算与曲线渲染心得

界面上的数字结论再清晰,也不如一条真实的抛物线直观。这个应用里最花时间、也最能看出开发者功力的部分,是用 Canvas 把曲线画出来。很多新手在这块卡住,并不是画线的方法不会,而是数学坐标系与屏幕坐标系的转换没想明白。

4.1 数学坐标到 Canvas 坐标换算

ArkUI 的 Canvas 坐标系和数学平面直角坐标系有一个显著差异:屏幕的 y 轴是向下增长的。数学里的点(0, 0)越往上 y 越大,但在屏幕上越往上 y 越小。也就是说,如果一个点的数学 y 值是 10,屏幕坐标需要拿canvasHeight去减掉换算后的高度差。

我首先在组件中通过onAreaChange拿到画布的实际宽高,然后封装了两个转换函数:

// 画布实际宽高 private canvasWidth: number = 0; private canvasHeight: number = 0; // 数学范围 private viewLeft: number = -10; private viewRight: number = 10; private viewBottom: number = -10; private viewTop: number = 10; private toPixelX(mx: number): number { const ratio = this.canvasWidth / (this.viewRight - this.viewLeft); return (mx - this.viewLeft) * ratio; } private toPixelY(my: number): number { const ratio = this.canvasHeight / (this.viewTop - this.viewBottom); return (this.viewTop - my) * ratio; }

这段代码中的关键是(this.viewTop - my)。当数学 y 值越大,像素值越小,点就越靠近画布上方。很多人在写这个函数时习惯用my - viewTop,结果图像上下颠倒,最后还得靠debugger一步步查。

4.2 用采样点实现曲线绘制

二次函数的图像是一条连续的抛物线,但落到 Canvas 上时,我们只能用多个离散点连接成折线。只要采样点足够密,肉眼就看不出折线感。我的方案是,把用户选择的区间均匀切分成 100 到 200 个点,然后逐点计算 y 值并连线。

在 ArkTS 中实现时,我会先设置CanvasRenderingContext2D的线条样式,再开始beginPath、循环lineTo,最后stroke

private drawCurve(): void { const ctx = this.ctx; ctx.lineWidth = 2; ctx.strokeStyle = '#007DFF'; ctx.beginPath(); const samples = 160; const startX = this.viewLeft; const endX = this.viewRight; const step = (endX - startX) / (samples - 1); let first = true; for (let i = 0; i < samples; i++) { const mx = startX + step * i; const my = this.calcY(mx); const px = this.toPixelX(mx); const py = this.toPixelY(my); if (first) { ctx.moveTo(px, py); first = false; } else { ctx.lineTo(px, py); } } ctx.stroke(); }

这里采样的初衷是避免曲线在某个区域变化太快时出现明显的直线段。为了视觉效果,我通常把采样点控制在 120 ~ 200 之间。数量太少会导致顶点附近不圆润,数量太多又不会带来本质提升,反而增加无意义的计算。

4.3 顶点、对称轴与坐标轴怎么标

顶点是二次函数最重要的几何特征,所以我在曲线上用一个小实心圆标出来,并在旁边附上坐标文本:

private drawVertex(mx: number, my: number, label: string): void { const ctx = this.ctx; const px = this.toPixelX(mx); const py = this.toPixelY(my); ctx.beginPath(); ctx.fillStyle = '#E84026'; ctx.arc(px, py, 4, 0, 2 * Math.PI); ctx.fill(); ctx.font = '12px sans-serif'; ctx.fillStyle = '#E84026'; ctx.textAlign = 'left'; ctx.fillText(label, px + 8, py - 8); }

对称轴画成虚线会更清晰。我用setLineDash([4, 4])设置虚线样式,然后从屏幕上方到下方画一条竖直向下的线。但有一个细节是,如果对称轴本身不在当前的可见 x 范围内,那么不需要画这条虚线。因为只有对称轴横坐标落在视图区间内,虚线才有意义。

坐标轴的绘制同样需要注意:y 轴并不是一定要从画面内部穿过的实线,而是根据数学原点的位置,动态判断是否在可视范围内。如果 x = 0 在viewLeftviewRight之间,那么 y 轴应该显示为一条实线;如果不在范围内,就只保留屏幕边缘的边框线。放在这个例子里,默认区间[-10, 10]包含 0,所以坐标轴会正常显示。

4.4 视图范围自适应

默认的抛物线在[-10, 10]区间上可能很高,也可能很低。如果 y 的范围设置不当,曲线要么冲出去,要么缩成一团。我采用的办法是先预先计算采样点的最小y和最大y,然后向外扩展 10% 作为画布的viewTopviewBottom

let yMin = Number.MAX_VALUE; let yMax = -Number.MAX_VALUE; for (let i = 0; i < samples; i++) { const mx = startX + step * i; const my = this.calcY(mx); yMin = Math.min(yMin, my); yMax = Math.max(yMax, my); } const padding = (yMax - yMin) * 0.1; this.viewBottom = yMin - padding; this.viewTop = yMax + padding;

当抛物线开口向上且顶点很高、曲线很陡时,如果 yMax 和 yMin 相差太大,图会变得很扁。这种场景下,我建议对 y 方向做适当截断,或者提示用户缩小 x 区间。我在代码中做的是如果高度差超过 10000,就自动把观察窗口切换到顶点附近,保证图像主体清晰可读。

5. 从模拟器到真机:参数用例、HDB 调试与交付前检查

代码写完之后,并不是能在预览器上跑起来就万事大吉。预览器里的 Canvas 尺寸、键盘弹出、字体渲染和真机存在不少差异。尤其是这个应用涉及输入框、计算、绘图三个环节,一步出问题都容易被误判成功能 Bug,所以我有一套自己的验证流程。

5.1 用数组典型用例验证算法

算法正确性不靠肉眼看,而是靠用例说线。我把几组已知结果的参数放进了“随机示例”里,每一组都经过手算验证:

用例abc区间顶点区间最小值区间最大值
开口向上1-32[-10, 10](1.5, -0.25)x=1.5, y=-0.25x=-10, y=132
开口向下-121[0, 4](1, 2)x=4, y=-7x=1, y=2
顶点不在区间10-4[4, 6](0, -4)x=4, y=12x=6, y=32
对称区间24-6[-3, 1](-1, -8)x=-1, y=-8端点比较取更大者

随机示例按钮的作用不只是演示,更是在我改完代码后快速回归算法的一个入口。按一次按钮,界面上的六个输入框就会载入一组参数,我可以立刻用肉眼比对结果卡片中的数字和手算结果是否一致。

5.2 真机部署与 HDB 命令行调试经验

在 DevEco Studio 中,真机运行通常很简单:连接设备后点击 Run 按钮即可。但在开发过程中,我发现只靠编辑器里的 Log 面板找问题很 inefficient,有些布局只在真机上闪烁,日志又不会打到哪里。

HarmonyOS 的调试工具链里经常会用到 HDB 这类命令行能力,不同版本的工具命令名称可能有差异,但思路是一致的:先列出已连接的设备,再应用进程里查看 println 或日志输出。下面是我常用的大致命令形态:

# 列出设备 hdb devices # 连接无线调试设备,设备 IP 与端口需在开发者选项里查看 hdb connect 192.168.x.x:port # 过滤应用日志 hdb shell hilog | grep QuadraticDemo

打开无线调试后,真机就不需要一直插着 USB 线。对于需要频繁抬手看画面的数学工具来说非常方便。我不需要反复弯腰插线,设置好一次,后面在同一个局域网内都能直接部署。需要注意的是,模拟器上 Canvas 的 onAreaChange 触发时机和真机略有不同,前几次真机运行时曲线画不出来,但模拟器一切正常。后来我把绘制逻辑触发点从onReady调整到首页的onPageShow和参数变更后的redraw,问题才解决。

5.3 三个我实际遇到、也建议你检查的坑

第一个坑是TextInput里输入负号。用户输入-3时,如果键盘没有正确弹出符号键,可能会输入成数字3再手动补负号,体验很差。实际在模拟器里受到系统软键盘影响,InputType.NUMBER_DECIMAL的键盘布局在不同版本上并不一致,所以我在帮助文案里写明了“可以直接输入负号”。

第二个坑是深色模式。默认的黑色文字在深色主题下可能变成深色,导致用户看不清结果。我在结果卡片上统一用了语义化颜色资源,并在代码里给 Canvas 的网格和坐标轴使用了“跟随主题”的颜色值,而顶点标注和曲线则使用品牌色,避免整张图在深色模式下糊成一团。

第三个坑是中文换行。表达式预览区的文本如果太长,比如 a、b、c 都是多位负数时,Text组件默认可能换行,看起来很奇怪。我给表达式区域设置了maxLines(1),并允许textOverflow({ overflow: TextOverflow.Ellipsis }),至少在窄屏上不会崩版。用户如果实在要完整查看,可以长按输入框旁的计算结果,复制到备忘录中确认。

6. 把计算和绘图拆成两个独立模块后,扩展比想象中更顺利

这个应用我本来以为一天就能写完,真正落地时发现大部分时间花在了“让结果在不同边界条件下都正确展示”上。反而是最初把计算逻辑单独抽成quadratic.ts、绘图逻辑单独抽成 Canvas 画图类之后,后面加功能变得非常轻松。

6.1 从二次函数到通用函数绘图器

很多看似独立的数学函数,绘图逻辑其实是共通的。我现在已经把这个项目的绘图部分抽象成了“输入一个(x: number) => number的函数,画出对应的曲线”。这意味着,只要把原来的calcY从“二次函数”替换成别的表达式,这个工具就能画出三次函数、正弦函数、绝对值函数等多种图像。

在二次函数之上,下一步很自然地做的就是求根:当Δ = b² - 4ac大于等于 0 时,用求根公式直接显示两个 x 截距,并和顶点标注在同一条曲线上。这个功能所需的抛物线数据、坐标系、采样方式已经全部现成,真正需要新增的只是把(-b ± sqrt(Δ)) / (2a)算出来,再判断截距是否落在当前视图区间内。

6.2 对教育类应用的后续建议

如果你做的是教学工具,建议在界面上把顶点式和一般式并列展示。顶点式能让学生一眼看出对称轴和顶点,但一般式更贴近题目原始给法。我的做法是在表达式预览区下方增加一行小字:“顶点式:f(x) = 1(x - 1.5)² - 0.25”。这行文字同样通过函数生成,本质上就是把 a、h、k 格式化后拼接出来。

另外,曲线图下方最好配上“缩放区间”的能力。用户想看 x = 100 附近的函数行为时,如果只能靠手动输入区间左端点右端点,多少有些繁琐。做一个“放大”“缩小”按钮,把viewLeftviewRight沿中心点向外扩或向内缩,操作的成就感会强很多。

6.3 我最后留在代码里的一条经验

做这类小工具,真正麻烦的不是算法本身,而是数值展示的规范性。比如浮点数计算后,-0.0000000001很容易显示成-0,顶点坐标可能看起来是(1.5, -0.0000001)。在我的实现里,所有结果显示前都会做一次近似处理:

function smartNumber(value: number): string { if (Math.abs(value) < 1e-9) { return '0'; } const rounded = Math.round(value * 1000000) / 1000000; return `${rounded}`; }

这样一个简单的函数解决了两类问题:一是消除浮点尾巴,二是避免输出NaN时让用户看到难看的报错。实际使用时,我几乎在每一个 Text 绑定数字的地方都调用了它,真机演示时结果界面才会干干净净。

如果你也想写一个类似的数学小应用,我的建议是先不要急着追求页面动画和渐变效果,把上面提到的输入边界、结果格式化、Canvas 坐标转换这三样做扎实,应用就已经具备了日常使用的稳定性。剩下再补一点随机样例和异常提示,体验就不会比市面上收费的数学工具差太多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询