$ brew install openjdk@25
$ brew install git
安装指南
环境要求
在开始之前,请确保您的计算机上已安装以下工具:
-
Java (JDK 25 …)
-
Git
安装前置依赖
macOS (Homebrew)
Windows (Winget)
winget install --id Microsoft.OpenJDK.25
winget install --id Git.Git
克隆开箱即用的内网项目
$ git clone https://github.com/Taack/intranet.git
该仓库包含一个最小化的内网骨架(Crew 用户管理 + Spring Security 插件),可直接用于开发。
启动您的内网
$ cd intranet
$ ./gradlew server:bootRun
|
|
Grails application running at http://localhost:9442
TODO: 在浏览器中访问该地址以进入内网。 请使用默认登录凭据:
-
用户名:
admin -
密码:
ChangeIt
若要修改默认的管理员密码,请编辑以下文件:
-
server/grails-app/conf/application.yml -
server/grails-app/conf/application-MACOSX.yml
找到以下行:
taack.admin.password: ChangeIt
完成修改后,删除 intranetDb.mv.db 文件以使新密码生效。
|
|
|
删除 |
配置持久化数据库
如果您希望持久化保存数据,请修改 server/grails-app/conf/application.yml 和
server/grails-app/conf/application-MACOSX.yml 文件以使用持久化数据库。
environments:
development:
dataSource:
dbCreate: update (1)
url: jdbc:h2:./intranetDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE (2)
| 1 | 创建模式(请参阅 Grails GORM 文档) |
| 2 | intranetDb:数据文件名的根名称 |
生成用于 Tomcat 的 Jar 文件
$ ./gradlew server:assemble
打包好的 JAR 文件(例如 server-0.6.jar)将位于 server/build/libs 目录下。
cd server/build/libs
java -jar server-[version].jar (1)
| 1 | 版本号位于 build.gradle 中,默认值为 0.6 |
|
启动新实例时,请确保该应用程序尚未在运行。 |
配置您的 IDE
我们强烈建议使用最新版本的 IntelliJ Ultimate Edition,以获得对 Groovy 和 Grails 的全面支持。
IntelliJ Ultimate Edition
我们建议安装 IntelliJ Taack 插件,您可以从 IntelliJ 插件市场 TaackUiASTAutocomplete 安装,或直接从 源代码仓库安装。
打开项目:依次点击 File > Open… 并选择 intranet/settings.gradle。
确保 Gradle 使用的是 JDK 25:

| 1 | 打开 Gradle 设置 |
| 2 | 确认 Gradle JVM 版本为 25(发行版提供商不限) |
Visual Studio Code
如果您更倾向于使用 Visual Studio Code,我们建议安装以下扩展来配合本框架使用:
|
VS Code 无法识别 Grails 和 Taack 框架的大部分特性,包导入、代码导航和代码提示功能可能会受到限制。为了获得完整支持,IntelliJ 仍然是首选 IDE。 |
第一个应用
应用如何与 Taack 框架配合工作?
您的内网项目包含两个部分:
-
位于
app目录下的应用插件是独立的微项目,每个微项目都有自己的依赖和构建配置等(例如 Crew)。 有关如何声明插件的更详细说明,请参阅 关于插件。 -
主服务器会引入其
build.gradle中声明的所有应用插件,并将它们显示在内网主页上。
创建新的应用插件
若要创建新的应用插件(即一个 Gradle 模块),请确保您已从我们的 GitHub 仓库拉取了最新版本的内网代码(请参阅 安装指南)。
$ ./gradlew -DmodName=myBooks server:generateTaackAppTask
这将创建一个新目录 app/myBooks。
|
在 |
为了让您的 IDE 识别新创建的模块,请点击下图所示的图标重新加载 Gradle:
管理后续更新
大多数情况下,更新您的内网只需更新 buildSrc 目录、gradle.properties 文件,极少数情况下才需要更新各个应用的 build.gradle 文件。
|
事实上,这种流程在长期软件开发中非常常见,新手无需对此感到担心。 |
构建应用全局菜单
TODO: 我们将构建两个菜单:一个用于书籍,另一个用于作者。在每个菜单中,您都可以添加新条目、进行过滤,并在表格中查看所有记录。
让我们先关注新应用 myBooks 的导航栏。
第一步是定义如何构建菜单。请打开 grails-app/services/my/books/MyBooksUiService。
static UiMenuSpecifier buildMenu(String q = null) { (1)
new UiMenuSpecifier().ui {
menu MyBooksController.&index as MC
}
}
| 1 | 如果还没有 static 关键字,请将其加上。 |
我们创建一个 UiMenuSpecifier 对象来定义菜单结构,然后指定要显示的内容。
menu MyBooksController.&index as MC
在这个例子中,menu 在导航栏中创建了一个简单的链接。index 是该链接显示的名称,而 MyBooksController.&index(使用 MethodClosure)则定义了点击链接后跳转的目标操作(Action)。
创建领域类
在 grails-app/domain/my/books 中创建一个名为 Author 的 Groovy 类。
import grails.compiler.GrailsCompileStatic
import taack.ast.annotation.TaackFieldEnum
@GrailsCompileStatic
@TaackFieldEnum
class Author {
String firstName
String lastName
Date dateOfBirth
}
在 grails-app/domain/my/books 中再创建一个名为 Book 的 Groovy 类。
import grails.compiler.GrailsCompileStatic
import taack.ast.annotation.TaackFieldEnum
@GrailsCompileStatic
@TaackFieldEnum
class Book {
String title
String authorName
int numberOfPages
}
|
为了兼顾性能与类型安全,请始终使用 |
创建表格
现在让我们在 listBook 页面中显示一个表格。请打开 grails-app/services/my/books/MyBooksUiService。
buildBookTable 的表格描述器(Specifier):UiTableSpecifier buildBookTable() {
Book book = new Book()
UiTableSpecifier bookTableSpecifier = new UiTableSpecifier()
bookTableSpecifier.ui {
//Add table content inside the closure here
}
}
这里我们定义了一个用于列出 Book 实例的新表格。请使用 import taack.ui.dsl.UiTableSpecifier。
bookTableSpecifier.ui {
// -- Header --
header {
column {
sortableFieldHeader book.title_ (1)
}
column {
label book.authorName_
}
column {
label book.numberOfPages_
}
column {
label "Delete book"
}
}
}
| 1 | 注意这里我们使用了 sortableFieldHeader,这会使该列支持排序,您可以通过点击 book.title 表头按字母顺序对书籍进行排序。
接下来我们将填充表格,使用表格 DSL 的 iterate 方法遍历数据库中的 Book 实例。 |
bookTableSpecifier.ui {
//table headers...
iterate(taackFilterService.getBuilder(Book) (1)
.setMaxNumberOfLine(8) (2)
.setSortOrder(TaackFilter.Order.DESC, book.title_) (3)
.build()) { Book bookIterator ->
rowColumn {
rowField bookIterator.title_ (4)
}
rowColumn {
rowField bookIterator.authorName_
}
rowColumn {
rowField bookIterator.numberOfPages_
}
rowColumn {
rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN, (5)
MyBooksController.&deleteBook as MethodClosure, bookIterator.id
}
}
}
| 1 | 请使用 import taack.domain.TaackFilterService。 |
| 2 | 每页仅显示前 8 本书。 |
| 3 | 设置按书名降序排列显示(请使用 import taack.domain.TaackFilter)。 |
| 4 | 此处末尾的下划线是必需的。 |
| 5 | 请使用 import taack.ui.dsl.common.ActionIcon
import taack.ui.dsl.common.IconStyle。 |
|
我们需要创建 |
打开 grails-app/controllers/my/books/MyBooksController.groovy。
index 操作之后添加以下方法:def deleteBook () {}
对于列表中的每一本书,我们都会生成新的一行:第一列显示书名,随后是作者、页数,第四列则是一个 Delete 按钮。
现在表格已经构建完成,我们只需将其渲染到页面上即可。
为了渲染前面构建好的 UiSpecifier,我们需要使用 taackUiService,它通常已经由 create-taack-app 命令在生成的控制器中自动导入了。
我们将在名为 listBook 的操作中完成渲染,并将 index 方法的内容替换为重定向到 listBook 操作。
listBook 操作:def index() {
redirect action: 'listBook'
}
listBook 的操作并添加以下代码:UiTableSpecifier tableBookSpecifier = myBooksUiService.buildBookTable() (1)
taackUiService.show(new UiBlockSpecifier().ui {
table tableBookSpecifier
}, MyBooksUiService.buildMenu())
| 1 | 从 MyBooksUiService 获取我们创建的 tableSpecifier。 |
请使用 import taack.ui.dsl.UiTableSpecifier。
taackUiService.show(UiBlockSpecifier block, UiMenuSpecifier menu) 负责渲染我们传入的界面描述。
我们将前面在 MyBooksUiService 中创建的静态方法 buildMenu() 作为 show() 的第二个参数,以便在页面上同时渲染菜单。
现在您可以启动服务器并访问您的新应用(myBooks.app)。
此时表格已经可以正常工作了,但由于数据库中还没有任何书籍,您目前只能看到表头。
接下来让我们继续创建表单并将对象保存到数据库中。
向表格区块添加按钮
让我们在书籍表格中添加一个按钮,点击后通过 AJAX 打开模态窗口以创建新书。
为此,我们需要在 table 中添加一个闭包。
taackUiService.show(new UiBlockSpecifier().ui {
table tableBookSpecifier, {
//Added Closure here
menuIcon ActionIcon.CREATE, this.&bookForm as MethodClosure (1)
}
}, MyBooksUiService.buildMenu())
| 1 | 您需要像定义 deleteBook 一样定义 bookForm 操作。 |
别忘了使用 import taack.ui.dsl.common.ActionIcon。
现在启动服务器并再次访问您的新应用,您将在表格右上角看到一个 Create 按钮。
menuIcon 方法包含以下参数:
-
按钮使用的图标,必须是
ActionIcon枚举值。 -
点击按钮后跳转的目标操作。
创建表单并保存对象
现在我们来构建用于创建和更新书籍的表单。
为了同时处理这两种情况,我们将在 MyBooksUiService 中判断请求参数是否传入了书籍 ID:若有则读取该书籍,否则初始化一个新的 Book 实例。
buildBookForm 的表单描述器:UiFormSpecifier buildBookForm(Book book) {
book ?= new Book(params) (1)
}
| 1 | 这表示如果 book 为 null,则使用请求参数为其赋一个新的实例。 |
接下来我们定义表单及其内容。
buildBookForm 中添加以下代码(并导入所需的类):UiFormSpecifier bookFormSpecifier = new UiFormSpecifier()
bookFormSpecifier.ui book, {
//Section of fields
section "Book details", {
field book.title_
field book.authorName_
field book.numberOfPages_
}
//Save button
formAction MyBooksController.&saveBook as MethodClosure (1)
}
| 1 | 您需要在 MyBooksController 中定义 saveBook 操作(与 deleteBook 类似)。 |
请使用 import taack.ui.dsl.UiFormSpecifier。
定义好表单后,您可以使用 taackUiService.show() 方法将其显示出来。
MyBooksController 的 bookForm 方法中添加以下代码:def bookForm(Book book) {
UiFormSpecifier tableFormSpecifier = myBooksUiService.buildBookForm book
taackUiService.show new UiBlockSpecifier().ui {
modal {
form tableFormSpecifier
}
}
}
请使用 import taack.ui.dsl.UiFormSpecifier。
这次我们在调用 show 时没有传入 buildMenu,因为我们不希望在模态窗口内部渲染菜单。
saveBook 操作(导入并初始化所需的类):@Transactional
def saveBook() {
taackSaveService.saveThenReloadOrRenderErrors(Book)
}
请使用 import taack.render.TaackSaveService 和 import grails.gorm.transactions.Transactional。
|
请参阅 关闭模态窗口并重新加载页面,了解如何在保存时避免使用 |
再次启动服务器,现在您应该可以点击按钮并填写表单了。
|
在左上角,您可以使用页码(1 或 2)在不同批次的书籍之间切换。出现该分页导航是因为列表限制为每页显示 8 本书。 |
查看对象详情
现在我们已经可以创建书籍并在表格中查看列表了,接下来让我们使用模态窗口展示每本书的详细信息。
同样地,我们先定义描述器,然后使用 taackUiService.show() 将其渲染在模态窗口区块中。
showBook(并导入所需的类):def showBook(Book book) {
// Define the show displayed fields
UiShowSpecifier showSpec = new UiShowSpecifier().ui({
fieldLabeled book.title_
fieldLabeled book.authorName_
})
taackUiService.show(new UiBlockSpecifier().ui {
modal {
show showSpec
}
})
}
请使用 import taack.ui.dsl.UiShowSpecifier。
我们还需要在表格中显示打开该模态窗口的链接。为此,请在 MyBooksUiService 中您希望按钮出现的同一个 rowColumn 内(书名字段附近)添加以下行:
buildBookTable 中添加 2 行代码:UiTableSpecifier buildBookTable() {
//Section of code
rowColumn {
//Add these 2 lines
rowAction ActionIcon.SHOW * IconStyle.SCALE_DOWN, (1)
MyBooksController.&showBook as MethodClosure, bookIterator.id
rowField bookIterator.title_
}
//Section of code
}
| 1 | 这里我们使用乘法运算符缩小了图标的尺寸。 |
这将在表格单元格中创建一个_小型_图标按钮,点击后会打开包含对应书籍详情的模态窗口。
注意,此处通过将 ActionIcon 与 IconStyle 相乘来调整图标大小。
点击眼睛图标即可显示书籍详情。
删除对象
还记得我们在表格中添加的 Delete 按钮吗?
现在让我们通过定义它所调用的 deleteBook 操作,使 Delete 按钮真正生效。
@Transactional
def deleteBook(Book book) {
book.delete()
redirect action: 'index'
}
|
在某些场景下,最好添加一个 |
我们使用 Grails 的 delete 方法从数据库中删除该书籍,然后重定向到包含书籍表格的 index 操作。
请注意,此时字段名称还不是直观的标签,而是像 book.numberOfPages.label,default.numberOfPages.label 这样的 i18n 键(指向 messages.properties 中的国际化键)。让我们来修改它!
命名表头与标签
若要设置表头名称,请打开 app/myBooks/grails-app/i18n/messages.properties。
default.numberOfPages.label 及其他标签设置为您想要的名称:default.authorName.label=Author Name
default.title.label=Title
default.numberOfPages.label=Number of Pages
myBooks.index.label=Books
myBooks.app=Book
myBooks.desc=Book storage
运行服务器即可查看更新后的标签名称。
过滤数据
如果添加的书籍过多,查找特定书籍可能会耗费大量时间,通过过滤功能可以让书籍搜索更加高效。让我们在 MyBooksUiService 中实现这一功能。
buildBookFilter 的新过滤器描述器:UiFilterSpecifier buildBookFilter(Book book) {
UiFilterSpecifier bookFilterSpecifier = new UiFilterSpecifier() (1)
bookFilterSpecifier.ui Book, { (2)
section "Book Filter", { (3)
filterField book.title_ (4)
}
}
}
| 1 | 创建过滤器。请使用 import taack.ui.dsl.UiFilterSpecifier。 |
| 2 | 通过 ui 方法传入对象类以及包含过滤器规格定义的闭包。 |
| 3 | 创建一个标题为 "Book Filter" 的分区。 |
| 4 | 向过滤器中添加字段,请注意字段名末尾的*下划线*。 |
下一步是将刚刚创建的过滤器显示出来。
listBook(位于 MyBooksController 中)里的 taackUiService.show:def listBook() {
Book book = new Book()
UiTableSpecifier tableBookSpecifier = myBooksUiService.buildBookTable()
UiFilterSpecifier filterBookSpecifier = myBooksUiService.buildBookFilter book
taackUiService.show(new UiBlockSpecifier().ui {
tableFilter filterBookSpecifier, tableBookSpecifier, {
menuIcon ActionIcon.CREATE, this.&bookForm as MethodClosure
}
}, MyBooksUiService.buildMenu())
}
请使用 import taack.ui.dsl.UiFilterSpecifier。
有关过滤功能的更多详细信息,请参阅过滤器与表格文档。
添加第二个菜单
还记得我们一开始创建的 Author 类吗?现在是时候使用它了!我们将创建第二个菜单,用于添加和过滤作者。
MyBooksUiService 的 buildMenu 中添加以下代码行:menu MyBooksController.&listAuthor as MC
这将在左上角创建第二个菜单,用于访问作者页面。
|
您也可以在主菜单中创建嵌套的子菜单。
这会创建一个名为 "Menu" 的子菜单,其中包含指向 |
MyBooksUiService 中添加以下构建方法:UiTableSpecifier buildAuthorTable(Boolean isSelect = false ) {
Author author = new Author()
UiTableSpecifier authorTableSpecifier = new UiTableSpecifier()
authorTableSpecifier.ui {
header {
column {
sortableFieldHeader author.firstName_
}
column {
sortableFieldHeader author.lastName_
}
column {
label author.dateOfBirth_
}
column {
label "Delete Author"
}
}
iterate(taackFilterService.getBuilder(Author)
.setMaxNumberOfLine(8)
.setSortOrder(TaackFilter.Order.DESC, author.lastName_)
.build()) { Author authorIterator ->
rowColumn {
rowField authorIterator.firstName_
rowAction ActionIcon.SHOW * IconStyle.SCALE_DOWN,
MyBooksController.&showAuthor as MethodClosure, authorIterator.id
}
rowColumn {
rowField authorIterator.lastName_
}
rowColumn {
rowField authorIterator.dateOfBirth_
}
rowColumn {
rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN,
MyBooksController.&deleteAuthor as MC, authorIterator.id
}
}
}
}
UiFormSpecifier buildAuthorForm(Author author) {
author ?= new Author(params)
UiFormSpecifier authorFormSpecifier = new UiFormSpecifier()
authorFormSpecifier.ui author, {
section "Author details", {
field author.firstName_
field author.lastName_
field author.dateOfBirth_
}
formAction MyBooksController.&saveAuthor as MC
}
}
UiFilterSpecifier buildAuthorFilter() {
Author author = new Author()
UiFilterSpecifier authorFilterSpecifier = new UiFilterSpecifier()
authorFilterSpecifier.ui Author, {
section "Author Filter", {
filterField author.lastName_
}
}
}
注意 buildAuthorTable 中的 isSelect 参数将在稍后用到。
MyBooksController 中创建 listAuthor 方法:def listAuthor() {
UiTableSpecifier tableAuthorSpecifier = myBooksUiService.buildAuthorTable()
UiFilterSpecifier filterAuthorSpecifier = myBooksUiService.buildAuthorFilter()
taackUiService.show(new UiBlockSpecifier().ui {
tableFilter filterAuthorSpecifier, tableAuthorSpecifier, {
menuIcon ActionIcon.CREATE, this.&authorForm as MethodClosure
}
}, MyBooksUiService.buildMenu())
}
deleteAuthor、saveAuthor、authorForm 和 showAuthor 方法:def authorForm(Author author) {
UiFormSpecifier tableFormSpecifier = myBooksUiService.buildAuthorForm author
taackUiService.show new UiBlockSpecifier().ui {
modal {
form tableFormSpecifier
}
}
}
@Transactional
def deleteAuthor(Author author) {
author.delete()
redirect action: 'index'
}
@Transactional
def saveAuthor() {
taackSaveService.saveThenReloadOrRenderErrors(Author)
}
def showAuthor(Author author) {
UiShowSpecifier showSpec = new UiShowSpecifier().ui(author, {
fieldLabeled author.firstName_
fieldLabeled author.lastName_
})
taackUiService.show(new UiBlockSpecifier().ui {
modal {
show showSpec
}
})
}
default.firstName.label=First Name
default.lastName.label=Last Name
default.dateOfBirth.label=Date of Birth
myBooks.listAuthor.label= Authors
现在您已经拥有了独立的书籍菜单和作者菜单,但它们目前还是各自独立运行的。如果在创建书籍时,可以直接从已注册的作者列表中进行选择会怎样?让我们来实现这个功能。
将作者关联到书籍
Author 类中添加以下方法:String toString() {
return firstName + ' ' + lastName
}
Book 类中的成员变量:Author author
|
请将 |
|
强烈建议不要随意修改类的名称或字段,因为这需要清除包含先前已添加数据的数据库文件。
您必须删除 |
messages.properties 中添加一行:default.author.label=Author Name
MyBooksUiService 中的 buildAuthorTable:rowColumn {
rowField authorIterator.firstName_
rowAction ActionIcon.SHOW * IconStyle.SCALE_DOWN,
MyBooksController.&showAuthor as MethodClosure, authorIterator.id
// Add the following if statement
if (isSelect)
rowAction tr('default.role.label'), ActionIcon.SELECT * IconStyle.SCALE_DOWN, authorIterator.id, authorIterator.toString()
}
这将添加一个选择按钮,以便在填写 bookForm 时选择作者。请使用 import static taack.render.TaackUiService.tr。
MyBooksUiService 中的 buildBookForm,替换 field book.author_:ajaxField book.author_, MyBooksController.&selectAuthor as MC
当在 bookForm 中选择作者时,该行代码会调用 selectAuthor。
接下来让我们实现 selectAuthor。
MyBooksController 中实现 selectAuthor:def selectAuthor() {
UiTableSpecifier t = myBooksUiService.buildAuthorTable true (1)
UiFilterSpecifier f = myBooksUiService.buildAuthorFilter()
taackUiService.show new UiBlockSpecifier().ui {
modal {
tableFilter f, t
}
}
}
| 1 | 注意此处 isSelect 为 true。
当为书籍表单选择作者时,这会渲染一个紧凑版的 Author 表格及其过滤器。 |
现在,您已经使用 Taack DSL 框架为 Book 和 Author 实体构建了功能完备的 CRUD 界面,而无需手动编写任何 HTML 或 GSP 视图。
现在您已经准备好探索 Taack UI 框架的更多高级特性了。
欢迎使用!
故障排除与常见错误
ERROR: Cannot resolve symbol 'MyBooksUiService'
您在创建插件后忘记重新加载 Gradle 了。请点击 IDE 中的 Gradle 重新加载按钮。
ERROR: Field 'authorName_' not found
您将 authorName 字段重命名为了 author(指向 Author 的引用)。请确保已在表单和表格中将所有的 authorName_ 替换为 author_。
ERROR: No matching method for action...
请确保已在 MyBooksController 中声明了对应的方法,并且正确使用了 MethodClosure 语法:MyBooksController.&myMethod as MethodClosure。
您可能遗漏了 saveBook 方法上的 @Transactional 注解,或者忘记导入 taackSaveService。
如果您修改了领域类中的字段,则必须删除数据库文件:
intranet/server/intranetDb.mv.db
intranet/server/intranetDb.trace.db。请注意,这会清除之前保存的所有数据。
您可能使用了错误的 @Transactional 注解。请注意应当使用来自 GORM 包的注解,而不是 Javax 包中的注解。
您可能希望检查内嵌 H2 数据库的数据完整性和生成的 DDL。
为此,您需要调整 Spring Security 的配置。
请前往 Root → /server → /grails-app → /conf
您会找到一个 application.groovy 文件。
将
[pattern: '/h2-console/**', access: ['permitAll']],
添加到以下配置块中:
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
[pattern: '/', access: ['permitAll']],
[pattern: '/error', access: ['permitAll']],
[pattern: '/index', access: ['permitAll']],
[pattern: '/index.gsp', access: ['permitAll']],
[pattern: '/shutdown', access: ['permitAll']],
[pattern: '/assets/**', access: ['permitAll']],
[pattern: '/**/js/**', access: ['permitAll']],
[pattern: '/**/css/**', access: ['permitAll']],
[pattern: '/**/images/**', access: ['permitAll']],
[pattern: '/**/favicon.ico', access: ['permitAll']]
]