Ext-js-数据驱动应用设计指南-全-

Ext.js 数据驱动应用设计指南(全)

原文:zh.annas-archive.org/md5/183ea97858cad594ed6b3d4496d6a387

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Ext JS 是一个领先的 JavaScript 框架,它包含丰富的组件、API 和广泛的文档,您可以使用这些资源来构建强大且交互式的桌面应用程序。通过使用 Ext JS,您还可以快速开发与所有主要浏览器兼容的丰富桌面 Web 应用程序。本书将逐步引导您使用现有数据库的信息,在 Ext JS 中构建一个清晰且用户友好的销售数据库。

这本书不仅解释了 MVC,而且是一本实用的指南,将带您了解构建应用程序的机制。到本书结束时,您将拥有一个可定制的、可工作的应用程序。您还可以在未来的项目中使用本书中的架构来简化控制、提高维护并轻松扩展应用程序。

您应该能够理解本书中介绍的示例数据结构。本书的编写前提是您熟悉 JavaScript,并具备 MySQL 的基本操作知识。

这本书涵盖的内容

第一章, 数据结构,专注于为您的数据库打下基本基础。它将处理现有虚拟公司的数据结构以及 MySQL 中的 SQL 和表的制作。

第二章, 应用程序设计规划,开发项目环境,同时介绍 Sencha Cmd。您将学习如何设计一个简单的应用程序并优化 Ajax 请求,以便使用 Ext Direct 和 Ext.util.History 通过 URL 控制屏幕。

第三章, 数据输入,讨论了制作一个用于输入数据的表单,然后通过 Ext Direct 将数据传输到服务器。您还将学习如何监控输入状态以及 Ext Direct 如何在服务器端验证它。

第四章, 列表 搜索,主要关于显示我们在第三章中读取的数据。然而,用户无疑会想要搜索数据,因此本章还将介绍数据搜索。

第五章, 报告,专注于报告的实现,在仪表板上以四种不同类型的图表显示。

第六章, 数据管理,专注于数据导入/导出的实现,用于恢复或备份数据。

您需要这本书的内容

为了使用这本书,您需要熟悉 JavaScript,并具备 MySQL 的基本操作知识。

在您开始阅读之前,您需要在系统中设置以下内容:

  • 最新的 Sencha Ext JS 版本(带 GPL)。您可以从 Sencha 网站下载,网站地址为 www.sencha.com/products/extjs/download/。本书基于 Ext JS 版本 4.2.2 编写。

  • 任何好的代码编辑器。

  • 网络浏览器,任何现代网络浏览器都可以。在这本书中,我们使用 Google Chrome,因此我们建议您如果可能的话也使用 Google Chrome。

适合本书的读者

这是一个针对 Sencha Ext JS 中级用户的教程,它解释了如何构建处理现有数据库的 UI 的过程。

这本书是为任何想要能够系统地学习如何从实施的第一步开始构建应用程序的人而写的。

术语约定

在这本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词如下所示:“Bill 表几乎与 Quotation 表相同。”

代码块设置如下:

UPDATE
    users
SET
    email='extkazuhiro@xenophy.com',
    passwd=SHA1(MD5('password')),
    lastname='Kotsutsumi',
    firstname='Kazuhiro',
    modified=NOW()
WHERE
    id=1

任何命令行输入或输出如下所示:

# The name of the package containing the theme scss for the app
app.theme=ext-theme-classic
↓
# The name of the package containing the theme scss for the app
app.theme=ext-theme-neptune

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“通过这样做,您可以通过继续点击删除按钮来继续删除。”

注意

警告或重要注意事项以如下框中的形式出现。

小贴士

小技巧和技巧如下所示。

读者反馈

我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中获得最大收益的标题非常重要。

要向我们发送一般反馈,只需发送一封电子邮件到 <feedback@packtpub.com>,并在邮件的主题中提及书名。

如果您在某个主题上具有专业知识,并且您对撰写或为本书做出贡献感兴趣,请参阅我们在 www.packtpub.com/authors 上的作者指南。

客户支持

现在,您已经自豪地拥有了一本 Packt 书籍,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

错误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过选择您的标题从 www.packtpub.com/support 查看任何现有勘误。

侵权

互联网上对版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 与我们联系,并提供涉嫌侵权材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面提供的帮助。

问题

如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章:数据结构

本书将逐步引导您通过使用现有数据库中的信息,在 Ext JS 中构建一个清晰且用户友好的销售管理数据库的过程。

本书面向具有 MySQL 操作知识的中级 Ext JS 开发者,他们希望提高编程技能并创建更高级的应用程序。

完成的应用程序将为您提供可工作的销售管理数据库。然而,本书的真正价值在于创建应用程序的动手过程,以及将本书中介绍的功能轻松转移到您自己的未来应用程序中的机会。

在构建此应用程序时,我们将关注的突出功能如下:

  • 支持历史记录的返回按钮功能:我们将自定义 Ext JS 函数,创建一种更轻量级的方法,在单页面上滚动前后,同时保持页面不变。

  • 更高效的屏幕管理:我们将学习如何通过简单地注册屏幕和命名约定来帮助您减少屏幕切换过程;这意味着您可以更多地关注每个屏幕背后的实现。此外,只需遵循这种架构,与历史记录的交互也将变得更加容易。

  • 使用 Ext.Direct 的通信方法:Ext.Direct 与 Ext 应用程序有紧密的亲和力,这使得连接、维护更加容易,并且消除了客户端需要更改 URL 的需求。此外,如果您使用 Ext.Direct,您还可以减少对服务器端的压力,因为它将多个服务器请求合并为一个请求。

  • 带有图表的数据显示方法:在 Ext JS 中,通过简单地调整存储和设置在网格中显示的数据结构,我们可以在图表中以图形方式显示数据。

本章将为您提供数据库的基本构建块。在本章中,您将编写 SQL 代码并在 MySQL 中创建表。

应用程序的结构 – 用户、客户、报价、报价单、账单和账单

首先,让我们看看我们即将构建的应用程序的结构。这是一个为用户注册客户、发送订单报价以及最终向客户开具账单而构建的销售管理应用程序。

用户可以将数据输入到“客户”表中。客户可以是个人或公司,无论哪种方式,每个客户都会收到一个唯一的 ID。

“报价”表代表发送给客户的最终报价。报价表包含报价中订购的个别项目。

账单是发送给客户的最终发票。与“报价”表类似,“账单”表指的是客户订购的个别项目。

用户

用户数据是一个简单的结构,用于登录系统。它有一个电子邮件地址、密码和姓名。

不要删除用户数据,并使用标志进行物理管理。它与其他数据结构通过联合所有权连接,记录创建日期和时间以及更新日期和时间。

当我们设计一个基于 MySQL 的模型表时,它看起来类似于以下表格。在执行 MD5 之后,我们执行 SHA1。然后,我们将有 40 个字符可以存储密码。

用户

客户

客户数据包含公司或客户的名称和地址。它允许引用账单表执行此数据的关联并使用这些数据。作为主数据,目前用户界面中不可用添加和删除。然而,随着您开发应用程序,您最终应该能够编辑这些数据。

下面的截图显示了注册客户的输入字段。名称列下的部分是需要为每个客户填写的字段。类型列指的是要输入的数据类型,如文字、数字和日期。列允许在不同表之间引用数据。

客户

引用和引用

引用引用表之间存在 1-N 关系。

引用中,您可以保存文档的基本信息,而在引用中,您可以存储每个正在订购的项目。

引用

下面的截图显示了引用所需的字段。表格标题与之前解释的客户表中的相同,因此让我们相应地填写。

引用

引用

这与之前相同,所以让我们继续填写。父级指的是引用(单个项目)表所属的整体报价。

引用

账单和账单

账单表几乎与引用表相同。然而,账单表有时可以包含相关联的引用表的 ID。

账单

下面的截图显示了账单表:

账单

账单

引用类似,在账单中,您可以存储每个已订购的项目:

账单

创建和处理客户结构表

我们将使用 MySQL,数据库字符集设置为utf8,校对设置为utf8_bin。当 SQL 描述我们之前定义的详细信息时,每个组件如下。

用户表

我们之前准备的用户表在执行以下代码后即可投入使用。重要的是要记住在id列中包含AUTO_INCREMENT;否则,您必须手动输入:

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS 'users';
CREATE TABLE 'users' (
  'id' bigint(20) NOT NULL AUTO_INCREMENT,
  'status' tinyint(1) NOT NULL DEFAULT '1',
  'email' varchar(255) NOT NULL,
  'passwd' char(40) NOT NULL,
  'lastname' varchar(20) NOT NULL,
  'firstname' varchar(20) NOT NULL,
  'modified' datetime DEFAULT NULL,
  'created' datetime NOT NULL,
  PRIMARY KEY ('id')
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

客户表

执行以下代码后,客户表即可投入使用:

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS 'customers';
CREATE TABLE 'customers' (
  'id' bigint(20) NOT NULL AUTO_INCREMENT,
  'status' tinyint(1) NOT NULL DEFAULT '1',
  'name' varchar(255) NOT NULL,
  'addr1' varchar(255) NOT NULL,
  'addr2' varchar(255) DEFAULT NULL,
  'city' varchar(50) NOT NULL,
  'state' varchar(50) NOT NULL,
  'zip' varchar(10) NOT NULL,
  'country' varchar(50) NOT NULL,
  'phone' varchar(50) NOT NULL,
  'fax' varchar(50) DEFAULT NULL,
  'modified' datetime DEFAULT NULL,
  'created' datetime NOT NULL,
  PRIMARY KEY ('id')
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

这是创建一组初始表的基石,这些表可以稍后填充数据。

引用表

这是 报价 表的对应代码。与 Customer 表一样,此代码片段将为我们的表奠定基础。

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS 'quotation';
CREATE TABLE 'quotation' (
  'id' bigint(20) NOT NULL AUTO_INCREMENT,
  'status' tinyint(1) NOT NULL DEFAULT '1',
  'customer' bigint(20) NOT NULL,
  'note' text NOT NULL,
  'modified' datetime DEFAULT NULL,
  'created' datetime NOT NULL,
  PRIMARY KEY ('id')
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

DROP TABLE IF EXISTS 'quotations';
CREATE TABLE 'quotations' (
  'id' bigint(20) NOT NULL AUTO_INCREMENT,
  'status' tinyint(1) NOT NULL DEFAULT '1',
  'parent' bigint(20) NOT NULL,
  'description' varchar(255) NOT NULL,
  'qty' int(11) NOT NULL,
  'price' int(11) NOT NULL,
  'sum' int(11) NOT NULL,
  'modified' datetime DEFAULT NULL,
  'created' datetime NOT NULL,
  PRIMARY KEY ('id'),
  KEY 'parent' ('parent')
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

账单表

与前两个代码片段类似,以下 Bill 表的代码与 报价 表非常相似,因此可以在 04_bill_table.sql 下的源文件中找到。

这些是我们数据库所需的所有表。现在,在每个操作创建后,让我们继续进行测试。

创建每个操作并测试

因为我们在后期阶段将使用 PHP,所以现在让我们为每个操作做好准备。在这里,我们将插入一些临时数据。

记得检查获取和更新操作是否正常工作。

用户认证

这些是一些你可以用来开发数据库的 SQL 代码。

你可以通过输入电子邮件地址和密码来查找用户。如果计数为 1,则可以假设操作成功。

为了提高密码安全性,在执行 MD5 加密后,你应该将密码存储为经过 SHA1 处理后的 40 个字符的字符串。

SELECT
    COUNT(id) as auth
FROM
    users
WHERE
    users.email = 'extkazuhiro@xenophy.com'
AND
    users.passwd = SHA1(MD5('password'))
AND
    users.status = 1;

选择用户列表

这用于当你想要收集用于网格中的数据时。请注意,我们不会使用 PagingToolbar 执行限制操作:

SELECT
    users.id,
    users.email,
    users.lastname,
    users.firstname
FROM
    users
WHERE
    users.status = 1;

添加用户

要添加用户,将当前时间放入 createdmodified

INSERT INTO users (
    email,
    passwd,
    lastname,
    firstname,
    modified,
    created
) VALUES (
    'someone@xenophy.com',
    SHA1(MD5('password')),
    'Kotsutsumi',
    'Kazuhiro',
    NOW(),
    NOW()
);

更新用户信息

每次修改文件时,应将其设置为 NOW() 以用作时间戳。其他字段应根据需要更新。

UPDATE
    users
SET
    email='extkazuhiro@xenophy.com',
    passwd=SHA1(MD5('password')),
    lastname='Kotsutsumi',
    firstname='Kazuhiro',
    modified=NOW()
WHERE
    id=1

删除用户

从此系统中删除不是永久删除用户数据的硬删除。相反,我们将使用软删除,即删除后用户数据不再显示但仍然保留在系统中。因此,请注意我们将使用 UPDATE 而不是 DELETE。在以下代码中,status=9 表示用户已被删除但未显示。(status=1 将表示用户是活跃的)。

UPDATE
    users
SET
    status=9
WHERE
    id=1

客户表

虽然 AddUpdateDelete 是必要的操作,但我们将在后面的章节中介绍这些操作,所以现在我们可以省略它们。

客户信息列表

这里我们正在准备 SQL 代码,以便稍后提取有关客户的信息:

SELECT
    customers.id,
    customers.name,
    customers.addr1,
    customers.addr2,
    customers.city,
    customers.state,
    customers.zip,
    customers.country,
    customers.phone,
    customers.fax
FROM
    customers
WHERE
    customers.status = 1;

选择报价列表

接下来是选择 报价 列表的代码。这与我们看到的客户信息列表类似。对于代码,请参阅 11_s_electing_quotation_list.sql 下的源文件。

项目

项目代码将从数据库中选择报价项。这将选择 quotations.status1quotation.parent1 的项:

SELECT quotations.description, 
  quotations.qty, 
  quotations.price, 
  quotations.sum
FROM
  quotations
WHERE
  quotations.'status' = 1
AND
  quotations.parent = 1

由于这与 Customers 类似,你现在可以省略 AddUpdateDelete

账单表

同样,现在让我们省略 AddUpdateDelete,因为 Bill 表与之前的类似。

一旦引用被接受,就会产生账单,这一点说得简单明了。因此,在我们的数据结构中,引用账单是相关的。唯一的区别是账单包含额外的引用ID,以在两者之间建立联系。

此外,记住客户信息列表几乎与报价列表相同。

摘要

在本章中,我们定义了本书中将使用的数据库结构。

你可能有自己的数据库,你希望在 Ext JS 中展示。这只是一个我们可以在接下来的章节中构建的示例数据库。

在下一章中,我们将开始构建整个应用程序的过程。别担心,我们会解释每一步。

第二章. 应用程序设计规划

在本章中,我们将介绍 Sencha Cmd 的同时设置项目的开发环境。

在本章中,你将学习如何:

  • 设计一个简单的应用程序

  • 优化 Ajax 请求以使用 Ext Direct 和Ext.util.History通过 URL 控制屏幕

设置 Sencha Cmd 和本地开发环境

通过使用 Sencha Cmd 设置本地开发环境,最终部署应用程序时,Sencha Cmd 将仅选择使用的组件。因此,这将优化最终应用程序。

使用 Sencha Cmd,你可以运行 Sencha Touch 的原生包,包括脚手架和构建主题。让我们开始使用 Sencha Cmd 生成你的项目。

  1. 安装可在www.oracle.com/technetwork/java/javase/downloads/index.html或 JRE 上找到的最新 JAVA 运行环境。

  2. compass-style.org/安装 Compass。

  3. 安装可在www.sencha.com/products/sencha-cmd/download找到的 Sencha Cmd。

  4. 下载可在www.sencha.com/products/extjs/download/找到的 Ext JS SDK。

  5. 提取 Ext JS SDK 并将其定位在你选择的本地目录中。例如,你可以创建一个名为ext的顶级目录。

将创建如以下截图所示的目录结构。这次,我们将使用 Ext JS 4.2.2 GPL。

设置 Sencha Cmd 和本地开发环境

示例在 Mac OS X 上。安装后,Sencha Cmd 位于以下路径,~/bin/Sencha/Cmd/4.0.0.203/

对于 Windows,Sencha Cmd 位于以下路径,C:/Users/(你的用户名)/bin

如果路径设置正确,你应该能够在 Sencha 下执行以下命令,并且以下列表中的选项、类别和命令应该出现在你的屏幕上。

# sencha
Sencha Cmd v4.0.0.203

通过在 Sencha Cmd 中使用以下定义的组合,我们可以使用 Sencha Cmd。

例如,要创建一个项目,使用我们提供的选项和类别,我们可以输入,sencha -sdk [/sdk/path] generate app App [/project/path]

要构建一个项目,我们可以使用类别和命令进入:sencha app build

选项

这里有一些你可以使用的选项:

  • * --debug, -d - : 这将设置日志级别为更高的详细程度

  • * --plain, -p - : 这将启用纯日志输出(无高亮)

  • * --quiet, -q - : 这将设置日志级别为仅警告和错误

  • * --sdk-path, -s - : 这将设置目标框架的路径

类别

这里有一些你可以使用的类别:

  • *app - : 使用这个,我们可以执行各种应用程序构建过程

  • *compile - : 这允许我们将源代码编译成连接输出和元数据

  • *fs - : 这是一组用于处理文件的实用操作

  • *generate - : 这将生成模型、控制器等,或者整个应用程序

  • *manifest - : 这将提取类元数据

  • *package - : 这将为 Sencha Touch 应用程序打包,以便在原生应用商店中发布

  • *theme - : 这将从给定的 HTML 页面生成一组主题图像

命令

这里有一些你可以使用的命令:

  • * ant - : 这将调用 Ant 并将一些有用的属性传回 Sencha Command

  • * build - : 这将从 JSB3 文件构建一个项目。

  • * config - : 这将加载一个配置文件或设置一个配置属性

  • * help - : 这将显示命令的帮助信息

  • * js - : 这将执行任意 JavaScript 文件(s)

  • * which - : 这将显示当前 Sencha Cmd 版本的路径

使用 Sencha Cmd 创建项目

首先,让我们使用 Sencha Cmd 生成一个项目。首先,将当前目录移动到项目目录中,并执行以下命令:

# sencha -sdk ./ext generate app MyApp ./

应该出现以下日志。它非常长,所以这是一个简化的版本,即日志的开始和结束部分:

[INF]    init-properties:
[INF]    init-sencha-command:
[INF]    init:
.
.
.
[INF]    app-refresh:
[INF]    -after-generate-app:
[INF]    generate-app:

然后,将生成以下目录:

使用 Sencha Cmd 创建项目

让我们用浏览器查看它;它应该如下所示。无论如何,通过 Web 服务器显示它。

使用 Sencha Cmd 创建项目

通过执行命令,Sencha Cmd 创建一个临时的视图和控制台,以便快速启动整个项目。我们可以根据我们自己的应用程序对其进行更改或添加。

创建组件测试

Sencha Cmd 生成了一个临时应用程序。然而,你不应该立即对其进行自定义。在你这样做之前,让我们做一个组件测试CT),以方便开发。一般来说,它测试源代码和组件之间的关系。CT 在视口中以及单独的测试视图中显示正在测试的源代码,而不移动整个源代码。通过使用 CT,开发团队可以同时平滑地构建应用程序,并在开发过程中早期发现问题。

查看其工作情况

首先,你可以检查自动生成的应用程序的工作情况,并查看 index.html 的详细信息(源文件:01_check_how_its_working/index.html)。

唯一可以读取的 CSS 文件是 ext/packages/ext-theme-neptune/build/resources/ext-theme-neptune-all-debug.css。至于 JS 文件,以下三个被读取。

  • ext/ext-dev.js

  • bootstrap.js

  • app/app.js

要运行 app.js,你需要 ext-dev.jsbootstrap.js。这些对于你的应用程序是必要的,并且其他 Ext JS 文件在 Ext Loader 中动态读取。如下面的截图所示,在 Google Chrome 浏览器开发工具的“网络”标签下,许多文件都是动态读取的:

查看其工作情况

这次我们将使用 Ext JS Neptune 主题来构建应用程序。

当你创建一个新的应用程序时,默认主题是 ext-theme-classic。让我们将其更改为 ext-theme-neptune

要修改的目标文件如下:

.sencha/app/sencha.cfg – L32

# The name of the package containing the theme scss for the app
app.theme=ext-theme-classic

将上一行更改为以下内容:

# The name of the package containing the theme scss for the app
app.theme=ext-theme-neptune

在进行此更改后继续到下一节,主题应更改为以下截图所示:

检查其工作情况

制作生产构建

当你使用 Ext Loader 时,在屏幕显示之前需要花费很多时间。随着应用程序规模的增长和文件数量的增加,它将变得更慢。这可能对于产品来说太慢了。

但不必担心,你手头已经有了解决方案——使用 Sencha Cmd 构建。首先,将当前项目移动到项目目录,并执行以下命令:

# sencha app build

执行以下命令时将创建以下目录:

制作生产构建

在这里生成的 app.js 文件是一个压缩的 JavaScript 文件,它将你编写的代码与 Ext JS SDK 的代码合并。启用 app.js 是有效的,因为代码已经被读取,Ext Loader 不需要再次读取代码,从而提高速度。此 app.js 文件是在压缩状态下生成的,去除了不必要的空行。

./build/production/MyApp/ 目录已生成并存储。让我们用浏览器检查生成的生产应用程序。

如果你想要调试,压缩 app.js 是一个非常糟糕的主意。相反,你应该执行以下命令进行测试:

# sencha app build testing

testing 目录随后在 production 目录中创建,如下截图所示。此目录下创建的 app.js 文件未压缩。

制作生产构建

单个视图中的 CT

如果你按常规方式执行 CT,你必须同时测试所有组件。然而,如果我们将 CT 拆分为单个视图,我们可以在各自的 HTML 中单独测试每个组件。

首先,我们创建一个名为 ct 的目录,并编写组件列表的 HTML。这仅仅是源文件中的 index.html 代码:03_ct_in_individual_views/ct/index.html

在这里,你将创建一个标题组件。首先让我们创建其外观。你可以为 header 组件创建一个 ct 目录,如下截图所示:

单个视图中的 CT

header 文件中创建 view.htmlview.js。代码如下:

view.html
<html>
<head>
<meta charset="utf-8" />
<title>[View]Header - Component Test</title>
<link rel="stylesheet" href="../../bootstrap.css" type="text/css">
<script language="JavaScript" type="text/javascript" src="img/ext-all.js"></script>
</head>
<body>
<script type="text/javascript" src="img/view.js"></script>
</body>
</html>

view.js
Ext.Loader.setConfig({
    enabled: true,
    paths: {
        MyApp: '../../app/'
    }
});

Ext.onReady(function() {
    Ext.create('MyApp.view.Header', {
        renderTo: Ext.getBody()
    });
});

视图组件

让我们创建 MyApp.view.Header 的标题组件。你应该在 app/view 目录下创建 Header.js 并创建 Header 组件(源文件:04_view_component/app/view/Header.js)。

Ext.define('MyApp.view.Header', {
    extend: 'MyApp.toolbar.Toolbar',
    alias: 'widget.myapp-header',
    height: 35,
    items: [{
        text: 'MyApp',
        action: 'dashboard'
    }, '->', {
        text: 'MyAccount',
        action: 'myaccount'
    }, {
        text: 'Log Out',
        action: 'logout'
    }]
});

当你创建 MyApp.toolbar.Toolbar 类时,MyApp 中的面板组件将工作。因此,在 app 目录下(源文件:04_view_component/app/toolbar/Toolbar.js)创建一个面板目录。

Ext.define('MyApp.toolbar.Toolbar', {
    extend: 'Ext.toolbar.Toolbar'
});

当你用浏览器显示它时,将出现以下截图:

视图组件

在这一点上,即使你点击按钮,也不会发生任何事情,因为你仍然需要实现事件处理器;但别担心,我们很快就会做到这一点。

添加控制器

外观已经制作好了。所以现在,让我们添加一个控制器。为此,在app/controller目录下创建Abstract.js,然后创建MyApp.controller.Abstract类(源文件:05_adding_controller/app/controller/Abstract.js)。

Ext.define('MyApp.controller.Abstract', {
    extend: 'Ext.app.Controller'
});

添加控制器非常简单,它只是扩展到Ext.app.Controller。这个类将实现所有控制器将创建的常见功能。所以,让我们为标题创建一个控制器。在同一个目录下创建Header.js,并定义为MyApp.controller.Header。在以下代码中,从我们之前创建的MyApp.controller.Abstract类中实现了init方法(源文件:05_adding_controller/app/controller/Header.js)。

Ext.define('MyApp.controller.Header', {
    extend: 'MyApp.controller.Abstract',
    init: function() {
        var me = this;
        me.control({
            'myapp-header [action=dashboard]': {
                click: function() {
                    console.log('dashboard');
                }
            },
            'myapp-header [action=myaccount]': {
                click: function () {
                    console.log('myaccount');
                }
            },
            'myapp-header [action=logout]': {
                click: function() {
                    console.log('logout');
                }
            }
        });
    }
});

以下是从上一段代码中获取的组件查询。这些查询获取位于标题中的按钮。有了这个,你现在已经完成了控制器的创建。

'myapp-header [action=dashboard]'
'myapp-header [action=myaccount]'
'myapp-header [action=logout]'

应该创建一个如下所示的文件:

添加控制器

让我们操作你成功创建的控制器。在目录中,你创建了view.htmlview.js的地方,你现在应该创建app.htmlapp.jsapp.html的 CT 大部分与view.html相同。你可以在这里看到源代码:06_app_test/ct/header/app.html

对于app.js,请参阅以下代码(源文件:06_app_test/ct/header/app.js)。

Ext.application({
    autoCreateViewport: false,
    name: 'MyApp',
    appFolder: '../../app',
    controllers: [
        'Header'
    ],
    launch: function() {
        var panel = Ext.create('MyApp.view.Header', {
            renderTo: Ext.getBody()
        });
        Ext.util.Observable.capture(panel, function() {
            console.log(arguments);
        });
    }
});

你应该在ct目录中为app.html添加一个链接到index.html。在app.js中调用Ext.application并仅启动标题应用程序。因为autoCreateViewport的默认值设置为true,你应该将其设置为false。这是因为Viewport对于这个测试是不必要的。

第二,将控制器中的标题设置为读取MyApp.controller.Header。你应该设置函数以创建视图。Ext.util.Observable.capture调用捕获了在第一个参数中设置的对象中触发的事件。

我们可以通过视觉确认,当点击按钮时,事件会做出反应。

添加控制器

我们只是创建了一个没有控制器的视图。原因是如果我们在这个阶段在控制器和视图之间建立关系,这将使测试视图变得非常困难。

我们创建了包含控制器逻辑的app.html。我们需要app.html以便能够同时测试视图和控制器。

此外,为了避免不正确的代码直接引用对象,将每个可以在 CT 中组合并稍后在视图中开发的组件和屏幕分开。

现在,让我们学习按顺序创建组件和 CT。

创建视图

我们需要创建应用程序的外观。您已经创建了头部。让我们将应用程序的导航菜单放在屏幕的左侧。定位您之前创建的头部在屏幕顶部。将中间区域但位于右侧的屏幕命名为屏幕。将屏幕放在这个中间区域。

屏幕分为四个部分:

  • 仪表板

  • MyAccount

  • Quotation

  • Bill

将这四个部分连接起来,形成一个如以下图表所示的粗略视图结构:

创建视图

导航

让我们创建导航部分。该组件扩展了树面板。通过使用树面板,可以由服务器处理显示菜单选项的流程,而不是手动输入菜单选项。将菜单选项分组的过程可以简化。

树面板需要一个树存储。所以现在让我们创建它(源文件:07_creating_views/app/store/Navigation.js)。

Ext.define('MyApp.store.Navigation', {
    extend: 'Ext.data.TreeStore',
    storeId: 'Navigation',
    root: {
        expanded: true,
        children: [{
            text: 'Dashboard',
            leaf: true
        }, {
            text: 'Quotation',
            leaf: true
        }, {
            text: 'Bill',
            leaf: true
        }, {
            text: 'MyAccount',
            leaf: true
        }]
    }
});

现在已经创建了一个存储,您可以定义树面板(源文件:07_creating_views/app/view/Navigation.js)。

Ext.define('MyApp.view.Navigation', {
    extend: 'Ext.tree.Panel',
    alias: 'widget.myapp-navigation',
    title: 'Navigation',
    store: 'Navigation',
    rootVisible: false,
    animate: false
});

现在,让我们创建一个 CT 来检查我们创建的内容。07_creating_views/ct/navigation/view.html 的代码与之前的 view.html 代码非常相似,所以如果您想查看此代码,请参阅源文件。以下代码是 view.js(源文件:07_creating_views/ct/navigation/view.js)。

Ext.onReady(function() {
    Ext.create('MyApp.store.Navigation', {
        storeId: 'Navigation'
    });
    Ext.create('MyApp.view.Navigation', {

    });
});

我们之前创建了一个存储,因为它是显示导航视图所必需的。当我们在浏览器中查看时,Navigation 视图显示如下截图所示:

导航

让我们以同样的方式创建剩余的组件。

仪表板

我们想要创建一个仪表板的面板(从现在起我们将创建许多类似的面板)。因此,应用程序将在 MyApp.panel.Screen 抽象类中创建。MyApp.panel.Screen 抽象类是整个应用程序的基本模具(源文件:07_creating_views/app/panel/Screen.js)。

Ext.define('MyApp.panel.Screen', {
    extend: 'Ext.panel.Panel',
    initComponent: function() {
        var me = this;
        me.callParent(arguments);
    }
});

我们能够创建抽象类。现在,让我们创建一个继承自该抽象类的仪表板类(源文件:07_creating_views/app/view/DashBoard.js)。

Ext.define('MyApp.view.DashBoard', {
    extend: 'MyApp.panel.Screen',
    alias: 'widget.myapp-dashboard'
});

现在,准备 CT 来检查外观。07_creating_views/ct/dashboard/view.html07_creating_views/ct/dashboard/view.js 都与 ct/header 类似,所以请参考源文件中的代码。

MyAccount

让我们创建用户的个人账户页面。让我们称它为“MyAccount”,并以我们创建仪表板相同的方式创建它。除了别名属性和标题属性外,它与仪表板完全相同。有关代码,请参阅源文件:07_creating_views/app/view/MyAccount.js

报价和账单

继续以同样的方式创建报价和账单。同样,除了别名属性和标题属性外,它与仪表板相同。请参阅源文件中的代码:

  • 07_creating_views/app/view/Quotation.js

  • 07_creating_views/app/view/Bill.js

视口

让我们在视口中组合我们迄今为止制作的组件。修改由 Sencha Cmd 自动生成的文件。尽管我们可以通过所有方式创建 CT,但有了视口,CT 就不再必要了。HTML 本身与 CT 索引相同。

首先,必须修改app/Application.js以使用导航存储(源文件:08_create_viewport/app/Application.js)。

Ext.define('MyApp.Application', {

    stores: [
        'Navigation'
    ]
});

对于在视口中设置的xtype类,在读取源代码之前必须完成读取。因此,我们将类名添加到application.js文件中。

为了反映新添加组件的 CSS,我们将使用 Sencha Cmd 构建应用程序一次。这将确保任何新组件的 CSS 都包含在sencha app build中。

通过这种方式,分配给bootstrap.css的组件样式将被更新。当我们访问位于文档路由下的index.html时,它将显示如下截图。我们制作的文件由 Ext Loader 动态读取。

视口

现在视图已经完成!

创建控制器

现在,我们将为我们的数据创建控制器。

Main

通常,每个视图组件都有一个具有相同前缀的对应控制器类。

但我们首先必须处理的控制器是名为 Main 的控制器。这个控制器已经被 Sencha Cmd 自动生成。如果我们需要为整个应用程序添加更多处理或逻辑,我们应该实现这个类。通常,view类和控制器类名称匹配,但这是一个特殊情况,它们不匹配,因为 Sencha Cmd 生成了 Main 类。所以,最好是让它保持原样,不要更改名称。

对于app/controller/Main.js代码,请参阅源文件:09_create_controller\app\controller

虽然我们谈了很多关于 Main 的内容,但我们目前不会使用它。

导航

现在,让我们将hrefTarget添加到导航存储中的数据(源文件:09_create_controller/app/store/Navigation.js)。

Ext.define('MyApp.store.Navigation', {
    …
    root: {
        children: [{
            text: 'Dashboard',
            hrefTarget: '#!/dashboard',
            leaf: true
        }, {
            text: 'Quotation',
            hrefTarget: '#!/quotation',
            leaf: true
        }, {
            text: 'Bill',
            hrefTarget: '#!/bill',
            leaf: true
        }, {
            text: 'MyAccount',
            hrefTarget: '#!/myaccount',
            leaf: true
        }]
    }
});

现在,创建一个导航控制器并描述itemclick事件处理程序,当选择菜单时捕获事件(源文件:09_create_controller/app/controller/Navigation.js)。

Ext.define('MyApp.controller.Navigation', {
    extend: 'MyApp.controller.Abstract',
    init: function() {
        var me = this;
        me.control({
            'myapp-navigation': {
                itemclick: function(row, model) {
                    if (!model.isLeaf()) {
                        if (model.isExpanded()) {
                            model.collapse();
                        } else {
                            model.expand();
                        }
                    } else {
                        if (model.data.hrefTarget) {
                            console.log('select:' + model.data.
                            hrefTarget);
                        }
                    }
                }
            }
        });
    }
});

将此添加到app/Application.js中,以便在视口中显示时使用。

Ext.define('MyApp.Application', {

    controllers: [
        'Main',
        'Header',
        'Navigation',
        'DashBoard',
        'MyAccount',
        'Quotation',
        'Bill'
    ],

});

此外,记住 CT 会从控制器自行准备操作。以下代码将执行 CT 进行导航(源代码:09_create_controller/ct/navigation/app.js)。

Ext.application({
    ...
    stores: [
        'Navigation'
    ],
    controllers: [
        'Navigation'
    ],
    launch: function() {
        var panel = Ext.create('MyApp.view.Navigation', {
            width: 300,
            height: 600,
            renderTo: Ext.getBody()
        });
        Ext.util.Observable.capture(panel, function() {
            console.log(arguments);
        });
    }
});

当我们执行此操作时,当我们从菜单中选择任何选项时,控制台会显示一条消息。

仪表板

让我们继续并创建仪表板的控制器。这里我们不描述任何特殊过程,这只是准备。只需实现以下代码(源文件:09_create_controller/app/controller/DashBoard.js):

Ext.define('MyApp.controller.DashBoard', {
    extend: 'MyApp.controller.Abstract',
    init: function() {
        var me = this;
        me.control({
        });
    }
});

MyAccount

MyAccount 与仪表板相同,只需将 Dashboard 替换为 MyAccount。对于代码,请参阅源文件:09_create_controller/app/controller/M yAccount.js

报价和账单

对于报价和账单也是如此。没有特殊处理,只是准备,这和仪表板也相同。对于代码,请参阅以下位置的源文件:

  • 09_create_controller/app/controller/Quotation.js

  • 09_create_controller/app/controller/Bill.js

到目前为止,你已经创建了多个文件。随着它们的创建,它们按照以下截图所示填充了目录:

报价和账单

使用 Ext.util.History 直接访问屏幕

Ext.util.History 管理历史记录。当页面发生变化时,应用程序会捕捉到这个变化,然后 Ext.util.History 触发一个事件。使用这个特性,我们可以控制应用程序中的操作。例如,通过使用 Ext.util.History,每次访问应用程序时都会显示仪表板 URL。

有时,当用户在 URL 中输入错误的哈希值时,应用程序将难以找到页面。然而,使用 Ext.util.History,它会跳转到开发者指定的屏幕。例如,当我们实现 Ext.util.History 时,用户可以直接访问报价页面,而不会出现异常屏幕行为。使用 Ext.util.History 还允许我们将 URL 附带到电子邮件中,以便直接访问文档。

首先,您需要创建 MyApp.util.History 类。当您看到它时,您可能会认为它是一个抽象类,实际上并非如此。MyApp.util.History 是一个用于在我们的应用程序中使用 Ext.util.History 集成 URL 控制的类。在这里要小心不要出错,否则 MyApp.util.History 将无法工作。Ext.util.History 类是一个单例类,MyApp.util.History 也会创建一个单例类。首先,让我们创建 MyApp.util.History 类。它非常简单(源文件:10_util_history/app/util/History.js)。

Ext.define('MyApp.util.History', {
    singleton: true,
    uses: ['Ext.util.History'],
    controllers: {},
    init: ... (implement)
    initNavigate: ... (implement)
    navigate: ... (implement)
    parseToken: ... (implement)
    push ... (implement)
    cleanUrl ... (implement)
    back: ... (implement)
    location: ... (implement)
});

小贴士

下载示例代码

您可以从您在 www.packtpub.com 的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

让我们通过以下列表实现方法:

  • init

  • initNavigate

  • navigate

  • parseToken

  • push

  • cleanUrl

  • back

  • location

请将它们实现为 MyApp.util.History 类的方法(源文件:10_util_history/app/util/History.js)。由于实现此列表的代码太长,无法包含在此文本中,因此请参阅源文件以获取代码。

当我们使用MyApp.util.History时,我们必须调用init方法。我将在稍后描述init方法的用法。我们不需要对这些方法进行太多解释,因为这会花费太多时间。重要的是要知道,这些方法对于浏览器导航非常重要。

在控制器中添加逻辑

添加MyApp.util.History和构造函数到MyApp.controller.Abstract(源文件:11_adding_controller/app/controller/Abstract.js)。

Ext.define('MyApp.controller.Abstract', {
    extend: 'Ext.app.Controller',
    uses: ['MyApp.util.History'],
    aliasPrefix: 'myapp-',
    constructor: function() {
        var me = this,
            screenName = me.screenName;

        Ext.apply(me, {
            history: MyApp.util.History
        });
        if (screenName) {
            me.history.controllers[screenName] = me;
            me.baseUrl = '#!/' + screenName;
            me.aliasName = me.aliasPrefix + screenName;
        }
        me.callParent(arguments);
    },
    loadIndex: function(url) {
        var me = this, url = url || me.baseUrl;
        if (url.substr(0, 2) !== '#!') {
            url = me.baseUrl + '/' + url;
        }
        me.history.push(url, {
            navigate: true,
            callback: function() {
                var self = Ext.ComponentQuery.query(me.aliasName)[0];
                self.fireEvent('myapp-show', self);
            }
        });
    },
    init: function() {
        var me = this, o = {};
        o[me.aliasName] = {
            'myapp-show': me.onShow,
            'myapp-hide': me.onHide
        };
        me.control(o);
    },
    onShow: function() {
        // Nothing todo
    },
    onHide: function() {
        // Nothing todo
    }
});

设置一个名为MyApp.util.History.init的匿名函数,这是我们之前制作的,并初始化它以启动。在参数中指定要使用的屏幕序列。指定源文件(11_adding_controller/app/Application.js)。

Ext.define('MyApp.Application', {

    stores: [
        'Navigation'
    ],

    screens: [
        'dashboard',
        'myaccount',
        'quotation',
        'bill'
    ],
    launch: function() {
        MyApp.util.History.init({
            screens: this.screens
        });
    }
});

之后,将screenName属性添加到每个类中(源文件:11_adding_controller/app/controller/DashBoard.js)。

Ext.define('MyApp.controller.DashBoard', {
    extend: 'MyApp.controller.Abstract',
    screenName: 'dashboard',

});

与仪表板相同,我们将为 MyAccount、报价和账单添加一个screenName属性。请检查源文件以获取代码:

  • 11_adding_controller/app/controller/MyAccount.js

  • 11_adding_controller/app/controller/Quotation.js

  • 11_adding_controller/app/controller/Bill.js

修复导航控制器。我们应该通过删除控制台并更改MyApp.util.History中的调用位置来更改区域设置(源文件:11_adding_controller/app/controller/Navigation.js)。

Ext.define('MyApp.controller.Navigation', {
    extend: 'MyApp.controller.Abstract',
    init: function() {
        var me = this;
        me.control({
            'myapp-navigation': {
                itemclick: function(row, model) {
                    …
                        if (model.data.hrefTarget) {
                            console.log('select:' + model.data.                 
                            hrefTarget);
                            me.history.location(model.data.
                            hrefTarget);
                        }

});

最后,如果我们对每个视图进行区分,那么获取该视图会变得更容易。所以让我们为每个视图添加itemId。首先,让我们添加到仪表板(源文件:11_adding_controller/app/view/DashBoard.js)。

Ext.define('MyApp.view.DashBoard', {
    …
    itemId: 'screen-dashboard',
    title: 'DashBoard'
});

再次,让我们以相同的方式为 MyAccount、报价和账单添加itemId。请检查以下源文件以获取代码:

  • 11_adding_controller/app/view/MyAccount.js

  • 11_adding_controller/app/view/Quotation.js

  • 11_adding_controller/app/view/Bill.js

因此,为了确认,我们在使用init设置后点击导航菜单时应该看到带有附加 hash 的 URL。

  • #!/dashboard

  • #!/quotation

  • #!/bill

  • #!/myaccount

设置 Ext Direct

这是漫长准备列表中的最后一项。大多数 Sencha Ext JS 应用程序都与服务器通信,获取数据和发送消息。在这种情况下,使用Ext.Ajax类进行通信非常简单和容易。

然而,这次让我们使用 Ext Direct。Ext Direct 通过包装 Ajax 通信、轮询和执行远程过程调用(RPC)来执行。

RPC 是 Ext Direct 的主要功能。然而,由于空间有限,我这里不会详细讨论它。如果您想了解具体细节,请访问 Sencha 主页www.sencha.com/products/extjs/extdirect

Ext Direct 简化了与服务器的通信。因此,JavaScript 代码减少了。此外,除了减少浏览器读取和负担外,通信通过组合请求函数自动优化。

当我们通过 Ext Direct 在 10 毫秒的间隔内同时执行 Ajax 通信并传递初始值时,我们可以将发送给路由器的请求合并为一次通信。但对我们来说,这和单独发送每个请求是一样的。

小贴士

对于许多开发者来说,这仍然不是常识,尤其是那些不了解 Ext Direct 优点的人。这真是个遗憾,因为这是你应用中一个非常方便的功能。

虽然一开始需要做很多准备工作,但如果在这里学习并保持准备就绪,未来的 Sencha Ext JS 开发将会变得简单,就像一个模板一样。

创建 Ext Direct 路由器

首先,我们需要创建路由器并在 PHP 中实现它。实际上,路由器是在一个示例中实现的。我们只需要定位它并使用它来启动。首先,从示例页面复制 PHP 代码。你可以在 Ext SDK 路径中找到示例页面,路径为 /examples/direct/。换句话说,就是 (Ext SDK path)/examples/direct/

创建 Ext Direct 路由器

在文档路由中创建一个名为 php 的文件夹,并复制之前截图显示的四个文件。在我们复制完文件后,从这个路径删除它们是可以接受的。

编辑 config.php 并将方法定义为服务器端显示的类。原始版本中有许多接口,但这次我们只使用一个。所以让我们删除其他的,以保持事情的可管理性(源文件:12_setup_ext_direct/php/config.php)。

<?php
$API = array(
    'TestAction' => array(
        'methods' => array(
            'doEcho' => array(
                'len' => 1
            )
        )
    )
);

创建 Ext Direct 模块

和之前一样,我们将在 TestAction 类中只使用 doEcho 方法。所以,让我们删除其他方法以保持一切的可管理性(源文件:12_setup_ext_direct/php/classes/TestAction.php)。

<?php
class TestAction {
    function doEcho($data) {
        return $data;
    }
}

将 Ext Direct 应用到客户端应用程序

现在,通过在 index.html(源文件:12_setup_ext_direct/index.html)中添加以下源代码,将客户端应用程序设置为立即从客户端调用 TestAction.doEcho

    ...
<!-- </x-compile> -->
<script src="img/api.php"></script>
</head>

接下来,调用 addProvider 并将其设置为在 Application.js(源文件:12_setup_ext_direct/app/Application.js)服务器端显示的成员。

Ext.require([
    'Ext.direct.*',
    'Ext.data.proxy.Direct',
    'Ext.form.action.DirectSubmit'
]);

Ext.onReady(function() {
    Ext.direct.Manager.addProvider(Ext.app.REMOTING_API);
});

Ext.define('MyApp.Application', {

测试 Ext Direct

现在我们可以测试它了。当点击注销按钮时,调用 doEcho 并确认它是否正常工作。然后,将处理添加到头部控制器中。

使用浏览器检查性能。当我们点击注销按钮时,代码中已经存在的字符串 message 将被发送到服务器端。然后我们可以检查 message 是否成功返回客户端。如果成功返回,那么一切正常!请参考源文件:12_setup_ext_direct/app/controller/Header.js

摘要

干得好!我们现在可以开发应用程序了。虽然过程很漫长,但请记住,它只是完成初始准备所需的时间。

从现在开始,你可以将在这里所做的一切应用到未来。如果你回顾我们刚刚所做的一切并自己理解它,你可以将此作为未来项目的模板,这将使准备阶段变得更加快速和顺畅。

当你准备好了,继续到下一章,我们将开始将数据输入到我们的数据库中。

第三章:数据输入

欢迎回来。所以,在前两个章节中,我们学习了数据结构,并在 SQL 和 MySQL 中创建了表。然后我们在熟悉 Sencha Cmd 的过程中创建了开发环境。

在本章中,我们将:

  • 学习如何创建输入数据的表单

  • 通过 Ext Direct 将数据传输到服务器

  • 学习如何监控输入状态

  • 学习如何使用 Ext Direct 在服务器端验证MyAccount表单

创建登录页面

让我们从创建这个应用程序的登录页面开始。在这里,我们将使用 PHP 进行简单的实现。我们将创建login.phplogout.php。然而,我们不会在这里过多地详细介绍登录逻辑,因为这本书的重点是 Ext JS 而不是 PHP。

在 SQL 中创建你将要输入的变量,然后只需从数据库中提取用户信息。

源代码非常长,所以请前往源文件查看01_making_the_login文件夹中的login.php文件和01_making_the_login中的logout.php

使用键USERINFO将用户数据存储在会话中。再次,为了能够执行登录检查,将index.html改为index.php并运行登录检查。除了开头的 PHP 代码外,它与index.html相同(源文件:01_making_the_login/index.php):

<?php
session_start();
if(!isset($_SESSION['USERINFO'])) {
  header("Location: ./login.php");
}
?>
<!DOCTYPE HTML>
...
</html>

创建 MyAccount 表单

现在,让我们构建将要部署到各种屏幕上的表单。首先是MyAccount

将表单面板添加到你构建的临时面板中。不要直接将表单安装到Screen面板上——这样做有原因。你将建立列表和详情之间的关系,因此有时屏幕内会有多个面板存在。

恰好MyAccount只需要一个屏幕。如果方法数量增加,你可能会需要多个面板。你可能需要一个面板来运行输入验证。但在此阶段很难确定。因此,有必要将屏幕布局改为卡片布局,这将使处理多个屏幕变得更加容易。

首先,让我们指定屏幕布局为一个卡片布局。在你之前在view目录底部创建的MyAccount.js脚本中,创建一个MyAccount目录并进入它。目录的层次结构将发生变化,因此我们还需要修改类名(源文件:02_making_the_account_form/app/view/myaccount/MyAccount.js):

Ext.define('MyApp.view.myaccount.MyAccount', {
extend: 'MyApp.panel.Screen',
    ....

让我们按照以下方式修改类名(源文件:02_making_the_account_form/app/controller/myaccount/MyAccount.js):

Ext.define('MyApp.controller.myaccount.MyAccount', {
extend: 'MyApp.controller.Abstract',
    ....

同样,编辑QuotationBillDashboard的控制器和视图。通过这样做,应用程序现在应该可以工作了。

由于包名已更改,让我们相应地修改Application.js控制器设置(源文件:02_making_the_account_form/app/Application.js):

Ext.application({
    ....
    controllers: [
        'Main',
        'Header',
        'Navigation',
        'dashboard.Dashboard',
        'myaccount.MyAccount',
        'quotation.Quotation',
        'bill.Bill'
    ],
    ....

此外,视图名称已更改,因此我们也必须修改它(源文件:02_making_the_account_form/app/view/Viewport.js):

Ext.define('MyApp.view.Viewport', {
    ....
    requires:[
        'Ext.panel.Panel',
        'Ext.layout.container.Border',
        'MyApp.view.Header',
        'MyApp.view.Navigation',
        'MyApp.view.dashboard.Dashboard',
        'MyApp.view.myaccount.MyAccount',
        'MyApp.view.quotation.Quotation',
        'MyApp.view.bill.Bill'
    ],
    ....

现在有了这个,我们可以自由地创建表单。首先,我们想要创建表单面板并将其放入MyAccount,但我们想要对表单面板进行数据抽象。所以,一开始我们就这样做,创建一个简单的继承。

与我们为app/panel/Screen.js创建类的方式相同,我们将创建一个继承自Ext.form.Panel的类(参考源文件:02_making_the_account_form/app/form/Panel.js)。

我们已经创建了继承。让我们创建一个edit面板(源文件:02_making_the_account_form/app/view/myaccount/Edit.js):

Ext.define('MyApp.view.myaccount.MyAccount', {
    extend: 'MyApp.form.Panel',
    alias : 'widget.myapp-myaccount-edit',
    itemId: 'screen-myaccount-edit',
    initComponent: function() {
        var me = this;
        Ext.apply(me, {
        });
        me.callParent(arguments);
    }
});

我们稍后会在表单内进行实现。首先,让我们嵌入这个面板(源文件:02_making_the_account_form/app/view/myaccount/MyAccount.js):

Ext.define('MyApp.view.myaccount.MyAccount', {
  ...
    requires: [
        'MyApp.view.myaccount.Edit'
    ],
    itemId: 'screen-myaccount',
    title: 'MyAccount',
    layout: 'card',
    items: [{
        xtype: 'myapp-myaccount-edit',
        border: false
    }]
});

确保浏览器没有错误。外观没有变化,所以目前可能不太有趣。

目前,它在显示上看起来不错,但你可能已经注意到出现了CT错误。这是因为我们之前更改了名称。现在让我们修复它。

MyAccount类(viewcontroller)的包名已更改,因此请继续修改以下类名(参考源文件):

02_making_the_account_form/ct/myaccount/view.js

02_making_the_account_form/ct/myaccount/app.js

与此同时,你也应该修复其他 CT 错误。

现在,让我们开始构建主表单。当你定义类时,你可以使用配置选项来定义它们,但让我们在initComponent中使用Ext.apply来设置。如果我们使用initComponent设置配置,我们可以在创建组件时设置更灵活的行为。

以下代码与Sencha Architect生成的代码非常相似(源文件:02_making_the_account_form/app/view/myaccount/Edit.js):

Ext.define('MyApp.view.myaccount.Edit', {
  ...
    initComponent: function() {
        var me = this;
        // Fields
        Ext.apply(me, {
            bodyPadding: 20,
            defaultType: 'textfield',
            items: [{
                fieldLabel: 'email'
           }, {
                fieldLabel: 'firstname'
           }, {
                fieldLabel: 'lastname'
           }]
        });
        // TopToolbar
    Ext.apply(me, {
        tbar: [{
text: 'Save',
action: 'save'
           }, {
            text: 'Reset',
            action: 'reset'
           }]
        });
    me.callParent(arguments);
    }
});

如果你运行前面的代码,它将看起来像以下截图:

创建 MyAccount 表单

我们以这种方式创建表单。如果你想到一个更复杂或更有吸引力的表单,可以尝试调整它。你可以在Sencha Ext samples上找到更多复杂的布局:dev.sencha.com/deploy/ext-4.0.0/examples/#sample-13

目前,我们只看到三个字段:email:firstname,和lastname:。这些字段通常不会经常修改,但随着我们开发应用程序,可能需要添加更多需要修改的字段。在这种情况下,你可以在myaccount.Edit中添加新字段。

创建报价表单

让我们继续创建QuotationBill的表单。与之前一样,首先在Screen面板中设置卡片布局。

view/myaccount/MyAccount.js中的方式相同,在Quotation类中,我们将添加EditList屏幕(源文件:03_making_the_quotation_form/app/view/quotation/Quotation.js):

Ext.define('MyApp.view.quotation.Quotation', {
  ...
  requires: [
        'MyApp.view.quotation.List',
        'MyApp.view.quotation.Edit'
    ],
  ...
items: [{
          xtype: 'myapp-quotation-list',
          border: false
    }, {
          xtype: 'myapp-quotation-edit',
          border: false
    }]
});

与之前的情况不同,现在存在两张卡片。这些是ListEdit。你将在后面的章节中实现List。在这里,让我们实现Edit

问题是,如果你不创建一个类,那么requires将无法读取它。所以,按照以下方式创建一个List类(源文件:03_making_the_quotation_form/app/view/quotation/List.js):

Ext.define('MyApp.view.quotation.List', {
    extend: 'MyApp.form.Panel',
    alias : 'widget.myapp-quotation-list',
    itemId: 'screen-quotation-list',
    initComponent: function() {
      var me = this;
      Ext.apply(me, {
        });
    me.callParent(arguments);
    }
});

接下来,让我们创建Edit。这是一个相当长的过程,所以让我们将其分解成几个部分,并逐一介绍(源文件:03_making_the_quotation_form/app/view/quotation/Edit.js):

Ext.define('MyApp.view.quotation.Edit', {
    extend: 'MyApp.form.Panel',
    alias : 'widget.myapp-quotation-edit',
    itemId: 'screen-quotation-edit',
    initComponent: function() {
    ...
    }
});

我们已经创建了一个空的MyApp.view.quotation.Edit组件,从现在起,我们将开始实现initComponent方法的内部。正如我们之前看到的,这将使组件更加灵活。

让我们将它分解为Storefieldgrid以及TopToolbar。以下各节提供了应插入到其特定点的代码。

Store

Store组件是数据将在浏览器中本地存储的地方。

让我们现在创建Store组件。将以下代码实现到MyApp.view.quotation.Edit类中的initComponent方法(源文件:03_making_the_quotation_form/app/view/quotation/Edit.js):

Ext.applyIf(me, {
    customerStore: Ext.create('Ext.data.Store', {
        fields: ['id', 'name'],
        data : [
            {"id": 0, "name": "Sencha"},
            {"id": 1, "name": "Xenophy"}
        ]
    }),
    itemStore: Ext.create('Ext.data.Store', {
        storeId:'billItemStore',
        fields:['desc', 'qty', 'price', 'sum'],
        data:{'items':[
            { 'desc': 'Sencha Complete', "qty":"5", "price":"995", 
             "sum": 4975 },
            { 'desc': 'Sencha Ext JS + Standard Support', "qty":"5", 
             "price":"595", "sum": 2975 }
        ]},
proxy: {
type: 'memory',
reader: {
type: 'json',
root: 'items'
            }
        }
    })
});

生成customerStoreitemStorecustomerStore是为组合框制作的,而itemStore是为grid面板制作的。这两个存储库暂时安装。这就是为什么我们使用Ext.applyIf。让我们修改它,以便在以后的阶段,你可以通过 Ext Direct 获取数据。

字段和网格组件

在本节中,我们将构建field组件(在这种情况下,这意味着屏幕顶部的组合框),以及将出现在其下方的grid面板。

这有点长,但在我们定义Store组件的步骤之后编写以下代码(源文件:03_making_the_quotation_form/app/view/quotation/Edit.js):

Ext.apply(me, {
    bodyPadding: 20,
    items: [{
        padding: '0 0 20 0',
        width: 500,
        xtype: 'combo',
        fieldLabel: 'customer',
        store: me.customerStore,
        editable: false,
        displayField: 'name',
        valueField: 'id'
    }, {
        // Grid Panel
        height: 400,
        padding: '0 0 20 0',
        xtype: 'grid',
        store: me.itemStore,
        plugins: [Ext.create('Ext.grid.plugin.CellEditing')],
        columns: [{
            text: 'Description',
            dataIndex: 'desc',
            flex: 1,
            editor: true
        }, {
            text: 'Qty',
            dataIndex: 'qty',
            editor: {
                xtype: 'numberfield',
                allowBlank: false,
                minValue: 0,
                maxValue: 10000
            }
        }, {
            text: 'Price',
            dataIndex: 'price',
            renderer: Ext.util.Format.usMoney,
            editor: {
                xtype: 'numberfield',
                allowBlank: false,
                minValue: 0,
                maxValue: 10000
            }
        }, {
            text: 'Sum',
            dataIndex: 'sum',
            renderer: Ext.util.Format.usMoney
        }],
        tbar: [{
            text: 'Add Item',
            action: 'add-item'
        }, '-', {
            text: 'Remove Item',
            action: 'remove-item'
        }]
   }, {
        fieldLabel: 'note',
        xtype: 'textarea',
        width: 500j
   }]
});

如果你布置了字段,你就是在布置grid面板。这里要记住的是,grid面板是通过插件Ext.grid.plugin.CellEditing设置的。为了打开各种单元格进行编辑,我们在列设置中设置了编辑键。默认情况下使用textfield表单字段,但你在qtyprice中只想输入数值,所以我们使用numberfield

TopToolbar

在本节中,我们将创建一个将出现在我们应用程序顶部工具栏中的Save按钮。

要做到这一点,在我们在上一步中编写的代码之后编写以下代码以显示Save按钮(源文件:03_making_the_quotation_form/app/view/quotation/Edit.js):

Ext.apply(me, {
    tbar: [{
        text: 'Save',
        action: 'save'
   }]
});

我们将安装“保存”按钮。现在,我们希望尽可能快地显示此按钮,但到目前为止,没有任何内容被显示。这是因为,在卡片布局中,在“引用”视图的激活项中,0将显示“列表”屏幕,而1将显示“编辑”。

在这种状态下,我们无法执行组件测试,所以首先,让我们开始准备 CT 的view_edit.html(源文件:03_making_the_quotation_form/ct/quotation/view_edit.html)。

基本上,这不会在view.html中改变太多,我们只是在view_edit.js中读取view.js

现在,Quotation视图的包名尚未修改,因此你现在可以更正它。对于源代码,请参阅源文件:03_making_the_quotation_form/ct/quotation/view_edit.js

当你生成MyApp.view.quotation.Quotation时,在配置选项中添加activeItem: 1。这样做,Edit将从一开始就显示。当然,功能尚未实现,因此项目不会被添加或自动计算。

在这里,我们添加了一个新组件,因此为了将新组件的映射应用到bootstrap.js,让我们执行sencha app build。在执行 Sencha app build 并成功将映射应用到bootstrap.js之后,应该出现以下屏幕:

TopToolbar

创建账单表单

接下来,让我们制作发票表单。BillQuotation表单的内容大部分相同。

这还需要描述更详细的细节,例如付款日期。因此,关于数据库的结构,ID 可以链接,以便报价可以为一个账单引发事件。

现在,让我们快速开始以与“引用”相同的方式制作卡片布局(源文件:04_making_the_bill_form/app/view/bill/Bill.js):

Ext.define('MyApp.view.bill.Bill', {
  ...
    title: 'Bill',
    layout: 'card',
    items: [{
        xtype: 'myapp-bill-list',
        border: false
    }, {
        xtype: 'myapp-bill-edit',
        border: false
    }]
});

接下来,让我们制作一个临时的空List组件来弥补我们之前写的,避免收到错误。

我们接下来要制作的类与dashboard.Dashboard类非常相似。所以,请在编码时参考它,并注意以下要点(源文件:04_making_the_bill_form/app/view/bill/List.js):

className: MyApp.view.bill.List
extend: MyApp.form.Panel
alias: widget.myapp-bill-list
itemId: screen-bill-list

此外,让我们为了与之前相同的原因实现空的initComponent,即为了List组件。

然后,在最后,以与“引用”相同的方式在“编辑”中进行“编辑”(源文件:04_making_the_bill_form/app/view/bill/Edit.js)。

再次强调,这个类与之前我们制作的MyApp.view.quotation.Edit类非常相似。

注意以下要点,并以相同的方式构建此类:

  • className: MyApp.view.bill.Edit

  • aliasName: widget.myapp-quotation-edit

  • itemId: screen-quotation-edit

您已经创建了视图,现在是时候实现各种控制器了。与其有一个控制器来管理分开成ListEditQuotationBill,不如为每个单独准备控制器(源文件:04_making_the_bill_form/app/controller/quotation/List.js):

Ext.define('MyApp.controller.quotation.List', {
    extend: 'MyApp.controller.Abstract',
    init: function() {
        var me = this;
        me.control({
        });
    }
});

让我们以完全相同的方式创建以下列出的其余控制器(只有类名不同)。

  • (源文件:04_making_the_bill_form/app/controller/quotation/Edit.js

  • (源文件:04_making_the_bill_form/app/controller/bill/List.js

  • (源文件:04_making_the_bill_form/app/controller/bill/Edit.js

为了让前面的控制器可以被读取,请在app/Application.js中添加一个后缀(源文件:04_making_the_bill_form/app/Application.js)。

值得检查的是,通过访问index.php没有错误显示。

管理脏和干净的应用程序

自然地,每次按下保存按钮时发生保存过程是很好的。

然而,如果可能的话,您是否希望仅在更改已被做出时才提供保存按钮?

在这里,我们将实现逻辑到控制器中,以判断输入后是否已更改。

MyAccount

首先,我们将从MyAccount实现一个简单的表单。但是,在此之前,QuotationBill已经分开控制器,但我们没有在MyAccount中做出更改!让我们快速将其分开并添加到app/Application.js中。

MyApp.controller.bill.Edit类为参考,让我们继续创建MyApp.controller.myaccount.Edit类。

除了类名之外,过程完全相同(源文件:05_management_of_dirty_and_undirty_myaccount/app/controller/myaccount/Edit.js)。

在您完成构建前面的类之后,让我们将myaccount.Edit控制器添加到app/Application.js的控制器属性中。

现在,以相同的方式将其添加到 CT 目录中的app.js(源文件:05_management_of_dirty_and_undirty_myaccount/ct/myaccount/app.js)。

现在,它应该能够在 CT 中运行,并带有控制器。首先,设置以下事件列表,在组件测试期间触发:

  • myapp-show(显示组件)

  • myapp-hide(隐藏组件)

  • myapp-dirty(当表单内的信息被更改时触发)

  • myapp-undirty(当更改的信息被记录或信息被回滚到之前的状态时触发)

在做任何事情之前,关于myapp-showmyapp-hide,我们需要通过MyApp.controller.myaccount.MyAccount将事件传递给ListEdit类(源文件:05_management_of_dirty_and_undirty_myaccount/app/controller/myaccount/MyAccount.js):

Ext.define('MyApp.controller.myaccount.MyAccount', {
    extend: 'MyApp.controller.Abstract',
    screenName: 'myaccount',
    refs: [{
        ref: 'editView', selector: 'myapp-myaccount-edit' 
    }],
    init: function() {
        var me = this;
        me.control({
            'myapp-myaccount': {
                'myapp-show': me.onShow,
                'myapp-hide': me.onHide
            }
        });
    },
    onShow: function() {
        var me = this,
        editView = me.getEditView();
        editView.fireEvent('myapp-show', editView);
    },
    onHide: function() {
        var me = this,
        editView = me.getEditView();
        editView.fireEvent('myapp-hide', editView);
    }
});

主要目的是设置refs并捕获editView。接下来,定义Edit的控制器(源文件:05_management_of_dirty_and_undirty_myaccount/app/controller/myaccount/Edit.js):

Ext.define('MyApp.controller.myaccount.Edit', {
extend: 'MyApp.controller.Abstract',
init: function() {
var me = this;
me.control({
            'myapp-myaccount-edit': {
                'myapp-show': me.onShow,
                'myapp-hide': me.onHide,
                'myapp-dirty': me.onDirty,
                'myapp-undirty': me.onUndirty
            }
        });
    },
    onShow: function() {
    },
    onHide: function() {
    },
    onDirty: function() {
    },
    onUndirty: function() {
    }
});

接下来,我们需要获取 field 事件。通过组件查询获取字段,然后设置事件监听器(源文件:05_management_of_dirty_and_undirty_myaccount/app/controller/myaccount/Edit.js):

init: function() {
        ....
        Ext.iterate([
            {name: 'email', xtype: 'textfield', fn: me.onChangeField},
            {name: 'firstname', xtype: 'textfield', fn: 
             me.onChangeField},
            {name: 'lastname', xtype: 'textfield', fn: 
             me.onChangeField}
        ], function(f) {
            var scope = me, o = {},
            format = Ext.String.format,
            key = format('{0} {1}[name="{2}"]', 'myapp-myaccount-edit', f.xtype, f.name);
            o[key] = {
change  : { fn: f.fn, scope: scope }
            };
        me.control(o);
        });
    },
    onChangeField: function(field) {
        console.log(field);
    },
    onShow: function() {
    ....

将每个名称属性添加到 View 字段中(源文件:05_management_of_dirty_and_undirty_myaccount/app/view/myaccount/Edit.js):

Ext.define('MyApp.view.myaccount.Edit', {
    ....
    initComponent: function() {
        var me = this;
        // Fields
        Ext.apply(me, {
            ....
            items: [{
                fieldLabel: 'email',
                name: 'email'
            }, {
                fieldLabel: 'firstname',
                name: 'firstname'
            }, {
                fieldLabel: 'lastname',
                name: 'lastname'
            }]
        });
        ....

如果我们在文本框中输入某些内容,它将在控制台输出日志。我们希望定义与其他类类似的处理,因此将以下逻辑移动到 Abstract 类中,并修改它以便可以调用(源文件:05_management_of_dirty_and_undirty_myaccount/app/controller/Abstract.js):

Ext.define('MyApp.controller.Abstract', {
    ....
setChangeFieldEvents: function(fields, xtype, fn, scope) {
var me = this,
format = Ext.String.format;
Ext.iterate(fields, function(type, fs) {
if(Ext.isString(fs)) {
fs = [fs];
            }
if(Ext.isArray(fs)) {
Ext.iterate(fs, function(fname) {
var o = {}, key;
key = format('{0} {1}[name="{2}"]', xtype, type, fname);
o[key] = {
change  : { fn: fn, scope: scope }
                    };
me.control(o);
                });
            }
        });
    },
    ....

让我们在 Edit.js 中以相同的方式实现(源文件:05_management_of_dirty_and_undirty_myaccount/app/controller/myaccount/Edit.js):

    ....
init: function() {
        ....
        me.setChangeFieldEvents({
            textfield: [
                'email',
                'firstname',
                'lastname'
            ]
        },
        'myapp-myaccount-edit',
         me.onChangeField,
         me);
    },
    onChangeField: function(field) {
        console.log(field);
    },
    ....

setChangeFieldEvents 是为了能够设置除了 textfield 之外的其他事件。setChangeFieldEvents 还可以支持你想用于其他字段(如 combobox)的情况。文本细节被修改的事实意味着你可以使用到目前为止实现的实现来处理它。

那么,接下来,让我们检查内容是否真的发生了变化。这意味着在 onChangeField 方法中的实现是成功的。

如果 MyApp.controller.myaccount.Edit 控制器的 init 方法记录的组件内容被修改,则实现 onChangeField 方法(源文件:05_management_of_dirty_and_undirty_myaccount/app/controller/myaccount/Edit.js)。

使用 getFieldValues 来判断字段是否有变化。Ext.Object.getKeys 将返回一个数组。因此,通过这一行代码,你可以判断内容是否已更改。

如果有变化,将触发 myapp-dirty 事件,如果它恢复到原始状态,将触发 myapp-undirty 事件。

因此,让我们添加逻辑,使工具栏按钮在 onDirty 时可用,在 onUndirty 时不可用,以完成 MyAccount 的实现(源文件:05_management_of_dirty_and_undirty_myaccount/app/controller/myaccount/Edit.js):

Ext.define('MyApp.controller.myaccount.Edit', {
    ....
    onDirty: function() {
        var me = this,
        editView = me.getEditView(),
        btnSave = editView.down('button[action=save]')
        btnSave.enable();
    },
    onUndirty: function() {
        var me = this,
        editView = me.getEditView(),
        btnSave = editView.down('button[action=save]')
        btnSave.disable();
    }
});

让我们在 Save 按钮的设置中添加 disabled。在 CT 错误期间,onShow 不起作用。此外,当按钮处于 onHide 状态时,你应该进行按钮的初始化(源文件:05_management_of_dirty_and_undirty_myaccount/app/view/myaccount/Edit.js):

Ext.define('MyApp.view.myaccount.Edit', {
    ....
    tbar: [{
        text: 'Save',
        action: 'save',
        disabled: true
    ....

最后,如果你能聚集足够的能量,实现它,以便当按下重置按钮时,从 Ext.form.Basic 调用重置方法。

Quotation 表单

Quotation 表单与 BillMyAccount 大致相同,但有一个很大的不同——表单面板中有一个网格。

乍一看,配置到 grid 面板的存储似乎将直接与服务器通信,但这并非事实。到最后,这个表单面板处理字段,所以它不直接处理网格,因此不需要分别发送表单数据和网格数据。

那么,我们如何解决这个问题呢?答案是提供一个隐藏字段,并使其以 JSON 格式保存网格数据。所以,网格存储可以保持为 MemoryStore

首先,将其实现到与 MyAccount 相同的点。因为 Quotation 将会保存一个列表,所以添加到其中,以便 refs 也能获取到列表视图。

这段源代码与以下类的前一步实现非常相似:

  • MyApp.controller.myaccount.MyAccount

  • MyApp.controller.myaccount.Edit

尝试为 Quotation 实现,记住我们从之前的类中使用的源代码。如果你遇到困难,请参考以下源文件:

  • 06_management_of_dirty_and_undirty_quotation/app/controller/quotation/Quotation.js

  • 06_management_of_dirty_and_undirty_quotation/app/controller/quotation/Edit.js

为了检查你迄今为止实施的应用,添加一个 CT 来检查 Edit(源文件:06_management_of_dirty_and_undirty_quotation/ct/quotation/app_edit.html)。

我们将在 CT 目录的子目录中重新创建这个文件。内容与 app.html 大致相同,但我们将会将 app.js 中的读取内容更改为 app_edit.js(源文件:06_management_of_dirty_and_undirty_quotation/ct/quotation/app_edit.js)。

这几乎与同一层的 app.js 相同,但我们将在控制器中添加一个 Edit 控制器,并将 activeItem:1 添加到 QuotationView 控制器的配置选项中。

需要记住的主要点是向控制器添加 quotation.Edit 并将 activeItem 设置为 1,从开始就显示 Edit 面板。所以,就像我们最初提到的,将配置在 grid 中的存储数据转换为 JSON,然后创建要存储在 Hidden 字段中的逻辑。首先,我们需要在 view 中布局 Hidden 字段(源文件:06_management_of_dirty_and_undirty_quotation/app/view/quotation/Edit.js):

Ext.define('MyApp.view.quotation.Edit', {
            ....
            }, {
            name: 'items',
            xtype: 'hidden'
            }, {
            fieldLabel: 'note',
            name: 'note',
            xtype: 'textarea',
            width: 500
            }]
        });
        // TopToolbar

在这种状态下,数据是固定的,无法获取到特定更改的事件。在此之前,让我们实现项目的添加和删除以及事件编辑。为了实现这一点,我们需要做很多事情。

首先是将 Store 组件改为一个类。你只需要将 view 内部已经生成的部分变成一个外部类(源文件:06_management_of_dirty_and_undirty_quotation/app/store/Customer.js):

Ext.define('MyApp.store.Customer',{
    extend: 'Ext.data.Store',
    storeId:'Customer',
    fields: ['id', 'name'],
    data:{'items':[
        {"id": 0, "name": "Sencha"},
        {"id": 1, "name": "Xenophy"}
    ]},
proxy: {
    type: 'memory',
    reader: {
        type: 'json',
        root: 'items'
        }
    }
});

还要更改以下内容(源文件:06_management_of_dirty_and_undirty_quotation/app/store/QuotationItem.js):

Ext.define('MyApp.store.QuotationItem',{
    extend: 'Ext.data.Store',
    storeId:'QuotationItem',
    fields:['desc', 'qty', 'price', 'sum'],
    data:{'items':[
    ]},
proxy: {
    ... // Customer Store
    }
});

为了使用这个存储类,将设置添加到 MyApp.controller.quotation.Edit 类中。

我们将把 CustomerQuotationItem 添加到 MyApp.controller.quotation.Edit 类的 stores 属性中(源文件:06_management_of_dirty_and_undirty_quotation/app/controller/quotation/Edit.js)。

由于我们在view外部创建了MyApp.store.CustomerMyApp.store.QuotationItem,我们需要调整view中的代码(源文件:06_management_of_dirty_and_undirty_quotation/app/view/quotation/Edit.js):

Ext.define('MyApp.view.quotation.Edit', {
        ....
        (remove store defines!!)
        ....
        // Fields & Grid
Ext.apply(me, {
    bodyPadding: 20,
    items: [{
                ....
    store: 'Customer',
                ....
            }, {
                // Grid Panel
                ....
            store: 'QuotationItem',
                ....

我们将store外部化的原因在于,如果我们使用Ext JS MVC 架构配置store类,则会生成getXXXXStore方法,然后我们可以从控制器中访问store组件。XXXXstore类的名称。

接下来,我们将修改以监控控制器中hidden的变化,并将初始值更改为hidden(源文件:06_management_of_dirty_and_undirty_quotation/app/view/quotation/Edit.js):

name: 'items',
xtype: 'hidden',
value: '[]'

此外,更改以下代码(源文件:06_management_of_dirty_and_undirty_quotation/app/controller/quotation/Edit.js):

me.setChangeFieldEvents({
combo: [
        'customer'
    ],
hidden: [
        'items'
    ],
textarea: [
        'note'
    ]
},

最后,我们可以实现操作网格的功能。首先定义按钮的事件处理器(源文件:06_management_of_dirty_and_undirty_quotation/app/controller/quotation/Edit.js)。

实现onAddItemonRemoveItem。在这里,将能够在网格中添加和删除项目(源文件:06_management_of_dirty_and_undirty_quotation/app/controller/quotation/Edit.js):

onAddItem: function() {
    var me = this,
    store = me.getQuotationItemStore();
    store.add({ desc: 'New Item', qty: 0, price: 0, sum: 0 });
},
onRemoveItem: function() {
    var me = this,
    store = me.getQuotationItemStore();
    editView = me.getEditView(),
    grid = editView.down('grid'),
    sm = grid.getSelectionModel();
    if(sm.getCount()) {
            var next = false;
            Ext.iterate(sm.getSelection(), function(item) {
        var flg = false;
        store.data.each(function(model) {
                if(flg) {
                next = model;
                flg = false;
                }
            if(model.id === item.id) {
                flg = true;
                }
            });
            store.remove(item);
        });
        if(next) {
            sm.select(next);
        } else {
            next = store.getAt(store.data.getCount() - 1);
            if(next) {
                sm.select(next);
            }
        }
    }
},

添加过程非常简单,只是使用存储的Add方法添加记录。然而,删除过程不仅仅是简单地删除。

已经设置为当发生删除时,从所选项目之后的下一个项目变为当前项目,并选择该项目。通过这样做,您可以通过继续点击删除按钮来继续删除。您应该将此视为一个小型附加功能!

因此,现在您可以在网格中添加和删除记录。接下来,我们将实现将存储数据转换为 JSON 并将其存储在hidden中的关键部分(源文件:06_management_of_dirty_and_undirty_quotation/app/controller/quotation/Edit.js):

Ext.define('MyApp.controller.quotation.Edit', {
    ....
    init: function() {
        ....
        var store = me.getQuotationItemStore(),
        updateGridData;
        updateGridData = function(store) {
          var f = me.getEditView().query('hidden[name=items]')[0],
          out = [];
          store.data.each(function(r) {
              out.push(Ext.clone(r.data));
            });
          f.setValue(Ext.encode(out));
        };
        store.on('update', function(store, r) {
          r.set('sum', r.get('qty')*r.get('price'));
          updateGridData(store);
        });
        store.on('add', function(store, r) {
          updateGridData(store);
        });
        store.on('remove', function(store, r) {
          updateGridData(store);
        });
    },

当网格数据被修改时,updateaddremove会作为单独的事件发生。您需要定义这些事件处理器。这个过程是通用的,所以首先在updateGridData中存储函数对象,然后使用它。

再次实现更新事件的附加功能。当内容被修改时,将qtyprice相乘,然后它会自动将值放入sum中。这次它被设置为不能编辑sum,在自动计算后设置为输入。您可以在ct/quotation/app_edit.html中检查它是否工作。

到目前为止,我们一直在视图中定义存储。然而,由于我们将存储改为类文件,您可能已经注意到ct/quotation/view_edit.html没有正常工作。在Quotation的末尾,让我们进行修改,以便 CT 可以正常工作(源文件:06_management_of_dirty_and_undirty_quotation/ct/quotation/view_edit.js):

Ext.onReady(function() {
    Ext.create('MyApp.store.Customer');
    Ext.create('MyApp.store.QuotationItem');
    Ext.create('MyApp.view.quotation.Quotation', {
      activeItem: 1,
      width: 800,
      height: 600,
      renderTo: Ext.getBody()
    });
});

因为没有控制器,所以不会创建store组件。因此,如果你自己定义和生成它,它将与storeId链接然后显示。当然,如果你按下显示的任何按钮,也不会发生任何事情,因为没有控制器。

Quotation的实现比之前的任何内容都要长且复杂。接下来是Bill,其实现方式大致相同。请在进行过程中仔细检查复杂部分。

账单表单

如我们所知,Bill几乎与Quotation相同,但让我们继续并实现Bill。首先,让我们制作BillItem存储类。

对于Bill,它只与QuotationItem的类名和StoreId不同。其余的都是相同的,所以让我们尝试自己编写代码。如果你想参考一些源文件,可以查看以下内容:

07_management_of_dirty_and_undirty_bill/app/store/BillItem.js

接下来是控制器设置。同样,以下实现与之前的Quotation部分极其相似,所以回想一下MyApp.controller.quotation.QuotationMyApp.controller.quotation.Edit,并亲自尝试。如果你需要帮助,可以在以下位置找到源文件:

  • 07_management_of_dirty_and_undirty_bill/app/controller/bill/Bill.js

  • 07_management_of_dirty_and_undirty_bill/app/controller/bill/Edit.js

最后,让我们修改view。同样,这与EditView非常相似,所以回想一下MyApp.view.quotation.Edit并亲自尝试。如果你需要一些帮助,以下源文件可供参考:

07_management_of_dirty_and_undirty_bill/app/view/bill/Edit.js

让我们修改并添加 CT。与上一步相同,为了验证EditView,我们将为Edit重新构建app.htmlview.html

内部发生的事情基本上是相同的,所以请参考以下源文件:

  • 07_management_of_dirty_and_undirty_bill/ct/bill/app_edit.html

  • 07_management_of_dirty_and_undirty_bill/ct/bill/app_edit.js

  • 07_management_of_dirty_and_undirty_bill/ct/bill/view_edit.html

  • 07_management_of_dirty_and_undirty_bill/ct/bill/view_edit.js

关于app_edit.js,为了与控制器进行相互验证,我们在屏幕上安装了一个按钮并触发了一个事件。

如果 CT 或整个应用程序没有出现错误,请转到下一部分。

使用 Ext Direct 实现读写过程

从这里开始,我们将开始实现与我们迄今为止制作的表单相关的读写数据处理。我们也将使用 Ext Direct 来完成这项工作。在我们继续以下实现之前,有一些要点需要你修改。其中之一是在登录时给会话添加一个ID。现在让我们修改它(源文件:08_implement_read_and_write_by_ext_direct/index.php):

....
"SELECT",
"    COUNT(id) as auth,",
"    users.id,", // <- add
"    users.email,",
....

然后,存储会话 ID:

....
$_SESSION["USERINFO"] = array(
    "id" => $row["id"], // <- add
    "email" => $row["email"],
    ....

接下来,将 session_start 添加到 Ext Direct 所使用的路由器的开头。通过这样做,你应该能够使用每个类(源文件:08_implement_read_and_write_by_ext_direct/php/router.php)中的方法访问会话。

<?php
session_start(); // <-- add
require('config.php');

你已经准备好了,现在让我们继续实现 MyAccountQuotationBill 的读取过程。

读取数据

在这里,我们将开始了解我们应用程序的各个部分如何读取数据。

MyAccount

首先,让我们开始处理 MyAccount。让我们为 Ext Direct 创建一个类。在这里,我们将添加新的 PHP 的源代码。

由于源代码将很长,不便添加到本文中,请参考您的源文件:09_reading_data_myaccount/php/classes/MyAppMyAccount.php

实现 readForm 方法。当有人登录时,它通过使用保存的 ID 作为参考从数据库获取账户信息。

我将暂时不解释这里发生的 PHP 处理。主要点是返回的关联数组键。

访问键是一个标志,它给出 truefalse 的值,表示表单的获取是否成功。每个字段的数配置在数据键中的关联数组中。为了在 Ext Direct 中使用此 readForm,更新 REMOTING_API 输出配置。

我们将为 Direct 添加一个新的类(源文件:09_reading_data_myaccount/php/config.php):

    'MyAppMyAccount' =>array(
        'methods' => array(
            'readForm' => array(
                'len' => 0
            )
        )
    )

现在还需要做一些更多的准备工作。MyAccount 的 CT 没有预设为使用 Ext Direct,因此我们应该修改这一点。首先,将 api.php 添加到 HTML 中。为了使我们的数据库在读取 app.js 之前读取 api.php,我们需要添加以下代码(源文件:09_reading_data_myaccount/ct/myaccount/app.html):

<script src="img/api.php"></script>

接下来,将 Ext Direct 设置添加到 app.js 中。让我们在 Ext.Loader.setConfig 之后添加以下代码(源文件:09_reading_data_myaccount/ct/myaccount/app.js):

Ext.app.REMOTING_API.url = "../../php/router.php";
Ext.direct.Manager.addProvider(Ext.app.REMOTING_API);

Ext.app.REMOTING_API.url 文件是由读取 app.htmlapi.php 创建的。在这个时候,CT 的 URL 路径不同,所以你应该覆盖这个。现在我们终于准备好了,所以让我们继续在 MyAccount 表单(view)中设置 Ext Direct。

我们将为 MyApp.ciew.myaccount.Edit 添加一个 api 属性(源文件:09_reading_data_myaccount/app/view/myaccount/Edit.js):

Ext.define('MyApp.view.myaccount.Edit', {
    ....
    api: {
      load    : 'MyAppMyAccount.readForm',
      submit  : 'MyAppMyAccount.writeForm'

在名为 APIconfig.php 文件中设置对象。在该键内部指定 loadsubmit。以与 loadsubmit 方法在 Ext.createExt.define 中的字符字符串中指定的类名相同的方式设置 Ext.Direct 函数的字符字符串。

现在,因为 MyAccount 想要在 onShow(显示时)读取,所以 CT 中的 myapp-show 事件不会触发。所以,让我们安装这个按钮,它将引发一个伪触发事件。

在 CT 子目录中,因为我们将添加一个对myapp-show的虚假复现必要的按钮,我们需要在launch方法内添加以下代码(源文件:09_reading_data_myaccount/ct/myaccount/app.js):

Ext.widget('button', {
text: 'fire myapp-show',
renderTo: Ext.getBody(),
scope: this,
handler: function() {
            this.getController('myaccount.MyAccount').loadIndex('#!/myaccount');
            }
        });

我们可以通过强制实现一个结构来自动调用我们之前制作的loadIndex方法来使myapp-show事件模拟触发。所以,如果您按下按钮,您可以使事件触发。现在,在onShow方法中实现表单数据的读取逻辑。

最后,在myapp-show触发后,我们将实现控制器行为(源文件:09_reading_data_myaccount/app/controller/myaccount/Edit.js):

onShow: function(p, owner, params) {
    var me = this,
    editView = me.getEditView(),
    form = editView.getForm();

    p.setLoading();
    form.trackResetOnLoad = true;
    form.isLoading = true;
    form.load({
      success: function(form, ret) {
        p.setLoading(false);
        p.fireEvent('myapp-undirty');
        form.isLoading = false;
        }
    });
},

这段代码相当简短。主要点是使用getForm获取BasicForm对象,并将trackResetOnLoad设置为true

我们可以使用isLoading来判断在异步读取的同时发生其他处理的情况。如果我们调用load方法,之前在API密钥中配置的对象将被执行,并将请求发送到服务器。PHP 从数据库获取数据并将其发送回来,变量会自动插入字段。我们不需要自己设置接收到的数据到字段中。

MyAccount控制器完成读取登录信息后,因为它是最新的用户信息,myapp-undirty事件被触发。从那时起,如果进行了修改,现在可以按下保存按钮。稍后,我们将实现一个部分,当按下保存按钮时,将数据反向发送。

引用表单

现在,让我们实现Quotation表单的读取操作。首先,在 CT 中设置 Ext Direct。

与我们在MyAccount中添加的方式相同,我们将以下代码添加到app_edit.js(源文件:10_reading_data_quotation/ct/quotation/app_edit.js):

Ext.app.REMOTING_API.url = "../../php/router.php";
Ext.direct.Manager.addProvider(Ext.app.REMOTING_API);

不要忘记读取API密钥,让我们现在设置它。

与我们在 HTML 端所做的方式相同,让它读取api.php文件以使用 Direct。如果您需要提醒,请参阅源文件10_reading_data_quotation/ct/quotation/app_edit.html

接下来,我们将在服务器端进行准备。如您至今所理解的,过程与MyAccount大致相同。

我们将在config.php(源文件:10_reading_data_quotation/ct/quotation/app_edit.js)中为Quotation添加一个新的API密钥:

    'MyAppQuotation'=>array(
        'methods'=>array(
            'readForm'=>array(
                'len'=>0
            ),
            'writeForm'=>array(
                'len'=>1,
                'formHandler'=>true
            )
        )
    ),
    'MyAppMyAccount'=>array(
        ....
    )

接下来,实现您在config.php中定义的类。我们将实现添加到config.php中的API密钥。

对于内容,请参阅以下源文件以供参考:

10_reading_data_quotation/php/classes/MyAppQuotation.php

实现readForm方法。使用writeForm方法时,在Quotation的情况下,如果需要执行复杂的判断过程以确定是更新还是新增,为了从列出的转换中判断是否有 ID,这一部分无法在本章实现,因此将其放在下一章实现。这与Bill相同。

因此,为了通过在服务器端使用类进行通信,请在表单面板中定义配置API的设置。

我们将以与MyApp.view.myaccount.Edit类(源文件:10_reading_data_quotation/app/view/quotation/Edit.js)相同的方式定义API属性。

定义myapp-show事件处理器。这将是一个非常长的源代码。因此,在仔细寻找onShow方法中的新增和编辑点的同时,请参考以下源文件进行测试:

10_reading_data_quotation/app/controller/quotation/Edit.js

函数已分为onEditShowonNewShow。这次,只有onEditShow方法将运行。通过后续列出的实现,新创建的过程以及编辑后的分割过程也将实现。

在读取完成后,触发myapp-undirty以了解它是否处于干净状态。触发myapp-show事件的按钮已经安装在 CT 中,因此如果您按下按钮,它将发出信号并开始读取。

当然,这是readForm方法的处理过程,所以只读取固定的测试数据。但我们可以实现与下一章中实现的writeForm一起的数据库读取。在服务器端指定名称的情况下,需要在客户端的字段中设置一个名称。如果名称设置正确,数据应该会自动插入。

Bill 表单

最后,以与Quotation相同的方式实现Bill。首先,在 CT 中配置 Ext Direct。

与上一步类似,我们将添加必要的代码以实现 Direct(源文件:11_reading_data_bill/ct/bill/app_edit.js)。

在配置时,不要忘记读取API密钥。这也会重复制作 HTML 读取api.php的过程(源文件:11_reading_data_bill/ct/bill/app_edit.html)。

现在准备服务器端。我们将为Bill类添加由Bill类使用的 Direct API类(源文件:11_reading_data_bill/php/config.php):

    'MyAppBill'=>array(
        'methods'=>array(
            'readForm'=>array(
                'len'=>0
            ),
            'writeForm'=>array(
                'len'=>1,
                'formHandler'=>true
            )
        )
    ),
    'MyAppQuotation'=>array(

接下来,实现由config.php定义的类。

由于代码非常长,请在此情况下参考以下源文件:

11_reading_data_bill/php/classes/MyAppBill.php

然后,在API配置文件中配置直接函数。让我们以与MyApp.view.quotation.Edit类(源文件:11_reading_data_bill/app/view/bill/Edit.js)相同的方式定义 API 属性。

最后,实现事件处理器 myapp-show。这实际上与 MyApp.controller.quotation.Edit 的实现非常相似,所以请亲自尝试。如果你需要,可以参考源文件 11_reading_data_bill/app/view/bill/Edit.js

写入数据和验证

关于写入过程,正如我之前提到的,只需实现 MyAccountMyAccountwriteForm

这也很长,所以请参考源文件 12_writing_data_and_validations/php/classes/MyAppMyAccount.php

处理内容很简单;然而,如果你查看 PHP 代码,它看起来相当复杂。我会尽量简单解释。

首先,为了像其他人一样以返回值的形式返回关联数组,我们必须将 success 设置为 true 并传达给客户端写入过程已正常完成。

在开始时,有一些地方我们设置了带有 errors 键的关联数组,然后在内部设置字段名键并插入一条消息。这是服务器端的输入检查。

如果你使用 Ext Direct,这就会完成输入检查。

在错误部分,输入错误目标的字段名,只需在其中输入错误消息,服务器端就会自动显示一个红色框架。如果你将鼠标悬停在其上,就会显示你在服务器端设置的消息。换句话说,你不需要在客户端编写任何编程代码来处理错误。

通常,你通过 AJAX 通信接收 JSON 数据,但我们需要在客户端和服务器端定义错误处理。所以,我相信我们都会更喜欢使用 Ext Direct,它会自动为我们处理这些。

小贴士

如果你习惯了这种开发方式,你可能会上瘾,而且你将无法回头。

再次强调,由于你可以完全分离客户端和服务器端,一个工程师不需要构建两者。相反,可以通过完全分离服务器端和客户端来推进工作。一旦输入检查通过,就会从保存会话 ID 的数据库中获取用户信息。

最后,设置成功键,当处理完成后显示 truewriteForm 是一个实现的方法,用于写入表单数据。在服务器端写入表单数据的成员中,你需要将 formHandler 方法设置为 true

每个人都已经将 formHandlerconfig.php 中设置为 true,但在你以类似方式定义自己的项目时,人们经常会忘记将 formHandler 方法设置为 true,这意味着数据不会被发送。

小贴士

formHandler 方法设置为 true

摘要

到目前为止,做得很好!这是一个相当长的章节,但这对我们的应用程序来说也是至关重要的(是的,所有章节都是至关重要的,但这一章更为重要!)。

在本章中,我们从登录屏幕开始,然后实现了使用表单的 Edit 屏幕。最后,我们学习了 Ext Direct。

在下一章中,我们将为每个使用 Ext Direct 的屏幕实现 ListSearch 函数。快速休息一下喝杯咖啡,准备好后继续吧!

第四章。列表和搜索

在前面的章节中,我们研究了准备数据结构和基本的 Ext JS 架构,在第三章,数据输入中,我们学习了如何输入数据。然而,我们无法实现从 Quotation 表单的数据写入,这是因为由于列表不存在,我们无法判断它是新增还是编辑过程。

当你实际构建一个应用程序时,最常见的情况可能是首先构建列表,然后创建表单。然而,这次我们学习了先读取表单然后保存。所以,只需选择你认为更容易构建的方式即可。

本章主要关于显示上一章读取的数据。然而,用户无疑会想要搜索数据,因此我们还将学习数据搜索。

在本章中,你将学习如何:

  • 从数据库获取数据

  • 将获取的数据应用到商店

  • 连接商店和网格

  • 将数据读取到字段中

  • 搜索列表

创建 Quotation 列表

因此,让我们直接开始准备 CT 来创建 Quotation 列表。我们将创建 view_list.htmview_list.js(源文件:01_creating_quotation_list/ct/quotation/view_list.html)。

view_list.html 文件是其他 view 文件的复制品,因此在内部读取时,将正在读取的视图中的 .js 文件更改为 view_list.js(源文件:01_creating_quotation_list/ct/quotation/view_list.js)。

view_list.js 文件也几乎相同。只是略有不同。

Ext.onReady(function() {
    Ext.create('MyApp.store.Customer');
    Ext.create('MyApp.store.QuotationItem');
    Ext.create('MyApp.store.Quotation');
    Ext.create('MyApp.view.quotation.Quotation', {
        activeItem: 0,
     ...

到目前为止,准备 CT 应该已经变成一个直接的过程。

与编辑不同,将列表中的 activeItem 设置为 0。如果你检查浏览器中的外观,只有 Quotation 面板将被显示。

让我们开始构建内部结构。

创建 Quotation 模型

首先,你想要建立一个商店,但在那之前让我们先构建一个模型(源文件:02_creating_quotation_model/app/model/Quotation.js)。

让我们实现一个具有新获得的 idcustomermodifiedcreatedQuotation 模型类。

定义 idcustomernamemodified 日期/时间created 日期/时间 参数。然后,我们将实现上一步中模型使用的商店(源文件:02_creating_quotation_model/app/store/Quotation.js)。

Ext.define('MyApp.store.Quotation', {
    extend: 'Ext.data.Store',
    storeId: 'QuotationList',
    model: 'MyApp.model.Quotation',
    remoteSort: true,
    pageSize: 100,
    proxy: {
        type: 'direct',
        directFn: 'MyAppQuotation.getGrid',
        reader: {
            type: 'json',
            root: 'items',
            totalProperty: 'total'
        }
    }
});

directFn 中指定 MyAppQuotation.getGrid。这是商店将要获取数据的方法名称。当然,这是一个新的构建。换句话说,向 PHP 类中添加一个方法,凭借你迄今为止的经验,你应该能够轻松地想象是否需要添加 config.php

因此,首先实现这个方法,即使它是空的(源文件:02_creating_quotation_model/php/classes/MyAppQuotation.php)。

有一个参数,为此将从商店发送一个搜索条件(源文件:02_creating_quotation_model/php/config.php)。

<?php
$API = array(
    ....
    'MyAppQuotation'=>array(
        'methods'=>array(
            ....
            'getGrid'=>array(
                'len'=>1
            )
        )
    ),
    ....
);

更新 Quotation 视图

你已经为显示网格准备了所有东西,所以让我们实现视图(源文件:03_update_the_quotation_view/app/view/quotation/List.js)。

Ext.define('MyApp.view.quotation.List', {
  ...
    initComponent: function() {
        var me = this;
        Ext.apply(me, {
            columns: [{
                text: 'Customer',
                dataIndex: 'customer',
                flex: 1
            }, {
                text: 'Modified',
                dataIndex: 'modified',
                width: 120
            }, {
                text: 'Created',
                dataIndex: 'created',
                width: 120
            }]
        });
        me.callParent(arguments);
    }
});

这里你只指定了列。为了抽象网格面板,我们正在创建MyApp.grid.Panel类。(源文件:03_update_the_quotation_view/app/grid/Panel.js)。

我们将创建一个纯继承自Ext.grid.Panel类的MyApp.grid.Panel类。

我们只是简单地继承了Ext.grid.Panel类。这就是抽象的全部。现在,如果你显示它,应该看起来如下:

更新报价视图

自从我们看到了图像以来已经有一段时间了,所以我们现在显示了一个列;然而,让我们开始创建这个列表所需的必要对象。

下一步要添加的是带有按钮的工具栏。让我们继续添加以下按钮(源文件:03_update_the_quotation_view/app/view/quotation/List.js):

  • 添加

  • 编辑

  • 删除

  • 更新

这可以通过以下代码实现:

Ext.define('MyApp.view.quotation.List', {
    ....
    initComponent: function() {
        var me = this;
        Ext.apply(me, {
            tbar: [{
                text: 'Add',
                disabled: true,
                action: 'add'
            }, {
                text     : 'Edit',
                disabled : true,
                action   : 'edit'
            }, {
                text     : 'Remove',
                disabled : true,
                action   : 'remove'
            }, '-', {
                text     : 'Refresh',
                disabled : true,
                action   : 'refresh'
            }]
        });
    ....

我们已经安装了按钮,现在向控制器描述处理这个事件的处理器(源文件:03_update_the_quotation_view/app/controller/quotation/List.js)。

MyApp.controller.quotation.List类的控制中,使用以下选择器,并将每个处理程序与click事件注册(同时我们也将实现每个空处理程序):

  • 对于selector: 'myapp-quotation-list button[action=add]'使用handler: onItemAdd

  • 对于selector: 'myapp-quotation-list button[action=edit]'使用handler: onItemEdit

  • 对于selector: 'myapp-quotation-list button[action=remove]'使用handler: onItemRemove

  • 对于selector: 'myapp-quotation-list button[action=refresh]'使用handler: onStoreRefresh

如果你想检查事件是否响应,可以将disabled设置为false,或者重置它然后检查性能。我们稍后会创建一个实现,以便禁用按钮的状态根据列表是否被选中而改变。

接下来,让我们实现SearchField以进行搜索。我们将将其放置在我们之前创建的顶部工具栏中。但在你能够做到这一点之前,你需要实现SearchFieldSearchField从一开始就是作为ux分发的,但这次我们将使用这个作为参考来构建一个新的(源文件:03_update_the_quotation_view/app/form/SearchField.js)。

Ext.define('MyApp.form.SearchField', {
    extend: 'Ext.form.field.Trigger',
    alias: 'widget.myapp-searchfield',
    trigger1Cls: Ext.baseCSSPrefix + 'form-clear-trigger',
    trigger2Cls: Ext.baseCSSPrefix + 'form-search-trigger',
    hasSearch : false,
    paramName : 'query',
    initComponent: function() {
        var me = this;
        me.callParent(arguments);
        me.on('specialkey', function(f, e){
            if(e.getKey() == e.ENTER) {
                me.onTrigger2Click();
            }
        });
    },
    afterRender: function() {
        this.callParent();
        this.triggerCell.item(0).setDisplayed(false);
    },
    onTrigger1Click : function() {
        var me = this;
        if(me.hasSearch) {
            me.setValue('');
            me.hasSearch = false;
            me.triggerCell.item(0).setDisplayed(false);
            location.href = me.urlRoot;
        }
    },
    onTrigger2Click : function() {
        var me = this,
            value = me.getValue();
        if(value.length > 0) {
            me.triggerCell.item(0).setDisplayed(true);
            location.href = Ext.String.format('{0}q={1}', me.urlRoot, 
            value);
        }
    }
});

接下来,添加分页工具栏。将其与SearchField一起安装。一次显示 100 到 1,000 个项目将是最优的。为了声明使用SearchField,在requires中设置MyApp.form.SearchField(源文件:03_update_the_quotation_view/app/view/quotation/List.js)。

       ....
    initComponent: function() {
        var me = this;
        Ext.apply(me, {
            tbar: [{
                ....
            }, '->', {
                xtype    : 'myapp-searchfield',
                disabled : true,
                width    : 150
            }],
            bbar: {
                xtype       : 'pagingtoolbar',
                displayInfo : true 
            }
    ....

最后,让我们稍微自定义一下网格。修改SelectionModel,然后通过复选框实现选择功能。我们将安装它,以便提供一个用户界面,允许您批量删除项目。为此,您将使用Ext.selection.CheckboxModel(源文件:03_update_the_quotation_view/app/view/quotation/List.js)。

Ext.define('MyApp.view.quotation.List', {
    ....
    requires: [
        'MyApp.form.SearchField',
        'Ext.selection.CheckboxModel'
    ],
    initComponent: function() {
        var me = this;
        Ext.apply(me, {
            selModel: Ext.create('Ext.selection.CheckboxModel')
        });
        Ext.apply(me, {
            tbar: {
                text: 'Add',
                ....

SearchField类似,在requires中定义Ext.selection.CheckboxModel的读取。再次,关于selModel,设置Ext.selection.CheckboxModel实例。如果你完成所有配置,它应该看起来像这样:

![更新引用视图

实现引用控制器

看起来越来越像真的了!所以,现在让我们实现实际读取和显示数据的部分。首先,像往常一样,从实现 CT 开始(源文件:04_implement_quotation_controller/ct/quotation/view_list.html)。

复制app_edit.html并更改正在读取的.js文件为app_list.js。再次小心,因为我们还将一起读取api.php文件(源文件:04_implement_quotation_controller/ct/quotation/view_list.js)。

...
Ext.application({
  ...
    controllers: [
        'quotation.Quotation',
        'quotation.Edit',
        'quotation.List'
    ],
    launch: function() {
        var panel = Ext.create('MyApp.view.quotation.Quotation', {
           width: 800,
           height: 600,
           activeItem: 0,
           renderTo: Ext.getBody()
        });
        Ext.util.Observable.capture(panel, function() {
            console.log(arguments);
        });
        Ext.widget('button', {
            text: 'fire myapp-show',
            renderTo: Ext.getBody(),
            scope: this,
            handler: function() {
                this.getController('quotation.Quotation').loadIndex('#!/quotation');
            }
        });
    }
});

注意,direct设置也在进行中。

现在准备模拟触发myapp-show事件的按钮。当然,我们还将添加List控制器(源文件:04_implement_quotation_controller/app/controller/quotation/List.js)。

在这里,我们将向storesrefsmyapp-quotationlist添加myapp-show事件,并最终实现onShow方法:

Ext.define('MyApp.controller.quotation.List', {
    extend: 'MyApp.controller.Abstract',
    stores: [
        'Quotation'
    ],
    refs: [{
        ref: 'listView', selector: 'myapp-quotation-list' 
    }],
    init: function() {
        var me = this;
        me.control({
            'myapp-quotation-list': {
                'myapp-show': me.onShow
            },
    ...
    },
    onShow: function(p, owner, params) {
        var me          = this,
            listView    = me.getListView(),
            btnAdd      = listView.down('button[action=add]'),
            btnEdit     = listView.down('button[action=edit]'),
            btnRemove   = listView.down('button[action=remove]'),
            btnRefresh  = listView.down('button[action=refresh]'),
            fieldSearch = listView.down('myapp-searchfield'),
            query       = params.q;
        btnAdd.disable();
        btnEdit.disable();
        btnRemove.disable();
        btnRefresh.disable();
        if(query) {
            fieldSearch.setValue(query);
            fieldSearch.triggerCell.item(0).setDisplayed(true);
            fieldSearch.hasSearch = true;
        }
        fieldSearch.urlRoot = '#!/quotation/';
        fieldSearch.disable();
        listView.getStore().load({
            params: {
                query: query
            },
            callback: function(records, operation, success) {
                btnAdd.enable();
                btnRefresh.enable();
                fieldSearch.enable();
            }
        });
    },
  ...
});

添加事件处理程序,然后实现onShow事件。从 store 获取列表视图并调用load方法。

为了获取list视图和 store,确保storesrefs已配置。这次,我们还将修改组件侧,即视图侧(源文件:04_implement_quotation_controller/app/view/quotation/List.js)。

Ext.define('MyApp.view.quotation.List', {
    ....
    initComponent: function() {
        var me = this,
            store = me.getStore();
        if(!store) {
            store = Ext.create('MyApp.store.Quotation');
            me.store = store;
        }
        Ext.apply(me, {
        ....

initComponent方法中设置store对象。运行 CT,如果按下按钮,store 将使用已设置的DirectFn方法,并发生传输。当然,服务器端实现尚未发生,所以列表中不会显示任何内容。

加载网格和实现工具栏按钮

一般而言,如果一个网格不能读取数据,那么它就不是网格。目前,让我们只从数据库中读取数据并在网格中显示(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/php/classes/MyAppQuotation.php)。

在这里,我们将为MyAppQuotation.php实现getGrid方法,该方法返回空数据。

注意

这段代码有点长,请参考源文件查看代码。

如果我们在数据库中放入任何旧数据,它将显示出来。但是,因为我们想要显示的项目略有增加,我们需要修改 JavaScript 源代码。将两个文件addrnote添加到Quotation模型中(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/app/model/Quotation.js)。

现在,让我们将之前步骤中添加的两个文件添加到MyApp.view.quotation.List中的columns属性(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/app/view/quotation/List.js):

  ...
            }, {
                text: 'Address',
                dataIndex: 'addr',
                flex: 1
            }, {
                text: 'Note',
                dataIndex: 'note',
                flex: 1
            }, {
    ...

我们已经添加了addrmodifiedcreated文件。数据是随机的,但列被显示,数据读取方式如下截图所示:

加载网格和实现工具栏按钮

接下来是注册过程的构建,这是我们留在第三章 数据输入中未完成的事情。为了做到这一点,首先实现工具栏按钮的事件处理程序,然后显示添加新编辑屏幕(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/app/controller/quotation/List.js)。

Ext.define('MyApp.controller.quotation.List', {
    ....
    onItemAdd: function() {
        var me          = this,
            listView    = me.getListView();
        listView.fireEvent('myapp-add');
    },

在列表视图组件中触发myapp-add事件(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/app/controller/quotation/Quotation.js)。

Ext.define('MyApp.controller.quotation.Quotation', {
    ....
    init: function() {
        var me = this,
            format = Ext.String.format;
        me.control({
            'myapp-quotation-list': {
                'myapp-add': function() {
                    location.href = format('#!/{0}/new', 
                    me.screenName);
                }
            },
    ....

然后在MyApp.controller.quotation.Quotation类中描述myapp-add事件处理程序。在这里,指定location.href中的 URL,然后移动屏幕。不在 CT 中,但如果你在index.php中显示整个应用程序,你应该能够检查点击添加按钮时屏幕如何变化。

加载网格和实现工具栏按钮

点击添加按钮后,您将看到以下截图:

加载网格和实现工具栏按钮

让我们实现添加新功能。CT 是开发它的地方。它将在ct/quotation/app_edit.htm中实现。

首先实现保存按钮。使用组件查询获取保存按钮,查询为'myapp-quotation-edit button[action=save]',并设置click事件。处理程序名称为onSave

让我们以这样的方式创建它,当按下保存按钮时,事件处理程序将在MyApp.controller.quotation.Edit类中注册(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/app/controller/quotation/Edit.js)。

        me.control({
            ....
            'myapp-quotation-edit button[action=save]': {
                'click': me.onSave
            }
        });

我们还将实现处理程序的内部。

    ...
 onSave: function() {
        var me      = this,
            p       = me.getEditView(),
            form    = p.getForm(),
            format  = Ext.String.format,
            id;
        p.setLoading();
        form.submit({
            success: function(form, action) {
                if(action.result.newid) {
                    p.fireEvent('myapp-list-reload');
                    location.href = format('#!/quotation/id={0}', 
                    action.result.newid);
                    return;
                }
                p.setLoading(false);
                form.load({
                    params: {
                        id: form.getValues()['id']
                    },
                    success: function(form, ret) {
                        p.fireEvent('myapp-loadform', p, ret);
                        p.fireEvent('myapp-undirty');
                        p.setLoading(false);
                    },
                    failure: function() {
                        p.setLoading(false);
                    }
                });
            },
            failure: function(form, action) {
                p.setLoading(false);
            }
        });
    },

onSave中调用表单的submit方法,并将位置传输到服务器端。接下来是服务器端实现。

Ext Direct 模块已经准备好,处理将在那里实现。你可能记得方法名为writeForm

在这里,我们将实现writeForm方法。代码非常长,所以请再次参考源文件(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/php/classes/MyAppQuotation.php)。

通过这种方法,接收到的位置正在存储在Quotation表中。我们已经在引用中存储了某些数据,所以也在不同的表中写入数据。

为了确保正确写入数据,您需要使用Transaction,因此使用beginrollbackcommit。在 MySQL 中,如果我们使用last_insert_id(),我们可以获取我们之前写入的 ID。

使用此方法并设置引用的父级。在 CT 中,即使我们执行屏幕转换过程,它加载时也会保持原样。它应该看起来像以下 URL。

<hostname>/ct/quotation/app_edit.html#!/quotation/id=XX

现在,如果我们运行应用程序,URL 将按以下方式更改:

<hostname>/#!/quotation/id=XX

在这种状态下,重新开始读取。您使用的具体数据已经消失,只显示一个项目。这就是我们在第三章中停止创建模拟数据的原因,数据输入

我们需要扩展readForm方法,这是所有这些功能的根源。然而,在此之前,为了使 ID 能够跨过readForm参数,首先修改config.php文件。

我们将把MyAppQuotation类的readForm方法中的参数从0改为1(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/php/config.php)。

一旦我们修改了config.php文件,我们将修改readForm方法,使其能够接收参数(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/php/classes/MyAppQuotation.php)。

<?php
class MyAppQuotation {
  public function readForm($id) {

readForm方法中将$id参数设置为标识目标。现在,这次在 JavaScript 端(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/app/view/quotation/Edit.js)也做同样的操作。

Ext.define('MyApp.view.quotation.Edit', {
    ....
    paramOrder: ['id'],
    ....

通过这种方式,ID 可以传输到服务器端。现在让我们实现主要的readForm方法。

这是修改了之前提到的参数的readForm方法内部的实现。由于源代码非常长,请参考源文件中的代码(源文件:05_loading_the_grid_and_implementing_toolbar_buttons/php/classes/MyAppQuotation.php)。

在您使用您设置的 ID 从Quotation表读取数据后,从Quotations表获取数据,将数组转换为 JSON,并保存。

客户名称依赖于MyApp.store.Customer数据。如果您想与数据库中的客户表匹配,请将其定制为通过direct直接读取数据。很简单!

根据网格选择状态管理工具栏按钮

现在读取和写入数据已完成。列表中只有添加功能,所以目前我们只能添加新信息。让我们创建它,这样我们就可以点击编辑删除按钮。之后我们可以实现各种功能。这个实现发生在ct/quotation/app_list.html

让我们控制列表中项目选择和取消选择时的事件。我们还将实现当双击项目时,它将以与点击编辑按钮相同的方式执行。

我们将向MyApp.controller.quotation.List类的init方法控制过程中添加以下事件处理器(源文件:06_management_toolbar_buttons_depend_on_grid_selection_status/app/controller/quotation/List.js)。

            ....
            'myapp-quotation-list': {
                'myapp-show': me.onShow,
                'select': me.onSelect,
                'itemdblclick': me.onItemDblClick,
                'deselect': me.onDeselect
            },
            ....

以下三个是监听特定事件的处理器:

  • select

  • itemdblclick

  • deselect

让我们继续实现将监听前三个事件的各个事件处理器。接下来,我们将实现前一步骤的事件处理器(源文件:06_management_toolbar_buttons_depend_on_grid_selection_status/app/controller/quotation/List.js)。

onSelect
onSelect: function() {
    var me = this,
        listView = me.getListView(),
        btnEdit = listView.down('button[action=edit]'),
        btnRemove = listView.down('button[action=remove]'),
        sm = listView.getSelectionModel(),
        cnt = sm.getCount();
    if(cnt === 1) {
        btnEdit.enable();
    } else {
        btnEdit.disable();
    }
    if(cnt > 0) {
        btnRemove.enable();
    } else {
        btnRemove.disable();
    }
},

onDeselect
onDeselect: function() {
    var me = this,
        listView = me.getListView(),
        btnEdit = listView.down('button[action=edit]'),
        btnRemove = listView.down('button[action=remove]'),
        sm = listView.getSelectionModel(),
        cnt = sm.getCount();
    if(cnt === 1) {
        btnEdit.enable();
    } else {
        btnEdit.disable();
    }
    if(cnt > 0) {
        btnRemove.enable();
    } else {
        btnRemove.disable();
    }
}

关于选择,当选择一个项目时,我们只会提供编辑按钮。即使选择多个项目,我们也会提供删除按钮。当没有选择任何项目时,编辑删除按钮都将不可用。

onItemDblClick
onItemDblClick: function(p, record, item, index, e, eOpts) {
    var me          = this,
        listView    = me.getListView();
    listView.fireEvent('myapp-edit', record.data.id);
},

当双击时,将触发myapp-edit事件,并将选定的项目 ID 添加到参数中。

现在剩下的只是实现当按下编辑删除按钮时的过程。编辑按钮的实现方式如下:

onItemEdit
onItemEdit: function() {
    var me = this,
        listView = me.getListView(),
        sm = listView.getSelectionModel(),
        record = sm.getLastSelected();
    listView.fireEvent('myapp-edit', record.data.id);
},

该过程与onItemDblClick类似,但是record对象不是通过参数传递的,因此从SelectionModel获取已选定的record对象。

因此,如果你实现了myapp-edit事件处理器,它将开始感觉像是结束。myapp-edit事件处理器将由MyApp.controller.quotation.Quotation控制器实现(源文件:06_management_toolbar_buttons_depend_on_grid_selection_status/app/controller/quotation/Quotation.js)。

Ext.define('MyApp.controller.quotation.Quotation', {
    ....
    init: function() {
        var me = this,
            format = Ext.String.format;
        me.control({
            'myapp-quotation-list': {
                'myapp-add': function() {
                    location.href = format('#!/{0}/new', 
                    me.screenName);
                },
                'myapp-edit': function(itemid) {
                    var query = this.requestParams.q;
                    if(query) {
                        location.href = format('#!/{0}/id={1}/q={2}', 
                        me.screenName, itemid, query);
                    } else {
                        location.href = format('#!/{0}/id={1}', 
                        me.screenName, itemid);
                    }
                }
            }
        });

查看requestParams属性,我们正在尝试决定是否存在查询。然而,这将在稍后实现的SearchField中配置。

按如下方式更改onShow方法实现中的一行:

me.requestParams = params = o;

通过这种方式,如果你双击或点击编辑按钮,URL 将改变。如果你检查整个应用程序,当你点击编辑按钮时,你可以检查正在读取数据的特定屏幕。

接下来,实现删除按钮(源文件:06_management_toolbar_buttons_depend_on_grid_selection_status/app/controller/quotation/List.js)。

onItemRemove: function() {
    var me          = this,
        listView    = me.getListView(),
        sm          = listView.getSelectionModel(),
        records     = sm.getSelection();
    Ext.MessageBox.confirm(
        'Remove Confirm',
        'May I delete that?',
        function(ret) {
            if(ret === 'yes') {
                listView.fireEvent('myapp-remove', records);
            }
        }
    );
},

在结构上,它与编辑按钮相同。它触发myapp-remove事件。同样,myapp-removeMyApp.controller.quotation.Quotation中实现了事件处理器(源文件:06_management_toolbar_buttons_depend_on_grid_selection_status/app/controller/quotation/Quotation.js)。

onRemove: function(records) {
    var me = this,
        format = Ext.String.format,
        listView = me.getListView(),
        ids = [];
    if(!Ext.isArray(records)) {
        records = [records];
    }
    Ext.iterate(records, function(r) {
        if(r.get) {
            ids.push(r.get('id'));
        } else {
            ids.push(r);
        }
    });
    listView.mask();
    MyAppQuotation.removeItems(ids, function() {
        me.getController(
            format(
                '{0}.List',
                me.screenName.split('-').join('.')
            )
        ).onStoreRefresh();
    listView.unmask();
    });
}

我们正在调用名为 MyAppQuotation.removeItemsdirect 函数。我们尚未实现此方法。它是用于删除项的,所以让我们快速完成它。

我们将向 MyAppQuotation 类添加一个新的 removeItems (len:1) 方法(源文件:06_management_toolbar_buttons_depend_on_grid_selection_status/php/config.php)。

让我们实现之前添加的方法。代码非常长,所以请参考源文件(源文件:06_management_toolbar_buttons_depend_on_grid_selection_status/php/classes/MyAppQuotation.php)。

此方法不是用于物理删除,而是在更新 UPDATE 文本的状态后进行逻辑删除。完成此过程后,我们将从客户端调用 onStoreRefresh;然而,它尚未实现,所以让我们实现它(源文件:06_management_toolbar_buttons_depend_on_grid_selection_status/app/controller/quotation/List.js)。

Ext.define('MyApp.controller.quotation.List', {
    ....
    onStoreRefresh: function() {    },
    ....

这实际上只是执行工具栏更新过程,但没有在工具栏上配置存储,它将不会运行。所以为了最后的润色,让我们在工具栏中设置存储(源文件:06_management_toolbar_buttons_depend_on_grid_selection_status/app/view/quotation/List.js)。

Ext.define('MyApp.view.quotation.List', {
    ....
    initComponent: function() {
        ....
        Ext.apply(me, {
            ....
            bbar: {
                xtype       : 'pagingtoolbar',
                store       : store,
                displayInfo : true 
            }
        ....

现在,在删除过程之后,它将自动执行重新加载过程。通过这种方式,我们已经实现了从开始到结束的过程。这是一段相当复杂且漫长的旅程!对于 Bill,请按照与 Quotation 相同的方式实现它,因为设置过程基本上是相同的。

使用搜索触发字段和关系 URL 哈希

最后,让我们在屏幕右上角实现 SearchField。实际上,它已经在客户端实现了。

当我们使用 cond 参数调用 getGrid 时,搜索条件正在被传输。也就是说,我们只需要在服务器端实现。让我们快速修改。

这只是显示了一个已修改的部分。要查看整个源代码部分,请参考源文件(源文件:07_using_search_trigger_field_and_relation_url_hash/php/classes/MyAppQuotation.php)。

        ....
            'ON',
            '    customers.id = quotation.customer',
            'WHERE',
            '    quotation.status = 1'
        ));
        $query = explode(' ', @$cond->query);
        foreach($query as $q) {
            if($q != '') {
                $sql .= ' ' . implode(" \n ", array(
                    'AND (',
                    '    customers.name like \'%' . $q . '%\'',
                    '    OR',
                    '    customers.addr1 like \'%' . $q . '%\'',
                    '    OR',
                    '    customers.`addr2` like \'%' . $q . '%\'',
                    '    OR',
                    '    quotation.`note` like \'%' . $q . '%\'',
                    ')'
                ));
            }
        }
        ....

使用 $cond,以下参数被发送:

  • query

  • 页面

  • start

  • limit

您在 SearchField 中输入的字符串将被存储在查询中。之后,您只需将那个字符串添加到 SQL 中的新条件即可。

我们在这里不会实现它,但通过将 pagestartlimit 应用到 SQL 中,分页过程将开始。

再次,为了在点击列时设置显示顺序,我们添加了排序功能并将其发送。使用这些数据,如果我们添加 ORDER BY,我们可以调整顺序。

因此,为了更接近真实的应用,尝试实现它。

摘要

到目前为止,我们创建了数据结构、应用程序架构,并实现了数据发送和接收方法。但当前我们只能在一个网格中看到数据。

想象一下,一个公司的经理想要将这类数据以图表的形式展示在报告或演示文稿中,这并不困难。因此,在下一章中,我们将学习如何设计各种类型的可视化图表。

第五章。报告

在本章中,我们将创建报告,并在仪表板上使用四种不同类型的图表来显示它。

你将学习到:

  • 创建一个饼图来显示报价和账单数据

  • 创建一个柱状图来显示客户数据

  • 创建一个折线图来按月显示报价和账单数据

  • 创建一个用于显示货币金额的雷达图

  • 在面板内显示每个图表

  • 在仪表板上布局四个面板

在仪表板上创建图表

首先,创建四个空面板,并确保组件测试CT)可以显示它们。这些面板用于饼图、柱状图、折线图和雷达图。这个过程非常简单,现在你应该已经准备好了四个空面板。

为了像以前一样创建每个图表类,我们将复制不同的 view.html 并创建一个新的 HTML 文件,修改标题和 JavaScript 文件。

CT 的饼图

让我们使用 MyApp.view.dashboard.Pie 创建类名。请参阅以下源文件以获取代码:

  • 01_making_charts_on_dashboard/ct/dashboard/pie_view.html

  • 01_making_charts_on_dashboard/ct/dashboard/pie_view.js

    ...
    Ext.onReady(function() {
        Ext.create('MyApp.view.dashboard.Pie', {
            width: 800,
            height: 600,
            renderTo: Ext.getBody()
        });
    });
    ...
    

我们现在将实现前一段代码中指定的类(源文件:01_making_charts_on_dashboard/app/view/dashboard/Pie.js)。

Ext.define('MyApp.view.dashboard.Pie', {
    extend: 'Ext.panel.Panel',
    alias : 'widget.myapp-dashboard-pie',
    title: 'Pie Chart'
});

CT 的柱状图

内容与柱状图相同。让我们使用 MyApp.view.dashboard.Bar 创建类名。请参阅以下源文件以获取代码:

  • 01_making_charts_on_dashboard/ct/dashboard/bar_view.html

  • 01_making_charts_on_dashboard/ct/dashboard/bar_view.js

  • 01_making_charts_on_dashboard/app/view/dashboard/Bar.js

    Ext.define('MyApp.view.dashboard.Bar', {
        extend: 'Ext.panel.Panel',
        alias : 'widget.myapp-dashboard-bar',
        title: 'Bar Chart'
    });
    

CT 的折线图

与饼图和柱状图相同,让我们使用 MyApp.view.dashboard.Line 创建类名。请参阅以下源文件以获取代码:

  • 01_making_charts_on_dashboard/ct/dashboard/line_view.html

  • 01_making_charts_on_dashboard/ct/dashboard/line_view.js

  • 01_making_charts_on_dashboard/app/view/dashboard/Line.js

    Ext.define('MyApp.view.dashboard.Line', {
        extend: 'Ext.panel.Panel',
        alias : 'widget.myapp-dashboard-line',
        title: 'Line Chart'
    });
    

CT 的雷达图

与其他图表一样,让我们使用 MyApp.view.dashboard.Radar 创建类名。请参阅以下源文件以获取代码:

  • 01_making_charts_on_dashboard/ct/dashboard/radar_view.html

  • 01_making_charts_on_dashboard/ct/dashboard/radar_view.js

  • 01_making_charts_on_dashboard/app/view/dashboard/Radar.js

    Ext.define('MyApp.view.dashboard.Radar', {
        extend: 'Ext.panel.Panel',
        alias : 'widget.myapp-dashboard-radar',
        title: 'Radar Chart'
    });
    

布局到仪表板

因此,让我们将这些四个面板排列到仪表板中(源文件:02_layout_to_dashboard/app/view/dashboard/DashBoard.js)。

Ext.define('MyApp.view.dashboard.DashBoard', {

    requires: [
        'MyApp.view.dashboard.Pie',
        'MyApp.view.dashboard.Bar',
        'MyApp.view.dashboard.Line',
        'MyApp.view.dashboard.Radar'
    ],
    title: 'Dashboard',
    layout: {
        type: 'hbox',
        pack: 'start',
        align: 'stretch'
    },
    items: [{
        xtype: 'container',
        flex: 1,
        padding: '20 10 20 20',
        layout: {
            type: 'vbox',
            pack: 'start',
            align: 'stretch'
        },
        items: [{
            flex: 1,
            padding: '0 0 10 0',
            xtype: 'myapp-dashboard-pie'
        }, {
            flex: 1,
            padding: '10 0 0 0',
            xtype: 'myapp-dashboard-bar'
        }]
    }, {
        xtype: 'container',
        flex: 1,
        padding: '20 20 20 10',
        layout: {
            type: 'vbox',
            pack: 'start',
            align: 'stretch'
        },
        items: [{
            flex: 1,
            padding: '0 0 10 0',
            xtype: 'myapp-dashboard-line'
        }, {
            flex: 1,
            padding: '10 0 0 0',
            xtype: 'myapp-dashboard-radar'
        }]
    }]
});

现在,在仪表板面板的 requires 参数中设置你之前制作的四个面板。你可以通过这种方式指定 xtype

接下来是布局部分。在 hbox 布局中将顶部和底部平均分成两个相等区域,然后在每个区域中,使用 vbox 布局将左右两边分开。

小贴士

一个重要点是container是由xtype指定的。在看过各种 Ext JS 代码后,在这种情况下,很多代码没有指定xtype。在这种情况下,panel将被指定为初始值。

如果你只想执行布局,你应该指定container参数。如果你不这样做而使用panel,将只为执行布局而创建不必要的 DOM,这将严重影响性能。

现在你已经成功将仪表板分为四个部分,为了调整外观,对填充进行调整。当然,直接使用 CSS 来做这个也可以。

在四个区域中的每一个,使用xtype设置图表面板。

希望你的数据库已经开始成形。让我们继续并实现各种图表。

布局到仪表板

创建饼图

首先,我们为了显示目的创建了 CT,但现在让我们创建一个可以运行的 CT。

我们将使用Direct函数,所以让我们也准备一下。实际上,我们已经这样做过了。

复制一个不同的app.html并像我们之前做的那样更改 JavaScript 文件。请参阅源文件中的代码:03_making_a_pie_chart/ct/dashboard/pie_app.html

实现 Direct 函数

接下来,准备读取数据的Direct函数。

首先,是config.php文件定义了API。让我们把它们放在一起并实现四个图表(源文件:04_implement_direct_function/php/config.php)。

    ....
    'MyAppDashBoard'=>array(
        'methods'=>array(
            'getPieData'=>array(
                'len'=>0
            ),
            'getBarData'=>array(
                'len'=>0
            ),
            'getLineData'=>array(
                'len'=>0
            ),
            'getRadarData'=>array(
                'len'=>0
            )
        )
    ....

接下来,让我们创建以下方法来获取各种图表的数据:

  • getPieData

  • getBarData

  • getLineData

  • getRadarData

首先,实现饼图的getPieData方法。我们将实现获取饼图数据的Direct方法。请参阅源代码的实际内容(源文件:04_implement_direct_function/php/classes/ MyAppDashBoard.php)。

这是在获取有效的报价和账单数据项。对于要发送回客户端的数据,设置数组在items中,并在键数组中设置各种名称和数据。现在,将下一个模型中的定义组合起来。

为饼图准备存储

图表需要一个存储,所以让我们定义存储和模型(源文件:05_prepare_the_store_for_the_pie_chart/app/model/ Pie.js)。

我们将创建具有namedata字段的MyApp.model.Pie类。将其与Direct函数的返回值设置的数据连接起来。如果你在刚刚定义的模型内部增加了字段数量,请确保修改返回字段值,否则它将不会应用于图表,所以请小心。我们将使用之前步骤中制作的模型并实现存储(源文件:05_prepare_the_store_for_the_pie_chart/app/model/ Pie.js)。

Ext.define('MyApp.store.Pie', {
    extend: 'Ext.data.Store',
    storeId: 'DashboardPie',
    model: 'MyApp.model.Pie',
    proxy: {
        type: 'direct',
        directFn: 'MyAppDashboard.getPieData',
        reader: {
            type: 'json',
            root: 'items'
        }
    }
})

然后,使用我们制作的模型定义存储,并在代理中设置我们之前制作的Direct函数。

创建视图

我们现在已经准备好了演示数据。现在,让我们快速创建视图来显示它(源文件:06_making_the_view/app/view/dashboard/Pie.js)。

Ext.define('MyApp.view.dashboard.Pie', {
    extend: 'Ext.panel.Panel',
    alias : 'widget.myapp-dashboard-pie',
    title: 'Pie Chart',
    layout: 'fit',
    requires: [
        'Ext.chart.Chart',
        'MyApp.store.Pie'
    ],
    initComponent: function() {
        var me = this, store;
        store = Ext.create('MyApp.store.Pie');
        Ext.apply(me, {
            items: [{
                xtype: 'chart',
                store: store,
                series: [{
                    type: 'pie',
                    field: 'data',
                    showInLegend: true,
                label: {
                    field: 'name',
                    display: 'rotate',
                    contrast: true,
                    font: '18px Arial'
                }
            }]
            }]
        });
        me.callParent(arguments);
    }
});

实现控制器

使用前面的代码,数据没有被存储读取,也没有任何内容被显示。

与使用 onShow 读取阅读内容的方式相同,让我们实现控制器(源文件:06_making_the_view/app/controller/DashBoard.js):

Ext.define('MyApp.controller.dashboard.DashBoard', {
    extend: 'MyApp.controller.Abstract',
    screenName: 'dashboard',
    init: function() {
        var me = this;
        me.control({
            'myapp-dashboard': {
                'myapp-show': me.onShow,
                'myapp-hide': me.onHide
            }
        });
    },
    onShow: function(p) {
        p.down('myapp-dashboard-pie chart').store.load();
    },
    onHide: function() {
    }
});

从现在开始创建的图表,在创建它们时,将阅读过程添加到 onShow 中会很好。让我们看看我们的饼图,如下所示:

实现控制器

创建条形图

与饼图相同,首先准备 CT。再次与创建饼图相同,让我们更改 JavaScript 文件并创建条形图 HTML(源文件:07_making_a_bar_chart/ct/dashboard/bar_app.html)。

除了创建的类名(MyApp.view.dashboard.Bar)外,这里的 JavaScript 文件与饼图相同(源文件:07_making_a_bar_chart/ct/dashboard/bar_app.js)。

实现 Direct 函数

现在,是 Direct 函数。由于定义已经在 config.php 中完成,所以这里不再重复。

让我们继续实现 Direct 方法(getBarData)以获取条形图的数据。如果您想查看源代码内容,请参考源文件(源文件:08_implement_direct_function/php/classes/MyAppDashBoard.php)。它将获取为每个客户创建的报价或账单级别的计数。

为图表准备存储

接下来是模型存储,与创建饼图的方式相同。名称是客户端名称;报价和账单是存储各种计数的地方。

现在我们将创建 MyApp.model.Bar 类,它包含字段,例如 namequotationbill(源文件:09_prepare_the_store_for_the_pie_chart/app/model/Bar.js)。

我们将使用之前步骤中创建的模型来实现存储。这与创建饼图的方式相同,所以如果需要参考,请查看源文件。请注意,模型 nameStoreIdDirect 方法名称不同(源文件:09_prepare_the_store_for_the_pie_chart/app/store/Bar.js)。

创建视图

让我们定义其内容,以便我们可以显示它(源文件:10_making_the_view/app/view/dashboard/Bar.js):

Ext.define('MyApp.view.dashboard.Bar', {
    extend: 'Ext.panel.Panel',
    alias : 'widget.myapp-dashboard-bar',
    title: 'Bar Chart',
    layout: 'fit',
    requires: [
        'Ext.chart.Chart',
        'MyApp.store.Bar'
    ],
    initComponent: function() {
        var me = this, store;
        store = Ext.create('MyApp.store.Bar');
        Ext.apply(me, {
            items: [{
                xtype: 'chart',
                store: store,
                axes: [{
                    type: 'Numeric',
                    position: 'bottom',
                    fields: ['quotation', 'bill'],
                    title: 'Document Count',
                    grid: true,
                    minimum: 0
                }, {
                    type: 'Category',
                    position: 'left',
                    fields: ['name'],
                    title: 'Customers'
                }],
                series: [{
                    type: 'bar',
                    axis: 'bottom',
                    highlight: true,
                    tips: {
                        trackMouse: true,
                        width: 140,
                        height: 28,
                        renderer: function(storeItem, item) {
                            var tail = '';
                            if(storeItem.get(item.yField) > 1) {
                                tail = 's';
                            }
                            this.setTitle([
                                 storeItem.get('name'),
                                 ': ',
                                 storeItem.get(item.yField),
                                 ' ',
                                 item.yField,
                            ].join(''));
                        }
                    },
                    label: {
                        display: 'insideEnd',
                        field: 'quotation',
                        renderer: Ext.util.Format.numberRenderer('0'),
                        orientation: 'horizontal',
                        color: '#333',
                        'text-anchor': 'middle'
                    },
                    xField: 'name',
                    yField: ['quotation', 'bill']
                }]
            }]
        });
        me.callParent(arguments);
    }
});

实现控制器

与我们在 onShow 中所做的一样,我将把这个实现放在附录中(源文件:11_making_the_view/app/controller/DashBoard.js)。

    ....
    onShow: function(p) {
        p.down('myapp-dashboard-pie chart').store.load();
        p.down('myapp-dashboard-bar chart').store.load();
    },
    ....

让我们看看我们的条形图,如下所示:

实现控制器

创建折线图

与之前一样,让我们从创建 CT 开始。与饼图相同,更改要读取的 JavaScript 文件并创建折线图 HTML(源文件:12_making_a_line_chart/ct/dashboard/line_app.html)。

在这个 JavaScript 文件中,除了创建类名 MyApp.view.dashboard.Line(源文件:12_making_a_line_chart/ct/dashboard/line_app.js)之外,一切与饼图相同。

实现 Direct 函数

以同样的方式,实现 Direct 函数。让我们实现 Direct 方法(getLineData)以获取与条形图相同的数据。如果您想查看源代码,请参考源文件(源文件:13_implement_direct_function/php/classes/ MyAppDashBoard.php)。

准备图表的存储

以类似的方式创建模型存储。我们将创建具有字段 namequotationbillMyApp.model.Line 类(源文件:14_prepare_the_store_for_the_pie_chart/app/model/ Line.js)。

我们现在将使用之前步骤中创建的模型实现存储。细节与饼图相同。有关代码,请参考以下源文件。再次,模型 nameStoreIdDirect 方法标题已更改,请注意这一点(源文件:14_prepare_the_store_for_the_pie_chart/app/store/ Line.js)。

创建视图

让我们将 quotationbill 变量设置为按月显示。使用 Direct 函数,最多可以接收 12 个月的输出,如果数据不存在,月份的数量将减少(源文件:15_making_the_view/app/view/dashboard/Line.js)。

Ext.define('MyApp.view.dashboard.Line', {
    extend: 'Ext.panel.Panel',
    alias : 'widget.myapp-dashboard-line',
    title: 'Line Chart',
    layout: 'fit',
    requires: [
        'MyApp.store.Line'
    ],
    initComponent: function() {
        var me = this, store;
        store = Ext.create('MyApp.store.Line');
        Ext.apply(me, {
            items: [{
                xtype: 'chart',
                store: store,
                legend: {
                    position: 'right'
                },
                axes: [{
                    type: 'Numeric',
                    minimum: 0,
                    position: 'left',
                    fields: ['quotation', 'bill'],
                    title: 'Documents',
                    minorTickSteps: 1
                }, {
                    type: 'Category',
                    position: 'bottom',
                    fields: ['mon'],
                    title: 'Month of the Year'
                }],
                series: [{
                    type: 'line',
                    highlight: {
                        size: 7,
                        radius: 7
                    },
                    axis: 'left',
                    xField: 'mon',
                    yField: 'quotation'
                }, {
                    type: 'line',
                    highlight: {
                        size: 7,
                        radius: 7
                    },
                    axis: 'left',
                    xField: 'mon',
                    yField: 'bill'
                }]
            }]
        });
        me.callParent(arguments);
    }
});

实现 controller

由于过程相同,我将添加关于读取控制器存储的命令的附录(源文件:16_making_the_view/app/controller/DashBoard.js)。

    ....
    onShow: function(p) {
        p.down('myapp-dashboard-pie chart').store.load();
        p.down('myapp-dashboard-bar chart').store.load();
        p.down('myapp-dashboard-line chart').store.load();
    },
    ....

让我们看看我们的线形图,它如下所示:

实现 controller

创建雷达图

现在,让我们制作最终的 CT。

与饼图相同的方式,修改 JavaScript 文件以读取并创建雷达图 HTML(源文件:17_making_a_radar_chart/ct/dashboard/radar_app.html)。

再次,在这个 JavaScript 文件中,除了创建类名 MyApp.view.dashboard.Radar(源文件:17_making_a_radar_chart/ct/dashboard/radar_app.js)之外,一切与饼图相同。

实现 Direct 函数

正如标题所说,让我们实现 Direct 函数。和之前一样,我们将实现 Direct 方法(getRadarData)以获取雷达图的数据。有关详细信息,请参考以下源文件(源文件:18_implement_direct_function/php/classes/ MyAppDashBoard.php)。

您已经猜到了,过程与 getLineData 完全相同。因此,没有必要不必要地实现它,但如果您想在雷达图上显示不同的数据,请修改此方法。

准备图表的存储

让我们准备存储。我们将创建具有 namequotationbill 字段的 MyApp.model.Radar 类(源文件:19_prepare_the_store_for_the_pie_chart/app/model/ Radar.js)。

再次,我们将使用我们在上一步中制作的模型来实现存储。细节与饼图相同,再次请注意,因为模型 nameStoreIdDirect 方法名称不同(源文件:19_prepare_the_store_for_the_pie_chart/app/store/ Radar.js)。

创建视图

接下来,我们将创建视图(源文件:20_making_the_view/app/view/dashboard/Radar.js):

Ext.define('MyApp.view.dashboard.Radar', {
    extend: 'Ext.panel.Panel',
    alias : 'widget.myapp-dashboard-radar',
    title: 'Radar Chart',
    layout: 'fit',
    requires: [
        'MyApp.store.Radar'
    ],
    initComponent: function() {
        var me = this, store;
        store = Ext.create('MyApp.store.Radar');
        Ext.apply(me, {
            items: [{
                xtype: 'chart',
                store: store,
                insetPadding: 20,
                legend: {
                    position: 'right'
                },
                axes: [{
                    type: 'Radial',
                    position: 'radial',
                    label: {
                        display: true
                    }
                }],
                series: [{
                    type: 'radar',
                    xField: 'mon',
                    yField: 'quotation',
                    showInLegend: true,
                    showMarkers: true,
                    markerConfig: {
                        radius: 5,
                        size: 5
                    },
                    style: {
                        'stroke-width': 2,
                        fill: 'none'
                    }
                },{
                    type: 'radar',
                    xField: 'mon',
                    yField: 'bill',
                    showInLegend: true,
                    showMarkers: true,
                    markerConfig: {
                        radius: 5,
                        size: 5
                    },
                    style: {
                        'stroke-width': 2,
                        fill: 'none'
                    }
                }]
            }]
        });
        me.callParent(arguments);
    }
});

实现控制器

onShow 中设置存储的数据读取设置(源文件:21_making_the_view/app/controller/DashBoard.js)。

    ....
    onShow: function(p) {
        p.down('myapp-dashboard-pie chart').store.load();
        p.down('myapp-dashboard-bar chart').store.load();
        p.down('myapp-dashboard-line chart').store.load();
        p.down('myapp-dashboard-radar chart').store.load();
    },
    ....

最后,让我们看看我们的雷达图表,它看起来如下:

实现控制器

在不久的将来,图表应该看起来像之前的图表。然而,在当前的 Ext JS 4.2.2-GPL 版本中,将存储设置到图表并从服务器获取数据会导致一个错误,该错误扭曲了线条和标签的显示。该错误已被确认为 EXTJSIV-7778。

在前一个图表的情况下,我们通过将服务器接收到的确切响应设置在存储的本地数据中,成功地显示了它。

实现控制器

摘要

您必须同意这已经开始看起来像一个应用程序了!

仪表板是您登录后看到的第一个屏幕。图表在视觉上检查大量复杂的数据时非常有效。如果您觉得需要,随时添加面板,这将增加其实用性。这个示例将成为您在未来的项目中可定制的基线。

现在,在下一章和最后一章中,我们将继续学习数据管理。

第六章. 数据管理

通过我们已经完成的发展,应用程序真的开始成形了。我们可以输入并查看数据。现在,如果我们根据需要自定义内容,我们将得到我们最初希望完成的最终应用程序。在本章的最后,我们将实现数据导入/导出以恢复或备份数据。这次,我们指的是报价账单数据。

设计导入和导出

让我们先考虑报价表。因为账单报价大部分相同,所以我们在这里只实现报价。尝试用账单亲自尝试这种实现。首先,我们在报价列表中添加导出导入按钮,使其开始工作。接下来,当按下导出按钮时,我们将使文件开始下载。我们将添加一个新的面板,以便我们可以直观地检查即将导入的选定的数据。所以让我们继续创建这种类型的数据导入和导出功能。

数据格式

在最初导出时,让我们考虑将要下载的数据格式。这次,我们考虑以TSV 格式输出,而不是 CSV 或 XML。TSV 对开发者来说更容易阅读,并且使用 JavaScript 生成非常简单。以下列表显示了输出的顺序。

报价报价单在一个文件中输出。在报价的情况下,输出的顺序如下:

  • quotation

  • id

  • status

  • customer

  • note

  • modified

  • created

在报价的情况下,输出的顺序如下:

  • quotations

  • id

  • status

  • parent

  • description

  • qty

  • price

  • sum

  • modified

  • created

对于第一个项目,输入表名,这将允许您同时输出多个表。

在报价中创建导入和导出视图

首先,我们将添加按钮功能并修改导入和导出的代码(源文件:01_making_the_import_and_export_view_in_quotation/app/view/quotation/List.js):

Ext.define('MyApp.view.quotation.List', {
    ....
    initComponent: function() {
        ....
        Ext.apply(me, {
            tbar: {
                ....
            }, '-', {
                text     : 'Import',
                action   : 'import'
            }, {
                text     : 'Export',
                action   : 'export'
            }, '-', {
            ....

您现在应该看到以下表单:

![在报价中创建导入和导出视图

接下来,实现按钮的事件处理器(源文件:01_making_the_import_and_export_view_in_quotation/app/controller/quotation/List.js):

Ext.define('MyApp.controller.quotation.List', {
    ....
    init: function() {
        var me = this;
        me.control({
            ....
            'myapp-quotation-list button[action=import]': {
                'click': me.onImport
            },
            'myapp-quotation-list button[action=export]': {
                'click': me.onExport
            }
        });
    },
    onImport: function() {
        // import
    },
    onExport: function() {
        // export
    },

检测在按钮功能中设置的导入导出操作,然后将onImportonExport分配给各种点击事件。当然,我们将从头开始创建onImportonExport

准备导出服务器

为了下载文件,我们将创建quotation-export.php文件。该文件可以安装在任何位置,但这次我们将其放置在包含index.php文件的文档路由中。

导出报价数据的过程将由 PHP 实现。这是一个较长的过程,因此请参考源文件中的代码(源文件:02_preparing_the_server_side_for_export/quotation-export.php)。

执行 SQL 并将输出与之前构思的数据格式相匹配。

为了将文件名改为第二个,引用之后的文件名将包含年份、月份、日期、小时、分钟和秒。然后,它将以.tsv文件扩展名下载。请检查你是否可以直接输入quotation-export.php URL 来下载文件。

接下来,让我们在按下我们刚刚制作的按钮时移动这个 URL。(源文件:02_preparing_the_server_side_for_export/app/controller/quotation/List.js):.

onExport: function() {
    location.href='quotation-export.php';
},

创建导入的临时视图

接下来,我们将继续实现导入过程。就像之前解释的那样,上传你将要导入的数据,然后实现它,这样你就可以在视觉上检查后再导入。

让我们在按下导入按钮时触发myapp-import事件,并实现它以便面板显示以确认导入数据。

首先,让我们实现监听器。当这个事件发生时,URL 将更改为#!/quotation/import(源文件:03_making_a_temporary_view_for_import/app/controller/quotation/Quotation.js):

Ext.define('MyApp.controller.quotation.Quotation', {
    ....
    init: function() {
        ....
        me.control({
            'myapp-quotation-list': {
                ....
                'myapp-import': function() {
                    location.href = '#!/quotation/import';
                }
            }
        });

接下来,实现事件触发(源文件:03_making_a_temporary_view_for_import/app/controller/quotation/List.js):

Ext.define('MyApp.controller.quotation.List', {
    ....
    init: function() {
        var me = this;
        me.control({
            ....
            'myapp-quotation-list button[action=import]': {
                'click': me.onImport
            },
            'myapp-quotation-list button[action=export]': {
                'click': me.onExport
            }
        });
    },
    onImport: function() {
        var me          = this,
            listView    = me.getListView();

        listView.fireEvent('myapp-import');
    },
    ....

实现我们之前实现的onImport方法的内容。从List视图对象中,使用fireEvent触发myapp-import事件。

当这个事件发生时,URL 将会改变。接下来,修改onShow,以便导入面板显示出来(源文件:03_making_a_temporary_view_for_import/app/controller/quotation/Quotation.js):

....
onShow: function(p) {
    var me = this,
        o = {},
        hash = location.hash,
        layout = p.getLayout(),
        listView = me.getListView(),
        editView = me.getEditView(),
        params;
    params = hash.substr(('#!/' + me.screenName + '/').length);
    if(params === 'new') {
        o.id = params;
    } else {
        if(!params || !Ext.isString(params)) {
            params = '';
        }
        params = params.split('/');
        Ext.iterate(params, function(text) {
            if(!text || !Ext.isString(text)) {
                text = '';
            }
            text = text.split('=');
            o[text[0]] = text[1];
        });
    }
    me.requestParams = params = o;

    if(params.hasOwnProperty('import')) {
        // Show Import
        layout.setActiveItem(2);
    } else if(params.id === 'new' || Number(params.id)) {
        // Show Edit
        layout.setActiveItem(1);
    } else {
        // Show List
        layout.setActiveItem(0);
    }
    // fire event 'myapp-show'
    layout.activeItem.fireEvent('myapp-show', layout.activeItem, p, 
    params);
},
....

参数分析是通过我们之前制作的过程进行的。使用此过程,如果你有一个导入属性,导入面板将会显示。

我们还没有制作主要的导入面板。在这里,我们只是制作了一个非常简单的面板,以便暂时显示(源文件:03_making_a_temporary_view_for_import/app/view/quotation/Import.js):

Ext.define('MyApp.view.quotation.Import', {
    extend: 'MyApp.form.Panel',
    alias : 'widget.myapp-quotation-import',
    itemId: 'screen-quotation-import',
    initComponent: function() {
        var me = this;

        // TopToolbar
        Ext.apply(me, {
           tbar: [{
               text: 'Cancel',
               action: 'cancel'
           }, '-', {
               text: 'Upload',
               action: 'upload'
           }]
        });

        me.callParent(arguments);
    }
});

将此面板(app/view/quotation/Quotation.js)添加到screen面板项(源文件:03_making_a_temporary_view_for_import/app/view/quotation/Quotation.js)。

当你配置一个项时,你应该指定xtype。为此,设置它所需的类名,以便可以进行动态加载。

在这种状态下,执行整个应用程序。当你按下导入按钮时,将显示以下面板:

创建导入的临时视图

导入面板仍然是临时的,我们之前实现的网格面板还没有显示;但我们可以通过onShow事件处理器来检查它是否正在正确地变化。

创建导入的 CT 视图

我们还没有创建我们通常已经制作的 CT。所以,让我们制作 CT 来显示我们之前制作的临时导入面板(源文件:04_making_the_ct_view_for_import/ct/quotation/view_import.html)。

对于 HTML,如果我们复制不同的 HTML 并修改 JavaScript 文件以相同的方式读取,应该不会有任何问题(源文件:04_making_the_ct_view_for_import/ct/quotation/view_import.js

...
Ext.onReady(function() {
    Ext.create('MyApp.store.Customer');
    Ext.create('MyApp.store.QuotationItem');
    Ext.create('MyApp.view.quotation.Quotation', {
        activeItem: 2,
        width: 800,
        height: 600,
        renderTo: Ext.getBody()
    });
});

现在,让我们为这个面板准备一个网格。上传完成后,它将从 JSON 数据中接收显示数据。所以,让我们使用memory代理创建一个存储(源文件:04_making_the_ct_view_for_import/app/store/QuotationImport.js

Ext.define('MyApp.store.QuotationImport',{
    extend: 'Ext.data.Store',
    storeId:'QuotationImport',
    fields:['id', 'status', 'customer', 'note', 'modified', 'created'],
    data:{'items':[
    ]},
    proxy: {
        type: 'memory',
        reader: {
            type: 'json',
            root: 'items'
        }
    }
});

还可以使用以下代码为此面板准备一个网格(源文件:04_making_the_ct_view_for_import/app/store/QuotationImportItems.js

Ext.define('MyApp.store.QuotationImportItems',{
    extend: 'Ext.data.Store',
    storeId:'QuotationImportItems',
    fields:[
        'id',
        'status',
        'parent',
        'description',
        'qty',
        'price',
        'sum',
        'modified',
        'created'
    ],
    data:{'items':[
    ]},
    proxy: {
        type: 'memory',
        reader: {
            type: 'json',
            root: 'items'
        }
    }
});

我们创建两个memory代理的原因是因为我们将为QuotationQuotations各自准备一个,然后制作两个网格。现在,我们将在Grid中使用这个,但我们将添加以下代码以确保它在 CT 中无问题显示(源文件:04_making_the_ct_view_for_import/ct/quotation/view_import.js

为了确保我们之前制作的两个存储可以使用 CT 目录中的view_import.js脚本,让我们将其修改为在启动时可以实例化。

现在,我们将快速制作视图。我们将重新设计之前制作的临时Import面板(源文件:04_making_the_ct_view_for_import/app/view/quotation/Import.js

Ext.define('MyApp.view.quotation.Import', {
    extend: 'MyApp.form.Panel',
    alias : 'widget.myapp-quotation-import',
    itemId: 'screen-quotation-import',
    initComponent: function() {
        var me = this,
            store = Ext.getStore('QuotationImport'),
            itemstore = Ext.getStore('QuotationImportItems'); 

        // Items
        Ext.apply(me, {
            layout: {
                type: 'border',
                padding: 5
            },
            items: [{
                region: 'north',
                xtype: 'grid',
                itemId: 'quotationgrid',
                split: true,
                title: 'Quotation',
                store: store,
                columns: [{
                    text: 'id',
                    dataIndex: 'id',
                    width: 50
                }, {
                    text: 'status',
                    dataIndex: 'status',
                    width : 50
                }, {
                    text: 'customer',
                    dataIndex: 'customer'
                }, {
                    text: 'note',
                    dataIndex: 'note',
                    flex: 1
                }, {
                    text: 'modified',
                    dataIndex: 'modified'
                }, {
                    text: 'created',
                    dataIndex: 'created'
                }],
                flex:1
            }, {
                region: 'center',
                xtype: 'grid',
                title: 'Quotation Items',
                itemId: 'quotationitemsgrid',
                store: itemstore,
                columns: [{
                    text: 'id',
                    dataIndex: 'id',
                    width: 50
                }, {
                    text: 'status',
                    dataIndex: 'status',
                    width : 50
                }, {
                    text: 'parent',
                    dataIndex: 'parent',
                    width : 50
                }, {
                    text: 'description',
                    dataIndex: 'description',
                    flex: 1
                }, {
                    text: 'qty',
                    dataIndex: 'qty'
                }, {
                    text: 'price',
                    dataIndex: 'price'
                }, {
                    text: 'sum',
                    dataIndex: 'sum'
                }, {
                    text: 'modified',
                    dataIndex: 'modified'
                }, {
                    text: 'created',
                    dataIndex: 'created'
                }],
                flex:1
            }]
        });

        // TopToolbar
        Ext.apply(me, {
           tbar: [{
               text: 'Cancel',
               action: 'cancel'
           }, '-', {
               text: 'Upload',
               action: 'upload'
           }]
        });

        me.callParent(arguments);
    }
});

如前所述,我们将制作两个grid面板。这是为QuotationQuotations准备的。再次强调,布局是边框布局,并指定了弹性。

经常使用 Ext JS 的用户可能会觉得这很奇怪;但根据当前的 Ext JS,你甚至可以在边框布局中指定弹性。

因此,如果你为northcenter都设置flex:1,它将平均安排表单的顶部和底部;并且当north设置为split:true时,分割器可以显示。

如果检查显示,它将看起来像以下这样:

创建导入的 CT 视图

创建上传和显示网格中的数据

这次,上传你导出的数据,让我们在网格中显示它。为此,在app.js中添加一个控制器。你可能需要回忆一下,因为上次使用已经有一段时间了!(源文件:05_making_upload_and_show_data_in_the_grid/app/Application.js

....

Ext.application({
    ....
    controllers: [
        ....
        'quotation.Import',
        ....
    ],

现在,我们将制作这个控制器(源文件:05_making_upload_and_show_data_in_the_grid/app/controller/quotation/Import.js

Ext.define('MyApp.controller.quotation.Import', {
    extend: 'MyApp.controller.Abstract',
    refs: [{
        ref: 'importView', selector: 'myapp-quotation-import' 
    }],
    stores: [
        'QuotationImport',
        'QuotationImportItems'
    ],
    init: function() {
        var me = this;
        me.control({
            'myapp-quotation-import': {
                'myapp-show': me.onShow,
                'myapp-hide': me.onHide
            },
            'myapp-quotation-import [action=upload]': {
                'change': me.onUploaded
            }
        });
    },
    onShow: function(p, owner, params) {
    },
    onHide: function() {
    },
    onUploaded: function(fb, v) {
        var me = this,
            p = me.getImportView(),
            form = p.getForm(),
            importView = me.getImportView(),
            btnExecute = importView.down('button[action=execute]');

        p.setLoading();
        btnExecute.disable();
        form.submit({
            success: function(form, action) {
                Ext.getStore('QuotationImport').loadData(action.
                result.quotation.items);
                Ext.getStore('QuotationImportItems').loadData(action.
                result.quotations.items);
                p.setLoading(false);
                btnExecute.enable();
            }
        });
    }
});

然后,暂时将之前创建的upload按钮更改为filefield(源文件:05_making_upload_and_show_data_in_the_grid/app/view/quotation/Import.js

Ext.define('MyApp.view.quotation.Import', {
    extend: 'MyApp.form.Panel',
    alias : 'widget.myapp-quotation-import',
    itemId: 'screen-quotation-import',
    api: {
        submit  : 'MyAppQuotation.importData'
    },
    paramOrder: ['importfile'],
    initComponent: function() {
        ....

        // TopToolbar
        Ext.apply(me, {
           tbar: [{
               text: 'Cancel',
               action: 'cancel'
           }, '-', {
               xtype: 'filefield',
               name: 'importfile',
               buttonText: 'Upload',
               buttonOnly: true,
               hideLabel: true,
               action: 'upload'
           }]
        });

        me.callParent(arguments);
    }
});

此外,在配置选项中添加API密钥,并在submit中指定Direct函数。这个面板是一个表单面板。使用submit方法上传文件。接下来,制作我们在submit中指定的Direct函数(源文件:05_making_upload_and_show_data_in_the_grid/php/config.php

我们将向MyAppQuotation类添加以下方法:

            'importData'=>array(
                'len'=>2,
                'formHandler'=>true
            )

我们将实际实现之前添加的方法。由于篇幅过长,无法包含在此文本中,请参考源代码文件(源文件:05_making_upload_and_show_data_in_the_grid/php/classes/MyAppQuotation.php)。

再次使用 JSON 返回上传数据。如果你打算构建一个应用程序,你应该执行输入检查,因为用户可以使用 TSV 文件输入他们喜欢的任何信息。

使用你之前实现的控制器,使用loadData方法并将从服务器端返回的数据读取到MemoryStore方法中。

执行导入数据

一旦你在网格中显示了读取的数据并检查了它,你就可以创建一个按钮将其应用到数据库中(源文件:06_execute_import_data /app/view/quotation/Import.js):

Ext.define('MyApp.view.quotation.Import', {
     ....
     initComponent: function() {
        ....

        // TopToolbar
        Ext.apply(me, {
           ....
           }, {
               text: 'Execute',
               disabled: true,
               action: 'execute'
           }]
        });

        me.callParent(arguments);
    }
});

让我们确保在初始状态下无法按下按钮。一旦之前的数据上传完成,按钮将被启用(源文件:06_execute_import_data /app/controller/quotation/Import.js):

Ext.define('MyApp.controller.quotation.Import', {
    ....
    init: function() {
        var me = this;
        me.control({
            ....
            'myapp-quotation-import [action=execute]': {
                'click': me.onExecute
            }
        });
    },
    ....
    onExecute: function() {
        var data = {
                quotation: [],
                quotations: [] 
            },
            store = Ext.getStore('QuotationImport'),
            items = Ext.getStore('QuotationImportItems');

        Ext.iterate(store.data.items, function(item) {
            data.quotation.push(Ext.clone(item.data));
        });

        Ext.iterate(items.data.items, function(item) {
            data.quotations.push(Ext.clone(item.data));
        });

        MyAppQuotation.executeImport(data, function() {
            location.href = '#!/quotation';
        });
    }
});

现在,我们正在调用executeImportDirect函数。没错,我们还没有完成。让我们实现这个功能,使其能够从客户端接收数据(源文件:06_execute_import_data /php/config.php):

    ....
    'MyAppQuotation'=>array(
        'methods'=>array(
            ....
            'executeImport'=>array(
                'len'=>1
            )
        )
    ),
    ....

我们将在config.php文件中实现之前添加的方法。同样,由于篇幅过长,无法在此展示,请参考源文件(源文件:06_execute_import_data /php/classes/MyAppQuotation.php)。

当数据被接收后,使用Truncate一次性清空数据库内部,然后插入新的数据。这样做是好是坏将取决于你构建的系统。

此外,如果你仔细查看代码,你会知道错误处理并没有达到高级别。如果你打算将这个项目商业化,你将不得不构建这个区域。已经,实现已经完成。但最后,让我们将 URL 更改为#!/quotation以将其更改为列表显示,然后完成。

摘要

我们已经完成了ExportImport数据的实现。还有许多要点需要覆盖,例如输入检查和错误检查。但学习了这个流程后,我认为你现在有了按照你想要的方式定制这个数据库的基础。

干得好!你已经读完了这本书的最后一页!

让我们回顾一下你所取得的成就:

  • 你创建了数据结构

  • 你创建了应用程序架构

  • 你通过使用浏览器上的网格和字段进行了输入和输出

  • 你开发了使用各种图表获取信息的应用程序

  • 你完成了数据导出和导入管理功能

每个功能对于此类应用程序都是基本的。从现在开始,如果你在下一个项目中考虑创建类似的东西,请将这个示例应用程序作为你应用程序的基础。你只需要更改几个点,然后你应该能够非常快速地创建一个定制应用程序。

将本书中的架构应用到你所制作或即将制作的应用程序中,将简化控制并提高维护性。它还将为你提供高级且通用的历史管理功能,并让你轻松添加新屏幕。

然而,本书中介绍的这个示例应用程序在实际应用中需要一些额外的功能。因此,我们在本章的结尾添加了一些内容,以完善这个应用程序。

本章的示例代码文件夹中可以找到额外编辑的代码文件列表。请参考此列表,查看我们对应用程序所做的最终修改。

此外,我们在任何修改旁边都包含了注释 // update code。只需在代码中搜索这个注释,你应该能够立即看到更改。

最后,记住,如果你在未来的某个项目中迷失了方向,或者遇到了难以逾越的障碍,只需再次查看这个应用程序,看看它是否能为你提供某种解决方案。

我希望你们能够创造出一些令人惊叹的应用程序!祝你们好运,编码愉快!

posted @ 2025-09-26 22:10  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报