diff --git a/.gitignore b/.gitignore index 5f313528..84af0c17 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ /.fleet /.vscode/*.log /cmd/answer/*.sh -/cmd/answer/upfiles/* +/cmd/answer/uploads/* /cmd/logs /configs/config-dev.yaml /go.work* diff --git a/Dockerfile b/Dockerfile index 8b55d58f..ac06839a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ RUN apk --no-cache add build-base git \ && make clean build \ && cp answer /usr/bin/answer -RUN mkdir -p /data/upfiles && chmod 777 /data/upfiles \ +RUN mkdir -p /data/uploads && chmod 777 /data/uploads \ && mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n # stage3 copy the binary and resource files into fresh container diff --git a/INSTALL.md b/INSTALL.md index 0d3969d2..7bc307b5 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -91,7 +91,7 @@ swaggerui: service_config: secret_key: "answer" #encryption key web_host: "http://127.0.0.1" #Page access using domain name address - upload_path: "./upfiles" #upload directory + upload_path: "./uploads" #upload directory ``` ## Compile the image @@ -100,4 +100,4 @@ If you have modified the source files and want to repackage the image, you can u docker build -t answer:v1.0.0 . ``` ## common problem - 1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`upfiles`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and upfiles directories. + 1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`uploads`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and uploads directories. diff --git a/INSTALL_CN.md b/INSTALL_CN.md index 300851a2..bc121771 100644 --- a/INSTALL_CN.md +++ b/INSTALL_CN.md @@ -1,80 +1,54 @@ # Answer 安装指引 -安装 Answer 之前,您需要先安装基本环境。 - - 数据库 - - [MySQL](http://dev.mysql.com):版本 >= 5.7 - -然后,您可以通过以下几种方式来安装 Answer: - - - 采用 Docker 部署 - - 二进制安装 - - 源码安装 - -## 使用 Docker-compose 安装 Answer +## 使用 docker 安装 +### 步骤 1: 使用 docker 命令启动项目 ```bash -$ mkdir answer && cd answer -$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml -$ docker-compose up +docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest +``` +### 步骤 2: 访问安装路径进行项目安装 +[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) + +选择语言后点击下一步选择合适的数据库,如果当前只是想体验,建议直接选择 sqlite 作为数据库,如下图所示 + + + +然后点击下一步会进行配置文件创建等操作,点击下一步输入网站基本信息和管理员信息,如下图所示 + + + +点击下一步即可安装完成 + +### 步骤 3:安装完成后访问项目路径开始使用 +[http://127.0.0.1:9080/](http://127.0.0.1:9080/) + +使用刚才创建的管理员用户名密码即可登录。 + +## 使用 docker-compose 安装 +### 步骤 1: 使用 docker-compose 命令启动项目 +```bash +mkdir answer && cd answer +wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml +docker-compose up ``` -启动完成后使用浏览器访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/). +### 步骤 2: 访问安装路径进行项目安装 +[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) -你可以使用默认的用户名:( **`admin@admin.com`** ) 和密码:( **`admin`** ) 进行登录. +具体配置与 docker 使用时相同 -## 使用Docker 安装 Answer -可以从 Docker Hub 或者 GitHub Container registry 下载最新的 tags 镜像 +### 步骤 3:安装完成后访问项目路径开始使用 +[http://127.0.0.1:9080/](http://127.0.0.1:9080/) -### 用法 -将配置和存储目录挂在到镜像之外 volume (/var/data -> /data),你可以修改外部挂载地址 +## 使用 二进制 安装 +### 步骤 1: 下载二进制文件 +[https://github.com/answerdev/answer/releases](https://github.com/answerdev/answer/releases) +请下载您当下系统所需要的对应版本 -``` -# 将镜像从 docker hub 拉到本地 -$ docker pull answerdev/answer:latest - -# 创建一个挂载目录 -$ mkdir -p /var/data - -# 先运行一遍镜像 -$ docker run --name=answer -p 9080:80 -v /var/data:/data answerdev/answer - -# 第一次启动后会在/var/data 目录下生成配置文件 -# /var/data/conf/config.yaml -# 需要修改配置文件中的Mysql 数据库地址 -vim /var/data/conf/config.yaml - -# 修改数据库连接 connection: [username]:[password]@tcp([host]:[port])/[DbName] -... - -# 配置好配置文件后可以再次启动镜像即可启动服务 -$ docker start answer -``` - -## 使用二进制 安装 Answer -可以使用编译完成的各个平台的二进制文件运行 Answer 项目 -### 用法 -从 GitHub 最新版本的tag中下载对应平台的二进制文件压缩包 - - 1. 解压压缩包 - 2. 使用命令 cd 进入到刚刚创建的目录 - 3. 执行命令 ./answer init - 4. Answer 会在当前目录生成 ./data 目录 - 5. 进入 data 目录修改 config.yaml 文件 - 6. 将数据库连接地址修改为你的数据库连接地址 - - connection: [username]:[password]@tcp([host]:[port])/[DbName] - 7. 退出 data 目录,执行 ./answer run -c ./data/conf/config.yaml - -## 当前支持的命令 -用法: answer [command] - -- help: 帮助 -- init: 初始化环境 -- run: 启动 -- check: 环境依赖检查 -- dump: 备份数据 - -## 配置文件 config.yaml 参数说明 +### 步骤 2: 使用命令行安装 +> 以下命令中 -C 指定的是 answer 所需的数据目录,您可以根据实际需要进行修改 +```bash +./answer init -C ./answer-data/ ``` server: http: diff --git a/cmd/answer/command.go b/cmd/answer/command.go index d0efb0ab..5d026f77 100644 --- a/cmd/answer/command.go +++ b/cmd/answer/command.go @@ -4,14 +4,14 @@ import ( "fmt" "os" + "github.com/answerdev/answer/internal/base/conf" "github.com/answerdev/answer/internal/cli" + "github.com/answerdev/answer/internal/install" "github.com/answerdev/answer/internal/migrations" "github.com/spf13/cobra" ) var ( - // configFilePath is the config file path - configFilePath string // dataDirPath save all answer application data in this directory. like config file, upload file... dataDirPath string // dumpDataPath dump data path @@ -21,9 +21,7 @@ var ( func init() { rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time) - initCmd.Flags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/") - - rootCmd.PersistentFlags().StringVarP(&configFilePath, "config", "c", "", "config path, eg: -c config.yaml") + rootCmd.PersistentFlags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/") dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/") @@ -49,6 +47,9 @@ To run answer, use: Short: "Run the application", Long: `Run the application`, Run: func(_ *cobra.Command, _ []string) { + cli.FormatAllPath(dataDirPath) + fmt.Println("config file path: ", cli.GetConfigFilePath()) + fmt.Println("Answer is string..........................") runApp() }, } @@ -59,18 +60,27 @@ To run answer, use: Short: "init answer application", Long: `init answer application`, Run: func(_ *cobra.Command, _ []string) { + // check config file and database. if config file exists and database is already created, init done cli.InstallAllInitialEnvironment(dataDirPath) - c, err := readConfig() - if err != nil { - fmt.Println("read config failed: ", err.Error()) - return + + configFileExist := cli.CheckConfigFile(cli.GetConfigFilePath()) + if configFileExist { + fmt.Println("config file exists, try to read the config...") + c, err := conf.ReadConfig(cli.GetConfigFilePath()) + if err != nil { + fmt.Println("read config failed: ", err.Error()) + return + } + + fmt.Println("config file read successfully, try to connect database...") + if cli.CheckDBTableExist(c.Data.Database) { + fmt.Println("connect to database successfully and table already exists, do nothing.") + return + } } - fmt.Println("read config successfully") - if err := migrations.InitDB(c.Data.Database); err != nil { - fmt.Println("init database error: ", err.Error()) - return - } - fmt.Println("init database successfully") + + // start installation server to install + install.Run(cli.GetConfigFilePath()) }, } @@ -80,7 +90,8 @@ To run answer, use: Short: "upgrade Answer version", Long: `upgrade Answer version`, Run: func(_ *cobra.Command, _ []string) { - c, err := readConfig() + cli.FormatAllPath(dataDirPath) + c, err := conf.ReadConfig(cli.GetConfigFilePath()) if err != nil { fmt.Println("read config failed: ", err.Error()) return @@ -100,7 +111,8 @@ To run answer, use: Long: `back up data`, Run: func(_ *cobra.Command, _ []string) { fmt.Println("Answer is backing up data") - c, err := readConfig() + cli.FormatAllPath(dataDirPath) + c, err := conf.ReadConfig(cli.GetConfigFilePath()) if err != nil { fmt.Println("read config failed: ", err.Error()) return @@ -120,8 +132,9 @@ To run answer, use: Short: "checking the required environment", Long: `Check if the current environment meets the startup requirements`, Run: func(_ *cobra.Command, _ []string) { + cli.FormatAllPath(dataDirPath) fmt.Println("Start checking the required environment...") - if cli.CheckConfigFile(configFilePath) { + if cli.CheckConfigFile(cli.GetConfigFilePath()) { fmt.Println("config file exists [✔]") } else { fmt.Println("config file not exists [x]") @@ -133,13 +146,13 @@ To run answer, use: fmt.Println("upload directory not exists [x]") } - c, err := readConfig() + c, err := conf.ReadConfig(cli.GetConfigFilePath()) if err != nil { fmt.Println("read config failed: ", err.Error()) return } - if cli.CheckDB(c.Data.Database) { + if cli.CheckDBConnection(c.Data.Database) { fmt.Println("db connection successfully [✔]") } else { fmt.Println("db connection failed [x]") diff --git a/cmd/answer/main.go b/cmd/answer/main.go index 08214244..0cc8ea74 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -2,13 +2,14 @@ package main import ( "os" - "path/filepath" + "time" "github.com/answerdev/answer/internal/base/conf" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/cli" + "github.com/answerdev/answer/internal/schema" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman" - "github.com/segmentfault/pacman/contrib/conf/viper" "github.com/segmentfault/pacman/contrib/log/zap" "github.com/segmentfault/pacman/contrib/server/http" "github.com/segmentfault/pacman/log" @@ -40,8 +41,7 @@ func main() { func runApp() { log.SetLogger(zap.NewLogger( log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) - - c, err := readConfig() + c, err := conf.ReadConfig(cli.GetConfigFilePath()) if err != nil { panic(err) } @@ -50,27 +50,15 @@ func runApp() { if err != nil { panic(err) } + constant.Version = Version + schema.AppStartTime = time.Now() + defer cleanup() if err := app.Run(); err != nil { panic(err) } } -func readConfig() (c *conf.AllConfig, err error) { - if len(configFilePath) == 0 { - configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName) - } - c = &conf.AllConfig{} - config, err := viper.NewWithPath(configFilePath) - if err != nil { - return nil, err - } - if err = config.Parse(&c); err != nil { - return nil, err - } - return c, nil -} - func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application { return pacman.NewApp( pacman.WithName(Name), diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index a965ed8b..7b7a6937 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -44,6 +44,7 @@ import ( auth2 "github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/collection_common" comment2 "github.com/answerdev/answer/internal/service/comment" + "github.com/answerdev/answer/internal/service/dashboard" export2 "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/follow" meta2 "github.com/answerdev/answer/internal/service/meta" @@ -58,6 +59,8 @@ import ( "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/internal/service/siteinfo" + "github.com/answerdev/answer/internal/service/siteinfo_common" tag2 "github.com/answerdev/answer/internal/service/tag" "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/uploader" @@ -76,7 +79,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, if err != nil { return nil, nil, err } - langController := controller.NewLangController(i18nTranslator) engine, err := data.NewDB(debug, dbConf) if err != nil { return nil, nil, err @@ -90,6 +92,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cleanup() return nil, nil, err } + siteInfoRepo := site_info.NewSiteInfo(dataData) + siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) + langController := controller.NewLangController(i18nTranslator, siteInfoCommonService) authRepo := auth.NewAuthRepo(dataData) authService := auth2.NewAuthService(authRepo) configRepo := config.NewConfigRepo(dataData) @@ -99,12 +104,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) emailRepo := export.NewEmailRepo(dataData) - siteInfoRepo := site_info.NewSiteInfo(dataData) emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) - userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) + userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) - uploaderService := uploader.NewUploaderService(serviceConf) + uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService) userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService) commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) @@ -148,7 +152,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - answerController := controller.NewAnswerController(answerService, rankService) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData) + answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) searchController := controller.NewSearchController(searchService) @@ -166,14 +171,15 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_backyard.NewThemeController() - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) + siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) - siteinfoController := controller.NewSiteinfoController(siteInfoService) + siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) notificationController := controller.NewNotificationController(notificationService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController) + dashboardController := controller.NewDashboardController(dashboardService) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter() authUserMiddleware := middleware.NewAuthUserMiddleware(authService) diff --git a/configs/config.yaml b/configs/config.yaml index 6830048f..8032deed 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -17,4 +17,4 @@ swaggerui: service_config: secret_key: "answer" web_host: "http://127.0.0.1:9080" - upload_path: "/data/upfiles" + upload_path: "/data/uploads" diff --git a/docs/docs.go b/docs/docs.go index f21f508c..eb097216 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -62,12 +62,6 @@ const docTemplate = `{ "description": "answer id or question title", "name": "query", "in": "query" - }, - { - "type": "string", - "description": "question id", - "name": "question_id", - "in": "query" } ], "responses": { @@ -119,6 +113,34 @@ const docTemplate = `{ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "security": [ @@ -487,14 +509,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -522,14 +544,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -558,25 +580,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], + "summary": "get site interface", "responses": { "200": { "description": "OK", @@ -604,14 +615,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -2710,14 +2721,14 @@ const docTemplate = `{ }, "/answer/api/v1/siteinfo": { "get": { - "description": "Get siteinfo", + "description": "get site info", "produces": [ "application/json" ], "tags": [ "site" ], - "summary": "Get siteinfo", + "summary": "get site info", "responses": { "200": { "description": "OK", @@ -5281,11 +5292,17 @@ const docTemplate = `{ "schema.SiteGeneralReq": { "type": "object", "required": [ + "contact_email", "description", "name", - "short_description" + "short_description", + "site_url" ], "properties": { + "contact_email": { + "type": "string", + "maxLength": 512 + }, "description": { "type": "string", "maxLength": 2000 @@ -5297,17 +5314,27 @@ const docTemplate = `{ "short_description": { "type": "string", "maxLength": 255 + }, + "site_url": { + "type": "string", + "maxLength": 512 } } }, "schema.SiteGeneralResp": { "type": "object", "required": [ + "contact_email", "description", "name", - "short_description" + "short_description", + "site_url" ], "properties": { + "contact_email": { + "type": "string", + "maxLength": 512 + }, "description": { "type": "string", "maxLength": 2000 @@ -5319,6 +5346,10 @@ const docTemplate = `{ "short_description": { "type": "string", "maxLength": 255 + }, + "site_url": { + "type": "string", + "maxLength": 512 } } }, @@ -5326,7 +5357,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5340,6 +5372,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5347,7 +5383,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5361,6 +5398,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/img/install-database.png b/docs/img/install-database.png new file mode 100644 index 00000000..09fbf36a Binary files /dev/null and b/docs/img/install-database.png differ diff --git a/docs/img/install-site-info.png b/docs/img/install-site-info.png new file mode 100644 index 00000000..b8166caf Binary files /dev/null and b/docs/img/install-site-info.png differ diff --git a/docs/swagger.json b/docs/swagger.json index 039f67f5..a45709d0 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -50,12 +50,6 @@ "description": "answer id or question title", "name": "query", "in": "query" - }, - { - "type": "string", - "description": "question id", - "name": "question_id", - "in": "query" } ], "responses": { @@ -107,6 +101,34 @@ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "security": [ @@ -475,14 +497,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -510,14 +532,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -546,25 +568,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], + "summary": "get site interface", "responses": { "200": { "description": "OK", @@ -592,14 +603,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -2698,14 +2709,14 @@ }, "/answer/api/v1/siteinfo": { "get": { - "description": "Get siteinfo", + "description": "get site info", "produces": [ "application/json" ], "tags": [ "site" ], - "summary": "Get siteinfo", + "summary": "get site info", "responses": { "200": { "description": "OK", @@ -5269,11 +5280,17 @@ "schema.SiteGeneralReq": { "type": "object", "required": [ + "contact_email", "description", "name", - "short_description" + "short_description", + "site_url" ], "properties": { + "contact_email": { + "type": "string", + "maxLength": 512 + }, "description": { "type": "string", "maxLength": 2000 @@ -5285,17 +5302,27 @@ "short_description": { "type": "string", "maxLength": 255 + }, + "site_url": { + "type": "string", + "maxLength": 512 } } }, "schema.SiteGeneralResp": { "type": "object", "required": [ + "contact_email", "description", "name", - "short_description" + "short_description", + "site_url" ], "properties": { + "contact_email": { + "type": "string", + "maxLength": 512 + }, "description": { "type": "string", "maxLength": 2000 @@ -5307,6 +5334,10 @@ "short_description": { "type": "string", "maxLength": 255 + }, + "site_url": { + "type": "string", + "maxLength": 512 } } }, @@ -5314,7 +5345,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5328,6 +5360,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5335,7 +5371,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5349,6 +5386,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c419b60e..17b2360a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -983,6 +983,9 @@ definitions: type: object schema.SiteGeneralReq: properties: + contact_email: + maxLength: 512 + type: string description: maxLength: 2000 type: string @@ -992,13 +995,21 @@ definitions: short_description: maxLength: 255 type: string + site_url: + maxLength: 512 + type: string required: + - contact_email - description - name - short_description + - site_url type: object schema.SiteGeneralResp: properties: + contact_email: + maxLength: 512 + type: string description: maxLength: 2000 type: string @@ -1008,10 +1019,15 @@ definitions: short_description: maxLength: 255 type: string + site_url: + maxLength: 512 + type: string required: + - contact_email - description - name - short_description + - site_url type: object schema.SiteInterfaceReq: properties: @@ -1024,9 +1040,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.SiteInterfaceResp: properties: @@ -1039,9 +1059,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.TagItem: properties: @@ -1394,10 +1418,6 @@ paths: in: query name: query type: string - - description: question id - in: query - name: question_id - type: string produces: - application/json responses: @@ -1434,6 +1454,23 @@ paths: summary: AdminSetAnswerStatus tags: - admin + /answer/admin/api/dashboard: + get: + consumes: + - application/json + description: DashboardInfo + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: DashboardInfo + tags: + - admin /answer/admin/api/language/options: get: description: Get language options @@ -1662,7 +1699,7 @@ paths: - admin /answer/admin/api/siteinfo/general: get: - description: Get siteinfo general + description: get site general information produces: - application/json responses: @@ -1677,11 +1714,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo general + summary: get site general information tags: - admin put: - description: Get siteinfo interface + description: update site general information parameters: - description: general in: body @@ -1698,19 +1735,12 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site general information tags: - admin /answer/admin/api/siteinfo/interface: get: - description: Get siteinfo interface - parameters: - - description: general - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.AddCommentReq' + description: get site interface produces: - application/json responses: @@ -1725,11 +1755,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: get site interface tags: - admin put: - description: Get siteinfo interface + description: update site info interface parameters: - description: general in: body @@ -1746,7 +1776,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site info interface tags: - admin /answer/admin/api/theme/options: @@ -3014,7 +3044,7 @@ paths: - Search /answer/api/v1/siteinfo: get: - description: Get siteinfo + description: get site info produces: - application/json responses: @@ -3027,7 +3057,7 @@ paths: data: $ref: '#/definitions/schema.SiteGeneralResp' type: object - summary: Get siteinfo + summary: get site info tags: - site /answer/api/v1/tag: diff --git a/go.mod b/go.mod index d6d85117..95796a93 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/goccy/go-json v0.9.11 github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 + github.com/grokify/html-strip-tags-go v0.0.1 github.com/jinzhu/copier v0.3.5 github.com/jinzhu/now v1.1.5 github.com/lib/pq v1.10.7 @@ -24,7 +25,7 @@ require ( github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 github.com/spf13/cobra v1.6.1 @@ -35,6 +36,7 @@ require ( golang.org/x/crypto v0.1.0 golang.org/x/net v0.1.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/yaml.v3 v3.0.1 xorm.io/builder v0.3.12 xorm.io/core v0.7.3 xorm.io/xorm v1.3.2 @@ -110,6 +112,5 @@ require ( gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index be85073b..f12aeb7a 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -594,8 +596,8 @@ github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A= diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 824c92e6..f3fe2602 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1,172 +1,1128 @@ -base: - success: - other: "Success." - unknown: - other: "Unknown error." - request_format_error: - other: "Request format is not valid." - unauthorized_error: - other: "Unauthorized." - database_error: - other: "Data server error." +# The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." -email: - other: "Email" -password: - other: "Password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email and password do not match." - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Answer do not found." - comment: - edit_without_permission: - other: "Comment are not allowed to edit." - not_found: - other: "Comment not found." email: - duplicate: - other: "Email already exists." - need_to_be_verified: - other: "Email should be verified." - verify_url_expired: - other: "Email verified URL has expired, please resend the email." - lang: - not_found: - other: "Language file not found." - object: - captcha_verification_failed: - other: "Captcha wrong." - disallow_follow: - other: "You are not allowed to follow." - disallow_vote: - other: "You are not allowed to vote." - disallow_vote_your_self: - other: "You can't vote for your own post." - not_found: - other: "Object not found." - verification_failed: - other: "Verification failed." - email_or_password_incorrect: - other: "Email and password do not match." - old_password_verification_failed: - other: "The old password verification failed" - new_password_same_as_previous_setting: - other: "The new password is the same as the previous one." - question: - not_found: - other: "Question not found." - rank: - fail_to_meet_the_condition: - other: "Rank fail to meet the condition." + other: "Email" + password: + other: "Password" + + email_or_password_wrong_error: &email_or_password_wrong + other: "Email and password do not match." + + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Answer do not found." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + theme: + not_found: + other: "Theme not found." + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + create_table_failed: + other: "Create table failed" + install: + create_config_failed: + other: "Can’t create the config.yaml file." report: - handle_failed: - other: "Report handle failed." - not_found: - other: "Report not found." - tag: - not_found: - other: "Tag not found." - theme: - not_found: - other: "Theme not found." - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "User not found." - suspended: - other: "User has been suspended." - username_invalid: - other: "Username is invalid." - username_duplicate: - other: "Username is already in use." - set_avatar: - other: "Avatar set failed." - -report: - spam: - name: - other: "spam" - description: - other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." - rude: - name: - other: "rude or abusive" - description: - other: "A reasonable person would find this content inappropriate for respectful discourse." - duplicate: - name: - other: "a duplicate" - description: - other: "This question has been asked before and already has an answer." - not_answer: - name: - other: "not an answer" - description: - other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." - not_need: - name: - other: "no longer needed" - description: - other: "This comment is outdated, conversational or not relevant to this post." - other: - name: - other: "something else" - description: - other: "This post requires staff attention for another reason not listed above." - -question: - close: - duplicate: + spam: name: other: "spam" + description: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + description: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" description: other: "This question has been asked before and already has an answer." - guideline: + not_answer: name: - other: "a community-specific reason" + other: "not an answer" description: - other: "This question doesn't meet a community guideline." - multiple: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: name: - other: "needs details or clarity" + other: "no longer needed" description: - other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" description: - other: "This post requires another reason not listed above." + other: "This post requires staff attention for another reason not listed above." -notification: - action: - update_question: - other: "updated question" - answer_the_question: - other: "answered question" - update_answer: - other: "updated answer" - adopt_answer: - other: "accepted answer" - comment_question: - other: "commented question" - comment_answer: - other: "commented answer" - reply_to_you: - other: "replied to you" - mention_you: - other: "mentioned you" - your_question_is_closed: - other: "Your question has been closed" - your_question_was_deleted: - other: "Your question has been deleted" - your_answer_was_deleted: - other: "Your answer has been deleted" - your_comment_was_deleted: - other: "Your comment has been deleted" + question: + close: + duplicate: + name: + other: "spam" + description: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + description: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + description: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + description: + other: "This post requires another reason not listed above." + + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + adopt_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + description: >- +
to make links
<https://url.com>
[Title](https://url.com)
put returns between paragraphs
_italic_ or **bold**
indent code by 4 spaces
quote by
+ placing >
at start of line
backtick escapes `like _this_`
create code fences with backticks `
```
code here
```
We do not allowed deleting tag with posts.
Please remove this tag + from the posts first.
+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: 'Must use the character set "a-z", "0-9", "+ # - ."' + description: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: 'MMM D, YYYY' + long_date_with_time: 'MMM D, YYYY [at] HH:mm' + now: now + x_seconds_ago: '{{count}}s ago' + x_minutes_ago: '{{count}}m ago' + x_hours_ago: '{{count}}h ago' + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid + answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are + adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_description: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: 'Describe what your question is about, at least one tag is required.' + no_result: No tags matched + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer 1>- the open-source software that power Q&A + communitiesAre you sure you want to add another answer?
You could use the + edit link to refine and improve your existing answer, instead.
+ empty: Answer cannot be empty. + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because + doing so deprives future readers of this knowledge.Repeated deletion + of answered questions can result in your account being blocked from asking. + Are you sure you wish to delete? + answer_accepted: >- +
We do not recommend deleting accepted answer because + doing so deprives future readers of this knowledge.
Repeated deletion + of accepted answers can result in your account being blocked from answering. + Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: '{{count}} Results' + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: '<1>[tag]1> search withing a tag' + user: '<1>user:username1> search by author' + answer: '<1>answers:01> unanswered questions' + score: '<1>score:31> posts with a 3+ score' + question: '<1>is:question1> search questions' + is_answer: '<1>is:answer1> search answers' + empty: We couldn't find anything.添加链接:
<https://url.com>
[标题](https://url.com)
段落之间使用空行分隔
_斜体_ 或者 + **粗体**
使用 4 + 个空格缩进代码
在行首添加>
表示引用
反引号进行转义
+ `像 _这样_`
使用```
创建代码块
```
//
+ 这是代码
```
不允许删除有关联问题的标签。
请先从关联的问题中删除此标签的引用。
+ content2: 确定要删除吗? + close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + form: + fields: + revision: + label: 编辑历史 + display_name: + label: 名称 + slug_name: + label: URL 固定链接 + info: '必须由 "a-z", "0-9", "+ # - ." 组成' + description: + label: 描述 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: MM月DD日 + long_date_with_year: YYYY年MM月DD日 + long_date_with_time: 'YYYY年MM月DD日 HH:mm' + now: 刚刚 + x_seconds_ago: '{{count}} 秒前' + x_minutes_ago: '{{count}} 分钟前' + x_hours_ago: '{{count}} 小时前' + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存 + btn_cancel: 取消 + show_more: 显示更多评论 + tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。 + tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名过滤 + no_description: 此标签无描述。 + more: 更多 + ask: + title: 提交新的问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + similar_questions: 相似的问题 + form: + fields: + revision: + label: 编辑历史 + title: + label: 标题 + placeholder: 请详细描述你的问题 + msg: + empty: 标题不能为空 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 直接发表回答 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: 选择至少一个与问题相关的标签。 + no_result: 没有匹配的标签 + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + profile: 用户主页 + setting: 账号设置 + logout: 退出登录 + admin: 后台管理 + search: + placeholder: 搜索 + footer: + build_on: >- + Built on <1> Answer 1>- the open-source software that power Q&A + communities您确定要提交一个新的回答吗?
您可以直接编辑和改善您之前的回答的。
+ empty: 回答内容不能为空。 + delete: + title: 删除 + question: >- + 我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +
我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。
如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_question_deleted: 此问题已被删除 + tip_answer_deleted: 此回答已被删除 + btns: + confirm: 确认 + cancel: 取消 + save: 保存 + delete: 删除 + login: 登录 + signup: 注册 + logout: 退出登录 + verify: 验证 + add_question: 我要提问 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: '{{count}} 个结果' + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + tips: + title: 高级搜索提示 + tag: '<1>[tag]1> 在指定标签中搜索' + user: '<1>user:username1> 根据作者搜索' + answer: '<1>answers:01> 搜索未回答的问题' + score: '<1>score:31> 评分 3 分或以上' + question: '<1>is:question1> 只搜索问题' + is_answer: '<1>is:answer1> 只搜索回答' + empty: 找不到任何相关的内容。hello
var a = \"good\"") + assert.Equal(t, expected, clearedText) + + // test link clear text + expected = "hello [example.com]" + clearedText = ClearText("
hello example.com
") + assert.Equal(t, expected, clearedText) + clearedText = ClearText("helloexample.com
") + assert.Equal(t, expected, clearedText) + + expected = "hello world" + clearedText = ClearText("hello world
", "...", 5) + assert.Equal(t, expected, text) + + // test mixed string + expected = "hello你好..." + text = FetchExcerpt("hello你好world
", "...", 7) + assert.Equal(t, expected, text) + + // test mixed string with emoticon + expected = "hello你好😂..." + text = FetchExcerpt("hello你好😂world
", "...", 8) + assert.Equal(t, expected, text) +} diff --git a/pkg/writer/writer.go b/pkg/writer/writer.go new file mode 100644 index 00000000..d2b6c440 --- /dev/null +++ b/pkg/writer/writer.go @@ -0,0 +1,31 @@ +package writer + +import ( + "bufio" + "os" +) + +// ReplaceFile remove old file and write new file +func ReplaceFile(filePath, content string) error { + _ = os.Remove(filePath) + return WriteFile(filePath, content) +} + +// WriteFile write file to path +func WriteFile(filePath, content string) error { + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666) + if err != nil { + return err + } + defer func() { + _ = file.Close() + }() + writer := bufio.NewWriter(file) + if _, err := writer.WriteString(content); err != nil { + return err + } + if err := writer.Flush(); err != nil { + return err + } + return nil +} diff --git a/script/entrypoint.sh b/script/entrypoint.sh index c66528d8..41ce1c7a 100755 --- a/script/entrypoint.sh +++ b/script/entrypoint.sh @@ -1,3 +1,4 @@ #!/bin/bash /usr/bin/answer init -/usr/bin/answer run -c /data/conf/config.yaml +/usr/bin/answer upgrade +/usr/bin/answer run -C /data/ diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 877a0039..6ecb4b49 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + root: true, env: { browser: true, es2021: true, @@ -19,7 +20,8 @@ module.exports = { }, ecmaVersion: 'latest', sourceType: 'module', - project: './tsconfig.json', + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], }, plugins: ['react', '@typescript-eslint'], rules: { @@ -64,7 +66,7 @@ module.exports = { position: 'before', }, { - pattern: '@answer/**', + pattern: '@/**', group: 'internal', }, { diff --git a/ui/commitlint.config.js b/ui/commitlint.config.js index 84dcb122..4944db0e 100644 --- a/ui/commitlint.config.js +++ b/ui/commitlint.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: ['@commitlint/config-conventional'], + extends: ['@commitlint/routes-conventional'], }; diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 06464361..0a5deeff 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -1,36 +1,51 @@ -const path = require('path'); +const { + addWebpackModuleRule, + addWebpackAlias +} = require("customize-cra"); + +const path = require("path"); +const i18nPath = path.resolve(__dirname, "../i18n"); module.exports = { - webpack: function (config, env) { - if (env === 'production') { + webpack: function(config, env) { + if (env === "production") { config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH; } - config.resolve.alias = { - ...config.resolve.alias, - '@': path.resolve(__dirname, 'src'), - '@answer/pages': path.resolve(__dirname, 'src/pages'), - '@answer/components': path.resolve(__dirname, 'src/components'), - '@answer/stores': path.resolve(__dirname, 'src/stores'), - '@answer/hooks': path.resolve(__dirname, 'src/hooks'), - '@answer/utils': path.resolve(__dirname, 'src/utils'), - '@answer/common': path.resolve(__dirname, 'src/common'), - '@answer/api': path.resolve(__dirname, 'src/services/api'), - }; + + addWebpackAlias({ + ["@"]: path.resolve(__dirname, "src"), + "@i18n": i18nPath + })(config); + + addWebpackModuleRule({ + test: /\.ya?ml$/, + use: "yaml-loader" + })(config); + + // add i18n dir to ModuleScopePlugin allowedPaths + const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === "ModuleScopePlugin"); + if (moduleScopePlugin) { + moduleScopePlugin.allowedPaths.push(i18nPath); + } return config; }, - - devServer: function (configFunction) { - return function (proxy, allowedHost) { + devServer: function(configFunction) { + return function(proxy, allowedHost) { const config = configFunction(proxy, allowedHost); config.proxy = { - '/answer': { - target: 'http://10.0.10.98:2060', + "/answer": { + target: "http://10.0.10.98:2060", changeOrigin: true, - secure: false, + secure: false }, + "/installation": { + target: "http://10.0.10.98:2060", + changeOrigin: true, + secure: false + } }; return config; }; - }, + } }; diff --git a/ui/package.json b/ui/package.json index 069456ef..c09fac27 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,12 +10,12 @@ "build:prod": "env-cmd -f .env.production react-app-rewired build", "build": "env-cmd -f .env react-app-rewired build", "test": "react-app-rewired test", - "eject": "react-scripts eject", "lint": "eslint . --cache --fix --ext .ts,.tsx", "prepare": "cd .. && husky install", "cz": "cz", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", - "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" + "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"", + "preinstall": "node ./scripts/preinstall.js" }, "config": { "commitizen": { @@ -101,7 +101,8 @@ "sass": "^1.54.4", "tsconfig-paths-webpack-plugin": "^4.0.0", "typescript": "*", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "yaml-loader": "^0.8.0" }, "packageManager": "pnpm@7.9.5", "engines": { @@ -109,4 +110,4 @@ "pnpm": ">=7" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 505ff738..98ebd589 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -77,6 +77,7 @@ specifiers: tsconfig-paths-webpack-plugin: ^4.0.0 typescript: '*' web-vitals: ^2.1.4 + yaml-loader: ^0.8.0 zustand: ^4.1.1 dependencies: @@ -159,6 +160,7 @@ devDependencies: tsconfig-paths-webpack-plugin: 4.0.0 typescript: 4.8.3 web-vitals: 2.1.4 + yaml-loader: 0.8.0 packages: @@ -7040,6 +7042,10 @@ packages: filelist: 1.0.4 minimatch: 3.1.2 + /javascript-stringify/2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + dev: true + /jest-changed-files/27.5.1: resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11682,6 +11688,15 @@ packages: /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yaml-loader/0.8.0: + resolution: {integrity: sha512-LjeKnTzVBKWiQBeE2L9ssl6WprqaUIxCSNs5tle8PaDydgu3wVFXTbMfsvF2MSErpy9TDVa092n4q6adYwJaWg==} + engines: {node: '>= 12.13'} + dependencies: + javascript-stringify: 2.1.0 + loader-utils: 2.0.2 + yaml: 2.1.1 + dev: true + /yaml/1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} diff --git a/ui/scripts/preinstall.js b/ui/scripts/preinstall.js new file mode 100644 index 00000000..076ad606 --- /dev/null +++ b/ui/scripts/preinstall.js @@ -0,0 +1,5 @@ +// There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed. +if (!/pnpm/.test(process.env.npm_execpath)) { + console.warn(`\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\u001b[39m\n`) + process.exit(1) +} \ No newline at end of file diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 5d4f6925..92e9da21 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,8 +1,10 @@ import { RouterProvider } from 'react-router-dom'; -import router from '@/router'; +import './i18n/init'; +import { routes, createBrowserRouter } from '@/router'; function App() { + const router = createBrowserRouter(routes); returnto make links
<https://url.com>
[Title](https://url.com)
put returns between paragraphs
_italic_ or **bold**
indent code by 4 spaces
quote by placing >
at start of line
backtick escapes `like _this_`
create code fences with backticks `
```
code here
```
We do not allowed deleting tag with posts.
Please remove this tag from the posts first.
", - "content2": "Are you sure you wish to delete?", - "close": "Close" - } - }, - "edit_tag": { - "title": "Edit Tag", - "default_reason": "Edit tag", - "form": { - "fields": { - "revision": { - "label": "Revision" - }, - "display_name": { - "label": "Display Name" - }, - "slug_name": { - "label": "URL Slug", - "info": "Must use the character set \"a-z\", \"0-9\", \"+ # - .\"" - }, - "description": { - "label": "Description" - }, - "edit_summary": { - "label": "Edit Summary", - "placeholder": "Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)" - } - } - }, - "btn_save_edits": "Save edits", - "btn_cancel": "Cancel" - }, - "dates": { - "long_date": "MMM D", - "long_date_with_year": "MMM D, YYYY", - "long_date_with_time": "MMM D, YYYY [at] HH:mm", - "now": "now", - "x_seconds_ago": "{{count}}s ago", - "x_minutes_ago": "{{count}}m ago", - "x_hours_ago": "{{count}}h ago" - }, - "comment": { - "btn_add_comment": "Add comment", - "reply_to": "Reply to", - "btn_reply": "Reply", - "btn_edit": "Edit", - "btn_delete": "Delete", - "btn_flag": "Flag", - "btn_save_edits": "Save edits", - "btn_cancel": "Cancel", - "show_more": "Show more comment", - "tip_question": "Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.", - "tip_answer": "Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting." - }, - "edit_answer": { - "title": "Edit Answer", - "default_reason": "Edit answer", - "form": { - "fields": { - "revision": { - "label": "Revision" - }, - "answer": { - "label": "Answer" - }, - "edit_summary": { - "label": "Edit Summary", - "placeholder": "Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)" - } - } - }, - "btn_save_edits": "Save edits", - "btn_cancel": "Cancel" - }, - "tags": { - "title": "Tags", - "sort_buttons": { - "popular": "Popular", - "name": "Name", - "newest": "newest" - }, - "button_follow": "Follow", - "button_following": "Following", - "tag_label": "questions", - "search_placeholder": "Filter by tag name", - "no_description": "The tag has no description.", - "more": "More" - }, - "ask": { - "title": "Add Question", - "edit_title": "Edit Question", - "default_reason": "Edit question", - "similar_questions": "Similar questions", - "form": { - "fields": { - "revision": { - "label": "Revision" - }, - "title": { - "label": "Title", - "placeholder": "Be specific and imagine you're asking a question to another person", - "msg": { - "empty": "Title cannot be empty.", - "range": "Title up to 150 characters" - } - }, - "body": { - "label": "Body", - "msg": { - "empty": "Body cannot be empty." - } - }, - "tags": { - "label": "Tags", - "msg": { - "empty": "Tags cannot be empty." - } - }, - "answer": { - "label": "Answer", - "msg": { - "empty": "Answer cannot be empty." - } - } - } - }, - "btn_post_question": "Post your question", - "btn_save_edits": "Save edits", - "answer_question": "Answer your own question", - "post_question&answer": "Post your question and answer" - }, - "tag_selector": { - "add_btn": "Add tag", - "create_btn": "Create new tag", - "search_tag": "Search tag", - "hint": "Describe what your question is about, at least one tag is required.", - "no_result": "No tags matched" - }, - "header": { - "nav": { - "question": "Questions", - "tag": "Tags", - "user": "Users", - "profile": "Profile", - "setting": "Settings", - "logout": "Log out", - "admin": "Admin" - }, - "search": { - "placeholder": "Search" - } - }, - "footer": { - "build_on": "Built on <1> Answer 1>- the open-source software that powers Q&A communitiesAre you sure you want to add another answer?
You could use the edit link to refine and improve your existing answer, instead.
", - "empty": "Answer cannot be empty." - } - }, - "delete": { - "title": "Delete this post", - "question": "We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete?", - "answer_accepted": "
We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.
Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?", - "other": "Are you sure you wish to delete?", - "tip_question_deleted": "This post has been deleted", - "tip_answer_deleted": "This answer has been deleted" - }, - "btns": { - "confirm": "Confirm", - "cancel": "Cancel", - "save": "Save", - "delete": "Delete", - "login": "Log in", - "signup": "Sign up", - "logout": "Log out", - "verify": "Verify", - "add_question": "Add question" - }, - "search": { - "title": "Search Results", - "keywords": "Keywords", - "options": "Options", - "follow": "Follow", - "following": "Following", - "counts": "{{count}} Results", - "more": "More", - "sort_btns": { - "relevance": "Relevance", - "newest": "Newest", - "active": "Active", - "score": "Score" - }, - "tips": { - "title": "Advanced Search Tips", - "tag": "<1>[tag]1> search withing a tag", - "user": "<1>user:username1> search by author", - "answer": "<1>answers:01> unanswered questions", - "score": "<1>score:31> posts with a 3+ score", - "question": "<1>is:question1> search questions", - "is_answer": "<1>is:answer1> search answers" - }, - "empty": "We couldn't find anything.添加链接:
<https://url.com>
[标题](https://url.com)
段落之间使用空行分隔
_斜体_ 或者 **粗体**
使用 4 个空格缩进代码
在行首添加>
表示引用
反引号进行转义 `像 _这样_`
使用```
创建代码块
```
// 这是代码
```
不允许删除有关联问题的标签。
请先从关联的问题中删除此标签的引用。
", - "content2": "确定要删除吗?", - "close": "关闭" - } - }, - "edit_tag": { - "title": "编辑标签", - "default_reason": "编辑标签", - "form": { - "fields": { - "revision": { - "label": "编辑历史" - }, - "display_name": { - "label": "名称" - }, - "slug_name": { - "label": "URL 固定链接", - "info": "必须由 \"a-z\", \"0-9\", \"+ # - .\" 组成" - }, - "description": { - "label": "描述" - }, - "edit_summary": { - "label": "编辑概要", - "placeholder": "简单描述更改原因 (错别字、文字表达、格式等等)" - } - } - }, - "btn_save_edits": "保存更改", - "btn_cancel": "取消" - }, - "dates": { - "long_date": "YYYY年MM月", - "long_date_with_year": "YYYY年MM月DD日", - "long_date_with_time": "YYYY年MM月DD日 HH:mm", - "now": "刚刚", - "x_seconds_ago": "{{count}} 秒前", - "x_minutes_ago": "{{count}} 分钟前", - "x_hours_ago": "{{count}} 小时前" - }, - "comment": { - "btn_add_comment": "添加评论", - "reply_to": "回复", - "btn_reply": "回复", - "btn_edit": "编辑", - "btn_delete": "删除", - "btn_flag": "举报", - "btn_save_edits": "保存", - "btn_cancel": "取消", - "show_more": "显示更多评论", - "tip_question": "使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。", - "tip_answer": "使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。" - }, - "edit_answer": { - "title": "编辑回答", - "default_reason": "编辑回答", - "form": { - "fields": { - "revision": { - "label": "编辑历史" - }, - "answer": { - "label": "回答内容" - }, - "edit_summary": { - "label": "编辑概要", - "placeholder": "简单描述更改原因 (错别字、文字表达、格式等等)" - } - } - }, - "btn_save_edits": "保存更改", - "btn_cancel": "取消" - }, - "tags": { - "title": "标签", - "sort_buttons": { - "popular": "热门", - "name": "名称", - "newest": "最新" - }, - "button_follow": "关注", - "button_following": "已关注", - "tag_label": "个问题", - "search_placeholder": "通过标签名过滤", - "no_description": "此标签无描述。", - "more": "更多" - }, - "ask": { - "title": "提交新的问题", - "edit_title": "编辑问题", - "default_reason": "编辑问题", - "similar_questions": "相似的问题", - "form": { - "fields": { - "revision": { - "label": "编辑历史" - }, - "title": { - "label": "标题", - "placeholder": "请详细描述你的问题", - "msg": { - "empty": "标题不能为空", - "range": "标题最多 150 个字符" - } - }, - "body": { - "label": "内容", - "msg": { - "empty": "内容不能为空" - } - }, - "tags": { - "label": "标签", - "msg": { - "empty": "必须选择一个标签" - } - }, - "answer": { - "label": "回答内容", - "msg": { - "empty": "回答内容不能为空" - } - } - } - }, - "btn_post_question": "提交问题", - "btn_save_edits": "保存更改", - "answer_question": "直接发表回答", - "post_question&answer": "提交问题和回答" - }, - "tag_selector": { - "add_btn": "添加标签", - "create_btn": "创建新标签", - "search_tag": "搜索标签", - "hint": "选择至少一个与问题相关的标签。", - "no_result": "没有匹配的标签" - }, - "header": { - "nav": { - "question": "问题", - "tag": "标签", - "user": "用户", - "profile": "用户主页", - "setting": "账号设置", - "logout": "退出登录", - "admin": "后台管理" - }, - "search": { - "placeholder": "搜索" - } - }, - "footer": { - "build_on": "Built on <1> Answer 1>- the open-source software that power Q&A communities您确定要提交一个新的回答吗?
您可以直接编辑和改善您之前的回答的。
", - "empty": "回答内容不能为空。" - } - }, - "delete": { - "title": "删除", - "question": "我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗?", - "answer_accepted": "
我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。
如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗?", - "other": "你确定要删除?", - "tip_question_deleted": "此问题已被删除", - "tip_answer_deleted": "此回答已被删除" - }, - "btns": { - "confirm": "确认", - "cancel": "取消", - "save": "保存", - "delete": "删除", - "login": "登录", - "signup": "注册", - "logout": "退出登录", - "verify": "验证", - "add_question": "我要提问" - }, - "search": { - "title": "搜索结果", - "keywords": "关键词", - "options": "选项", - "follow": "关注", - "following": "已关注", - "counts": "{{count}} 个结果", - "more": "更多", - "sort_btns": { - "relevance": "相关性", - "newest": "最新的", - "active": "活跃的", - "score": "评分" - }, - "tips": { - "title": "高级搜索提示", - "tag": "<1>[tag]1> 在指定标签中搜索", - "user": "<1>user:username1> 根据作者搜索", - "answer": "<1>answers:01> 搜索未回答的问题", - "score": "<1>score:31> 评分 3 分或以上", - "question": "<1>is:question1> 只搜索问题", - "is_answer": "<1>is:answer1> 只搜索回答" - }, - "empty": "找不到任何相关的内容。{t('post')} | -{t('votes')} | -{t('created')} | -{t('status')} | - {curFilter !== 'deleted' &&{t('action')} | } +{t('post')} | +{t('votes')} | +{t('created')} | +{t('status')} | + {curFilter !== 'deleted' && ( +{t('action')} | + )}{li.vote_count} | @@ -153,7 +165,10 @@ const Answers: FC = () => { {curFilter !== 'deleted' && (- | diff --git a/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx new file mode 100644 index 00000000..8f5048f8 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx @@ -0,0 +1,31 @@ +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +const AnswerLinks = () => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( +
---|
{t('post')} | -{t('votes')} | -{t('answers')} | +{t('post')} | +{t('votes')} | +{t('answers')} | {t('created')} | -{t('status')} | - {curFilter !== 'deleted' &&{t('action')} | } +{t('status')} | + {curFilter !== 'deleted' && ( +{t('action')} | + )}{li.vote_count} | - {li.answer_count} - + |
|
{curFilter !== 'deleted' && (
- |
diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx
index f6714255..33de30ab 100644
--- a/ui/src/pages/Admin/Smtp/index.tsx
+++ b/ui/src/pages/Admin/Smtp/index.tsx
@@ -2,10 +2,9 @@ import React, { FC, useEffect, useState } from 'react';
import { Form, Button, Stack } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
-import type * as Type from '@answer/common/interface';
-import { useToast } from '@answer/hooks';
-import { useSmtpSetting, updateSmtpSetting } from '@answer/api';
-
+import type * as Type from '@/common/interface';
+import { useToast } from '@/hooks';
+import { useSmtpSetting, updateSmtpSetting } from '@/services';
import pattern from '@/common/pattern';
const Smtp: FC = () => {
diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx
index f15db7b5..58b849d1 100644
--- a/ui/src/pages/Admin/Users/index.tsx
+++ b/ui/src/pages/Admin/Users/index.tsx
@@ -1,18 +1,18 @@
-import { FC, useState } from 'react';
+import { FC } from 'react';
import { Button, Form, Table, Badge } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { useQueryUsers } from '@answer/api';
import {
Pagination,
FormatTime,
BaseUserCard,
Empty,
QueryGroup,
-} from '@answer/components';
-import * as Type from '@answer/common/interface';
-import { useChangeModal } from '@answer/hooks';
+} from '@/components';
+import * as Type from '@/common/interface';
+import { useChangeModal } from '@/hooks';
+import { useQueryUsers } from '@/services';
import '../index.scss';
@@ -33,11 +33,11 @@ const bgMap = {
const PAGE_SIZE = 10;
const Users: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
- const [userName, setUserName] = useState('');
- const [urlSearchParams] = useSearchParams();
+ const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
const curPage = Number(urlSearchParams.get('page') || '1');
+ const curQuery = urlSearchParams.get('query') || '';
const {
data,
isLoading,
@@ -45,7 +45,7 @@ const Users: FC = () => {
} = useQueryUsers({
page: curPage,
page_size: PAGE_SIZE,
- ...(userName ? { username: userName } : {}),
+ query: curQuery,
...(curFilter === 'all' ? {} : { status: curFilter }),
});
const changeModal = useChangeModal({
@@ -59,6 +59,11 @@ const Users: FC = () => {
});
};
+ const handleFilter = (e) => {
+ urlSearchParams.set('query', e.target.value);
+ urlSearchParams.delete('page');
+ setUrlSearchParams(urlSearchParams);
+ };
return (
<>
---|
{t('name')} | -{t('reputation')} | +{t('name')} | +{t('reputation')} | {t('email')} | -+ | {t('created_at')} | {(curFilter === 'deleted' || curFilter === 'suspended') && ( -+ | {curFilter === 'deleted' ? t('delete_at') : t('suspend_at')} | )} -{t('status')} | - {curFilter !== 'deleted' ?{t('action')} | : null} +{t('status')} | + {curFilter !== 'deleted' ? ( +{t('action')} | + ) : null}
{user.status !== 'deleted' && (
+
+ );
+};
+
+export default Index;
diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx
new file mode 100644
index 00000000..78b4aac8
--- /dev/null
+++ b/ui/src/pages/Install/components/FirstStep/index.tsx
@@ -0,0 +1,75 @@
+import { FC, useEffect, useState } from 'react';
+import { Form, Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import type { LangsType, FormValue, FormDataType } from '@/common/interface';
+import Progress from '../Progress';
+import { getInstallLangOptions } from '@/services';
+
+interface Props {
+ data: FormValue;
+ changeCallback: (value: FormDataType) => void;
+ nextCallback: () => void;
+ visible: boolean;
+}
+const Index: FC{t('ready_title')}+
+ {t('good_luck')} + +
+
+
+
+
+
+
+ );
+};
+
+export default Index;
diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx
new file mode 100644
index 00000000..0f7d7b88
--- /dev/null
+++ b/ui/src/pages/Install/components/FourthStep/index.tsx
@@ -0,0 +1,265 @@
+import { FC, FormEvent } from 'react';
+import { Form, Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import type { FormDataType } from '@/common/interface';
+import Progress from '../Progress';
+
+interface Props {
+ data: FormDataType;
+ changeCallback: (value: FormDataType) => void;
+ nextCallback: () => void;
+ visible: boolean;
+}
+const Index: FC{t('admin_account')}+
+
+
+
+ );
+};
+
+export default Index;
diff --git a/ui/src/pages/Install/components/Progress/index.tsx b/ui/src/pages/Install/components/Progress/index.tsx
new file mode 100644
index 00000000..97f33ffe
--- /dev/null
+++ b/ui/src/pages/Install/components/Progress/index.tsx
@@ -0,0 +1,22 @@
+import { FC, memo } from 'react';
+import { ProgressBar } from 'react-bootstrap';
+
+interface IProps {
+ step: number;
+}
+
+const Index: FC
+
+ );
+};
+
+export default memo(Index);
diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx
new file mode 100644
index 00000000..6c7125f8
--- /dev/null
+++ b/ui/src/pages/Install/components/SecondStep/index.tsx
@@ -0,0 +1,245 @@
+import { FC, FormEvent } from 'react';
+import { Form, Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import Progress from '../Progress';
+import type { FormDataType } from '@/common/interface';
+
+interface Props {
+ data: FormDataType;
+ changeCallback: (value: FormDataType) => void;
+ nextCallback: () => void;
+ visible: boolean;
+}
+
+const sqlData = [
+ {
+ value: 'mysql',
+ label: 'MariaDB/MySQL',
+ },
+ {
+ value: 'sqlite3',
+ label: 'SQLite',
+ },
+ {
+ value: 'postgres',
+ label: 'PostgreSQL',
+ },
+];
+
+const Index: FC
+
+
+
+ );
+};
+
+export default Index;
diff --git a/ui/src/pages/Install/components/ThirdStep/index.tsx b/ui/src/pages/Install/components/ThirdStep/index.tsx
new file mode 100644
index 00000000..ce79acb3
--- /dev/null
+++ b/ui/src/pages/Install/components/ThirdStep/index.tsx
@@ -0,0 +1,54 @@
+import { FC } from 'react';
+import { Form, Button, FormGroup } from 'react-bootstrap';
+import { useTranslation, Trans } from 'react-i18next';
+
+import Progress from '../Progress';
+
+interface Props {
+ visible: boolean;
+ errorMsg;
+ nextCallback: () => void;
+}
+
+const Index: FC
+
+ );
+};
+
+export default Index;
diff --git a/ui/src/pages/Install/components/index.ts b/ui/src/pages/Install/components/index.ts
new file mode 100644
index 00000000..ecc22539
--- /dev/null
+++ b/ui/src/pages/Install/components/index.ts
@@ -0,0 +1,7 @@
+import FirstStep from './FirstStep';
+import SecondStep from './SecondStep';
+import ThirdStep from './ThirdStep';
+import FourthStep from './FourthStep';
+import Fifth from './FifthStep';
+
+export { FirstStep, SecondStep, ThirdStep, FourthStep, Fifth };
diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx
new file mode 100644
index 00000000..52dca0ec
--- /dev/null
+++ b/ui/src/pages/Install/index.tsx
@@ -0,0 +1,326 @@
+/* eslint-disable prettier/prettier */
+import { FC, useState, useEffect } from 'react';
+import { Container, Row, Col, Card, Alert } from 'react-bootstrap';
+import { useTranslation, Trans } from 'react-i18next';
+
+import type { FormDataType } from '@/common/interface';
+import { PageTitle } from '@/components';
+import {
+ dbCheck,
+ installInit,
+ installBaseInfo,
+ checkConfigFileExists,
+} from '@/services';
+import { Storage } from '@/utils';
+import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants';
+
+import {
+ FirstStep,
+ SecondStep,
+ ThirdStep,
+ FourthStep,
+ Fifth,
+} from './components';
+
+const Index: FC = () => {
+ const { t } = useTranslation('translation', { keyPrefix: 'install' });
+ const [step, setStep] = useState(1);
+ const [loading, setLoading] = useState(true);
+ const [errorData, setErrorData] = useState<{ [propName: string]: any }>({
+ msg: '',
+ });
+ const [checkData, setCheckData] = useState({
+ db_table_exist: false,
+ db_connection_success: false,
+ });
+
+ const [formData, setFormData] = useState{t('config_yaml.title')}+ + {errorMsg?.msg?.length > 0 ? ( + <> +
+
+
+ {t('config_yaml.info')}
+ >
+ ) : (
+ {t('config_yaml.label')}
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default Index;
diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx
index 69c5195d..7539c2fa 100644
--- a/ui/src/pages/Layout/index.tsx
+++ b/ui/src/pages/Layout/index.tsx
@@ -1,59 +1,18 @@
-import { FC, useEffect } from 'react';
-import { useTranslation } from 'react-i18next';
+import { FC, memo } from 'react';
import { Outlet } from 'react-router-dom';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import { SWRConfig } from 'swr';
-import {
- userInfoStore,
- siteInfoStore,
- interfaceStore,
- toastStore,
-} from '@answer/stores';
-import { Header, AdminHeader, Footer, Toast } from '@answer/components';
-import { useSiteSettings, useCheckUserStatus } from '@answer/api';
+import { siteInfoStore, toastStore } from '@/stores';
+import { Header, Footer, Toast } from '@/components';
-import Storage from '@/utils/storage';
-
-let isMounted = false;
const Layout: FC = () => {
- const { siteInfo, update: siteStoreUpdate } = siteInfoStore();
- const { update: interfaceStoreUpdate } = interfaceStore();
- const { data: siteSettings } = useSiteSettings();
- const { data: userStatus } = useCheckUserStatus();
- useEffect(() => {
- if (siteSettings) {
- siteStoreUpdate(siteSettings.general);
- interfaceStoreUpdate(siteSettings.interface);
- }
- }, [siteSettings]);
- const updateUser = userInfoStore((state) => state.update);
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
- const { i18n } = useTranslation();
-
+ const { siteInfo } = siteInfoStore.getState();
const closeToast = () => {
toastClear();
};
- if (!isMounted) {
- isMounted = true;
- const lang = Storage.get('LANG');
- const user = Storage.get('userInfo');
- if (user) {
- updateUser(user);
- }
- if (lang) {
- i18n.changeLanguage(lang);
- }
- }
-
- if (userStatus?.status) {
- const user = Storage.get('userInfo');
- if (userStatus.status !== user.status) {
- user.status = userStatus?.status;
- updateUser(user);
- }
- }
return (
{t('title')}+
+
+ )}
+
+ {step === 7 && (
+ {t('warn_title')}+
+
+
+ )}
+
+ {step === 8 && (
+ {t('db_failed')}+
+
+
+ )}
+ {t('installed')}+{t('installed_description')} +
+
+ );
+};
+
+export default Index;
diff --git a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx
index 16388963..99a8026a 100644
--- a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx
+++ b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx
@@ -2,7 +2,7 @@ import { memo } from 'react';
import { Accordion, ListGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
-import { Icon } from '@answer/components';
+import { Icon } from '@/components';
import './index.scss';
diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx
index abcc86f6..3ed72f84 100644
--- a/ui/src/pages/Questions/Ask/index.tsx
+++ b/ui/src/pages/Questions/Ask/index.tsx
@@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import classNames from 'classnames';
-import { Editor, EditorRef, TagSelector, PageTitle } from '@answer/components';
+import { Editor, EditorRef, TagSelector, PageTitle } from '@/components';
+import type * as Type from '@/common/interface';
import {
saveQuestion,
questionDetail,
@@ -14,8 +15,7 @@ import {
useQueryRevisions,
postAnswer,
useQueryQuestionByTitle,
-} from '@answer/api';
-import type * as Type from '@answer/common/interface';
+} from '@/services';
import SearchQuestion from './components/SearchQuestion';
@@ -281,9 +281,11 @@ const Ask = () => {
+ (=‘_‘=)
+
+ {t('description')}
+ |
---|