发布v2.3.5版本,本次为优化版本。更新内容请查看:https://github.com/bufanyun/hotgo/blob/v2.0/docs/guide-zh-CN/start-update-log.md

This commit is contained in:
孟帅
2023-02-26 14:18:22 +08:00
parent 34c373c11e
commit ab912d0ba6
111 changed files with 3068 additions and 9329 deletions

View File

@@ -5,7 +5,7 @@
- [系统介绍](../../README.md)
- [环境搭建](start-environment.md)
- [系统安装](start-installation.md)
- 生产部署
- [生产部署](start-deploy.md)
- [如何提问](start-questions.md)
- [常见问题](start-issue.md)
- [更新历史](start-update-log.md)
@@ -14,11 +14,12 @@
#### 系统开发
- [目录结构](sys-catalog.md)
- 开发规范
- [开发规范](sys-exploit.md)
- [控制台](sys-console.md)
- 请求中间件和WebHook
- 权限控制
- 代码生成
- [权限控制](sys-auth.md)
- [数据库](sys-db.md)
- [代码生成](sys-code.md)
- 定时任务
- [消息队列](sys-queue.md)
- [功能扩展库](sys-library.md)
@@ -40,11 +41,10 @@
### 前端开发
- 表单组件
- 权限
- [表单组件](web-form.md)
- Websocket客户端
- 工具库
- 发布部署
- [独立部署](web-deploy.md)
#### 附录
- [网址收录](append-website.md)

View File

@@ -14,10 +14,21 @@
1、HotGo 后台进入 开发工具->插件管理->找到创建新插件,根据引导进行创建即可。
> 创建成功后会在 根目录的 addons 目录下生成插件文件
```
创建成功后默认情况下会在以下目录中生成插件文件假设新生成的插件名为hgexample
1. /server/addons/hgexample/ # 插件模块目录
2. /server/addons/modules/hgexample.go # 隐式注册插件文件
3. /server/resource/template/addons/hgexample # pc模板目录
4. /web/src/api/addons/hgexample # webApi目录
5. /web/src/views/addons/hgexample # web页面目录
# 默认情况下没有为web页面生成菜单权限因为在实际场景中插件不一定需要用到web页面所以如有需要请手动到后台 权限管理 -> 菜单权限->自行添加菜单和配置权限
```
2、创建插件完毕重启服务端后插件管理中会出现你新创建的插件信息。操作栏有几个按钮在此进行说明
- 安装:会自动执行 server/xxx插件/main.go 文件中的Install方法方法中的具体逻辑默认为空可以根据实际情况自行配置。如生成后台菜单、生成插件配置表初始化数据、迁移home页面、web项目文件等。
- 安装:会自动执行 server/hgexample/main.go 文件中的Install方法方法中的具体逻辑默认为空可以根据实际情况自行配置。如生成后台菜单、生成插件配置表初始化数据、迁移home页面、web项目文件等。
```
// Install 安装模块
func (m *module) Install(ctx context.Context) (err error) {
@@ -26,7 +37,7 @@ func (m *module) Install(ctx context.Context) (err error) {
}
```
- 更新:会自动执行 server/xxx插件/main.go 文件中的Upgrade方法方法中的具体逻辑默认为空可以根据实际情况自行配置。
- 更新:会自动执行 server/hgexample/main.go 文件中的Upgrade方法方法中的具体逻辑默认为空可以根据实际情况自行配置。
```
// Upgrade 更新模块
func (m *module) Upgrade(ctx context.Context) (err error) {
@@ -35,7 +46,7 @@ func (m *module) Upgrade(ctx context.Context) (err error) {
}
```
- 卸载:会自动执行 server/xxx插件/main.go 文件中的UnInstall方法方法中的具体逻辑默认为空可以根据实际情况自行配置。如会清除所有的数据表和已安装的信息等。
- 卸载:会自动执行 server/hgexample/main.go 文件中的UnInstall方法方法中的具体逻辑默认为空可以根据实际情况自行配置。如会清除所有的数据表和已安装的信息等。
```
// UnInstall 卸载模块
func (m *module) UnInstall(ctx context.Context) (err error) {
@@ -57,7 +68,7 @@ func (m *module) UnInstall(ctx context.Context) (err error) {
一个简单的例子:
> 假设hgexample插件模块要通过主模块的服务接口更新插件配置
文件\server\addons\hgexample\model\input\sysin\config.go
插件模块input\server\addons\hgexample\model\input\sysin\config.go
```go
package sysin

View File

@@ -8,6 +8,7 @@
#### 模块结构
- 文件路径server/internal/library/addons/module.go
```go
// Skeleton 模块骨架
type Skeleton struct {
@@ -74,7 +75,7 @@ func test(ctx context.Context) {
}
```
- 更多辅助方法请参考:\server\internal\library\addons
- 更多辅助方法请参考插件功能库server/internal/library/addons
#### 插件路由规则
- 如果你不喜欢现在的路由风格,可以自行调整。修改位置在:\server\internal\library\addons\addons.go的RouterPrefix方法。

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -0,0 +1,94 @@
## 生产部署
目录
- 编译配置
- 编译
- 修改生产配置文件
- 启动服务
- Nginx配置
### 编译配置
- 配置文件server/hack/config.yaml以下是默认配置
```yaml
gfcli:
build:
name: "hotgo" # 编译后的可执行文件名称
# arch: "all" #不填默认当前系统架构可选386,amd64,arm,all
# system: "all" #不填默认当前系统平台可选linux,darwin,windows,all
mod: "none"
cgo: 0
packSrc: "resource" # 将resource目录打包进可执行文件静态资源无需单独部署
packDst: "internal/packed/packed.go" # 打包后生成的Go文件路径一般使用相对路径指定到本项目目录中
version: ""
output: "./temp/hotgo" # 可执行文件生成路径
extra: ""
```
### 编译
- 以下方式任选其一即可
1、 make一键编译 linux或mac环境推荐
```shell
cd server && make build
```
2、 按步骤手动编译
```shell
cd server # 切换到服务端目录下
rm -rf ./resource/public/admin/ # 删除之前的web资源
mkdir ./resource/public/admin/ # 重新创建web资源存放目录除首次编译后续可以跳过执行此步骤
cd ../web && yarn build # 切换到web项目下编译web项目
\cp -rf ./dist/* ../server/resource/public/admin/ # 将编译好的web资源复制到server对应的资源存放路径下
echo "y" | gf build # 编译hotgo服务端
# 不出意外你已经编译好了hotgo可执行文件
```
3、分服务编译
待写。
### 修改生产配置文件
- 配置文件server/manifest/config/config.yaml
> 如关闭代码生成功能、修改数据库地址、缓存驱动、队列驱动、日志路径等
### 启动服务
> 这里推可以接使用gf官方推荐的启动方式请参考https://goframe.org/pages/viewpage.action?pageId=1114403
### Nginx配置
```
# websocket
location ^~ /socket {
proxy_pass http://127.0.0.1:8000/socket;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-NginX-Proxy true;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_connect_timeout 600s;
proxy_read_timeout 600;
proxy_send_timeout 600s;
}
# http
location ^~ / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8000/; # 设置代理服务器的协议和地址
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
}
```

View File

@@ -46,11 +46,6 @@ database:
* /hotgo/web/.env.production
* /hotgo/web/.env
其中必改配置
```
VITE_PROXY=[["/admin","http://你的IP:8000/admin"]]
```
三、 启动服务

View File

@@ -11,6 +11,14 @@
> 如果升级(覆盖)代码后打开会出现 sql 报错, 请检查更新的数据库格式或自行调整
### v2.3.5
updated 2023.02.26
- 优化: 调整消息队列消费初始化方式,支持在插件模块下注册消费者
- 修复:后台用户添加模态框无法正常显示问题
- 优化创建新插件生成增加web页面的生成
- 增加:增加生产部署、开发规范、生成代码、表单组件等相关文档
- 修复修复后台已知的一些小bug
### v2.2.10
updated 2023.02.23

View File

@@ -0,0 +1,155 @@
## 权限控制
目录
- 一、菜单权限
- 二、数据权限
### 一、菜单权限
> 菜单权限可以控制web后台菜单功能每个菜单可以绑定一个或多个API权限API权限决定了用户是否可以调用服务端接口。
#### 为角色添加菜单权限
1、HotGo后台 -> 权限管理 -> 菜单权限 -> 找到添加菜单,按照表单提示添加你的菜单信息。
2、添加菜单完成后到 权限管理 -> 角色权限 -> 找到一位已有角色或者添加新角色 -> 在列表右侧操作栏找到菜单权限打开 -> 将第1步中添加的菜单分配给该角色即可。
#### 细粒度权限
> 一般用于web后台页面上的功能按钮对不同的角色展示不同的按钮时会用到。
1、参考【为角色添加菜单权限】添加一个按钮类型的菜单。并为菜单分配API权限假设你分配权限是/test/index
2、在web页面中创建一个按钮如下
```vue
<template>
<div>
<n-button v-if="hasPermission(['/test/index'])">拥有[/test/index]权限可见</n-button>
</div>
</template>
<script lang="ts" setup>
import { usePermission } from '@/hooks/web/usePermission';
const { hasPermission } = usePermission();
</script>
```
#### 菜单表主要字段解释
- 代码片段:[server/internal/model/input/adminin/menu.go](../../server/internal/model/input/adminin/menu.go)
```go
type MenuRouteMeta struct {
// 解释参考https://naive-ui-admin-docs.vercel.app/guide/router.html#%E5%A4%9A%E7%BA%A7%E8%B7%AF%E7%94%B1
Title string `json:"title"` // 菜单名称 一般必填
//Disabled bool `json:"disabled,omitempty"` // 禁用菜单
Icon string `json:"icon,omitempty"` // 菜单图标
KeepAlive bool `json:"keepAlive,omitempty"` // 缓存该路由
Hidden bool `json:"hidden,omitempty"` // 隐藏菜单
Sort int `json:"sort,omitempty"` // 排序越小越排前
AlwaysShow bool `json:"alwaysShow,omitempty"` // 取消自动计算根路由模式
ActiveMenu string `json:"activeMenu,omitempty"` // 当路由设置了该属性,则会高亮相对应的侧边栏。
// 这在某些场景非常有用,比如:一个列表页路由为:/list/basic-list
// 点击进入详情页,这时候路由为/list/basic-info/1但你想在侧边栏高亮列表的路由就可以进行如下设置
// 注意是配置高亮路由 `name`不是path
IsRoot bool `json:"isRoot,omitempty"` // 是否跟路由 顶部混合菜单,必须传 true否则左侧会显示异常场景就是分割菜单之后当一级菜单没有子菜单
FrameSrc string `json:"frameSrc,omitempty" ` // 内联外部地址
Permissions string `json:"permissions,omitempty"` // 菜单包含权限集合,满足其中一个就会显示
Affix bool `json:"affix,omitempty"` // 是否固定 设置为 true 之后 多页签不可删除
Type int `json:"type"` // 菜单类型
}
```
#### API权限验证流程
```mermaid
graph TD
A(用户请求API) --> B(验证中间件<br> server/internal/logic/middleware/admin_auth.go)
B -->|没有登录,但API需要登录| H(提示登录)
B -->|没有登录,但API不需要登录| G(验证通过,进入业务)
B -->|已登录| C(检查用户角色是否拥有API权限<br> server/internal/logic/admin/role.go,方法:Verify) -->|有权限| G(验证通过,进入业务)
C -->|API无需验证权限| G(验证通过,进入业务流程)
C -->|无权限| D(提示无权限)
```
#### 菜单权限添加或修改后多久生效?
- API权限实时生效web后台菜单刷新页面后生效无需重启服务
### 二、数据权限
> 数据权限是某人只能看到某些数据,可以为某人设置一定的数据范围,让其只能看到允许他看到的数据。
> 例如:公司存在多个销售部门,不同的部门仅能查看到自己所属部门下的数据;再如:用户有多级代理商,只能查看自己和自己下级的数据。
#### 目前已经支持的数据权限如下
| 数据范围 | 描述 |
|---------|--------------------------------------------|
| 全部权限 | 不做过滤,可以查看所有数据 |
| 当前部门 | 仅可以看到所属部门下的数据 |
| 当前以及下部门 | 仅可以看到所属以及下级部门的数据 |
| 自定义部门 | 可以看到设置的指定部门,支持多个 |
| 仅自己 | 仅可以看到自己的数据 |
| 自己和直属下级 | 仅可以看到自己和直属下级的数据直属下级是指当前用户往下1级的用户 |
| 自己和全部下级 | 仅可以看到自己和全部下级的数据,全部下级是指当前用户所有的下级用户,包含其下级的下级 |
#### 如何区分部门和下级用户?
- 在实际使用时,部门更多的是在公司或机构中使用,可以通过在 组织管理 -> 后台用户 ->为用户绑定部门
- 下级用户在代理商或分销系统中比较常见,后台用户由谁添加的,那么被添加的用户就是其下级用户。后续也将开放邀请码绑定下级功能。
#### 如何判断数据是谁的?
- 在数据库建表时,只要表中包含字段:`created_by``member_id`即可过滤 [仅自己、自己和直属下级、自己和全部下级] 类型的数据权限
#### 如何在具体业务中使用数据权限过滤?
- 只需在查询、更新或删除等ORM链式操作中加入相应`Handler`方法即可
- 如果你想对Handler有更多详细了解请查看https://goframe.org/pages/viewpage.action?pageId=20087340
- 下面提供一个简单使用例子:
```go
package main
import (
"context"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm/handler"
)
func test(ctx context.Context) {
dao.AdminPost.Ctx(ctx).Where("id", 1).Handler(handler.FilterAuth).Scan(&res)
}
```
- 如果表中没有字段:`created_by``member_id`,也可以使用自定义字段方法来过滤数据权限,如下:
```go
package main
import (
"context"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm/handler"
)
func test(ctx context.Context) {
dao.AdminPost.Ctx(ctx).Where("id", 1).Handler(handler.FilterAuthWithField("test_field")).Scan(&res)
}
```
- 过滤方法实现请参考:[server/internal/library/hgorm/handler/filter_auth.go](../../server/internal/library/hgorm/handler/filter_auth.go)

View File

@@ -71,7 +71,7 @@
| --- --- global | 项目内主要的全局变量和系统的一些初始化操作 |
| --- --- logic | 业务逻辑封装管理,特定的业务逻辑实现和封装往往是项目中最复杂的部分 |
| --- --- model | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义 |
| --- --- --- input | 对内接口。用于controller调用service或service之间调用时的输入/输出结构定义输入过滤和预处理 |
| --- --- --- input | 对内接口。用于controller调用service或service之间调用时的输入/输出结构定义,以及输入过滤和预处理 |
| --- --- router | 注册对外接口和分组中间件 |
| --- --- service | 用于业务模块解耦的接口定义层具体的接口实现在logic中进行注入 |
| --- main.go | 插件始化文件和模块插拔接口 |
@@ -93,7 +93,7 @@
| --- model | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义 |
| --- --- do | 用于dao数据操作中业务模型与实例模型转换由工具维护用户不能修改 |
| --- --- entity | 与数据集合绑定的程序数据结构定义,通常和数据表一一对应 |
| --- --- input | 对内接口。用于controller调用service或service之间调用时的输入/输出结构定义输入过滤和预处理 |
| --- --- input | 对内接口。用于controller调用service或service之间调用时的输入/输出结构定义,以及输入过滤和预处理 |
| --- packed | 将静态资源打包进可执行文件,无需单独部署 |
| --- queues | 为项目内所有的消息队列的消费者提供统一的初始化和处理 |
| --- router | 注册对外接口和分组中间件 |

View File

@@ -0,0 +1,235 @@
## 代码生成
目录
- 使用条件
- 生成配置
- 一个生成增删改查列表例子
- 内置gf-cli
- 自定义生成模板
> 在HotGo中可以通过后台开发工具快速的一键生成CRUD自动生成Api、控制器、业务逻辑、Web页面、表单组件、菜单权限等。
### 使用条件
- hotgo 版本号 >= 2.2.10
- 使用前必须先看 [数据库](sys-db.md)
#### 增删改查列表
- 表自增长字段为 `id`
#### 关系树列表
- 表自增长字段为 `id`
### 生成配置
- 注意线上环境务必将allowedIPs参数设为空考虑到项目安全问题请勿线上生成使用
```yaml
hggen:
allowedIPs: ["127.0.0.1", "*"] # 白名单,*代表所有只有允许的IP后台才能使用生成代码功能
selectDbs: [ "default" ] # 可选生成表的数据库配置名称,支持多库
disableTables : ["hg_sys_gen_codes","hg_admin_role_casbin"] # 禁用的表,禁用以后将不会在选择表中看到
delimiters: ["@{", "}"] # 模板引擎变量分隔符号
# 生成应用模型所有生成模板允许自定义可以参考default模板进行改造
application:
# CRUD模板
crud:
templates:
# 默认的主包模板
- group: "default" # 分组名称
isAddon: false # 是否为插件模板 falsetrue
masterPackage: "sys" # 主包名称需和controllerPath、logicPath、inputPath保持关联
templatePath: "./resource/generate/default/curd" # 模板路径
apiPath: "./api/admin" # gfApi生成路径
controllerPath: "./internal/controller/admin/sys" # 控制器生成路径
logicPath : "./internal/logic/sys" # 主要业务生成路径
inputPath: "./internal/model/input/sysin" # 表单过滤器生成路径
routerPath : "./internal/router/genrouter" # 生成路由表路径
sqlPath : "./storage/data/generate" # 生成sql语句路径
webApiPath: "../web/src/api" # webApi生成路径
webViewsPath : "../web/src/views" # web页面生成路径
# 默认的插件包模板
- group: "addon" # 分组名称
isAddon: true # 是否为插件模板 falsetrue
masterPackage: "sys" # 主包名称需和controllerPath、logicPath、inputPath保持关联
templatePath: "./resource/generate/default/curd" # 模板路径
apiPath: "./addons/{$name}/api/admin" # gfApi生成路径
controllerPath: "./addons/{$name}/controller/admin/sys" # 控制器生成路径
logicPath : "./addons/{$name}/logic/sys" # 主要业务生成路径
inputPath: "./addons/{$name}/model/input/sysin" # 表单过滤器生成路径
routerPath : "./addons/{$name}/router/genrouter" # 生成路由表路径
sqlPath : "./storage/data/generate/addons" # 生成sql语句路径
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
webViewsPath : "../web/src/views/addons/{$name}" # web页面生成路径
# 关系树列表模板
tree:
templates:
- group: "default"
templatePath: "./resource/generate/default/tree"
# 消息队列模板
queue:
templates:
- group: "default"
templatePath: "./resource/generate/default/queue"
# 定时任务模板
cron:
templates:
- group: "default"
templatePath: "./resource/generate/default/cron"
```
### 一个生成增删改查列表例子
- 推荐使用热编译方式启动HotGo这样生成完成页面自动刷新即可看到新生成内容无需手动重启
- 服务端热编译启动:`gf run main.go`, web前端启动`yarn dev`
1、创建数据表
1.1 创建测试表格表`hg_test_table`
```mysql
CREATE TABLE `hg_test_table` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`category_id` bigint(20) NOT NULL COMMENT '分类ID',
`title` varchar(64) NOT NULL COMMENT '标题',
`description` varchar(255) NOT NULL COMMENT '描述',
`content` text NOT NULL COMMENT '内容',
`image` varchar(255) DEFAULT NULL COMMENT '单图',
`attachfile` varchar(255) DEFAULT NULL COMMENT '附件',
`city_id` bigint(20) DEFAULT '0' COMMENT '所在城市',
`switch` int(11) DEFAULT '1' COMMENT '显示开关',
`sort` int(11) NOT NULL COMMENT '排序',
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
`created_by` bigint(20) DEFAULT '0' COMMENT '创建者',
`updated_by` bigint(20) DEFAULT '0' COMMENT '更新者',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='测试表格';
```
1.2 测试分类表`hg_test_category`:
- 注意:该表为了方便功能演示已经内置无需再次创建,此处提示表结构只是为了方便大家梳理关联表关系
```mysql
CREATE TABLE `hg_test_category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`name` varchar(255) NOT NULL COMMENT '分类名称',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`sort` int(11) NOT NULL COMMENT '排序',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='测试分类';
```
1.3 插入测试数据
```mysql
INSERT INTO `hg_test_table` (`id`, `category_id`, `title`, `description`, `content`, `image`, `attachfile`, `city_id`, `switch`, `sort`, `status`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 1, '测试标题', '描述', '<h2><strong>不知道写点啥!</strong></h2><p><br></p><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"https://media.w3.org/2010/05/sintel/trailer.mp4\"></iframe><p><br></p><p><img src=\"http://bufanyun.cn-bj.ufileos.com/hotgo/attachment/2023-02-09/cqdq9iuv0phsg8patk.png\"></p>', 'https://bufanyun.cn-bj.ufileos.com/hotgo/logo.sig.png', 'http://bufanyun.cn-bj.ufileos.com/hotgo/attachment/2022-12-30/cpf1x44idoycrtajf2.xlsx', 110102, 1, 10, 1, 0, 1, '2022-12-15 19:30:14', '2023-02-23 13:55:32', NULL);
```
2、创建生成配置
- 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,选择和填写如下参数:
![生成添加演示图](images/sys-code-add.png)
3、自定义配置
- 确认配置无误后,点击生成配置会自动跳转到生成配置页面,如下:
![生成配置页面](images/sys-code-config-init.png)
- 你可以在该页面点击`预览代码`查看生成的内容,如果无需调整亦可以直接点击`提交生成`,以下是预览代码效果:
![生成预览](images/sys-code-preview.png)
- 如果你对默认的生成配置不满意,可以根据页面表单提示,自定义表格属性和字段属性
- 这里假设我们要一个关联表,让表`hg_test_table`字段`category_id`去关联 表`hg_test_category`字段`id`,我们可以这样做:
![生成关联配置](images/sys-code-config-join.png)
- 如果你想调整主表每列字段的显示方式,可以点击上方导航栏中的 [主表字段] 进行调整
- 这里我们不做任何调整直接进入下一步。目的是为了后续演示对最终生成结果不满意的再次调整方案
![生成关联配置](images/sys-code-master.png)
4、以上内容都已配置无误后点击提交生成即可。
- 如果你使用的热编译,那么页面会在生成成功后立即刷新,刷新完成你即可在后台菜单栏中看到`测试表格`菜单。如果不是使用热编译启动,请手动重启服务后刷新。
> 注意热编译自动刷新时考虑到实际开发环境电脑配置不同web端可能会优先于服务端重启加载完成此时会出现新生成菜单没有加载出来或某接口请求超时问题这是服务端正在启动中导致的一般稍等几秒再试即可。
- 接下来让我们看看生成的表格页面,效果如下:
![生成测试表格页面](images/sys-code-list.png)
5、假设我们对生成结果不满意有新的优化需求如下
1.`单图``附件`改换成上传组件
2.`创建者``更新者`隐藏
3.`分类ID`隐藏,然后把关联表中的`分类名称`显示出来
> 那么我们可以回到 开发工具 -> 代码生成 -> 找到刚刚生成的记录 -> 在右侧操作栏中点击生成配置,再次进入配置页面做如下操作即可:
1. 点击上方导航栏[主表字段],调整字段选项:
![修改主表配置](images/sys-code-master-save.png)
2. 点击上方导航栏[关联表],调整字段选项:
![修改关联表配置](images/sys-code-config-join-save.png)
3. 返回[基本信息],勾选强制覆盖,然后点击`预览代码`,确认生成代码无误后点击`提交生成`
![强制覆盖并提交生成](images/sys-code-config-post.png)
- 生成完成刷新页面后,再次来到`测试表格`,发现表格已经调整到了我们预期效果
1. 列表效果:
![最终列表效果](images/sys-code-list-ok.png)
2. 编辑表单效果
![最终编辑表单效果](images/sys-code-list-edit-ok.png)
- 至此生成增删改查列表示例结束!
### 内置gf-cli
> 由于gf版本更新较常出现向下不兼容的情况所以我们为了保证生成代码的依赖稳定性我们将gf-cli工具内置到了系统中并做了一些在线执行的调整。
- 我们会定期更新和gf最新版本生成功能保持一致这样不论是你通过gf命令还是通过后台生成的代码格式都是一样的遵循相同的代码规范和开发方式
- 后续我们也将开放在线运行`gf gen dao``gf gen service`功能。在做插件开发时也会支持到在线生成插件下的service接口这将会使得插件开发更加方便
### 自定义生成模板
> 系统内置了两组CURD生成模板请参考[生成配置]。default是默认的生成到主模块下addon是默认生成到指定的插件下
- 如果你在实际的开发过程中默认模板需要调整的地方较多时HotGo允许你新建新的模板分组来满足你的需求。新的模板可根据现有模板基础拷贝一份出来做改造默认模板目录[server/resource/generate/default](../../server/resource/generate/default)

113
docs/guide-zh-CN/sys-db.md Normal file
View File

@@ -0,0 +1,113 @@
## 数据库
目录
- 字段类型
- 特殊字段默认表单组件
- 特殊字段默认表单验证器
- SQL默认查询方式
- 其他默认选项
- 常见问题
### 字段类型
- 创建数据库表当按如下的规则进行字段命名、类型、属性设置和备注后再生成CRUD代码时会自动生成对应的Api、控制器、业务逻辑、Web页面、[表单组件](web-form.md)等的一些默认属性
- 当你了解这些默认技巧后,会有效提高你在实际开发中的生产效率
| 数据库类型 | 额外属性 | 转换Go类型 | 转换Ts类型 | 表单组件 |
|---------------------------------------------------------------|--------------|--------------|---------|-----------------------|
| int, tinyint,small_int,smallint,medium_int,mediumint,serial | / | int | number | InputNumber(数字输入框) |
| int, tinyint,small_int,smallint,medium_int,mediumint,serial | unsigned | uint | number | InputNumber(数字输入框) |
| big_int,bigint,bigserial | / | int64 | number | InputNumber(数字输入框) |
| big_int,bigint,bigserial | unsigned | uint64 | number | InputNumber(数字输入框) |
| real | / | float32 | number | InputNumber(数字输入框) |
| float,double,decimal,money,numeric,smallmoney | / | float64 | number | InputNumber(数字输入框) |
| bit(1) 、bit(true)、bit(false) | / | bool | boolean | Input(文本输入框,默认) |
| bit | / | int64-bytes | array | InputDynamic(动态KV表单) |
| bit | unsigned | uint64-bytes | array | InputDynamic (动态KV表单) |
| bool | / | bool | boolean | Input(文本输入框,默认) |
| date | / | *gtime.Time | string | Date(日期选择器) |
| datetime,timestamp,timestamptz | / | *gtime.Time | string | Time(时间选择器) |
| json | / | *gjson.Json | string | Input(文本输入框) |
| jsonb | / | *gjson.Json | string | Input(文本输入框) |
| 以下为物理类型中包含字段部分时的转换方式,默认情况 | / | / | / | / |
| text,char,character | / | string | string | Input(文本输入框) |
| float,double,numeric | / | string | string | Input(文本输入框) |
| bool | / | bool | boolean | Input(文本输入框,默认) |
| binary,blob | / | []byte | string | Input(文本输入框,默认) |
| int | / | int | number | InputNumber(数字输入框) |
| int | unsigned | int | number | InputNumber(数字输入框) |
| time | / | *gtime.Time | string | Time(时间选择器) |
| date | / | *gtime.Time | string | Date(日期选择器) |
| 没有满足以上任何条件的 | / | string | string | Input(文本输入框) |
### 特殊字段默认表单组件
- 以下字段在不设置表单组件时会默认使用的表单组件
| 数据库字段 | 字段名称 | 表单组件 |
|--------------|----------------------|----------------------|
| status | 状态字段任意int类型 | Select (单选下拉框) |
| created_at | 创建时间字段 | TimeRange (时间范围选择) |
| province_id | 省份字段任意int类型 | CitySelector (省市区选择) |
| city_id | 省份字段任意int类型 | CitySelector (省市区选择) |
| 任意字串符字段 | 长度>= 200 and <= 500 | InputTextarea (文本域) |
| 任意字串符字段 | 长度> 500 | InputEditor (富文本) |
### 特殊字段默认表单验证器
- 以下字段在不设置表单组件时会默认使用的表单验证器
| 数据库字段/Go类型 | 字段名称 | 表单验证规则 |
|-------------------|--------|-----------------------|
| mobile | 手机号 | 不为空时必须是手机号码(国内) |
| qq | QQ | 不为空时必须是QQ号码 |
| email | 邮箱地址 | 不为空时必须是邮箱格式 |
| id_card | 身份证号码 | 不为空时必须是15或18位身份证号码 |
| bank_card | 银行卡号码 | 银行卡号码 |
| password | 密码 | 密码验证必须包含6-18为字母和数字 |
| price | 价格 | 金额验证最多允许输入10位整数及2位小数 |
| Go类型为uint、uint64 | 正整数 | 非零正整数验证 |
### SQL默认查询方式
- Go类型取决于数据库物理类型请参考 [字段类型] 部分
| Go类型 | 查询方式 |
|-------------------------|--------------------------------------|
| string | LIKE |
| date,datetime | = |
| int,uint,int64,uint64 | = |
| []int,[]int64,[]uint64 | IN (...) |
| float32,float64 | = |
| []byte4 | =(默认) |
| time.Time,*gtime.Time | = |
| *gjson.Json | JSON_CONTAINS(json_doc, val[, path]) |
### 其他默认选项
#### 默认字典选项
- 数据库字段为 `status`且类型为任意数字类型的会使用系统默认的状态字典
#### 默认属性
- 默认必填,当数据库字段存在非空`IS_NULLABLE`属性时,默认勾选必填验证
- 默认唯一,当数据库字段属性存在`UNI`时,默认勾选唯一值验证
- 默认主键,当数据库字段属性存在`PRI`时,默认为主键,不允许编辑
- 默认最大排序,当数据库字段存在`sort`时,默认开启排序,添加表单自动获取最大排序增量值并填充表单
- 默认列名,默认使用字段注释作为表格的列名。当数据库字段未设置注释时,默认使用字段名称作为列名
#### 自动更新/插入
- 自动更新,当数据库字段为`updated_at`(更新时间),`updated_by`(更新者)
- 自动插入,当数据库字段为`created_at`(创建时间),`created_by`(创建者)
- 软删除,表存在字段`deleted_at`使用表的Orm模型查询条件将会自动加入[ `deleted_at` IS NULL ],删除时只更新删除时间而不会真的删除数据
- 树表:不论更新插入,都会根据表中字段`pid`(上级ID)自动维护`level`(树等级)和`tree`(关系树)
> 这里只列举了较为常用的默认规则,其他更多默认规则请参考:[server/internal/library/hggen/views/column_default.go](../../server/internal/library/hggen/views/column_default.go)
#### 常见问题
待补充。

View File

@@ -0,0 +1,197 @@
## 开发规范
目录
- 规范说明
- 规范建议
- 命名规范
- 编码规范
- 框架规范
- 插件规范
- 数据库规范
- 包功能规范
### 规范说明
- 开发规范意义是是让大家尽可能写出风格统一的代码,让团队代码有更好的可读性与可维护性。
- 在实际业务开发中,除了要提高业务开发效率,保证线上业务高性能外,好的编程习惯也是一个开发人员基本素养之一。
### 规范建议
- 在我们实际开发中,有很多开发人可能是由某一语言转到另外一个语言领域,在转到另外一门语言后, 我们都会保留着对旧语言的编程习惯,在这里,我建议的是,虽然不同语言之前的某些规范可能是相通的, 但是我们最好能够按照官方的一些demo来熟悉是渐渐适应当前语言的编程规范而不是直接将原来语言的编程规范也随之迁移过来。
### 命名规范
#### 命名准则
* 当变量名称在定义和最后一次使用之间的距离很短时,简短的名称看起来会更好
* 变量命名应尽量描述其内容,而不是类型
* 常量命名应尽量描述其值,而不是如何使用这个值
* 在遇到forif等循环或分支时推荐单个字母命名来标识参数和返回值
* package名称也是命名的一部分推荐全部使用小写请尽量将其利用起来
* 使用一致的命名风格
#### 文件命名规范
* 全部小写
* 除unit test外避免下划线(_)
* 文件名称不宜过长,但不必为了简短而忽略含义
#### 变量命名规范参考
* 驼峰命名
* 见名知义,避免拼音替代英文
* 不建议包含下划线(_)
* 不建议包含数字
#### 函数、常量命名规范
* 驼峰式命名
* 可导出的必须首字母大写
* 不可导出的必须首字母小写
* 避免全部大写与下划线(_)组合
#### 参考文档
* [Practical Go: Real world advice for writing maintainable Go programs](https://dave.cheney.net/practical-go/presentations/gophercon-singapore-2019.html#_simplicity)
### 编码规范
#### import
* 单行import不建议用圆括号包裹
* 按照`官方包`NEW LINE`当前工程包`NEW LINE`第三方依赖包`顺序引入
```go
import (
"context"
"string"
"greet/user/internal/config"
"google.golang.org/grpc"
)
```
#### 函数返回
* 对象避免非指针返回
* 遵循有正常值返回则一定无error有error则一定无正常值返回的原则
#### 错误处理
* 有error必须处理如果不能处理就必须抛出。
* 避免下划线(_)接收error
#### 函数体编码
* 建议一个block结束空一行如if、for等
```go
func main (){
if x==1{
// do something
}
fmt.println("xxx")
}
```
* return前尽可能空一行
```go
func getUser(id string)(string,error){
....
return "xx",nil
}
```
### 框架规范
- 项目启动初始化方法都放在`server/internal/global/init.go`,插件放在:`server/addons/xxx插件/global/init.go`
- Api 对外接口层 都放在`server/api`,根据实际应用和应用具体功能划分子目录,插件放在:`server/addons/xxx插件/api`
- Router 路由注册方式应尽可能按gf2.0版本风格定义参考https://goframe.org/pages/viewpage.action?pageId=30736904
- Controller 控制器层 都放在`server/internal/controller`,根据应用和具体功能拆分子目录,插件放在:`server/addons/xxx插件/controller`
- Input 对内接口层 都放在`server/internal/model/input`,根据应用和具体功能拆分子目录并以`in`结尾,插件放在:`server/addons/xxx插件/model/input`
- Logic 业务逻辑层 统一放在`server/internal/logic` 根据实际业务需求按logic/xxx/*.go拆分插件放在`server/addons/xxx插件/logic`
- Orm 模型实体层 统一放在`server/internal/model/entity`
- Dao 数据访问层 统一放在`server/internal/dao`
- 自定义工具函数都放在 `server/utility`,且应根据不同类型的方法做分包,相同类型的方法放在同一个包内
- 状态枚举统一调用`server/internal/consts`中的常量和属性
### 插件规范
- 插件命名统一全部小写
- 资源文件在 `addons/xxx插件/resource`或`server/resource`
- 每个插件之前应该是完全独立的,尽可能避免插件于插件之间的调用依赖
- 插件和主模块之前的调用应尽可能通过 Service 接口来调用
- 插件业务初始化顺序下应在主模块之后应尽可能避免使用init方法隐式初始化
### 数据库规范
#### 命名规范
- 数据表名小写,多关键字使用下划线分割(关键字尽量全称)
- 字段名小写,多关键字使用下划线分割(关键字尽量全称)
- 禁止使用保留字并且尽量少用含有关键词来命名
- 临时表必须以tmp_开头、以日期结尾备份表必须以bak_开头、以日期结尾
- 插件模块表名建议以`hg_addon_`开头,如:`hg_addon_hgexample_table`,含义:`插件_案例_表格`。在生成代码时可自动识别实体命名为:`Table`
#### 基础规范
- 所有的字段必须添加注释
- 尽可能地使用InnoDB作为表的存储引擎
- 所有的表和字段必须添加注释
- 尽量控制表行数在500万以内
- 尽可能采用冷热数据分离策略
- 禁止以图片、文件等二进制数据
#### 表设计规范
- 尽可能每张表的索引数量控制在5个以内
- 每一张InnoDB表都必须含有一个主键自增主键必须是`bigint`类型
- 尽可能避免使用外键约束
- 设置数据表架构应考虑后期扩展型
- 遵循范式与冗余平衡原则
#### 字段设计规范
- 尽可能将所有的数据列定义为 `NOT NULL` 类型
- 避免 ENUM 数据类型
- json 存储的数据用 `json`字段代替 `text`
- 表与表关联的键名保持一致或以关联表名的缩写为前缀
- 固定长度的字符串字段务必使用 `char`
- 使用 `UNSIGNEG` 存储非负整数
- 禁止敏感数据以明文形式存储
- 金额相关的数据必须使用 `DECIMAL` 数据类型
- 尽量所有表有 `status` 字段来标注数据状态(1:启用,2:禁用,3:已删除),业务状态请使用其他字段;`status`字段类型 为带符号的 `tinyint(1)`。如果还需要其他的数据状态 请先判断该状态的数据是有用的数据还是无意义的数据,有用的数据状态 > 0,无意义的数据状态 < -1
- 所有的删除(除开清空回收站操作) 请标记 `status` 为 3
- 创建时间字段为`created_at`,修改时间字段为`updated_at`,删除时间字段为`deleted_at`,类型`datetime`
- 用户关联 id 字段为 `member_id` 或 `created_by`
- 排序字段为 `sort`
- 区分应用字段为 `app_id`
- 区分插件来源需要增加字段为 `addon_name` 和 `is_addon`
- 关系树表中必须包含字段:`id`(主键)、`pid`(上级ID)、`level`(树等级)、`tree`(关系树)
- 多选、多图、多文件字段类型应尽量使用类型`json`
### 包功能规范
##### Input(对内接口 - 过滤验证)
- 接收Api 对外接口数据和参数过滤
- 对提交的复杂数据参数进行验证处理返回
##### View(视图)
- 数据处理显示
- 页面渲染
##### Controller(控制器)
- 数据接收
- 服务调用
- 简单CURD入库
- 简单业务逻辑
##### Logic(业务逻辑 - 服务接口功能)
- 数据逻辑处理
- 数据入库
- 数据查询

View File

@@ -3,7 +3,7 @@
目录
- 缓存驱动
- 上下文(待写)
- 请求上下文(待写)
- JWT待写
- 地理定位(待写)
- 通知(待写)
@@ -48,9 +48,42 @@ func test() {
```
### 上下文
### 请求上下文
- 主要用于在处理HTTP和websocket请求时通过中间件将用户、应用、插件等信息绑定到上下文中方便在做业务处理时用到这些信息
```go
// 待写
package admin
import (
"fmt"
"context"
"hotgo/internal/library/contexts"
"hotgo/internal/library/addons"
)
func test(ctx context.Context) {
// 获取当前请求的所有上下文变量
var ctxModel = contexts.Get(ctx)
fmt.Printf("当前请求的所有上下文变量:%+v\n", ctxModel)
// 获取当前请求的应用模块
var module = contexts.GetModule(ctx)
fmt.Printf("当前请求的应用:%+v\n", module)
// 获取当前请求的用户信息
var member = contexts.GetUser(ctx)
fmt.Printf("当前访问用户信息:%+v\n", member)
// 获取当前请求的插件模块
fmt.Printf("当前是否为插件请求:%v", contexts.IsAddonRequest(ctx))
if contexts.IsAddonRequest(ctx) {
fmt.Printf("当前插件名称:%v", contexts.GetAddonName(ctx))
fmt.Printf("当前插件信息:%v", addons.GetModule(contexts.GetAddonName(ctx)))
}
}
```
### JWT

View File

@@ -44,7 +44,7 @@ queue:
### 一个例子
每个被发送到队列的消息应该被定义为一个文件结构。
每个被发送到队列的消息应该被定义为一个单独的文件结构。
例如,如果您需要异步记录系统日志,内容大致如下:
@@ -63,7 +63,7 @@ import (
)
func init() {
jobList = append(jobList, SysLog)
queue.RegisterConsumer(SysLog)
}
// SysLog 系统日志
@@ -71,13 +71,13 @@ var SysLog = &qSysLog{}
type qSysLog struct{}
// getTopic 主题
func (q *qSysLog) getTopic() string {
// GetTopic 主题
func (q *qSysLog) GetTopic() string {
return consts.QueueLogTopic
}
// handle 处理消息
func (q *qSysLog) handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
// Handle 处理消息
func (q *qSysLog) Handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
var data entity.SysLog
if err = json.Unmarshal(mqMsg.Body, &data); err != nil {
return err
@@ -85,7 +85,6 @@ func (q *qSysLog) handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
return service.SysLog().RealWrite(ctx, data)
}
```
下面是将消息添加到队列的方式,大概内容如下:
@@ -101,7 +100,9 @@ import (
)
func test() {
data := &entity.SysLog{}
data := &entity.SysLog{
//...
}
if err := queue.Push(consts.QueueLogTopic, data); err != nil {
fmt.Printf("queue.Push err:%+v", err)
}

View File

@@ -0,0 +1,261 @@
## 独立部署
目录
- 构建
- 旧版浏览器兼容
- 预览
- 分析构建文件体积
- 压缩
- 部署
> 在实际开发中web前端也可能需要独立部署所以在此提供一下部署方案。
### 构建
项目开发完成之后,执行以下命令进行构建
```shell
yarn build
```
构建打包成功之后,会在根目录生成 dist 文件夹,里面就是构建打包好的文件
### 旧版浏览器兼容
在 .env.production 内
设置 `VITE_LEGACY=true` 即可打包出兼容旧版浏览器的代码
```shell
VITE_LEGACY = true
```
### 预览
发布之前可以在本地进行预览,有多种方式,这里介绍两种
##### 不能直接打开构建后的 html 文件
使用项目自定的命令进行预览(推荐)
```shell
# 先打包在进行预览
yarn preview
# 直接预览本地 dist 文件目录
yarn preview:dist
```
- 本地服务器预览(通过 live-server)
```shell
# 1.全局安装live-server
yarn global add live-server
# 2. 进入打包的后目录
cd ./dist
# 本地预览默认端口8080
live-server
# 指定端口
live-server --port 9000
```
### 分析构建文件体积
如果你的构建文件很大,可以通过项目内置 [rollup-plugin-analyzer](https://github.com/doesdev/rollup-plugin-analyzer) 插件进行代码体积分析,从而优化你的代码。
```shell
yarn report
```
运行之后,在自动打开的页面可以看到具体的体积分布,以分析哪些依赖有问题。
- 左上角可以切换 显示 gzip 或者 brotli
![start-deploy-report.png](./images/start-deploy-report.png)
### 压缩
#### 开启 gzip 压缩
开启 gzip并配合 nginx 的 `gzip_static` 功能可以大大加快页面访问速度
- 只需开启 `VITE_BUILD_COMPRESS='gzip'` 即可在打包的同时生成 .gz 文件
```shell
# 根据自己路径来配置更改
# 例如部署在nginx /next/路径下 则VITE_PUBLIC_PATH=/next/
VITE_PUBLIC_PATH=/
```
#### 开启 brotli 压缩
brotli 是比 gzip 压缩率更高的算法,可以与 gzip 共存不会冲突,需要 nginx 安装指定模块并开启即可。
- 只需开启 VITE_BUILD_COMPRESS='brotli' 即可在打包的同时生成 .br 文件
```shell
# 根据自己路径来配置更改
# 例如部署在nginx /next/路径下 则VITE_PUBLIC_PATH=/next/
VITE_PUBLIC_PATH=/
```
#### 同时开启 gzip 与 brotli
只需开启 VITE_BUILD_COMPRESS='brotli,gzip' 即可在打包的同时生成 .gz 和 .br 文件。
#### gzip 与 brotli 在 nginx 内的配置
```
http {
# 开启gzip
gzip on;
# 开启gzip_static
# gzip_static 开启后可能会报错,需要安装相应的模块, 具体安装方式可以自行查询
# 只有这个开启vue文件打包的.gz文件才会有效果否则不需要开启gzip进行打包
gzip_static on;
gzip_proxied any;
gzip_min_length 1k;
gzip_buffers 4 16k;
#如果nginx中使用了多层代理 必须设置这个才可以开启gzip。
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";
# 开启 brotli压缩
# 需要安装对应的nginx模块,具体安装方式可以自行查询
# 可以与gzip共存不会冲突
brotli on;
brotli_comp_level 6;
brotli_buffers 16 8k;
brotli_min_length 20;
brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;
}
```
### 部署
- 注意:项目默认是在生产环境开启 Mock这样做非常不好只是为了演示环境有数据不建议在生产环境使用 Mock而应该使用真实的后台接口并将 Mock 关闭。
#### 发布
简单的部署只需要将最终生成的静态文件dist 文件夹的静态文件发布到你的 cdn 或者静态服务器即可,需要注意的是其中的 index.html 通常会是你后台服务的入口页面,在确定了 js 和 css 的静态之后可能需要改变页面的引入路径。
例如上传到 nginx `/srv/www/project/index.html`
```
# nginx配置
location / {
# 不缓存html防止程序更新后缓存继续生效
if ($request_filename ~* .*\.(?:htm|html)$) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
access_log on;
}
# 这里是vue打包文件dist内的文件的存放路径
root /srv/www/project/;
index index.html index.htm;
}
```
部署时可能会发现资源路径不对,只需要修改`.env.production`文件即可。
```shell
# 根据自己路径来配置更改
# 注意需要以 / 开头和结尾
VITE_PUBLIC_PATH=/
VITE_PUBLIC_PATH=/xxx/
```
#### 前端路由与服务端的结合
项目前端路由使用的是 vue-router所以你可以选择两种方式history 和 hash。
- hash 默认会在 url 后面拼接#
- history 则不会,不过 history 需要服务器配合
可在 src/router/index.ts 内进行 mode 修改
```vue
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
createRouter({
history: createWebHashHistory(),
// or
history: createWebHistory(),
});
```
#### history 路由模式下服务端配置
开启 history 模式需要服务器配置,更多的服务器配置详情可以看 [history-mode](https://router.vuejs.org/guide/essentials/history-mode.html#html5-mode)
这里以 nginx 配置为例
##### 部署到根目录
```
server {
listen 80;
location / {
# 用于配合 History 使用
try_files $uri $uri/ /index.html;
}
}
```
#### 部署到非根目录
1. 首先需要在打包的时候更改配置
```shell
# 在.env.production内配置子目录路径
VITE_PUBLIC_PATH = /sub/
```
```
server {
listen 80;
server_name localhost;
location /sub/ {
# 这里是vue打包文件dist内的文件的存放路径
alias /srv/www/project/;
index index.html index.htm;
try_files $uri $uri/ /sub/index.html;
}
}
```
#### 使用 nginx 处理跨域
使用 nginx 处理项目部署后的跨域问题
1. 配置前端项目接口地址
```
# 在.env.production内配置接口地址
VITE_GLOB_API_URL=/api
```
2. 在 nginx 配置请求转发到后台
```
server {
listen 8080;
server_name localhost;
# 接口代理,用于解决跨域问题
location /api {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 后台接口地址
proxy_pass http://110.110.1.1:8080/api;
proxy_redirect default;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
}
}
```

View File

@@ -0,0 +1,821 @@
## 表单组件
目录
- 文本输入 Input
- 数字输入 Input Number
- 文本域 InputTextarea
- 富文本 InputEditor
- 动态键值对 InputDynamic
- 日期选择 Date(Y-M-D)
- 日期范围选择 DateRange
- 时间选择 Time(Y-M-D H:i:s)
- 时间范围选择 TimeRange
- 单选按钮 Radio
- 复选框 Checkbox
- 单选下拉框 Select
- 多选下拉框 SelectMultiple
- 树型选择 Tree Select
- 单图上传 UploadImage
- 多图上传 UploadImage
- 单文件上传 UploadFile
- 多文件上传 UploadFile
- 开关 Switch
- 评分 Rate
- 省市区选择器 CitySelector
- 图标选择器 IconSelector
### 文本输入 Input
```vue
<template>
<n-input v-model:value="value" type="text" placeholder="基本的 Input" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 数字输入 Input Number
```vue
<template>
<n-input-number v-model:value="value" clearable />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 文本域 InputTextarea
```vue
<template>
<n-input
v-model:value="value"
type="textarea"
placeholder="基本的 Textarea"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 富文本 InputEditor
```vue
<template>
<Editor style="height: 450px" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Editor from '@/components/Editor/editor.vue';
const value = ref(null);
</script>
```
### 动态键值对 InputDynamic
```vue
<template>
<n-dynamic-input
v-model:value="value"
preset="pair"
key-placeholder="键名"
value-placeholder="键值"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 日期选择 Date(Y-M-D)
```vue
<template>
<DatePicker v-model:formValue="value" type="date" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import DatePicker from '@/components/DatePicker/datePicker.vue';
const value = ref(null);
</script>
```
### 日期范围选择 DateRange
```vue
<template>
<DatePicker
v-model:startValue="startValue"
v-model:endValue="endValue"
type="datetimerange"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import DatePicker from '@/components/DatePicker/datePicker.vue';
const startValue = ref(null);
const endValue = ref(null);
</script>
```
### 时间选择 Time(Y-M-D H:i:s)
```vue
<template>
<n-time-picker :default-formatted-value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 时间范围选择 TimeRange
```vue
<template>
<template>
<n-space>
<n-time-picker :default-value="startValue" />
<n-time-picker :default-value="endValue" />
</n-space>
</template>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const startValue = ref(null);
const endValue = ref(null);
</script>
```
### 单选按钮 Radio
```vue
<template>
<n-space vertical>
<n-radio-group v-model:value="value" name="radiobuttongroup1">
<n-radio-button
v-for="song in songs"
:key="song.value"
:value="song.value"
:disabled="
(song.label === 'Live Forever' && disabled1) ||
(song.label === 'Shakermaker' && disabled2)
"
:label="song.label"
/>
</n-radio-group>
<n-space>
<n-checkbox v-model:checked="disabled2" style="margin-right: 12px">
禁用 Shakemaker
</n-checkbox>
<n-checkbox v-model:checked="disabled1">
禁用 Live Forever
</n-checkbox>
</n-space>
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
value: ref(null),
disabled2: ref(false),
disabled1: ref(false),
songs: [
{
value: "Rock'n'Roll Star",
label: "Rock'n'Roll Star"
},
{
value: 'Shakermaker',
label: 'Shakermaker'
},
{
value: 'Live Forever',
label: 'Live Forever'
},
{
value: 'Up in the Sky',
label: 'Up in the Sky'
},
{
value: '...',
label: '...'
}
].map((s) => {
s.value = s.value.toLowerCase()
return s
})
}
}
})
</script>
```
### 复选框 Checkbox
```vue
<template>
<n-space item-style="display: flex;" align="center">
<n-checkbox v-model:checked="value">
复选框
</n-checkbox>
<n-checkbox v-model:checked="value" />
<n-checkbox v-model:checked="value" :disabled="disabled">
复选框
</n-checkbox>
<n-button size="small" @click="disabled = !disabled">
禁用
</n-button>
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
value: ref(false),
disabled: ref(true)
}
}
})
</script>
```
### 单选下拉框 Select
```vue
<template>
<n-space vertical>
<n-select v-model:value="value" :options="options" />
<n-select v-model:value="value" disabled :options="options" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
value: ref(null),
options: [
{
label: "Everybody's Got Something to Hide Except Me and My Monkey",
value: 'song0',
disabled: true
},
{
label: 'Drive My Car',
value: 'song1'
},
{
label: 'Norwegian Wood',
value: 'song2'
},
{
label: "You Won't See",
value: 'song3',
disabled: true
},
{
label: 'Nowhere Man',
value: 'song4'
},
{
label: 'Think For Yourself',
value: 'song5'
},
{
label: 'The Word',
value: 'song6'
},
{
label: 'Michelle',
value: 'song7',
disabled: true
},
{
label: 'What goes on',
value: 'song8'
},
{
label: 'Girl',
value: 'song9'
},
{
label: "I'm looking through you",
value: 'song10'
},
{
label: 'In My Life',
value: 'song11'
},
{
label: 'Wait',
value: 'song12'
}
]
}
}
})
</script>
```
### 多选下拉框 SelectMultiple
```vue
<template>
<n-space vertical>
<n-select v-model:value="value" multiple :options="options" />
<n-select v-model:value="value" multiple disabled :options="options" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
value: ref(['song3']),
options: [
{
label: "Everybody's Got Something to Hide Except Me and My Monkey",
value: 'song0',
disabled: true
},
{
label: 'Drive My Car',
value: 'song1'
},
{
label: 'Norwegian Wood',
value: 'song2'
},
{
label: "You Won't See",
value: 'song3',
disabled: true
},
{
label: 'Nowhere Man',
value: 'song4'
},
{
label: 'Think For Yourself',
value: 'song5'
},
{
label: 'The Word',
value: 'song6'
},
{
label: 'Michelle',
value: 'song7',
disabled: true
},
{
label: 'What goes on',
value: 'song8'
},
{
label: 'Girl',
value: 'song9'
},
{
label: "I'm looking through you",
value: 'song10'
},
{
label: 'In My Life',
value: 'song11'
},
{
label: 'Wait',
value: 'song12'
}
]
}
}
})
</script>
```
### 树型选择 Tree Select
```vue
<template>
<n-tree-select
:options="options"
default-value="Drive My Car"
@update:value="handleUpdateValue"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { TreeSelectOption } from 'naive-ui'
export default defineComponent({
setup () {
return {
handleUpdateValue (
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option)
},
options: [
{
label: 'Rubber Soul',
key: 'Rubber Soul',
children: [
{
label:
"Everybody's Got Something to Hide Except Me and My Monkey",
key: "Everybody's Got Something to Hide Except Me and My Monkey"
},
{
label: 'Drive My Car',
key: 'Drive My Car',
disabled: true
},
{
label: 'Norwegian Wood',
key: 'Norwegian Wood'
},
{
label: "You Won't See",
key: "You Won't See",
disabled: true
},
{
label: 'Nowhere Man',
key: 'Nowhere Man'
},
{
label: 'Think For Yourself',
key: 'Think For Yourself'
},
{
label: 'The Word',
key: 'The Word'
},
{
label: 'Michelle',
key: 'Michelle',
disabled: true
},
{
label: 'What goes on',
key: 'What goes on'
},
{
label: 'Girl',
key: 'Girl'
},
{
label: "I'm looking through you",
key: "I'm looking through you"
},
{
label: 'In My Life',
key: 'In My Life'
},
{
label: 'Wait',
key: 'Wait'
}
]
},
{
label: 'Let It Be',
key: 'Let It Be Album',
children: [
{
label: 'Two Of Us',
key: 'Two Of Us'
},
{
label: 'Dig A Pony',
key: 'Dig A Pony'
},
{
label: 'Across The Universe',
key: 'Across The Universe'
},
{
label: 'I Me Mine',
key: 'I Me Mine'
},
{
label: 'Dig It',
key: 'Dig It'
},
{
label: 'Let It Be',
key: 'Let It Be'
},
{
label: 'Maggie Mae',
key: 'Maggie Mae'
},
{
label: "I've Got A Feeling",
key: "I've Got A Feeling"
},
{
label: 'One After 909',
key: 'One After 909'
},
{
label: 'The Long And Winding Road',
key: 'The Long And Winding Road'
},
{
label: 'For You Blue',
key: 'For You Blue'
},
{
label: 'Get Back',
key: 'Get Back'
}
]
}
]
}
}
})
</script><template>
<n-tree-select
:options="options"
default-value="Drive My Car"
@update:value="handleUpdateValue"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { TreeSelectOption } from 'naive-ui'
export default defineComponent({
setup () {
return {
handleUpdateValue (
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option)
},
options: [
{
label: 'Rubber Soul',
key: 'Rubber Soul',
children: [
{
label:
"Everybody's Got Something to Hide Except Me and My Monkey",
key: "Everybody's Got Something to Hide Except Me and My Monkey"
},
{
label: 'Drive My Car',
key: 'Drive My Car',
disabled: true
},
{
label: 'Norwegian Wood',
key: 'Norwegian Wood'
},
{
label: "You Won't See",
key: "You Won't See",
disabled: true
},
{
label: 'Nowhere Man',
key: 'Nowhere Man'
},
{
label: 'Think For Yourself',
key: 'Think For Yourself'
},
{
label: 'The Word',
key: 'The Word'
},
{
label: 'Michelle',
key: 'Michelle',
disabled: true
},
{
label: 'What goes on',
key: 'What goes on'
},
{
label: 'Girl',
key: 'Girl'
},
{
label: "I'm looking through you",
key: "I'm looking through you"
},
{
label: 'In My Life',
key: 'In My Life'
},
{
label: 'Wait',
key: 'Wait'
}
]
},
{
label: 'Let It Be',
key: 'Let It Be Album',
children: [
{
label: 'Two Of Us',
key: 'Two Of Us'
},
{
label: 'Dig A Pony',
key: 'Dig A Pony'
},
{
label: 'Across The Universe',
key: 'Across The Universe'
},
{
label: 'I Me Mine',
key: 'I Me Mine'
},
{
label: 'Dig It',
key: 'Dig It'
},
{
label: 'Let It Be',
key: 'Let It Be'
},
{
label: 'Maggie Mae',
key: 'Maggie Mae'
},
{
label: "I've Got A Feeling",
key: "I've Got A Feeling"
},
{
label: 'One After 909',
key: 'One After 909'
},
{
label: 'The Long And Winding Road',
key: 'The Long And Winding Road'
},
{
label: 'For You Blue',
key: 'For You Blue'
},
{
label: 'Get Back',
key: 'Get Back'
}
]
}
]
}
}
})
</script>
```
### 单图上传 UploadImage
```vue
<template>
<UploadImage :maxNumber="1" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import UploadImage from '@/components/Upload/uploadImage.vue';
const value = ref(null);
</script>
```
### 多图上传 UploadImage
```vue
<template>
<UploadImage :maxNumber="10" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import UploadImage from '@/components/Upload/uploadImage.vue';
const value = ref(null);
</script>
```
### 单文件上传 UploadFile
```vue
<template>
<UploadFile :maxNumber="1" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import UploadFile from '@/components/Upload/uploadFile.vue';
const value = ref(null);
</script>
```
### 多文件上传 UploadFile
```vue
<template>
<UploadFile :maxNumber="10" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import UploadFile from '@/components/Upload/uploadFile.vue';
const value = ref(null);
</script>
```
### 开关 Switch
```vue
<template>
<n-switch v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 评分 Rate
```vue
<template>
<n-rate allow-half :default-value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 省市区选择器 CitySelector
```vue
<template>
<CitySelector v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import CitySelector from '@/components/CitySelector/citySelector.vue';
const value = ref(null);
</script>
```
### 图标选择器 IconSelector
```vue
<template>
<IconSelector style="width: 100%" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import IconSelector from '@/components/IconSelector/index.vue';
const value = ref(null);
</script>
```
更多组件请参考https://www.naiveui.com/zh-CN/os-theme/components/button