mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/1.0.4/comment' into feat/plugin/connector/github
# Conflicts: # internal/migrations/migrations.go # internal/migrations/v6.go # internal/service/user_common/user.go # internal/service/user_service.go # pkg/checker/reserved_username.go # ui/build/index.html
This commit is contained in:
commit
6a8d5e5984
|
@ -0,0 +1,41 @@
|
|||
name: "Goreleaser"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: [self-hosted, linux]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Node Build
|
||||
run: make install-ui-packages ui
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: answer
|
||||
path: ./dist/*
|
|
@ -15,7 +15,6 @@
|
|||
/configs/config-dev.yaml
|
||||
/go.work*
|
||||
/logs
|
||||
/ui/build
|
||||
/ui/node_modules
|
||||
/vendor
|
||||
Thumbs*.db
|
||||
|
|
|
@ -2,7 +2,7 @@ env:
|
|||
- GO11MODULE=on
|
||||
- GO111MODULE=on
|
||||
- GOPROXY=https://goproxy.io
|
||||
- CGO_ENABLED=1
|
||||
- CGO_ENABLED=0
|
||||
|
||||
before:
|
||||
hooks:
|
||||
|
@ -39,8 +39,8 @@ builds:
|
|||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm64
|
||||
- id: build-arm7
|
||||
- arm64
|
||||
- id: build-arm7
|
||||
main: ./cmd/answer/.
|
||||
binary: answer
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
|
@ -64,7 +64,7 @@ builds:
|
|||
- darwin
|
||||
goarch:
|
||||
- arm64
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
flags: -v
|
||||
- id: build-darwin-amd64
|
||||
main: ./cmd/answer/.
|
||||
|
@ -76,7 +76,7 @@ builds:
|
|||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
flags: -v
|
||||
|
||||
|
||||
|
@ -96,9 +96,9 @@ changelog:
|
|||
- '^test:'
|
||||
|
||||
|
||||
# sudo apt-get install build-essential
|
||||
# sudo apt-get install build-essential
|
||||
# sudo apt-get install gcc-multilib g++-multilib
|
||||
# sudo apt-get install gcc-mingw-w64
|
||||
# sudo apt-get -y install gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf
|
||||
# sudo apt-get install clang llvm
|
||||
# goreleaser release --snapshot --rm-dist
|
||||
# goreleaser release --snapshot --rm-dist
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
# Contributing to answer
|
||||
## Coding and documentation Style
|
||||
|
||||
To be developed.
|
||||
|
||||
## Submitting Modifications
|
||||
|
||||
To be developed.
|
69
INSTALL.md
69
INSTALL.md
|
@ -1,69 +0,0 @@
|
|||
# Answer installation guide
|
||||
## Environment Preparation
|
||||
- Memory >= 512M
|
||||
- If using MySQL version >= 5.7
|
||||
|
||||
## Installing with docker
|
||||
### Step 1: Start the project with the docker command
|
||||
```bash
|
||||
docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
|
||||
```
|
||||
|
||||
### Step 2: Visit the installation url
|
||||
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
|
||||
|
||||
After selecting the language click next to select the appropriate database, if you just want to experience it currently, it is recommended to select sqlite as the database directly, as shown below
|
||||
|
||||

|
||||
|
||||
Then click next to create the configuration file, click next to enter the basic website information and administrator information, as shown below
|
||||
|
||||

|
||||
|
||||
Click Next to complete the installation
|
||||
|
||||
### Step 3: After installation, visit the project path to start using
|
||||
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
|
||||
|
||||
Login with the administrator username and password you just created.
|
||||
|
||||
## Installing with docker-compose
|
||||
### Step 1: Start the project with the docker-compose command
|
||||
```bash
|
||||
mkdir answer && cd answer
|
||||
wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Step 2: Visit the installation url
|
||||
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
|
||||
|
||||
The exact configuration is the same as for docker use
|
||||
|
||||
### Step 3: After installation, visit home page
|
||||
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
|
||||
|
||||
## Install with binary
|
||||
### Step 1: Download the binaries
|
||||
[https://github.com/answerdev/answer/releases](https://github.com/answerdev/answer/releases)
|
||||
Download the version you need for your current system
|
||||
|
||||
### Step 2: Install using command line
|
||||
> The following command -C specifies the data directory required for answer, you can modify it as you see fit
|
||||
|
||||
```bash
|
||||
. /answer init -C . /answer-data/
|
||||
```
|
||||
|
||||
Then visit: [http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) to install, the configuration is the same as using docker installation
|
||||
|
||||
### Step 3: Start with command line
|
||||
After the installation is complete, the program will exit, so use the command to start the project formally
|
||||
```bash
|
||||
. /answer run -C . /answer-data/
|
||||
```
|
||||
|
||||
After normal startup you can access [http://127.0.0.1:9080/](http://127.0.0.1:9080/) to log in using the administrator username password specified during installation
|
||||
|
||||
## Installation FAQ
|
||||
- Having trouble reinstalling using docker? The default command we give is to use `answer-data` to name the volume, so if you don't need the original data again, please delete it voluntarily `docker volume rm answer-data`
|
|
@ -1,68 +0,0 @@
|
|||
# Answer 安装指引
|
||||
## 环境准备
|
||||
- 内存 >= 512M
|
||||
- 如果使用 MySQL 版本 >= 5.7
|
||||
|
||||
## 使用 docker 安装
|
||||
### 步骤 1: 使用 docker 命令启动项目
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
### 步骤 2: 访问安装路径进行项目安装
|
||||
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
|
||||
|
||||
具体配置与 docker 使用时相同
|
||||
|
||||
### 步骤 3:安装完成后访问项目路径开始使用
|
||||
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
|
||||
|
||||
## 使用 二进制 安装
|
||||
### 步骤 1: 下载二进制文件
|
||||
[https://github.com/answerdev/answer/releases](https://github.com/answerdev/answer/releases)
|
||||
请下载您当下系统所需要的对应版本
|
||||
|
||||
### 步骤 2: 使用命令行安装
|
||||
> 以下命令中 -C 指定的是 answer 所需的数据目录,您可以根据实际需要进行修改
|
||||
|
||||
```bash
|
||||
./answer init -C ./answer-data/
|
||||
```
|
||||
|
||||
然后访问:[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) 进行安装,具体配置与使用 docker 安装相同
|
||||
|
||||
### 步骤 3: 使用命令行启动
|
||||
安装完成之后程序会退出,请使用命令正式启动项目
|
||||
```bash
|
||||
./answer run -C ./answer-data/
|
||||
```
|
||||
|
||||
正常启动后可以访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/) 使用安装时指定的管理员用户名密码进行登录
|
||||
|
||||
## 安装常见问题
|
||||
- 使用 docker 重新安装遇到问题?默认我们给出的命令是使用 `answer-data` 命名卷,所以如果重新不需要原来的数据,请主动进行删除 `docker volume rm answer-data`
|
18
Makefile
18
Makefile
|
@ -1,30 +1,30 @@
|
|||
.PHONY: build clean ui
|
||||
|
||||
VERSION=1.0.1
|
||||
VERSION=1.0.3
|
||||
BIN=answer
|
||||
DIR_SRC=./cmd/answer
|
||||
DOCKER_CMD=docker
|
||||
|
||||
#GO_ENV=CGO_ENABLED=0
|
||||
GO_ENV=CGO_ENABLED=0 GO111MODULE=on
|
||||
Revision=$(shell git rev-parse --short HEAD)
|
||||
GO_FLAGS=-ldflags="-X answercmd.Version=$(VERSION) -X 'answercmd.Revision=$(Revision)' -X 'answercmd.Time=`date`' -extldflags -static"
|
||||
GO=$(GO_ENV) $(shell which go)
|
||||
|
||||
build:
|
||||
@$(GO_ENV) $(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
|
||||
build: generate
|
||||
@$(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
|
||||
|
||||
# https://dev.to/thewraven/universal-macos-binaries-with-go-1-16-3mm3
|
||||
universal:
|
||||
universal: generate
|
||||
@GOOS=darwin GOARCH=amd64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_amd64 $(DIR_SRC)
|
||||
@GOOS=darwin GOARCH=arm64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_arm64 $(DIR_SRC)
|
||||
@lipo -create -output ${BIN} ${BIN}_amd64 ${BIN}_arm64
|
||||
@rm -f ${BIN}_amd64 ${BIN}_arm64
|
||||
|
||||
generate:
|
||||
go get github.com/google/wire/cmd/wire@latest
|
||||
go install github.com/golang/mock/mockgen@v1.6.0
|
||||
go generate ./...
|
||||
go mod tidy
|
||||
@$(GO) get github.com/google/wire/cmd/wire@v0.5.0
|
||||
@$(GO) get github.com/golang/mock/mockgen@v1.6.0
|
||||
@$(GO) generate ./...
|
||||
@$(GO) mod tidy
|
||||
|
||||
test:
|
||||
@$(GO) test ./internal/repo/repo_test
|
||||
|
|
|
@ -26,13 +26,13 @@ To learn more about the project, visit [answer.dev](https://answer.dev).
|
|||
docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
|
||||
```
|
||||
|
||||
For more information, see [INSTALL.md](./INSTALL.md)
|
||||
For more information, see [Installation](https://answer.dev/docs/installation)
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are always welcome!
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for ways to get started.
|
||||
See [CONTRIBUTING](https://answer.dev/docs/development/contributing/) for ways to get started.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -26,13 +26,13 @@
|
|||
docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
|
||||
```
|
||||
|
||||
其他安装配置细节请参考 [INSTALL_CN.md](./INSTALL_CN.md)
|
||||
其他安装配置细节请参考 [Installation](https://answer.dev/docs/installation)
|
||||
|
||||
## 贡献
|
||||
|
||||
我们随时欢迎你的贡献!
|
||||
|
||||
参考 [CONTRIBUTING.md](CONTRIBUTING.md) 开始贡献。
|
||||
参考 [CONTRIBUTING](https://answer.dev/docs/development/contributing/) 开始贡献。
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -187,7 +187,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
reportAdminService := report_admin.NewReportAdminService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo)
|
||||
controller_adminReportController := controller_admin.NewReportController(reportAdminService)
|
||||
userAdminRepo := user.NewUserAdminRepo(dataData, authRepo)
|
||||
userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon)
|
||||
userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo)
|
||||
userAdminController := controller_admin.NewUserAdminController(userAdminService)
|
||||
reasonRepo := reason.NewReasonRepo(configRepo)
|
||||
reasonService := reason2.NewReasonService(reasonRepo)
|
||||
|
|
285
docs/docs.go
285
docs/docs.go
|
@ -3047,7 +3047,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "UserTop",
|
||||
"parameters": [
|
||||
|
@ -3255,6 +3255,45 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/post/render": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "render post content",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Upload"
|
||||
],
|
||||
"summary": "render post content",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "PostRenderReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.PostRenderReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
@ -3270,7 +3309,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "update question",
|
||||
"parameters": [
|
||||
|
@ -3307,7 +3346,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "add question",
|
||||
"parameters": [
|
||||
|
@ -3344,7 +3383,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "delete question",
|
||||
"parameters": [
|
||||
|
@ -3383,7 +3422,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "close question msg list",
|
||||
"responses": {
|
||||
|
@ -3403,7 +3442,7 @@ const docTemplate = `{
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "GetQuestion Question",
|
||||
"description": "get question details",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3411,9 +3450,9 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "GetQuestion Question",
|
||||
"summary": "get question details",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -3436,7 +3475,7 @@ const docTemplate = `{
|
|||
},
|
||||
"/answer/api/v1/question/page": {
|
||||
"get": {
|
||||
"description": "SearchQuestionList \u003cbr\u003e \"order\" Enums(newest, active,frequent,score,unanswered)",
|
||||
"description": "get questions by page",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3444,17 +3483,17 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "SearchQuestionList",
|
||||
"summary": "get questions by page",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "QuestionSearch",
|
||||
"description": "QuestionPageReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionSearch"
|
||||
"$ref": "#/definitions/schema.QuestionPageReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -3462,7 +3501,34 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/pager.PageModel"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.QuestionPageResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3483,7 +3549,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "reopen question",
|
||||
"parameters": [
|
||||
|
@ -3507,40 +3573,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/search": {
|
||||
"post": {
|
||||
"description": "SearchQuestionList",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
],
|
||||
"summary": "SearchQuestionList",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "QuestionSearch",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionSearch"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/similar": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -3556,7 +3588,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "add question title like",
|
||||
"parameters": [
|
||||
|
@ -3589,7 +3621,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "Search Similar Question",
|
||||
"parameters": [
|
||||
|
@ -3627,7 +3659,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "Close question",
|
||||
"parameters": [
|
||||
|
@ -5502,7 +5534,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "UserList",
|
||||
"parameters": [
|
||||
|
@ -5886,10 +5918,15 @@ const docTemplate = `{
|
|||
},
|
||||
"schema.AnswerAddReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
|
@ -5903,10 +5940,15 @@ const docTemplate = `{
|
|||
},
|
||||
"schema.AnswerUpdateReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"edit_summary": {
|
||||
"description": "edit_summary",
|
||||
|
@ -7088,6 +7130,14 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.PostRenderReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionAdd": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -7124,27 +7174,115 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionSearch": {
|
||||
"schema.QuestionPageReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"order": {
|
||||
"description": "Search order by",
|
||||
"type": "string"
|
||||
"orderCond": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"newest",
|
||||
"active",
|
||||
"frequent",
|
||||
"score",
|
||||
"unanswered"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Query number of pages",
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"page_size": {
|
||||
"description": "Search page size",
|
||||
"type": "integer"
|
||||
"pageSize": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"tag": {
|
||||
"description": "Tags []string ` + "`" + `json:\"tags\" form:\"tags\"` + "`" + ` // Search tag",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionPageResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accepted_answer_id": {
|
||||
"description": "answer information",
|
||||
"type": "string"
|
||||
},
|
||||
"answer_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"collection_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"follow_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_answer_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"operated_at": {
|
||||
"description": "operator information",
|
||||
"type": "integer"
|
||||
},
|
||||
"operation_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"$ref": "#/definitions/schema.QuestionPageRespOperator"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.TagResp"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"unique_view_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"url_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"view_count": {
|
||||
"description": "question statistical information",
|
||||
"type": "integer"
|
||||
},
|
||||
"vote_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionPageRespOperator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank": {
|
||||
"type": "integer"
|
||||
},
|
||||
"username": {
|
||||
"description": "Search username",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
@ -7362,10 +7500,6 @@ const docTemplate = `{
|
|||
},
|
||||
"schema.SiteBrandingReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"logo",
|
||||
"square_icon"
|
||||
],
|
||||
"properties": {
|
||||
"favicon": {
|
||||
"type": "string",
|
||||
|
@ -7387,10 +7521,6 @@ const docTemplate = `{
|
|||
},
|
||||
"schema.SiteBrandingResp": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"logo",
|
||||
"square_icon"
|
||||
],
|
||||
"properties": {
|
||||
"favicon": {
|
||||
"type": "string",
|
||||
|
@ -7893,11 +8023,6 @@ const docTemplate = `{
|
|||
"type": "string",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"bio_html": {
|
||||
"description": "bio",
|
||||
"type": "string",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"display_name": {
|
||||
"description": "display_name",
|
||||
"type": "string",
|
||||
|
|
|
@ -3035,7 +3035,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "UserTop",
|
||||
"parameters": [
|
||||
|
@ -3243,6 +3243,45 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/post/render": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "render post content",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Upload"
|
||||
],
|
||||
"summary": "render post content",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "PostRenderReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.PostRenderReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
@ -3258,7 +3297,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "update question",
|
||||
"parameters": [
|
||||
|
@ -3295,7 +3334,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "add question",
|
||||
"parameters": [
|
||||
|
@ -3332,7 +3371,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "delete question",
|
||||
"parameters": [
|
||||
|
@ -3371,7 +3410,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "close question msg list",
|
||||
"responses": {
|
||||
|
@ -3391,7 +3430,7 @@
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "GetQuestion Question",
|
||||
"description": "get question details",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3399,9 +3438,9 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "GetQuestion Question",
|
||||
"summary": "get question details",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -3424,7 +3463,7 @@
|
|||
},
|
||||
"/answer/api/v1/question/page": {
|
||||
"get": {
|
||||
"description": "SearchQuestionList \u003cbr\u003e \"order\" Enums(newest, active,frequent,score,unanswered)",
|
||||
"description": "get questions by page",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3432,17 +3471,17 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "SearchQuestionList",
|
||||
"summary": "get questions by page",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "QuestionSearch",
|
||||
"description": "QuestionPageReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionSearch"
|
||||
"$ref": "#/definitions/schema.QuestionPageReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -3450,7 +3489,34 @@
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/pager.PageModel"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.QuestionPageResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3471,7 +3537,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "reopen question",
|
||||
"parameters": [
|
||||
|
@ -3495,40 +3561,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/search": {
|
||||
"post": {
|
||||
"description": "SearchQuestionList",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
],
|
||||
"summary": "SearchQuestionList",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "QuestionSearch",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionSearch"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/similar": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -3544,7 +3576,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "add question title like",
|
||||
"parameters": [
|
||||
|
@ -3577,7 +3609,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "Search Similar Question",
|
||||
"parameters": [
|
||||
|
@ -3615,7 +3647,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "Close question",
|
||||
"parameters": [
|
||||
|
@ -5490,7 +5522,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "UserList",
|
||||
"parameters": [
|
||||
|
@ -5874,10 +5906,15 @@
|
|||
},
|
||||
"schema.AnswerAddReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
|
@ -5891,10 +5928,15 @@
|
|||
},
|
||||
"schema.AnswerUpdateReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"edit_summary": {
|
||||
"description": "edit_summary",
|
||||
|
@ -7076,6 +7118,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.PostRenderReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionAdd": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -7112,27 +7162,115 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionSearch": {
|
||||
"schema.QuestionPageReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"order": {
|
||||
"description": "Search order by",
|
||||
"type": "string"
|
||||
"orderCond": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"newest",
|
||||
"active",
|
||||
"frequent",
|
||||
"score",
|
||||
"unanswered"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Query number of pages",
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"page_size": {
|
||||
"description": "Search page size",
|
||||
"type": "integer"
|
||||
"pageSize": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"tag": {
|
||||
"description": "Tags []string `json:\"tags\" form:\"tags\"` // Search tag",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionPageResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accepted_answer_id": {
|
||||
"description": "answer information",
|
||||
"type": "string"
|
||||
},
|
||||
"answer_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"collection_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"follow_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_answer_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"operated_at": {
|
||||
"description": "operator information",
|
||||
"type": "integer"
|
||||
},
|
||||
"operation_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"$ref": "#/definitions/schema.QuestionPageRespOperator"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.TagResp"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"unique_view_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"url_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"view_count": {
|
||||
"description": "question statistical information",
|
||||
"type": "integer"
|
||||
},
|
||||
"vote_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionPageRespOperator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank": {
|
||||
"type": "integer"
|
||||
},
|
||||
"username": {
|
||||
"description": "Search username",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
@ -7350,10 +7488,6 @@
|
|||
},
|
||||
"schema.SiteBrandingReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"logo",
|
||||
"square_icon"
|
||||
],
|
||||
"properties": {
|
||||
"favicon": {
|
||||
"type": "string",
|
||||
|
@ -7375,10 +7509,6 @@
|
|||
},
|
||||
"schema.SiteBrandingResp": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"logo",
|
||||
"square_icon"
|
||||
],
|
||||
"properties": {
|
||||
"favicon": {
|
||||
"type": "string",
|
||||
|
@ -7881,11 +8011,6 @@
|
|||
"type": "string",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"bio_html": {
|
||||
"description": "bio",
|
||||
"type": "string",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"display_name": {
|
||||
"description": "display_name",
|
||||
"type": "string",
|
||||
|
|
|
@ -218,6 +218,8 @@ definitions:
|
|||
properties:
|
||||
content:
|
||||
description: content
|
||||
maxLength: 65535
|
||||
minLength: 6
|
||||
type: string
|
||||
html:
|
||||
description: html
|
||||
|
@ -225,11 +227,15 @@ definitions:
|
|||
question_id:
|
||||
description: question_id
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
type: object
|
||||
schema.AnswerUpdateReq:
|
||||
properties:
|
||||
content:
|
||||
description: content
|
||||
maxLength: 65535
|
||||
minLength: 6
|
||||
type: string
|
||||
edit_summary:
|
||||
description: edit_summary
|
||||
|
@ -246,6 +252,8 @@ definitions:
|
|||
title:
|
||||
description: title
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
type: object
|
||||
schema.AvatarInfo:
|
||||
properties:
|
||||
|
@ -1076,6 +1084,11 @@ definitions:
|
|||
type:
|
||||
type: string
|
||||
type: object
|
||||
schema.PostRenderReq:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
type: object
|
||||
schema.QuestionAdd:
|
||||
properties:
|
||||
content:
|
||||
|
@ -1104,23 +1117,82 @@ definitions:
|
|||
- tags
|
||||
- title
|
||||
type: object
|
||||
schema.QuestionSearch:
|
||||
schema.QuestionPageReq:
|
||||
properties:
|
||||
order:
|
||||
description: Search order by
|
||||
orderCond:
|
||||
enum:
|
||||
- newest
|
||||
- active
|
||||
- frequent
|
||||
- score
|
||||
- unanswered
|
||||
type: string
|
||||
page:
|
||||
description: Query number of pages
|
||||
minimum: 1
|
||||
type: integer
|
||||
page_size:
|
||||
description: Search page size
|
||||
pageSize:
|
||||
minimum: 1
|
||||
type: integer
|
||||
tag:
|
||||
description: Tags []string `json:"tags" form:"tags"` // Search
|
||||
tag
|
||||
maxLength: 100
|
||||
type: string
|
||||
username:
|
||||
description: Search username
|
||||
maxLength: 100
|
||||
type: string
|
||||
type: object
|
||||
schema.QuestionPageResp:
|
||||
properties:
|
||||
accepted_answer_id:
|
||||
description: answer information
|
||||
type: string
|
||||
answer_count:
|
||||
type: integer
|
||||
collection_count:
|
||||
type: integer
|
||||
created_at:
|
||||
type: integer
|
||||
description:
|
||||
type: string
|
||||
follow_count:
|
||||
type: integer
|
||||
id:
|
||||
type: string
|
||||
last_answer_id:
|
||||
type: string
|
||||
operated_at:
|
||||
description: operator information
|
||||
type: integer
|
||||
operation_type:
|
||||
type: string
|
||||
operator:
|
||||
$ref: '#/definitions/schema.QuestionPageRespOperator'
|
||||
status:
|
||||
type: integer
|
||||
tags:
|
||||
items:
|
||||
$ref: '#/definitions/schema.TagResp'
|
||||
type: array
|
||||
title:
|
||||
type: string
|
||||
unique_view_count:
|
||||
type: integer
|
||||
url_title:
|
||||
type: string
|
||||
view_count:
|
||||
description: question statistical information
|
||||
type: integer
|
||||
vote_count:
|
||||
type: integer
|
||||
type: object
|
||||
schema.QuestionPageRespOperator:
|
||||
properties:
|
||||
display_name:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
rank:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
schema.QuestionUpdate:
|
||||
|
@ -1284,9 +1356,6 @@ definitions:
|
|||
square_icon:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- logo
|
||||
- square_icon
|
||||
type: object
|
||||
schema.SiteBrandingResp:
|
||||
properties:
|
||||
|
@ -1302,9 +1371,6 @@ definitions:
|
|||
square_icon:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- logo
|
||||
- square_icon
|
||||
type: object
|
||||
schema.SiteCustomCssHTMLReq:
|
||||
properties:
|
||||
|
@ -1638,10 +1704,6 @@ definitions:
|
|||
description: bio
|
||||
maxLength: 4096
|
||||
type: string
|
||||
bio_html:
|
||||
description: bio
|
||||
maxLength: 4096
|
||||
type: string
|
||||
display_name:
|
||||
description: display_name
|
||||
maxLength: 30
|
||||
|
@ -3857,7 +3919,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: UserTop
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/personal/rank/page:
|
||||
get:
|
||||
description: user personal rank list
|
||||
|
@ -3962,6 +4024,30 @@ paths:
|
|||
summary: user's votes
|
||||
tags:
|
||||
- Activity
|
||||
/answer/api/v1/post/render:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: render post content
|
||||
parameters:
|
||||
- description: PostRenderReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.PostRenderReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: render post content
|
||||
tags:
|
||||
- Upload
|
||||
/answer/api/v1/question:
|
||||
delete:
|
||||
consumes:
|
||||
|
@ -3985,7 +4071,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: delete question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
|
@ -4008,7 +4094,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: add question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
|
@ -4031,7 +4117,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: update question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/closemsglist:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -4048,12 +4134,12 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: close question msg list
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/info:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: GetQuestion Question
|
||||
description: get question details
|
||||
parameters:
|
||||
- default: "1"
|
||||
description: Question TagID
|
||||
|
@ -4070,31 +4156,43 @@ paths:
|
|||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: GetQuestion Question
|
||||
summary: get question details
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/page:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: SearchQuestionList <br> "order" Enums(newest, active,frequent,score,unanswered)
|
||||
description: get questions by page
|
||||
parameters:
|
||||
- description: QuestionSearch
|
||||
- description: QuestionPageReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.QuestionSearch'
|
||||
$ref: '#/definitions/schema.QuestionPageReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
summary: SearchQuestionList
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
allOf:
|
||||
- $ref: '#/definitions/pager.PageModel'
|
||||
- properties:
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/schema.QuestionPageResp'
|
||||
type: array
|
||||
type: object
|
||||
type: object
|
||||
summary: get questions by page
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/reopen:
|
||||
put:
|
||||
consumes:
|
||||
|
@ -4118,29 +4216,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: reopen question
|
||||
tags:
|
||||
- api-question
|
||||
/answer/api/v1/question/search:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: SearchQuestionList
|
||||
parameters:
|
||||
- description: QuestionSearch
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.QuestionSearch'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
summary: SearchQuestionList
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/similar:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -4164,7 +4240,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: add question title like
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/similar/tag:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -4186,7 +4262,7 @@ paths:
|
|||
type: string
|
||||
summary: Search Similar Question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/status:
|
||||
put:
|
||||
consumes:
|
||||
|
@ -4210,7 +4286,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: Close question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/tags:
|
||||
get:
|
||||
description: get tag list
|
||||
|
@ -5362,7 +5438,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: UserList
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/robots.txt:
|
||||
get:
|
||||
description: get site robots information
|
||||
|
|
19
go.mod
19
go.mod
|
@ -5,6 +5,7 @@ go 1.18
|
|||
require (
|
||||
github.com/Chain-Zhang/pinyin v0.1.3
|
||||
github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
||||
github.com/bwmarrin/snowflake v0.3.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
|
@ -22,7 +23,7 @@ require (
|
|||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/jinzhu/now v1.1.5
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/microcosm-cc/bluemonday v1.0.21
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/ory/dockertest/v3 v3.9.1
|
||||
github.com/otiai10/copy v1.7.0
|
||||
|
@ -43,6 +44,7 @@ require (
|
|||
golang.org/x/net v0.1.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.14.2
|
||||
xorm.io/builder v0.3.12
|
||||
xorm.io/core v0.7.3
|
||||
xorm.io/xorm v1.3.2
|
||||
|
@ -55,6 +57,7 @@ require (
|
|||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/containerd/continuity v0.3.0 // indirect
|
||||
github.com/docker/cli v20.10.14+incompatible // indirect
|
||||
|
@ -72,18 +75,21 @@ require (
|
|||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
|
||||
github.com/lestrrat-go/strftime v1.0.6 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
|
@ -96,6 +102,7 @@ require (
|
|||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/afero v1.9.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
|
@ -112,6 +119,7 @@ require (
|
|||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
golang.org/x/image v0.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/tools v0.2.0 // indirect
|
||||
|
@ -119,5 +127,14 @@ 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
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
modernc.org/cc/v3 v3.35.18 // indirect
|
||||
modernc.org/ccgo/v3 v3.12.82 // indirect
|
||||
modernc.org/libc v1.11.87 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.0.5 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/strutil v1.1.1 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
|
14
go.sum
14
go.sum
|
@ -79,9 +79,13 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
|
|||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
|
@ -140,6 +144,7 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
|
|||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
|
@ -301,6 +306,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
|
|||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
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=
|
||||
|
@ -476,6 +483,8 @@ github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
|
|||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
|
@ -792,6 +801,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -1222,7 +1232,9 @@ modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3
|
|||
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
|
||||
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
|
||||
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
|
||||
modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA=
|
||||
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
|
||||
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
|
||||
|
@ -1274,9 +1286,11 @@ modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
|
|||
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
|
||||
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/tcl v1.8.13 h1:V0sTNBw0Re86PvXZxuCub3oO9WrSTqALgrwNZNvLFGw=
|
||||
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.2.19 h1:BGyRFWhDVn5LFS5OcX4Yd/MlpRTOc7hOPTdcIpCiUao=
|
||||
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
|
156
i18n/de_DE.yaml
156
i18n/de_DE.yaml
|
@ -2,65 +2,65 @@
|
|||
backend:
|
||||
base:
|
||||
success:
|
||||
other: "Success."
|
||||
other: "Erfolgreich."
|
||||
unknown:
|
||||
other: "Unknown error."
|
||||
other: "Unbekannter Fehler."
|
||||
request_format_error:
|
||||
other: "Request format is not valid."
|
||||
unauthorized_error:
|
||||
other: "Unauthorized."
|
||||
other: "Nicht autorisiert."
|
||||
database_error:
|
||||
other: "Data server error."
|
||||
role:
|
||||
name:
|
||||
user:
|
||||
other: "User"
|
||||
other: "Nutzer"
|
||||
admin:
|
||||
other: "Admin"
|
||||
moderator:
|
||||
other: "Moderator"
|
||||
description:
|
||||
user:
|
||||
other: "Default with no special access."
|
||||
other: "Standard ohne speziellen Zugriff."
|
||||
admin:
|
||||
other: "Have the full power to access the site."
|
||||
moderator:
|
||||
other: "Has access to all posts except admin settings."
|
||||
other: "Hat Zugriff auf alle Beiträge außer Admin-Einstellungen."
|
||||
email:
|
||||
other: "Email"
|
||||
other: "E-Mail"
|
||||
password:
|
||||
other: "Password"
|
||||
other: "Passwort"
|
||||
email_or_password_wrong_error:
|
||||
other: "Email and password do not match."
|
||||
other: "E-Mail und Password stimmen nicht überein."
|
||||
error:
|
||||
admin:
|
||||
email_or_password_wrong:
|
||||
other: Email and password do not match.
|
||||
other: E-Mail und Password stimmen nicht überein.
|
||||
answer:
|
||||
not_found:
|
||||
other: "Answer do not found."
|
||||
cannot_deleted:
|
||||
other: "No permission to delete."
|
||||
other: "Keine Berechtigung zum Löschen."
|
||||
cannot_update:
|
||||
other: "No permission to update."
|
||||
other: "Keine Berechtigung zum Aktualisieren."
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: "Comment are not allowed to edit."
|
||||
other: "Kommentar kann nicht bearbeitet werden."
|
||||
not_found:
|
||||
other: "Comment not found."
|
||||
other: "Kommentar wurde nicht gefunden."
|
||||
email:
|
||||
duplicate:
|
||||
other: "Email already exists."
|
||||
other: "E-Mail existiert bereits."
|
||||
need_to_be_verified:
|
||||
other: "Email should be verified."
|
||||
other: "E-Mail muss überprüft werden."
|
||||
verify_url_expired:
|
||||
other: "Email verified URL has expired, please resend the email."
|
||||
lang:
|
||||
not_found:
|
||||
other: "Language file not found."
|
||||
other: "Sprachdatei nicht gefunden."
|
||||
object:
|
||||
captcha_verification_failed:
|
||||
other: "Captcha wrong."
|
||||
other: "Captcha ist falsch."
|
||||
disallow_follow:
|
||||
other: "You are not allowed to follow."
|
||||
disallow_vote:
|
||||
|
@ -70,22 +70,22 @@ backend:
|
|||
not_found:
|
||||
other: "Object not found."
|
||||
verification_failed:
|
||||
other: "Verification failed."
|
||||
other: "Verifizierung fehlgeschlagen."
|
||||
email_or_password_incorrect:
|
||||
other: "Email and password do not match."
|
||||
other: "E-Mail und Password stimmen nicht überein."
|
||||
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."
|
||||
other: "Das neue Passwort ist das gleiche wie das vorherige Passwort."
|
||||
question:
|
||||
not_found:
|
||||
other: "Question not found."
|
||||
other: "Frage nicht gefunden."
|
||||
cannot_deleted:
|
||||
other: "No permission to delete."
|
||||
other: "Keine Berechtigung zum Löschen."
|
||||
cannot_close:
|
||||
other: "No permission to close."
|
||||
cannot_update:
|
||||
other: "No permission to update."
|
||||
other: "Keine Berechtigung zum Aktualisieren."
|
||||
rank:
|
||||
fail_to_meet_the_condition:
|
||||
other: "Rank fail to meet the condition."
|
||||
|
@ -96,7 +96,7 @@ backend:
|
|||
other: "Report not found."
|
||||
tag:
|
||||
not_found:
|
||||
other: "Tag not found."
|
||||
other: "Schlagwort nicht gefunden."
|
||||
recommend_tag_not_found:
|
||||
other: "Recommend Tag is not exist."
|
||||
recommend_tag_enter:
|
||||
|
@ -107,6 +107,9 @@ backend:
|
|||
other: "No permission to update."
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "You cannot set the synonym of the current tag as itself."
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "The From Name cannot be a email address."
|
||||
theme:
|
||||
not_found:
|
||||
other: "Theme not found."
|
||||
|
@ -129,21 +132,21 @@ backend:
|
|||
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."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
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:
|
||||
spam:
|
||||
name:
|
||||
|
@ -181,7 +184,7 @@ backend:
|
|||
name:
|
||||
other: "spam"
|
||||
desc:
|
||||
other: "This question has been asked before and already has an answer."
|
||||
other: "Diese Frage ist bereits gestellt worden und hat bereits eine Antwort."
|
||||
guideline:
|
||||
name:
|
||||
other: "a community-specific reason"
|
||||
|
@ -197,6 +200,13 @@ backend:
|
|||
other: "something else"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -230,24 +240,24 @@ ui:
|
|||
desc: >-
|
||||
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code><https://url.com><br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">put returns between paragraphs</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by placing <code>></code> at start of line</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">create code fences with backticks <code>`</code></p><pre class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul>
|
||||
pagination:
|
||||
prev: Prev
|
||||
next: Next
|
||||
prev: Zurück
|
||||
next: Weiter
|
||||
page_title:
|
||||
question: Question
|
||||
questions: Questions
|
||||
tag: Tag
|
||||
tags: Tags
|
||||
question: Frage
|
||||
questions: Fragen
|
||||
tag: Schlagwort
|
||||
tags: Schlagwörter
|
||||
tag_wiki: tag wiki
|
||||
edit_tag: Edit Tag
|
||||
ask_a_question: Add Question
|
||||
edit_question: Edit Question
|
||||
edit_answer: Edit Answer
|
||||
search: Search
|
||||
search: Suche
|
||||
posts_containing: Posts containing
|
||||
settings: Settings
|
||||
settings: Einstellungen
|
||||
notifications: Notifications
|
||||
login: Log In
|
||||
sign_up: Sign Up
|
||||
login: Anmelden
|
||||
sign_up: Registrieren
|
||||
account_recovery: Account Recovery
|
||||
account_activation: Account Activation
|
||||
confirm_email: Confirm Email
|
||||
|
@ -367,7 +377,7 @@ ui:
|
|||
unordered_list:
|
||||
text: Bulleted List
|
||||
table:
|
||||
text: Table
|
||||
text: Tabelle
|
||||
heading: Heading
|
||||
cell: Cell
|
||||
close_modal:
|
||||
|
@ -586,13 +596,13 @@ ui:
|
|||
empty: Name cannot be empty.
|
||||
range: Name up to 30 characters.
|
||||
email:
|
||||
label: Email
|
||||
label: E-Mail
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
empty: E-Mail-Feld darf nicht leer sein.
|
||||
password:
|
||||
label: Password
|
||||
label: Passwort
|
||||
msg:
|
||||
empty: Password cannot be empty.
|
||||
empty: Passwort-Feld darf nicht leer sein.
|
||||
different: The passwords entered on both sides are inconsistent
|
||||
account_forgot:
|
||||
page_title: Forgot Your Password
|
||||
|
@ -600,7 +610,7 @@ ui:
|
|||
send_success: >-
|
||||
If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.
|
||||
email:
|
||||
label: Email
|
||||
label: E-Mail
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
change_email:
|
||||
|
@ -674,24 +684,24 @@ ui:
|
|||
radio: "Answers to your questions, comments, and more"
|
||||
account:
|
||||
heading: Account
|
||||
change_email_btn: Change email
|
||||
change_pass_btn: Change password
|
||||
change_email_btn: E-Mail-Adresse ändern
|
||||
change_pass_btn: Passwort ändern
|
||||
change_email_info: >-
|
||||
We've sent an email to that address. Please follow the confirmation instructions.
|
||||
email:
|
||||
label: Email
|
||||
msg: Email cannot be empty.
|
||||
password_title: Password
|
||||
label: E-Mail
|
||||
msg: E-Mail-Feld darf nicht leer sein.
|
||||
password_title: Passwort
|
||||
current_pass:
|
||||
label: Current Password
|
||||
label: Aktuelles Passwort
|
||||
msg:
|
||||
empty: Current Password cannot be empty.
|
||||
length: The length needs to be between 8 and 32.
|
||||
different: The two entered passwords do not match.
|
||||
new_pass:
|
||||
label: New Password
|
||||
label: Neues Passwort
|
||||
pass_confirm:
|
||||
label: Confirm New Password
|
||||
label: Neues Passwort bestätigen
|
||||
interface:
|
||||
heading: Interface
|
||||
lang:
|
||||
|
@ -747,10 +757,10 @@ ui:
|
|||
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
|
||||
confirm: Bestätigen
|
||||
cancel: Abbrechen
|
||||
save: Speichern
|
||||
delete: Löschen
|
||||
login: Log in
|
||||
signup: Sign up
|
||||
logout: Log out
|
||||
|
@ -801,6 +811,11 @@ ui:
|
|||
confirm_new_email: Your email has been updated.
|
||||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -871,7 +886,7 @@ ui:
|
|||
placeholder: root
|
||||
msg: Username cannot be empty.
|
||||
db_password:
|
||||
label: Password
|
||||
label: Passwort
|
||||
placeholder: root
|
||||
msg: Password cannot be empty.
|
||||
db_host:
|
||||
|
@ -991,13 +1006,14 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
latest: Latest
|
||||
check_failed: Check failed
|
||||
"yes": "Yes"
|
||||
"no": "No"
|
||||
"yes": "Ja"
|
||||
"no": "Nein"
|
||||
not_allowed: Not allowed
|
||||
allowed: Allowed
|
||||
enabled: Enabled
|
||||
|
@ -1045,7 +1061,7 @@ ui:
|
|||
users:
|
||||
title: Users
|
||||
name: Name
|
||||
email: Email
|
||||
email: E-Mail
|
||||
reputation: Reputation
|
||||
created_at: Created Time
|
||||
delete_at: Deleted Time
|
||||
|
@ -1184,7 +1200,7 @@ ui:
|
|||
ssl: SSL
|
||||
none: None
|
||||
smtp_port:
|
||||
label: SMTP Port
|
||||
label: SMTP-Port
|
||||
msg: SMTP port must be number 1 ~ 65535.
|
||||
text: The port to your mail server.
|
||||
smtp_username:
|
||||
|
|
|
@ -52,6 +52,8 @@ backend:
|
|||
other: "Comment are not allowed to edit."
|
||||
not_found:
|
||||
other: "Comment not found."
|
||||
cannot_edit_after_deadline:
|
||||
other: "The comment time has been too long to modify."
|
||||
email:
|
||||
duplicate:
|
||||
other: "Email already exists."
|
||||
|
@ -135,21 +137,21 @@ backend:
|
|||
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."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
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:
|
||||
spam:
|
||||
name:
|
||||
|
@ -204,7 +206,13 @@ backend:
|
|||
other: "something else"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -601,7 +609,8 @@ ui:
|
|||
msg:
|
||||
empty: Cannot be empty.
|
||||
login:
|
||||
page_title: Welcome to Answer
|
||||
page_title: Welcome to {{site_name}}
|
||||
login_to_continue: Log in to continue
|
||||
info_sign: Don't have an account? <1>Sign up</1>
|
||||
info_login: Already have an account? <1>Log in</1>
|
||||
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
|
||||
|
@ -841,6 +850,11 @@ ui:
|
|||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was
|
||||
already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -1037,6 +1051,7 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
|
@ -1254,14 +1269,14 @@ ui:
|
|||
branding:
|
||||
page_title: Branding
|
||||
logo:
|
||||
label: Logo
|
||||
label: Logo (optional)
|
||||
msg: Logo cannot be empty.
|
||||
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
|
||||
mobile_logo:
|
||||
label: Mobile Logo (optional)
|
||||
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
|
||||
square_icon:
|
||||
label: Square Icon
|
||||
label: Square Icon (optional)
|
||||
msg: Square icon cannot be empty.
|
||||
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
|
||||
favicon:
|
||||
|
|
1074
i18n/es_ES.yaml
1074
i18n/es_ES.yaml
File diff suppressed because it is too large
Load Diff
952
i18n/fr_FR.yaml
952
i18n/fr_FR.yaml
File diff suppressed because it is too large
Load Diff
124
i18n/it_IT.yaml
124
i18n/it_IT.yaml
|
@ -14,18 +14,18 @@ backend:
|
|||
role:
|
||||
name:
|
||||
user:
|
||||
other: "User"
|
||||
other: "Utente"
|
||||
admin:
|
||||
other: "Admin"
|
||||
other: "Amministratore"
|
||||
moderator:
|
||||
other: "Moderator"
|
||||
other: "Moderatore"
|
||||
description:
|
||||
user:
|
||||
other: "Default with no special access."
|
||||
other: "Predefinito senza alcun accesso speciale."
|
||||
admin:
|
||||
other: "Have the full power to access the site."
|
||||
other: "Avere il pieno potere di accedere al sito."
|
||||
moderator:
|
||||
other: "Has access to all posts except admin settings."
|
||||
other: "Ha accesso a tutti i post tranne le impostazioni di amministratore."
|
||||
email:
|
||||
other: "email"
|
||||
password:
|
||||
|
@ -40,9 +40,9 @@ backend:
|
|||
not_found:
|
||||
other: "Risposta non trovata"
|
||||
cannot_deleted:
|
||||
other: "No permission to delete."
|
||||
other: "Permesso per cancellare mancante."
|
||||
cannot_update:
|
||||
other: "No permission to update."
|
||||
other: "Nessun permesso per l'aggiornamento."
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: "Non si hanno di privilegi sufficienti per modificare il commento"
|
||||
|
@ -81,11 +81,11 @@ backend:
|
|||
not_found:
|
||||
other: "domanda non trovata"
|
||||
cannot_deleted:
|
||||
other: "No permission to delete."
|
||||
other: "Permesso per cancellare mancante."
|
||||
cannot_close:
|
||||
other: "No permission to close."
|
||||
other: "Nessun permesso per chiudere."
|
||||
cannot_update:
|
||||
other: "No permission to update."
|
||||
other: "Nessun permesso per l'aggiornamento."
|
||||
rank:
|
||||
fail_to_meet_the_condition:
|
||||
other: "Condizioni non valide per il grado"
|
||||
|
@ -98,23 +98,26 @@ backend:
|
|||
not_found:
|
||||
other: "Etichetta non trovata"
|
||||
recommend_tag_not_found:
|
||||
other: "Recommend Tag is not exist."
|
||||
other: "Il Tag consigliato non esiste."
|
||||
recommend_tag_enter:
|
||||
other: "Please enter at least one required tag."
|
||||
other: "Inserisci almeno un tag."
|
||||
not_contain_synonym_tags:
|
||||
other: "Should not contain synonym tags."
|
||||
other: "Non deve contenere tag sinonimi."
|
||||
cannot_update:
|
||||
other: "No permission to update."
|
||||
other: "Nessun permesso per l'aggiornamento."
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "You cannot set the synonym of the current tag as itself."
|
||||
other: "Non puoi impostare il sinonimo del tag corrente come se stesso."
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "The From Name cannot be a email address."
|
||||
theme:
|
||||
not_found:
|
||||
other: "tema non trovato"
|
||||
revision:
|
||||
review_underway:
|
||||
other: "Can't edit currently, there is a version in the review queue."
|
||||
other: "Non è possibile modificare al momento, c'è una versione nella coda di revisione."
|
||||
no_permission:
|
||||
other: "No permission to Revision."
|
||||
other: "Nessun permesso per la revisione."
|
||||
user:
|
||||
email_or_password_wrong:
|
||||
other:
|
||||
|
@ -128,75 +131,82 @@ backend:
|
|||
username_duplicate:
|
||||
other: "utente già in uso"
|
||||
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."
|
||||
other: "Inserimento dell'Avatar non riuscito."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
other: "Non puoi modificare il tuo ruolo."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
other: "Al momento il sito non è aperto per la registrazione"
|
||||
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:
|
||||
spam:
|
||||
name:
|
||||
other: "spam"
|
||||
other: "posta indesiderata"
|
||||
desc:
|
||||
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."
|
||||
other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente."
|
||||
rude:
|
||||
name:
|
||||
other: "scortese o violento"
|
||||
desc:
|
||||
other: "A reasonable person would find this content inappropriate for respectful discourse."
|
||||
other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso."
|
||||
duplicate:
|
||||
name:
|
||||
other: "duplicato"
|
||||
desc:
|
||||
other: "This question has been asked before and already has an answer."
|
||||
other: "Questa domanda è già stata posta e ha già una risposta."
|
||||
not_answer:
|
||||
name:
|
||||
other: "non è una risposta"
|
||||
desc:
|
||||
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."
|
||||
other: "Questo è stato pubblicato come una risposta, ma non tenta di rispondere alla domanda. Dovrebbe forse essere una modifica, un commento, un'altra domanda, o cancellata del tutto."
|
||||
not_need:
|
||||
name:
|
||||
other: "non più necessario"
|
||||
desc:
|
||||
other: "This comment is outdated, conversational or not relevant to this post."
|
||||
other: "Questo commento è obsoleto, conversazionale o non pertinente per questo post."
|
||||
other:
|
||||
name:
|
||||
other: "altro"
|
||||
desc:
|
||||
other: "This post requires staff attention for another reason not listed above."
|
||||
other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra."
|
||||
question:
|
||||
close:
|
||||
duplicate:
|
||||
name:
|
||||
other: "spam"
|
||||
other: "posta indesiderata"
|
||||
desc:
|
||||
other: "This question has been asked before and already has an answer."
|
||||
other: "Questa domanda è già stata posta e ha già una risposta."
|
||||
guideline:
|
||||
name:
|
||||
other: "motivo legato alla community"
|
||||
desc:
|
||||
other: "This question doesn't meet a community guideline."
|
||||
other: "Questa domanda non soddisfa le linee guida della comunità."
|
||||
multiple:
|
||||
name:
|
||||
other: "richiede maggiori dettagli o chiarezza"
|
||||
desc:
|
||||
other: "This question currently includes multiple questions in one. It should focus on one problem only."
|
||||
other: "Questa domanda attualmente include più domande in uno. Dovrebbe concentrarsi su un solo problema."
|
||||
other:
|
||||
name:
|
||||
other: "altro"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
other: "Questo articolo richiede un'altro motivo non listato sopra."
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -206,7 +216,7 @@ backend:
|
|||
update_answer:
|
||||
other: "risposta aggiornata"
|
||||
accept_answer:
|
||||
other: "risposta accepted"
|
||||
other: "risposta accettata"
|
||||
comment_question:
|
||||
other: "domanda commentata"
|
||||
comment_answer:
|
||||
|
@ -226,21 +236,21 @@ backend:
|
|||
#The following fields are used for interface presentation(Front-end)
|
||||
ui:
|
||||
how_to_format:
|
||||
title: How to Format
|
||||
title: Come formattare
|
||||
desc: >-
|
||||
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code><https://url.com><br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">put returns between paragraphs</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by placing <code>></code> at start of line</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">create code fences with backticks <code>`</code></p><pre class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul>
|
||||
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code><https://url.com><br/><br/>[Title](https://url. om)</code></pre></li><li><p class="mb-2">mette i rendimenti tra i paragrafi</p></li><li><p class="mb-2"><em>_italic_</em> o **<strong>grassetto</strong>**</p></li><li><p class="mb-2">trattino di codice per 4 spazi</p></li><li><p class="mb-2">preventivo inserendo <code>></code> all'inizio della riga</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">crea recinzioni di codice con backticks <code>`</code></p><pre class="mb-0">````<code><br/>codice qui<br/>`````</code></pre></li></ul>
|
||||
pagination:
|
||||
prev: Prev
|
||||
next: Next
|
||||
prev: Prec
|
||||
next: Successivo
|
||||
page_title:
|
||||
question: Question
|
||||
questions: Questions
|
||||
question: Domanda
|
||||
questions: Domande
|
||||
tag: Tag
|
||||
tags: Tags
|
||||
tag_wiki: tag wiki
|
||||
edit_tag: Edit Tag
|
||||
ask_a_question: Add Question
|
||||
edit_question: Edit Question
|
||||
edit_tag: Modifica Tag
|
||||
ask_a_question: Aggiungi una domanda
|
||||
edit_question: Modifica Domanda
|
||||
edit_answer: Edit Answer
|
||||
search: Search
|
||||
posts_containing: Posts containing
|
||||
|
@ -801,6 +811,11 @@ ui:
|
|||
confirm_new_email: Your email has been updated.
|
||||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -991,6 +1006,7 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
|
|
|
@ -107,6 +107,9 @@ backend:
|
|||
other: "No permission to update."
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "You cannot set the synonym of the current tag as itself."
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "The From Name cannot be a email address."
|
||||
theme:
|
||||
not_found:
|
||||
other: "Theme not found."
|
||||
|
@ -129,21 +132,21 @@ backend:
|
|||
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."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
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:
|
||||
spam:
|
||||
name:
|
||||
|
@ -197,6 +200,13 @@ backend:
|
|||
other: "something else"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -801,6 +811,11 @@ ui:
|
|||
confirm_new_email: Your email has been updated.
|
||||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -991,6 +1006,7 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
|
|
|
@ -107,6 +107,9 @@ backend:
|
|||
other: "No permission to update."
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "You cannot set the synonym of the current tag as itself."
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "The From Name cannot be a email address."
|
||||
theme:
|
||||
not_found:
|
||||
other: "Theme not found."
|
||||
|
@ -129,21 +132,21 @@ backend:
|
|||
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."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
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:
|
||||
spam:
|
||||
name:
|
||||
|
@ -197,6 +200,13 @@ backend:
|
|||
other: "something else"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -801,6 +811,11 @@ ui:
|
|||
confirm_new_email: Your email has been updated.
|
||||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -991,6 +1006,7 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
backend:
|
||||
base:
|
||||
success:
|
||||
other: "Success."
|
||||
other: "Sucesso."
|
||||
unknown:
|
||||
other: "Unknown error."
|
||||
other: "Erro desconhecido."
|
||||
request_format_error:
|
||||
other: "Request format is not valid."
|
||||
other: "Formato de solicitação não é válido."
|
||||
unauthorized_error:
|
||||
other: "Unauthorized."
|
||||
other: "Não autorizado."
|
||||
database_error:
|
||||
other: "Data server error."
|
||||
other: "Erro no servidor de dados."
|
||||
role:
|
||||
name:
|
||||
user:
|
||||
|
@ -107,6 +107,9 @@ backend:
|
|||
other: "No permission to update."
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "You cannot set the synonym of the current tag as itself."
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "The From Name cannot be a email address."
|
||||
theme:
|
||||
not_found:
|
||||
other: "Theme not found."
|
||||
|
@ -129,21 +132,21 @@ backend:
|
|||
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."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
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:
|
||||
spam:
|
||||
name:
|
||||
|
@ -197,6 +200,13 @@ backend:
|
|||
other: "something else"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -801,6 +811,11 @@ ui:
|
|||
confirm_new_email: Your email has been updated.
|
||||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -991,6 +1006,7 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
|
|
|
@ -107,6 +107,9 @@ backend:
|
|||
other: "No permission to update."
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "You cannot set the synonym of the current tag as itself."
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "The From Name cannot be a email address."
|
||||
theme:
|
||||
not_found:
|
||||
other: "Theme not found."
|
||||
|
@ -129,21 +132,21 @@ backend:
|
|||
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."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
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:
|
||||
spam:
|
||||
name:
|
||||
|
@ -197,6 +200,13 @@ backend:
|
|||
other: "something else"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -801,6 +811,11 @@ ui:
|
|||
confirm_new_email: Your email has been updated.
|
||||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -991,6 +1006,7 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
|
|
|
@ -107,6 +107,9 @@ backend:
|
|||
other: "No permission to update."
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "You cannot set the synonym of the current tag as itself."
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "The From Name cannot be a email address."
|
||||
theme:
|
||||
not_found:
|
||||
other: "Theme not found."
|
||||
|
@ -129,21 +132,21 @@ backend:
|
|||
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."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
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:
|
||||
spam:
|
||||
name:
|
||||
|
@ -197,6 +200,13 @@ backend:
|
|||
other: "something else"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -801,6 +811,11 @@ ui:
|
|||
confirm_new_email: Your email has been updated.
|
||||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -991,6 +1006,7 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
|
|
412
i18n/zh_CN.yaml
412
i18n/zh_CN.yaml
|
@ -2,15 +2,15 @@
|
|||
backend:
|
||||
base:
|
||||
success:
|
||||
other: "成功"
|
||||
other: "成功。"
|
||||
unknown:
|
||||
other: "未知错误"
|
||||
other: "未知错误。"
|
||||
request_format_error:
|
||||
other: "请求格式错误"
|
||||
other: "请求格式错误。"
|
||||
unauthorized_error:
|
||||
other: "未登录"
|
||||
other: "未授权。"
|
||||
database_error:
|
||||
other: "数据服务异常"
|
||||
other: "数据服务器错误。"
|
||||
role:
|
||||
name:
|
||||
user:
|
||||
|
@ -23,38 +23,38 @@ backend:
|
|||
user:
|
||||
other: "默认没有特殊访问权限。"
|
||||
admin:
|
||||
other: "拥有进入网站的全部权限。"
|
||||
other: "拥有管理网站的全部权限。"
|
||||
moderator:
|
||||
other: "有权访问所有的帖子,无法进入管理员设置页面。"
|
||||
other: "拥有访问除管理员设置以外的所有权限。"
|
||||
email:
|
||||
other: "邮箱"
|
||||
password:
|
||||
other: "密码"
|
||||
email_or_password_wrong_error:
|
||||
other: "邮箱或密码错误"
|
||||
other: "邮箱和密码不匹配。"
|
||||
error:
|
||||
admin:
|
||||
email_or_password_wrong:
|
||||
other: 邮箱或密码错误
|
||||
other: 邮箱和密码不匹配。
|
||||
answer:
|
||||
not_found:
|
||||
other: "答案未找到"
|
||||
other: "没有找到答案。"
|
||||
cannot_deleted:
|
||||
other: "无删除权限"
|
||||
other: "没有删除权限。"
|
||||
cannot_update:
|
||||
other: "无修改权限"
|
||||
other: "没有更新权限。"
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: "不允许编辑评论"
|
||||
other: "不允许编辑评论。"
|
||||
not_found:
|
||||
other: "评论未找到"
|
||||
other: "评论未找到。"
|
||||
email:
|
||||
duplicate:
|
||||
other: "邮箱已经存在"
|
||||
other: "邮箱已经存在。"
|
||||
need_to_be_verified:
|
||||
other: "邮箱需要验证"
|
||||
other: "邮箱需要验证。"
|
||||
verify_url_expired:
|
||||
other: "邮箱验证的网址已过期,请重新发送邮件"
|
||||
other: "邮箱验证的网址已过期,请重新发送邮件。"
|
||||
lang:
|
||||
not_found:
|
||||
other: "语言未找到"
|
||||
|
@ -106,7 +106,10 @@ backend:
|
|||
cannot_update:
|
||||
other: "没有更新标签权限。"
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "你无法将当前标签的同义词设置为当前标签自己"
|
||||
other: "您不能将当前标签的同义词设置为本身。"
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "发件人名称不能是电子邮件地址。"
|
||||
theme:
|
||||
not_found:
|
||||
other: "主题未找到"
|
||||
|
@ -129,75 +132,81 @@ backend:
|
|||
other: "用户名已被使用"
|
||||
set_avatar:
|
||||
other: "头像设置错误"
|
||||
config:
|
||||
read_config_failed:
|
||||
other: "读取配置失败"
|
||||
database:
|
||||
connection_failed:
|
||||
other: "数据连接异常!"
|
||||
create_table_failed:
|
||||
other: "创建表失败"
|
||||
install:
|
||||
create_config_failed:
|
||||
other: "无法创建配置文件"
|
||||
cannot_update_your_role:
|
||||
other: "你无法修改自己的角色"
|
||||
other: "您不能修改自己的角色。"
|
||||
not_allowed_registration:
|
||||
other: "目前该网站尚未开放注册"
|
||||
|
||||
other: "目前该站点未开放注册"
|
||||
config:
|
||||
read_config_failed:
|
||||
other: "读取配置失败"
|
||||
database:
|
||||
connection_failed:
|
||||
other: "数据库连接失败"
|
||||
create_table_failed:
|
||||
other: "创建表失败"
|
||||
install:
|
||||
create_config_failed:
|
||||
other: "无法创建 config.yaml 文件。"
|
||||
report:
|
||||
spam:
|
||||
name:
|
||||
other: "垃圾信息"
|
||||
desc:
|
||||
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."
|
||||
other: "这个帖子是一个广告,或是破坏性行为。它对当前的主题没有用处,也不相关。"
|
||||
rude:
|
||||
name:
|
||||
other: "粗鲁或辱骂的"
|
||||
desc:
|
||||
other: "A reasonable person would find this content inappropriate for respectful discourse."
|
||||
other: "一个有理智的人都会认为这种内容不适合进行尊重性的讨论。"
|
||||
duplicate:
|
||||
name:
|
||||
other: "重复信息"
|
||||
desc:
|
||||
other: "This question has been asked before and already has an answer."
|
||||
other: "此问题以前就有人问过,而且已经有了答案。"
|
||||
not_answer:
|
||||
name:
|
||||
other: "不是答案"
|
||||
desc:
|
||||
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."
|
||||
other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。"
|
||||
not_need:
|
||||
name:
|
||||
other: "不再需要"
|
||||
desc:
|
||||
other: "This comment is outdated, conversational or not relevant to this post."
|
||||
other: "此评论已过时,对话或与此帖子无关。"
|
||||
other:
|
||||
name:
|
||||
other: "其他原因"
|
||||
desc:
|
||||
other: "This post requires staff attention for another reason not listed above."
|
||||
other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。"
|
||||
question:
|
||||
close:
|
||||
duplicate:
|
||||
name:
|
||||
other: "垃圾信息"
|
||||
desc:
|
||||
other: "This question has been asked before and already has an answer."
|
||||
other: "此问题以前就有人问过,而且已经有了答案。"
|
||||
guideline:
|
||||
name:
|
||||
other: "社区特定原因"
|
||||
desc:
|
||||
other: "This question doesn't meet a community guideline."
|
||||
other: "此问题不符合社区准则。"
|
||||
multiple:
|
||||
name:
|
||||
other: "需要细节或澄清"
|
||||
desc:
|
||||
other: "This question currently includes multiple questions in one. It should focus on one problem only."
|
||||
other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。"
|
||||
other:
|
||||
name:
|
||||
other: "其他原因"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
other: "这个帖子需要上面没有列出的另一个原因。"
|
||||
operation_type:
|
||||
asked:
|
||||
other: "提问于"
|
||||
answered:
|
||||
other: "回答于"
|
||||
modified:
|
||||
other: "修改于"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -207,7 +216,7 @@ backend:
|
|||
update_answer:
|
||||
other: "更新了答案"
|
||||
accept_answer:
|
||||
other: "接受了答案"
|
||||
other: "已接受的回答"
|
||||
comment_question:
|
||||
other: "评论了问题"
|
||||
comment_answer:
|
||||
|
@ -229,15 +238,7 @@ ui:
|
|||
how_to_format:
|
||||
title: 如何设定文本格式
|
||||
desc: >-
|
||||
<ul class="mb-0"><li><p class="mb-2">添加链接:</p><pre
|
||||
class="mb-2"><code><https://url.com><br/><br/>[标题](https://url.com)</code></pre></li><li><p
|
||||
class="mb-2">段落之间使用空行分隔</p></li><li><p class="mb-2"><em>_斜体_</em> 或者
|
||||
**<strong>粗体</strong>**</p></li><li><p class="mb-2">使用 4
|
||||
个空格缩进代码</p></li><li><p
|
||||
class="mb-2">在行首添加<code>></code>表示引用</p></li><li><p class="mb-2">反引号进行转义
|
||||
<code>`像 _这样_`</code></p></li><li><p
|
||||
class="mb-2">使用<code>```</code>创建代码块</p><pre class="mb-0"><code>```<br/>//
|
||||
这是代码<br/>```</code></pre></li></ul>
|
||||
<ul class="mb-0"><li><p class="mb-2">添加链接:</p><pre class="mb-2"><code><https://url.com><br/><br/>[标题](https://url.com)</code></pre></li><li><p class="mb-2">段落之间使用空行分隔</p></li><li><p class="mb-2"><em>_斜体_</em> 或者 **<strong>粗体</strong>**</p></li><li><p class="mb-2">使用 4 个空格缩进代码</p></li><li><p class="mb-2">在行首添加<code>></code>表示引用</p></li><li><p class="mb-2">反引号进行转义 <code>`像 _这样_`</code></p></li><li><p class="mb-2">使用<code>```</code>创建代码块</p><pre class="mb-0"><code>```<br/>// 这是代码<br/>```</code></pre></li></ul>
|
||||
pagination:
|
||||
prev: 上一页
|
||||
next: 下一页
|
||||
|
@ -266,7 +267,7 @@ ui:
|
|||
install: Answer 安装
|
||||
upgrade: Answer 升级
|
||||
maintenance: 网站维护
|
||||
users: Users
|
||||
users: 用户
|
||||
notifications:
|
||||
title: 通知
|
||||
inbox: 收件箱
|
||||
|
@ -290,7 +291,7 @@ ui:
|
|||
class_diagram: 类图
|
||||
state_diagram: 状态图
|
||||
entity_relationship_diagram: ER 图
|
||||
user_defined_diagram: User defined diagram
|
||||
user_defined_diagram: 用户自定义图表
|
||||
gantt_chart: 甘特图
|
||||
pie_chart: 饼图
|
||||
code:
|
||||
|
@ -339,7 +340,7 @@ ui:
|
|||
only_image: 只能上传图片文件。
|
||||
max_size: 图片文件大小不能超过 4 MB。
|
||||
desc:
|
||||
label: 图片描述(可选)
|
||||
label: 描述(可选)
|
||||
tab_url: 网络图片
|
||||
form_url:
|
||||
fields:
|
||||
|
@ -410,13 +411,13 @@ ui:
|
|||
range: 不能超过 35 个字符
|
||||
slug_name:
|
||||
label: URL 固定链接
|
||||
desc: '必须由 "a-z", "0-9", "+ # - ." 组成'
|
||||
desc: '必须使用字符集 "a-z"、"0-9"、"+ # - ."'
|
||||
msg:
|
||||
empty: 不能为空
|
||||
range: 不能超过 35 个字符
|
||||
character: 包含非法字符
|
||||
desc:
|
||||
label: 标签描述(可选)
|
||||
label: 描述(可选)
|
||||
btn_cancel: 取消
|
||||
btn_submit: 提交
|
||||
tag_info:
|
||||
|
@ -564,8 +565,7 @@ ui:
|
|||
placeholder: 搜索
|
||||
footer:
|
||||
build_on: >-
|
||||
Built on <1> Answer </1>- the open-source software that powers Q&A
|
||||
communities<br />Made with love © 2022 Answer
|
||||
基于<1>Answer</1>--为问答社区提供动力的开源软件。<br />Made with love © {{cc}}.
|
||||
upload_img:
|
||||
name: 更改图片
|
||||
loading: 加载中...
|
||||
|
@ -618,7 +618,7 @@ ui:
|
|||
btn_cancel: 取消
|
||||
btn_update: 更新电子邮件地址
|
||||
send_success: >-
|
||||
If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.
|
||||
如果账户与<strong>{{mail}}</strong>相匹配,您应该很快就会收到一封电子邮件,说明如何重置您的密码。
|
||||
email:
|
||||
label: 新邮箱
|
||||
msg:
|
||||
|
@ -647,8 +647,8 @@ ui:
|
|||
account: 账号
|
||||
interface: 界面
|
||||
profile:
|
||||
heading: Profile
|
||||
btn_name: Save
|
||||
heading: 个人资料
|
||||
btn_name: 保存
|
||||
display_name:
|
||||
label: 昵称
|
||||
msg: 昵称不能为空
|
||||
|
@ -666,7 +666,7 @@ ui:
|
|||
custom: 自定义
|
||||
btn_refresh: 刷新
|
||||
custom_text: 您可以上传您的图片。
|
||||
default: System
|
||||
default: 系统
|
||||
msg: 请上传头像
|
||||
bio:
|
||||
label: 关于我 (可选)
|
||||
|
@ -678,12 +678,12 @@ ui:
|
|||
label: 位置 (可选)
|
||||
placeholder: "城市, 国家"
|
||||
notification:
|
||||
heading: Notifications
|
||||
heading: 通知
|
||||
email:
|
||||
label: 邮件通知
|
||||
radio: "你的提问有新的回答,评论,和其他"
|
||||
account:
|
||||
heading: Account
|
||||
heading: 账号
|
||||
change_email_btn: 更改邮箱
|
||||
change_pass_btn: 更改密码
|
||||
change_email_info: >-
|
||||
|
@ -703,7 +703,7 @@ ui:
|
|||
pass_confirm:
|
||||
label: 确认新密码
|
||||
interface:
|
||||
heading: Interface
|
||||
heading: 界面
|
||||
lang:
|
||||
label: 界面语言
|
||||
text: 设置用户界面语言,在刷新页面后生效。
|
||||
|
@ -711,7 +711,7 @@ ui:
|
|||
update: 更新成功
|
||||
update_password: 更改密码成功。
|
||||
flag_success: 感谢您的标记,我们会尽快处理。
|
||||
forbidden_operate_self: Forbidden to operate on yourself
|
||||
forbidden_operate_self: 禁止自己操作
|
||||
review: 您的修订将在审核通过后显示。
|
||||
related_question:
|
||||
title: 相关问题
|
||||
|
@ -737,16 +737,16 @@ ui:
|
|||
write_answer:
|
||||
title: 你的回答
|
||||
btn_name: 提交你的回答
|
||||
add_another_answer: Add another answer
|
||||
add_another_answer: 添加另一个答案
|
||||
confirm_title: 继续回答
|
||||
continue: 继续
|
||||
confirm_info: >-
|
||||
<p>您确定要提交一个新的回答吗?</p><p>您可以直接编辑和改善您之前的回答的。</p>
|
||||
empty: 回答内容不能为空。
|
||||
reopen:
|
||||
title: Reopen this post
|
||||
content: Are you sure you want to reopen?
|
||||
success: This post has been reopened
|
||||
title: 重新打开这个帖子
|
||||
content: 确定要重新打开吗?
|
||||
success: 这个帖子已被重新打开
|
||||
delete:
|
||||
title: 删除
|
||||
question: >-
|
||||
|
@ -811,6 +811,11 @@ ui:
|
|||
confirm_new_email: 你的电子邮箱已更新
|
||||
confirm_new_email_invalid: >-
|
||||
抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了?
|
||||
unsubscribe:
|
||||
page_title: 退订
|
||||
success_title: 取消订阅成功
|
||||
success_desc: 您已成功地从此订阅者列表中移除,并且将不会再收到我们的任何电子邮件。
|
||||
link: 更改设置
|
||||
question:
|
||||
following_tags: 已关注的标签
|
||||
edit: 编辑
|
||||
|
@ -873,9 +878,9 @@ ui:
|
|||
done: 完成
|
||||
config_yaml_error: 无法创建配置文件
|
||||
lang:
|
||||
label: Please Choose a Language
|
||||
label: 请选择一种语言
|
||||
db_type:
|
||||
label: Database Engine
|
||||
label: 数据库引擎
|
||||
db_username:
|
||||
label: 用户名
|
||||
placeholder: root
|
||||
|
@ -890,17 +895,17 @@ ui:
|
|||
msg: 数据库地址不能为空
|
||||
db_name:
|
||||
label: 数据库名
|
||||
placeholder: answer
|
||||
placeholder: 回答
|
||||
msg: 数据库名称不能为空。
|
||||
db_file:
|
||||
label: Database File
|
||||
label: 数据库文件
|
||||
placeholder: /data/answer.db
|
||||
msg: 数据库文件不能为空。
|
||||
config_yaml:
|
||||
title: 创建 config.yaml
|
||||
label: 已创建 config.yaml 文件。
|
||||
desc: >-
|
||||
You can create the <1>config.yaml</1> file manually in the <1>/var/wwww/xxx/</1> directory and paste the following text into it.
|
||||
您可以手动在 <1>/var/wwww/xxx/</1> 目录中创建<1>config.yaml</1> 文件并粘贴以下文本。
|
||||
info: "完成后,点击“下一步”按钮。"
|
||||
site_information: 站点信息
|
||||
admin_account: 管理员账户
|
||||
|
@ -909,52 +914,52 @@ ui:
|
|||
msg: 站点名称不能为空。
|
||||
site_url:
|
||||
label: 站点地址(URL)
|
||||
text: The address of your site.
|
||||
text: 此网站的地址。
|
||||
msg:
|
||||
empty: 站点URL不能为空。
|
||||
incorrect: 站点URL格式不正确。
|
||||
contact_email:
|
||||
label: 联系邮箱
|
||||
text: Email address of key contact responsible for this site.
|
||||
text: 负责本网站的主要联系人的电子邮件地址。
|
||||
msg:
|
||||
empty: Contact Email cannot be empty.
|
||||
incorrect: Contact Email incorrect format.
|
||||
empty: 联系人邮箱地址不能为空。
|
||||
incorrect: 联系人邮箱地址不正确。
|
||||
admin_name:
|
||||
label: Name
|
||||
msg: Name cannot be empty.
|
||||
label: 昵称
|
||||
msg: 昵称不能为空。
|
||||
admin_password:
|
||||
label: Password
|
||||
label: 密码
|
||||
text: >-
|
||||
You will need this password to log in. Please store it in a secure location.
|
||||
msg: Password cannot be empty.
|
||||
您需要此密码才能登录。请将其存储在一个安全的位置。
|
||||
msg: 密码不能为空。
|
||||
admin_email:
|
||||
label: Email
|
||||
text: You will need this email to log in.
|
||||
label: 邮箱
|
||||
text: 您需要此电子邮件才能登录。
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
incorrect: Email incorrect format.
|
||||
ready_title: Your Answer is Ready!
|
||||
empty: 邮箱不能为空。
|
||||
incorrect: 邮箱格式不正确。
|
||||
ready_title: 你的答案已经准备好了!
|
||||
ready_desc: >-
|
||||
If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.
|
||||
good_luck: "Have fun, and good luck!"
|
||||
warn_title: Warning
|
||||
如果你想改变更多的设置,请访问<1>管理员部分</1>;在网站菜单中找到它。
|
||||
good_luck: "玩得愉快,祝您好运!"
|
||||
warn_title: 警告
|
||||
warn_desc: >-
|
||||
The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.
|
||||
install_now: You may try <1>installing now</1>.
|
||||
文件<1>config.yaml</1>已存在。如果您需要重置此文件中的任何配置项,请先删除它。
|
||||
install_now: 您可以尝试<1>现在安装</1>。
|
||||
installed: 已安裝
|
||||
installed_desc: >-
|
||||
You appear to have already installed. To reinstall please clear your old database tables first.
|
||||
您似乎已经安装过了。要重新安装,请先清除旧的数据库表。
|
||||
db_failed: 数据连接异常!
|
||||
db_failed_desc: >-
|
||||
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down.
|
||||
这或者意味着数据库信息在 <1>config.yaml</1> 文件不正确,或者无法与数据库服务器建立联系。这可能意味着您的主机数据库服务器已关闭。
|
||||
page_404:
|
||||
desc: 页面不存在
|
||||
desc: "很抱歉,此页面不存在。"
|
||||
back_home: 回到主页
|
||||
page_50X:
|
||||
desc: 服务器遇到了一个错误,无法完成你的请求。
|
||||
back_home: 回到主页
|
||||
page_maintenance:
|
||||
desc: "We are under maintenance, we’ll be back soon."
|
||||
desc: "我们正在进行维护,我们将很快回来。"
|
||||
nav_menus:
|
||||
dashboard: 后台管理
|
||||
contents: 内容管理
|
||||
|
@ -972,10 +977,10 @@ ui:
|
|||
tos: 服务条款
|
||||
privacy: 隐私政策
|
||||
seo: SEO
|
||||
customize: Customize
|
||||
themes: Themes
|
||||
customize: 自定义
|
||||
themes: 主题
|
||||
css-html: CSS/HTML
|
||||
login: Login
|
||||
login: 登录
|
||||
admin:
|
||||
admin_header:
|
||||
title: 后台管理
|
||||
|
@ -1001,6 +1006,7 @@ ui:
|
|||
answer_links: 回答链接
|
||||
documents: 文档
|
||||
feedback: 用户反馈
|
||||
support: 帮助
|
||||
review: 审查
|
||||
config: 配置
|
||||
update_to: 更新到
|
||||
|
@ -1025,11 +1031,11 @@ ui:
|
|||
btn_cancel: 取消
|
||||
btn_submit: 提交
|
||||
normal_name: 正常
|
||||
normal_desc: 正常状态的用户可以提问和回答。
|
||||
normal_desc: 普通用户可以提问和回答。
|
||||
suspended_name: 封禁
|
||||
suspended_desc: 被封禁的用户将无法登录。
|
||||
deleted_name: 删除
|
||||
deleted_desc: 删除用户的个人信息,认证等等。
|
||||
deleted_desc: "删除个人资料和身份验证关联。"
|
||||
inactive_name: 不活跃
|
||||
inactive_desc: 不活跃的用户必须重新验证邮箱。
|
||||
confirm_title: 删除此用户
|
||||
|
@ -1040,11 +1046,11 @@ ui:
|
|||
status_modal:
|
||||
title: "更改 {{ type }} 状态为..."
|
||||
normal_name: 正常
|
||||
normal_desc: 所有用户都可以访问
|
||||
normal_desc: 所有用户都可以访问的普通帖子。
|
||||
closed_name: 关闭
|
||||
closed_desc: 不能回答,但仍然可以编辑、投票和评论。
|
||||
closed_desc: "关闭的问题不能回答,但仍然可以编辑、投票和评论。"
|
||||
deleted_name: 删除
|
||||
deleted_desc: 所有获得/损失的声望将会恢复。
|
||||
deleted_desc: 获得和丧失的所有信誉积分将被恢复。
|
||||
btn_cancel: 取消
|
||||
btn_submit: 提交
|
||||
btn_next: 下一步
|
||||
|
@ -1079,32 +1085,32 @@ ui:
|
|||
change_status: 更改状态
|
||||
change_role: 更改角色
|
||||
show_logs: 显示日志
|
||||
add_user: Add user
|
||||
add_user: 添加用户
|
||||
new_password_modal:
|
||||
title: Set new password
|
||||
title: 设置新密码
|
||||
form:
|
||||
fields:
|
||||
password:
|
||||
label: Password
|
||||
text: The user will be logged out and need to login again.
|
||||
msg: Password must be at 8 - 32 characters in length.
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
label: 密码
|
||||
text: 用户将被注销,需要再次登录。
|
||||
msg: 密码的长度必须是8-32个字符。
|
||||
btn_cancel: 取消
|
||||
btn_submit: 提交
|
||||
user_modal:
|
||||
title: Add new user
|
||||
title: 添加新用户
|
||||
form:
|
||||
fields:
|
||||
display_name:
|
||||
label: Display Name
|
||||
msg: display_name must be at 4 - 30 characters in length.
|
||||
label: 昵称
|
||||
msg: 昵称的长度必须是4-30个字符。
|
||||
email:
|
||||
label: Email
|
||||
msg: Email is not valid.
|
||||
label: 邮箱
|
||||
msg: 电子邮箱无效。
|
||||
password:
|
||||
label: Password
|
||||
msg: Password must be at 8 - 32 characters in length.
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
label: 密码
|
||||
msg: 密码的长度必须是8-32个字符。
|
||||
btn_cancel: 取消
|
||||
btn_submit: 提交
|
||||
questions:
|
||||
page_title: 问题
|
||||
normal: 正常
|
||||
|
@ -1118,7 +1124,7 @@ ui:
|
|||
action: 操作
|
||||
change: 更改
|
||||
filter:
|
||||
placeholder: "Filter by title, question:id"
|
||||
placeholder: "按标题过滤,问题:id"
|
||||
answers:
|
||||
page_title: 回答
|
||||
normal: 正常
|
||||
|
@ -1130,21 +1136,31 @@ ui:
|
|||
action: 操作
|
||||
change: 更改
|
||||
filter:
|
||||
placeholder: "Filter by title, answer:id"
|
||||
placeholder: "按标题筛选,答案:id"
|
||||
general:
|
||||
page_title: 一般
|
||||
name:
|
||||
label: 站点名称
|
||||
msg: 不能为空
|
||||
text: 站点的名称,作为站点的标题(HTML 的 title 标签)。
|
||||
text: "站点的名称,作为站点的标题(HTML 的 title 标签)。"
|
||||
site_url:
|
||||
label: 网站网址
|
||||
msg: 网站网址不能为空。
|
||||
validate: 请输入一个有效的 URL。
|
||||
text: 此网站的地址。
|
||||
short_desc:
|
||||
label: 简短的站点标语 (可选)
|
||||
msg: 不能为空
|
||||
text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。
|
||||
label: 简短网站描述(可选)
|
||||
msg: 简短网站描述不能为空。
|
||||
text: "简短的标语,作为网站主页的标题(Html 的 title 标签)。"
|
||||
desc:
|
||||
label: 网站描述 (可选)
|
||||
msg: 不能为空
|
||||
text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。
|
||||
msg: 网站描述不能为空。
|
||||
text: "使用一句话描述本站,作为网站的描述(Html 的 meta 标签)。"
|
||||
contact_email:
|
||||
label: 联系人邮箱
|
||||
msg: 联系人邮箱不能为空。
|
||||
validate: 联系人邮箱无效。
|
||||
text: 负责本网站的主要联系人的电子邮件地址。
|
||||
interface:
|
||||
page_title: 界面
|
||||
logo:
|
||||
|
@ -1160,9 +1176,9 @@ ui:
|
|||
msg: 不能为空
|
||||
text: 设置用户界面语言,在刷新页面后生效。
|
||||
time_zone:
|
||||
label: Timezone
|
||||
msg: Timezone cannot be empty.
|
||||
text: Choose a city in the same timezone as you.
|
||||
label: 时区
|
||||
msg: 时区不能为空。
|
||||
text: 选择一个与您相同时区的城市。
|
||||
smtp:
|
||||
page_title: SMTP
|
||||
from_email:
|
||||
|
@ -1198,97 +1214,97 @@ ui:
|
|||
text: 提供用于接收测试邮件的邮箱地址。
|
||||
msg: 地址无效
|
||||
smtp_authentication:
|
||||
label: Enable authentication
|
||||
title: SMTP Authentication
|
||||
label: 启用身份验证
|
||||
title: SMTP身份验证
|
||||
msg: 不能为空
|
||||
"yes": "是"
|
||||
"no": "否"
|
||||
branding:
|
||||
page_title: Branding
|
||||
page_title: 品牌
|
||||
logo:
|
||||
label: Logo
|
||||
msg: Logo cannot be empty.
|
||||
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
|
||||
label: 图标
|
||||
msg: 图标不能为空。
|
||||
text: 在你的网站左上方的Logo图标。使用一个高度为56,长宽比大于3:1的宽长方形图像。如果留空,将显示网站标题文本。
|
||||
mobile_logo:
|
||||
label: Mobile Logo (optional)
|
||||
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
|
||||
label: 移动端图标(可选)
|
||||
text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空,将使用 "Logo"设置中的图像。
|
||||
square_icon:
|
||||
label: Square Icon
|
||||
msg: Square icon cannot be empty.
|
||||
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
|
||||
label: 方形图标
|
||||
msg: 方形图标不能为空。
|
||||
text: 用作元数据图标的基础的图像。最好是大于512x512。
|
||||
favicon:
|
||||
label: Favicon (optional)
|
||||
text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used.
|
||||
label: 收藏夹图标(可选)
|
||||
text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空,将使用“方形图标”。
|
||||
legal:
|
||||
page_title: Legal
|
||||
page_title: 法律条款
|
||||
terms_of_service:
|
||||
label: Terms of Service
|
||||
text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here."
|
||||
label: 服务条款
|
||||
text: "您可以在此添加服务内容的条款。如果您已经在别处托管了文档,请在这里提供完整的URL。"
|
||||
privacy_policy:
|
||||
label: Privacy Policy
|
||||
text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here."
|
||||
label: 隐私条款
|
||||
text: "您可以在此添加隐私政策内容。如果您已经在别处托管了文档,请在这里提供完整的URL。"
|
||||
write:
|
||||
page_title: Write
|
||||
page_title: 编辑
|
||||
recommend_tags:
|
||||
label: Recommend Tags
|
||||
text: "Please input tag slug above, one tag per line."
|
||||
label: 推荐标签
|
||||
text: "请输入以上标签,每行一个标签。"
|
||||
required_tag:
|
||||
title: Required Tag
|
||||
label: Set recommend tag as required
|
||||
text: "Every new question must have at least one recommend tag."
|
||||
title: 必需的标签
|
||||
label: 根据需要设置推荐标签
|
||||
text: "每个新问题必须至少有一个推荐标签。"
|
||||
reserved_tags:
|
||||
label: Reserved Tags
|
||||
text: "Reserved tags can only be added to a post by moderator."
|
||||
label: 保留标签
|
||||
text: "保留的标签只能由版主添加到一个帖子中。"
|
||||
seo:
|
||||
page_title: SEO
|
||||
page_title: 搜索引擎优化
|
||||
permalink:
|
||||
label: Permalink
|
||||
text: Custom URL structures can improve the usability, and forward-compatibility of your links.
|
||||
label: 固定链接
|
||||
text: 自定义URL结构可以提高可用性,以及你的链接的向前兼容性。
|
||||
robots:
|
||||
label: robots.txt
|
||||
text: This will permanently override any related site settings.
|
||||
text: 这将永久覆盖任何相关的网站设置。
|
||||
themes:
|
||||
page_title: Themes
|
||||
page_title: 主题
|
||||
themes:
|
||||
label: Themes
|
||||
text: Select an existing theme.
|
||||
label: 主题
|
||||
text: 选择一个现有主题。
|
||||
navbar_style:
|
||||
label: Navbar Style
|
||||
text: Select an existing theme.
|
||||
label: 导航栏样式
|
||||
text: 选择一个现有主题。
|
||||
primary_color:
|
||||
label: Primary Color
|
||||
text: Modify the colors used by your themes
|
||||
label: 主色调
|
||||
text: 修改您主题使用的颜色
|
||||
css_and_html:
|
||||
page_title: CSS and HTML
|
||||
page_title: CSS 与 HTML
|
||||
custom_css:
|
||||
label: Custom CSS
|
||||
text: This will insert as <link>
|
||||
label: 自定义CSS
|
||||
text: 这将在 <link> 之前插入
|
||||
head:
|
||||
label: Head
|
||||
text: This will insert before </head>
|
||||
label: 头部
|
||||
text: 这将在 </head> 之前插入
|
||||
header:
|
||||
label: Header
|
||||
text: This will insert after <body>
|
||||
label: 标题
|
||||
text: 这将在 <body> 之前插入
|
||||
footer:
|
||||
label: Footer
|
||||
text: This will insert before </html>.
|
||||
label: 页脚
|
||||
text: 这将在 </html> 之前插入
|
||||
login:
|
||||
page_title: Login
|
||||
page_title: 登录
|
||||
membership:
|
||||
title: Membership
|
||||
label: Allow new registrations
|
||||
text: Turn off to prevent anyone from creating a new account.
|
||||
title: 会员
|
||||
label: 允许新注册
|
||||
text: 关闭以防止任何人创建新帐户。
|
||||
private:
|
||||
title: Private
|
||||
label: Login required
|
||||
text: Only logged in users can access this community.
|
||||
title: 非公开的
|
||||
label: 需要登录
|
||||
text: 只有登录用户才能访问这个社区。
|
||||
form:
|
||||
empty: cannot be empty
|
||||
invalid: is invalid
|
||||
btn_submit: Save
|
||||
not_found_props: "Required property {{ key }} not found."
|
||||
empty: 不能为空
|
||||
invalid: 是无效的
|
||||
btn_submit: 保存
|
||||
not_found_props: "所需属性 {{ key }} 未找到。"
|
||||
page_review:
|
||||
review: Review
|
||||
review: 评论
|
||||
proposed: 提案
|
||||
question_edit: 问题编辑
|
||||
answer_edit: 回答编辑
|
||||
|
@ -1326,11 +1342,11 @@ ui:
|
|||
comment: 评论
|
||||
no_data: "空空如也"
|
||||
users:
|
||||
title: Users
|
||||
users_with_the_most_reputation: Users with the highest reputation scores
|
||||
users_with_the_most_vote: Users who voted the most
|
||||
staffs: Our community staff
|
||||
reputation: reputation
|
||||
votes: votes
|
||||
title: 用户
|
||||
users_with_the_most_reputation: 信誉积分最高的用户
|
||||
users_with_the_most_vote: 投票最多的用户
|
||||
staffs: 我们的社区工作人员
|
||||
reputation: 声望值
|
||||
votes: 投票
|
||||
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ backend:
|
|||
success:
|
||||
other: "成功!"
|
||||
unknown:
|
||||
other: "Unknown error."
|
||||
other: "未知的錯誤。"
|
||||
request_format_error:
|
||||
other: "Request format is not valid."
|
||||
other: "請求的格式無效。"
|
||||
unauthorized_error:
|
||||
other: "Unauthorized."
|
||||
other: "未授權。"
|
||||
database_error:
|
||||
other: "Data server error."
|
||||
role:
|
||||
|
@ -29,13 +29,13 @@ backend:
|
|||
email:
|
||||
other: "Email"
|
||||
password:
|
||||
other: "Password"
|
||||
other: "密碼"
|
||||
email_or_password_wrong_error:
|
||||
other: "Email and password do not match."
|
||||
other: "電子郵箱和密碼不匹配。"
|
||||
error:
|
||||
admin:
|
||||
email_or_password_wrong:
|
||||
other: Email and password do not match.
|
||||
other: 電子郵箱和密碼不匹配。
|
||||
answer:
|
||||
not_found:
|
||||
other: "Answer do not found."
|
||||
|
@ -70,22 +70,22 @@ backend:
|
|||
not_found:
|
||||
other: "Object not found."
|
||||
verification_failed:
|
||||
other: "Verification failed."
|
||||
other: "驗證失敗。"
|
||||
email_or_password_incorrect:
|
||||
other: "Email and password do not match."
|
||||
other: "電子郵箱和密碼不匹配。"
|
||||
old_password_verification_failed:
|
||||
other: "The old password verification failed"
|
||||
other: "舊密碼驗證失敗"
|
||||
new_password_same_as_previous_setting:
|
||||
other: "The new password is the same as the previous one."
|
||||
other: "新密碼與先前的一樣。"
|
||||
question:
|
||||
not_found:
|
||||
other: "Question not found."
|
||||
other: "找不到問題。"
|
||||
cannot_deleted:
|
||||
other: "No permission to delete."
|
||||
other: "沒有刪除的權限。"
|
||||
cannot_close:
|
||||
other: "No permission to close."
|
||||
other: "沒有關閉的權限。"
|
||||
cannot_update:
|
||||
other: "No permission to update."
|
||||
other: "沒有更新的權限。"
|
||||
rank:
|
||||
fail_to_meet_the_condition:
|
||||
other: "Rank fail to meet the condition."
|
||||
|
@ -107,9 +107,12 @@ backend:
|
|||
other: "No permission to update."
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "You cannot set the synonym of the current tag as itself."
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "The From Name cannot be a email address."
|
||||
theme:
|
||||
not_found:
|
||||
other: "Theme not found."
|
||||
other: "未找到主題。"
|
||||
revision:
|
||||
review_underway:
|
||||
other: "Can't edit currently, there is a version in the review queue."
|
||||
|
@ -129,21 +132,21 @@ backend:
|
|||
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."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
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:
|
||||
spam:
|
||||
name:
|
||||
|
@ -197,6 +200,13 @@ backend:
|
|||
other: "something else"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -801,6 +811,11 @@ ui:
|
|||
confirm_new_email: Your email has been updated.
|
||||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -991,6 +1006,7 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package constant
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
CommentEditDeadline = time.Minute * 5
|
||||
)
|
|
@ -7,10 +7,10 @@ import (
|
|||
"github.com/answerdev/answer/pkg/dir"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/segmentfault/pacman/cache"
|
||||
"github.com/segmentfault/pacman/contrib/cache/memory"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
_ "modernc.org/sqlite"
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
ormlog "xorm.io/xorm/log"
|
||||
|
@ -43,6 +43,7 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) {
|
|||
if err := dir.CreateDirIfNotExist(dbFileDir); err != nil {
|
||||
log.Errorf("create database dir failed: %s", err)
|
||||
}
|
||||
dataConf.MaxOpenConn = 1
|
||||
}
|
||||
engine, err := xorm.NewEngine(dataConf.Driver, dataConf.Connection)
|
||||
if err != nil {
|
||||
|
|
|
@ -21,7 +21,7 @@ type RespBody struct {
|
|||
// TrMsg translate the reason cause as a message
|
||||
func (r *RespBody) TrMsg(lang i18n.Language) *RespBody {
|
||||
if len(r.Message) == 0 {
|
||||
r.Message = translator.GlobalTrans.Tr(lang, r.Reason)
|
||||
r.Message = translator.Tr(lang, r.Reason)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -4,10 +4,35 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
langMapping = map[i18n.Language]bool{
|
||||
i18n.LanguageChinese: true,
|
||||
i18n.LanguageChineseTraditional: true,
|
||||
i18n.LanguageEnglish: true,
|
||||
i18n.LanguageGerman: true,
|
||||
i18n.LanguageSpanish: true,
|
||||
i18n.LanguageFrench: true,
|
||||
i18n.LanguageItalian: true,
|
||||
i18n.LanguageJapanese: true,
|
||||
i18n.LanguageKorean: true,
|
||||
i18n.LanguagePortuguese: true,
|
||||
i18n.LanguageRussian: true,
|
||||
i18n.LanguageVietnamese: true,
|
||||
}
|
||||
)
|
||||
|
||||
// ExtractAndSetAcceptLanguage extract accept language from header and set to context
|
||||
func ExtractAndSetAcceptLanguage(ctx *gin.Context) {
|
||||
// The language of our front-end configuration, like en_US
|
||||
lang := handler.GetLang(ctx)
|
||||
ctx.Set(constant.AcceptLanguageFlag, lang)
|
||||
if langMapping[lang] {
|
||||
ctx.Set(constant.AcceptLanguageFlag, lang)
|
||||
return
|
||||
}
|
||||
|
||||
// default language
|
||||
ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ const (
|
|||
const (
|
||||
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
|
||||
CommentNotFound = "error.comment.not_found"
|
||||
CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline"
|
||||
QuestionNotFound = "error.question.not_found"
|
||||
QuestionCannotDeleted = "error.question.cannot_deleted"
|
||||
QuestionCannotClose = "error.question.cannot_close"
|
||||
|
|
|
@ -106,3 +106,12 @@ func CheckLanguageIsValid(lang string) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Tr use language to translate data. If this language translation is not available, return default english translation.
|
||||
func Tr(lang i18n.Language, data string) string {
|
||||
translation := GlobalTrans.Tr(lang, data)
|
||||
if translation == data {
|
||||
return GlobalTrans.Tr(i18n.DefaultLanguage, data)
|
||||
}
|
||||
return translation
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
chineseTraditional "github.com/go-playground/locales/zh_Hant_TW"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/go-playground/validator/v10/non-standard/validators"
|
||||
"github.com/go-playground/validator/v10/translations/en"
|
||||
"github.com/go-playground/validator/v10/translations/es"
|
||||
"github.com/go-playground/validator/v10/translations/fr"
|
||||
|
@ -99,10 +100,11 @@ func getTran(lo locales.Translator) ut.Translator {
|
|||
|
||||
func createDefaultValidator(la i18n.Language) *validator.Validate {
|
||||
validate := validator.New()
|
||||
_ = validate.RegisterValidation("notblank", validators.NotBlank)
|
||||
validate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) {
|
||||
defer func() {
|
||||
if len(res) > 0 {
|
||||
res = translator.GlobalTrans.Tr(la, res)
|
||||
res = translator.Tr(la, res)
|
||||
}
|
||||
}()
|
||||
if jsonTag := fld.Tag.Get("json"); len(jsonTag) > 0 {
|
||||
|
@ -168,7 +170,7 @@ func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err
|
|||
return nil, nil
|
||||
}
|
||||
for _, errField := range errFields {
|
||||
errField.ErrorMsg = translator.GlobalTrans.Tr(m.Lang, errField.ErrorMsg)
|
||||
errField.ErrorMsg = translator.Tr(m.Lang, errField.ErrorMsg)
|
||||
}
|
||||
return errFields, err
|
||||
}
|
||||
|
|
|
@ -12,8 +12,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
DefaultConfigFileName = "config.yaml"
|
||||
DefaultCacheFileName = "cache.db"
|
||||
DefaultConfigFileName = "config.yaml"
|
||||
DefaultCacheFileName = "cache.db"
|
||||
DefaultReservedUsernamesConfigFileName = "reserved-usernames.json"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -40,6 +41,7 @@ func InstallAllInitialEnvironment(dataDirPath string) {
|
|||
FormatAllPath(dataDirPath)
|
||||
installUploadDir()
|
||||
installI18nBundle()
|
||||
installReservedUsernames()
|
||||
fmt.Println("install all initial environment done")
|
||||
}
|
||||
|
||||
|
@ -112,3 +114,16 @@ func installI18nBundle() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installReservedUsernames() {
|
||||
reservedUsernamesJsonFilePath := filepath.Join(ConfigFileDir, DefaultReservedUsernamesConfigFileName)
|
||||
if !dir.CheckFileExist(reservedUsernamesJsonFilePath) {
|
||||
err := writer.WriteFile(reservedUsernamesJsonFilePath, string(configs.ReservedUsernames))
|
||||
if err != nil {
|
||||
fmt.Printf("[%s] write file fail: %s\n", DefaultReservedUsernamesConfigFileName, err)
|
||||
} else {
|
||||
fmt.Printf("[%s] write file success\n", DefaultReservedUsernamesConfigFileName)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,6 +111,7 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
|
||||
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentEdit, req.CommentID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
|
@ -31,7 +30,7 @@ func NewQuestionController(questionService *service.QuestionService, rankService
|
|||
// RemoveQuestion delete question
|
||||
// @Summary delete question
|
||||
// @Description delete question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -62,7 +61,7 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
|
|||
// CloseQuestion Close question
|
||||
// @Summary Close question
|
||||
// @Description Close question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -92,7 +91,7 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
|
|||
// ReopenQuestion reopen question
|
||||
// @Summary reopen question
|
||||
// @Description reopen question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -119,10 +118,10 @@ func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetQuestion godoc
|
||||
// @Summary GetQuestion Question
|
||||
// @Description GetQuestion Question
|
||||
// @Tags api-question
|
||||
// GetQuestion get question details
|
||||
// @Summary get question details
|
||||
// @Description get question details
|
||||
// @Tags Question
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
@ -161,7 +160,7 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
|||
// SimilarQuestion godoc
|
||||
// @Summary Search Similar Question
|
||||
// @Description Search Similar Question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param question_id query string true "question_id" default()
|
||||
|
@ -181,65 +180,34 @@ func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
// Index godoc
|
||||
// @Summary SearchQuestionList
|
||||
// @Description SearchQuestionList <br> "order" Enums(newest, active,frequent,score,unanswered)
|
||||
// @Tags api-question
|
||||
// QuestionPage get questions by page
|
||||
// @Summary get questions by page
|
||||
// @Description get questions by page
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.QuestionSearch true "QuestionSearch"
|
||||
// @Success 200 {string} string ""
|
||||
// @Param data body schema.QuestionPageReq true "QuestionPageReq"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}}
|
||||
// @Router /answer/api/v1/question/page [get]
|
||||
func (qc *QuestionController) Index(ctx *gin.Context) {
|
||||
req := &schema.QuestionSearch{}
|
||||
func (qc *QuestionController) QuestionPage(ctx *gin.Context) {
|
||||
req := &schema.QuestionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
list, count, err := qc.questionService.SearchList(ctx, req, userID)
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
questions, total, err := qc.questionService.GetQuestionPage(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, gin.H{
|
||||
"list": list,
|
||||
"count": count,
|
||||
})
|
||||
}
|
||||
|
||||
// SearchList godoc
|
||||
// @Summary SearchQuestionList
|
||||
// @Description SearchQuestionList
|
||||
// @Tags api-question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.QuestionSearch true "QuestionSearch"
|
||||
// @Router /answer/api/v1/question/search [post]
|
||||
// @Success 200 {string} string ""
|
||||
func (qc *QuestionController) SearchList(c *gin.Context) {
|
||||
Request := new(schema.QuestionSearch)
|
||||
err := c.BindJSON(Request)
|
||||
if err != nil {
|
||||
handler.HandleResponse(c, err, nil)
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
userID := middleware.GetLoginUserIDFromContext(c)
|
||||
list, count, err := qc.questionService.SearchList(ctx, Request, userID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(c, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(c, nil, gin.H{
|
||||
"list": list,
|
||||
"count": count,
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions))
|
||||
}
|
||||
|
||||
// AddQuestion add question
|
||||
// @Summary add question
|
||||
// @Description add question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -309,7 +277,7 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
|
|||
// UpdateQuestion update question
|
||||
// @Summary update question
|
||||
// @Description update question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -366,7 +334,7 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
|
|||
// CloseMsgList close question msg list
|
||||
// @Summary close question msg list
|
||||
// @Description close question msg list
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -380,7 +348,7 @@ func (qc *QuestionController) CloseMsgList(ctx *gin.Context) {
|
|||
// SearchByTitleLike add question title like
|
||||
// @Summary add question title like
|
||||
// @Description add question title like
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -397,7 +365,7 @@ func (qc *QuestionController) SearchByTitleLike(ctx *gin.Context) {
|
|||
// UserTop godoc
|
||||
// @Summary UserTop
|
||||
// @Description UserTop
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -417,7 +385,7 @@ func (qc *QuestionController) UserTop(ctx *gin.Context) {
|
|||
// UserList godoc
|
||||
// @Summary UserList
|
||||
// @Description UserList
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
|
|
@ -91,8 +91,8 @@ func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInf
|
|||
|
||||
// Index question list
|
||||
func (tc *TemplateController) Index(ctx *gin.Context) {
|
||||
req := &schema.QuestionSearch{
|
||||
Order: "newest",
|
||||
req := &schema.QuestionPageReq{
|
||||
OrderCond: "newest",
|
||||
}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
tc.Page404(ctx)
|
||||
|
@ -124,8 +124,8 @@ func (tc *TemplateController) Index(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
func (tc *TemplateController) QuestionList(ctx *gin.Context) {
|
||||
req := &schema.QuestionSearch{
|
||||
Order: "newest",
|
||||
req := &schema.QuestionPageReq{
|
||||
OrderCond: "newest",
|
||||
}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
tc.Page404(ctx)
|
||||
|
@ -139,6 +139,9 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) {
|
|||
}
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl)
|
||||
if page > 1 {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions?page=%d", siteInfo.General.SiteUrl, page)
|
||||
}
|
||||
|
||||
UrlUseTitle := false
|
||||
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
|
||||
|
@ -327,6 +330,9 @@ func (tc *TemplateController) TagList(ctx *gin.Context) {
|
|||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl)
|
||||
if req.Page > 1 {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags?page=%d", siteInfo.General.SiteUrl, req.Page)
|
||||
}
|
||||
siteInfo.Title = fmt.Sprintf("%s - %s", "Tags", siteInfo.General.Name)
|
||||
tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{
|
||||
"page": page,
|
||||
|
@ -353,6 +359,9 @@ func (tc *TemplateController) TagInfo(ctx *gin.Context) {
|
|||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag)
|
||||
if req.Page > 1 {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s?page=%d", siteInfo.General.SiteUrl, tag, req.Page)
|
||||
}
|
||||
siteInfo.Description = htmltext.FetchExcerpt(taginifo.ParsedText, "...", 240)
|
||||
if len(taginifo.ParsedText) == 0 {
|
||||
siteInfo.Description = "The tag has no description."
|
||||
|
@ -437,6 +446,7 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
|
|||
data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead
|
||||
data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
|
||||
data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter
|
||||
data["Version"] = constant.Version
|
||||
_, ok := data["path"]
|
||||
if !ok {
|
||||
data["path"] = ""
|
||||
|
|
|
@ -11,8 +11,8 @@ import (
|
|||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionSearch) ([]*schema.QuestionInfo, int64, error) {
|
||||
return t.questionService.SearchList(ctx, req, req.UserID)
|
||||
func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionPageReq) ([]*schema.QuestionPageResp, int64, error) {
|
||||
return t.questionService.GetQuestionPage(ctx, req)
|
||||
}
|
||||
|
||||
func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfo, err error) {
|
||||
|
|
|
@ -15,19 +15,20 @@ func (q *TemplateRenderController) TagList(ctx context.Context, req *schema.GetT
|
|||
return
|
||||
}
|
||||
|
||||
func (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionInfo, questionCount int64, err error) {
|
||||
func (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionPageResp, questionCount int64, err error) {
|
||||
dto := &schema.GetTagInfoReq{}
|
||||
_ = copier.Copy(dto, req)
|
||||
resp, err = q.tagService.GetTagInfo(ctx, dto)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
searchQuestion := &schema.QuestionSearch{}
|
||||
searchQuestion := &schema.QuestionPageReq{}
|
||||
searchQuestion.Page = req.Page
|
||||
searchQuestion.PageSize = req.PageSize
|
||||
searchQuestion.Order = "newest"
|
||||
searchQuestion.OrderCond = "newest"
|
||||
searchQuestion.Tag = req.Name
|
||||
questionList, questionCount, err = q.questionService.SearchList(ctx, searchQuestion, "")
|
||||
searchQuestion.LoginUserID = req.UserID
|
||||
questionList, questionCount, err = q.questionService.GetQuestionPage(ctx, searchQuestion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ package controller
|
|||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -63,3 +65,21 @@ func (uc *UploadController) UploadFile(ctx *gin.Context) {
|
|||
}
|
||||
handler.HandleResponse(ctx, err, url)
|
||||
}
|
||||
|
||||
// PostRender render post content
|
||||
// @Summary render post content
|
||||
// @Description render post content
|
||||
// @Tags Upload
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.PostRenderReq true "PostRenderReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/post/render [post]
|
||||
func (uc *UploadController) PostRender(ctx *gin.Context) {
|
||||
req := &schema.PostRenderReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, converter.Markdown2HTML(req.Content))
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
|
@ -124,7 +124,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
|
|||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "e_mail",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields)
|
||||
return
|
||||
|
@ -151,14 +151,14 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
|
||||
code, err := uc.userService.RetrievePassWord(ctx, req)
|
||||
handler.HandleResponse(ctx, err, code)
|
||||
_, err := uc.userService.RetrievePassWord(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UseRePassWord godoc
|
||||
|
@ -236,7 +236,7 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
|
@ -245,7 +245,8 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
|
|||
resp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req)
|
||||
if len(errFields) > 0 {
|
||||
for _, field := range errFields {
|
||||
field.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), field.ErrorMsg)
|
||||
field.ErrorMsg = translator.
|
||||
Tr(handler.GetLang(ctx), field.ErrorMsg)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, errFields)
|
||||
} else {
|
||||
|
@ -312,7 +313,7 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
|
@ -350,7 +351,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
|||
if !oldPassVerification {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "old_pass",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)
|
||||
return
|
||||
|
@ -358,7 +359,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
|||
if req.OldPass == req.Pass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields)
|
||||
return
|
||||
|
@ -384,8 +385,11 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := uc.userService.UpdateInfo(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
errFields, err := uc.userService.UpdateInfo(ctx, req)
|
||||
for _, field := range errFields {
|
||||
field.ErrorMsg = translator.Tr(handler.GetLang(ctx), field.ErrorMsg)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, errFields)
|
||||
}
|
||||
|
||||
// UserUpdateInterface update user interface config
|
||||
|
@ -488,7 +492,7 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
|
|||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
|
|
|
@ -22,11 +22,6 @@ var AdminQuestionSearchStatusIntToString = map[int]string{
|
|||
QuestionStatusDeleted: "deleted",
|
||||
}
|
||||
|
||||
type QuestionTag struct {
|
||||
Question `xorm:"extends"`
|
||||
TagRel `xorm:"extends"`
|
||||
}
|
||||
|
||||
// Question question
|
||||
type Question struct {
|
||||
ID string `xorm:"not null pk BIGINT(20) id"`
|
||||
|
|
|
@ -5,6 +5,10 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
|
@ -77,11 +81,23 @@ type InitBaseInfoReq struct {
|
|||
SiteName string `validate:"required,gt=0,lte=30" json:"site_name"`
|
||||
SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"`
|
||||
ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"`
|
||||
AdminName string `validate:"required,gt=4,lte=30" json:"name"`
|
||||
AdminName string `validate:"required,gt=3,lte=30" json:"name"`
|
||||
AdminPassword string `validate:"required,gte=8,lte=32" json:"password"`
|
||||
AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"`
|
||||
}
|
||||
|
||||
func (r *InitBaseInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if checker.IsInvalidUsername(r.AdminName) {
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "name",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *InitBaseInfoReq) FormatSiteUrl() {
|
||||
parsedUrl, err := url.Parse(r.SiteURL)
|
||||
if err != nil {
|
||||
|
|
|
@ -261,7 +261,7 @@ func initConfigTable(engine *xorm.Engine) error {
|
|||
{ID: 30, Key: "answer.vote_up", Value: `0`},
|
||||
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
|
||||
{ID: 32, Key: "question.follow", Value: `0`},
|
||||
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`},
|
||||
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"}`},
|
||||
{ID: 35, Key: "tag.follow", Value: `0`},
|
||||
{ID: 36, Key: "rank.question.add", Value: `1`},
|
||||
{ID: 37, Key: "rank.question.edit", Value: `200`},
|
||||
|
|
|
@ -55,6 +55,7 @@ var migrations = []Migration{
|
|||
NewMigration("add activity timeline", addActivityTimeline, false),
|
||||
NewMigration("add user role", addRoleFeatures, false),
|
||||
NewMigration("add theme and private mode", addThemeAndPrivateMode, true),
|
||||
NewMigration("add new answer notification", addNewAnswerNotification, true),
|
||||
NewMigration("add plugin", addPlugin, false),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +1,41 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addPlugin(x *xorm.Engine) error {
|
||||
defaultConfigTable := []*entity.Config{
|
||||
{ID: 118, Key: "plugin.status", Value: `{}`},
|
||||
func addNewAnswerNotification(x *xorm.Engine) error {
|
||||
cond := &entity.Config{Key: "email.config"}
|
||||
exists, err := x.Get(cond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get email config failed: %w", err)
|
||||
}
|
||||
for _, c := range defaultConfigTable {
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
|
||||
if !exists {
|
||||
// This should be impossible except that the config was deleted manually by user.
|
||||
_, err = x.InsertOne(&entity.Config{
|
||||
Key: "email.config",
|
||||
Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"}`,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("update config failed: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
|
||||
log.Errorf("insert %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("add config failed: %w", err)
|
||||
return fmt.Errorf("add email config failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return x.Sync(new(entity.PluginConfig))
|
||||
m := make(map[string]interface{})
|
||||
_ = json.Unmarshal([]byte(cond.Value), &m)
|
||||
m["new_answer_title"] = "[{{.SiteName}}] {{.DisplayName}} answered your question"
|
||||
m["new_answer_body"] = "<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
|
||||
m["new_comment_title"] = "[{{.SiteName}}] {{.DisplayName}} commented on your post"
|
||||
m["new_comment_body"] = "<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
|
||||
|
||||
val, _ := json.Marshal(m)
|
||||
_, err = x.ID(cond.ID).Update(&entity.Config{Value: string(val)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update email config failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session,
|
|||
func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) {
|
||||
sum := &entity.ActivityRankSum{}
|
||||
_, err := ar.data.DB.Table(entity.Activity{}.TableName()).
|
||||
Select("sum(rank) as rank").
|
||||
Select("sum(`rank`) as `rank`").
|
||||
Where("user_id =?", userID).
|
||||
And("object_id = ?", objectID).
|
||||
And("cancelled =0").
|
||||
|
@ -113,7 +113,7 @@ func (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activi
|
|||
func (ar *ActivityRepo) GetUsersWhoHasGainedTheMostReputation(
|
||||
ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) {
|
||||
rankStat = make([]*entity.ActivityUserRankStat, 0)
|
||||
session := ar.data.DB.Select("user_id, SUM(rank) AS rank_amount").Table("activity")
|
||||
session := ar.data.DB.Select("user_id, SUM(`rank`) AS rank_amount").Table("activity")
|
||||
session.Where("has_rank = 1 AND cancelled = 0")
|
||||
session.Where("created_at >= ?", startTime)
|
||||
session.Where("created_at <= ?", endTime)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||
"github.com/answerdev/answer/internal/service/unique"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// collectionRepo collection repository
|
||||
|
@ -29,15 +30,28 @@ func NewCollectionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) collec
|
|||
|
||||
// AddCollection add collection
|
||||
func (cr *collectionRepo) AddCollection(ctx context.Context, collection *entity.Collection) (err error) {
|
||||
id, err := cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName())
|
||||
if err == nil {
|
||||
collection.ID = id
|
||||
_, err = cr.data.DB.Insert(collection)
|
||||
_, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
|
||||
var has bool
|
||||
dbcollection := &entity.Collection{}
|
||||
result = nil
|
||||
has, err = session.Where("user_id = ? and object_id = ?", collection.UserID, collection.ObjectID).Get(dbcollection)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil
|
||||
if has {
|
||||
return
|
||||
}
|
||||
id, err := cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName())
|
||||
if err == nil {
|
||||
collection.ID = id
|
||||
_, err = session.Insert(collection)
|
||||
if err != nil {
|
||||
return result, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveCollection delete collection
|
||||
|
|
|
@ -205,70 +205,40 @@ func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize i
|
|||
return questionIDList, nil
|
||||
}
|
||||
|
||||
// GetQuestionPage get question page
|
||||
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) {
|
||||
// GetQuestionPage query question page
|
||||
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
|
||||
questionList []*entity.Question, total int64, err error) {
|
||||
questionList = make([]*entity.Question, 0)
|
||||
total, err = pager.Help(page, pageSize, questionList, question, qr.data.DB.NewSession())
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SearchList
|
||||
func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionSearch) ([]*entity.QuestionTag, int64, error) {
|
||||
var count int64
|
||||
var err error
|
||||
rows := make([]*entity.QuestionTag, 0)
|
||||
if search.Page > 0 {
|
||||
search.Page = search.Page - 1
|
||||
} else {
|
||||
search.Page = 0
|
||||
session := qr.data.DB.Where("question.status = ? OR question.status = ?",
|
||||
entity.QuestionStatusAvailable, entity.QuestionStatusClosed)
|
||||
if len(tagID) > 0 {
|
||||
session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id")
|
||||
session.And("tag_rel.tag_id = ?", tagID)
|
||||
session.And("tag_rel.status = ?", entity.TagRelStatusAvailable)
|
||||
}
|
||||
if search.PageSize == 0 {
|
||||
search.PageSize = constant.DefaultPageSize
|
||||
if len(userID) > 0 {
|
||||
session.And("question.user_id = ?", userID)
|
||||
}
|
||||
offset := search.Page * search.PageSize
|
||||
session := qr.data.DB.Table("question")
|
||||
|
||||
if len(search.TagIDs) > 0 {
|
||||
session = session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id")
|
||||
session = session.And("tag_rel.tag_id =?", search.TagIDs[0])
|
||||
// session = session.In("tag_rel.tag_id ", search.TagIDs)
|
||||
session = session.And("tag_rel.status =?", entity.TagRelStatusAvailable)
|
||||
}
|
||||
|
||||
if len(search.UserID) > 0 {
|
||||
session = session.And("question.user_id = ?", search.UserID)
|
||||
}
|
||||
|
||||
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
|
||||
// if search.Status > 0 {
|
||||
// session = session.And("question.status = ?", search.Status)
|
||||
// }
|
||||
// switch
|
||||
// newest, active,frequent,score,unanswered
|
||||
switch search.Order {
|
||||
switch orderCond {
|
||||
case "newest":
|
||||
session = session.OrderBy("question.created_at desc")
|
||||
session.OrderBy("question.created_at DESC")
|
||||
case "active":
|
||||
session = session.OrderBy("question.post_update_time desc,question.updated_at desc")
|
||||
session.OrderBy("question.post_update_time DESC, question.updated_at DESC")
|
||||
case "frequent":
|
||||
session = session.OrderBy("question.view_count desc")
|
||||
session.OrderBy("question.view_count DESC")
|
||||
case "score":
|
||||
session = session.OrderBy("question.vote_count desc,question.view_count desc")
|
||||
session.OrderBy("question.vote_count DESC, question.view_count DESC")
|
||||
case "unanswered":
|
||||
session = session.And("question.last_answer_id = 0")
|
||||
session = session.OrderBy("question.created_at desc")
|
||||
session.Where("question.last_answer_id = 0")
|
||||
session.OrderBy("question.created_at DESC")
|
||||
}
|
||||
session = session.Limit(search.PageSize, offset)
|
||||
session = session.Select("question.id,question.user_id,last_edit_user_id,question.title,question.original_text,question.parsed_text,question.status,question.view_count,question.unique_view_count,question.vote_count,question.answer_count,question.collection_count,question.follow_count,question.accepted_answer_id,question.last_answer_id,question.created_at,question.updated_at,question.post_update_time,question.revision_id")
|
||||
count, err = session.FindAndCount(&rows)
|
||||
|
||||
total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return rows, count, err
|
||||
}
|
||||
return rows, count, nil
|
||||
return questionList, total, err
|
||||
}
|
||||
|
||||
func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error) {
|
||||
|
|
|
@ -29,7 +29,7 @@ var (
|
|||
"`question`.`id` as `question_id`",
|
||||
"`title`",
|
||||
"`parsed_text`",
|
||||
"`question`.`created_at`",
|
||||
"`question`.`created_at` as `created_at`",
|
||||
"`user_id`",
|
||||
"`vote_count`",
|
||||
"`answer_count`",
|
||||
|
@ -42,7 +42,7 @@ var (
|
|||
"`question_id`",
|
||||
"`question`.`title` as `title`",
|
||||
"`answer`.`parsed_text` as `parsed_text`",
|
||||
"`answer`.`created_at`",
|
||||
"`answer`.`created_at` as `created_at`",
|
||||
"`answer`.`user_id` as `user_id`",
|
||||
"`answer`.`vote_count` as `vote_count`",
|
||||
"0 as `answer_count`",
|
||||
|
|
|
@ -60,7 +60,7 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, hasR
|
|||
cond := &entity.Tag{}
|
||||
session := tr.data.DB.Where("")
|
||||
if name != "" {
|
||||
session.Where("slug_name LIKE ?", name+"%")
|
||||
session.Where("slug_name LIKE ? or display_name LIKE ?", name+"%", name+"%")
|
||||
} else {
|
||||
session.UseBool("recommend")
|
||||
cond.Recommend = true
|
||||
|
|
|
@ -127,8 +127,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
//question
|
||||
r.GET("/question/info", a.questionController.GetQuestion)
|
||||
r.POST("/question/search", a.questionController.SearchList)
|
||||
r.GET("/question/page", a.questionController.Index)
|
||||
r.GET("/question/page", a.questionController.QuestionPage)
|
||||
r.GET("/question/similar/tag", a.questionController.SimilarQuestion)
|
||||
r.GET("/personal/qa/top", a.questionController.UserTop)
|
||||
r.GET("/personal/question/page", a.questionController.UserList)
|
||||
|
@ -146,7 +145,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
r.GET("/tags/following", a.tagController.GetFollowingTags)
|
||||
r.GET("/tag", a.tagController.GetTagInfo)
|
||||
r.GET("/tag/synonyms", a.tagController.GetTagSynonyms)
|
||||
r.GET("/question/index", a.questionController.Index)
|
||||
|
||||
//search
|
||||
r.GET("/search", a.searchController.Search)
|
||||
|
@ -222,6 +220,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
// upload file
|
||||
r.POST("/file", a.uploadController.UploadFile)
|
||||
r.POST("/post/render", a.uploadController.PostRender)
|
||||
|
||||
// activity
|
||||
r.GET("/activity/timeline", a.activityController.GetObjectTimeline)
|
||||
|
|
|
@ -20,10 +20,10 @@ const (
|
|||
)
|
||||
|
||||
type AnswerAddReq struct {
|
||||
QuestionID string `json:"question_id" ` // question_id
|
||||
Content string `json:"content" ` // content
|
||||
HTML string `json:"html" ` // html
|
||||
UserID string `json:"-" ` // user_id
|
||||
QuestionID string `json:"question_id" ` // question_id
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content" ` // content
|
||||
HTML string `json:"html" ` // html
|
||||
UserID string `json:"-" ` // user_id
|
||||
}
|
||||
|
||||
func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
|
@ -32,13 +32,13 @@ func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err err
|
|||
}
|
||||
|
||||
type AnswerUpdateReq struct {
|
||||
ID string `json:"id"` // id
|
||||
QuestionID string `json:"question_id" ` // question_id
|
||||
UserID string `json:"-" ` // user_id
|
||||
Title string `json:"title" ` // title
|
||||
Content string `json:"content"` // content
|
||||
HTML string `json:"html" ` // html
|
||||
EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary
|
||||
ID string `json:"id"` // id
|
||||
QuestionID string `json:"question_id" ` // question_id
|
||||
UserID string `json:"-" ` // user_id
|
||||
Title string `json:"title" ` // title
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` // content
|
||||
HTML string `json:"html" ` // html
|
||||
EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary
|
||||
NoNeedReview bool `json:"-"`
|
||||
// whether user can edit it
|
||||
CanEdit bool `json:"-"`
|
||||
|
|
|
@ -51,7 +51,8 @@ type UpdateCommentReq struct {
|
|||
// parsed comment content
|
||||
ParsedText string `validate:"omitempty" json:"parsed_text"`
|
||||
// user id
|
||||
UserID string `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
IsAdmin bool `json:"-"`
|
||||
}
|
||||
|
||||
func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
)
|
||||
|
@ -217,20 +219,72 @@ type UserQuestionInfo struct {
|
|||
ViewCount int `json:"view_count"`
|
||||
AnswerCount int `json:"answer_count"`
|
||||
CollectionCount int `json:"collection_count"`
|
||||
CreateTime int `json:"create_time"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
AcceptedAnswerID string `json:"accepted_answer_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type QuestionSearch struct {
|
||||
Page int `json:"page" form:"page"` // Query number of pages
|
||||
PageSize int `json:"page_size" form:"page_size"` // Search page size
|
||||
Order string `json:"order" form:"order"` // Search order by
|
||||
// Tags []string `json:"tags" form:"tags"` // Search tag
|
||||
Tag string `json:"tag" form:"tag"` //Search tag
|
||||
TagIDs []string `json:"-" form:"-"` // Search tag
|
||||
UserName string `json:"username" form:"username"` // Search username
|
||||
UserID string `json:"-" form:"-"`
|
||||
const (
|
||||
QuestionOrderCondNewest = "newest"
|
||||
QuestionOrderCondActive = "active"
|
||||
QuestionOrderCondFrequent = "frequent"
|
||||
QuestionOrderCondScore = "score"
|
||||
QuestionOrderCondUnanswered = "unanswered"
|
||||
)
|
||||
|
||||
// QuestionPageReq query questions page
|
||||
type QuestionPageReq struct {
|
||||
Page int `validate:"omitempty,min=1" form:"page"`
|
||||
PageSize int `validate:"omitempty,min=1" form:"page_size"`
|
||||
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
|
||||
Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"`
|
||||
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
|
||||
|
||||
LoginUserID string `json:"-"`
|
||||
UserIDBeSearched string `json:"-"`
|
||||
TagID string `json:"-"`
|
||||
}
|
||||
|
||||
const (
|
||||
QuestionPageRespOperationTypeAsked = "asked"
|
||||
QuestionPageRespOperationTypeAnswered = "answered"
|
||||
QuestionPageRespOperationTypeModified = "modified"
|
||||
)
|
||||
|
||||
type QuestionPageResp struct {
|
||||
ID string `json:"id" `
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Title string `json:"title"`
|
||||
UrlTitle string `json:"url_title"`
|
||||
Description string `json:"description"`
|
||||
Status int `json:"status"`
|
||||
Tags []*TagResp `json:"tags"`
|
||||
|
||||
// question statistical information
|
||||
ViewCount int `json:"view_count"`
|
||||
UniqueViewCount int `json:"unique_view_count"`
|
||||
VoteCount int `json:"vote_count"`
|
||||
AnswerCount int `json:"answer_count"`
|
||||
CollectionCount int `json:"collection_count"`
|
||||
FollowCount int `json:"follow_count"`
|
||||
|
||||
// answer information
|
||||
AcceptedAnswerID string `json:"accepted_answer_id"`
|
||||
LastAnswerID string `json:"last_answer_id"`
|
||||
LastAnsweredUserID string `json:"-"`
|
||||
LastAnsweredAt time.Time `json:"-"`
|
||||
|
||||
// operator information
|
||||
OperatedAt int64 `json:"operated_at"`
|
||||
Operator *QuestionPageRespOperator `json:"operator"`
|
||||
OperationType string `json:"operation_type"`
|
||||
}
|
||||
|
||||
type QuestionPageRespOperator struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Rank int `json:"rank"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
type AdminQuestionSearch struct {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package schema
|
||||
|
||||
// PostRenderReq post render request
|
||||
type PostRenderReq struct {
|
||||
Content string `json:"content"`
|
||||
}
|
|
@ -46,9 +46,9 @@ type SiteInterfaceReq struct {
|
|||
|
||||
// SiteBrandingReq site branding request
|
||||
type SiteBrandingReq struct {
|
||||
Logo string `validate:"required,gt=0,lte=512" form:"logo" json:"logo"`
|
||||
Logo string `validate:"omitempty,gt=0,lte=512" form:"logo" json:"logo"`
|
||||
MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"`
|
||||
SquareIcon string `validate:"required,gt=0,lte=512" form:"square_icon" json:"square_icon"`
|
||||
SquareIcon string `validate:"omitempty,gt=0,lte=512" form:"square_icon" json:"square_icon"`
|
||||
Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"`
|
||||
}
|
||||
|
||||
|
@ -134,7 +134,7 @@ type SiteThemeResp struct {
|
|||
func (s *SiteThemeResp) TrTheme(ctx context.Context) {
|
||||
la := handler.GetLangByCtx(ctx)
|
||||
for _, option := range s.ThemeOptions {
|
||||
tr := translator.GlobalTrans.Tr(la, option.Value)
|
||||
tr := translator.Tr(la, option.Value)
|
||||
// if tr is equal the option value means not found translation, so use the original label
|
||||
if tr != option.Value {
|
||||
option.Label = tr
|
||||
|
|
|
@ -2,12 +2,12 @@ package schema
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -229,7 +229,7 @@ type UserEmailLogin struct {
|
|||
// UserRegisterReq user register request
|
||||
type UserRegisterReq struct {
|
||||
// name
|
||||
Name string `validate:"required,gt=4,lte=30" json:"name"`
|
||||
Name string `validate:"required,gt=3,lte=30" json:"name"`
|
||||
// email
|
||||
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" `
|
||||
// password
|
||||
|
@ -278,13 +278,13 @@ type UpdateInfoRequest struct {
|
|||
// display_name
|
||||
DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"`
|
||||
// username
|
||||
Username string `validate:"omitempty,gt=0,lte=30" json:"username"`
|
||||
Username string `validate:"omitempty,gt=3,lte=30" json:"username"`
|
||||
// avatar
|
||||
Avatar AvatarInfo `json:"avatar"`
|
||||
// bio
|
||||
Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"`
|
||||
// bio
|
||||
BioHTML string `validate:"omitempty,gt=0,lte=4096" json:"bio_html"`
|
||||
BioHTML string `json:"-"`
|
||||
// website
|
||||
Website string `validate:"omitempty,gt=0,lte=500" json:"website"`
|
||||
// location
|
||||
|
@ -299,19 +299,18 @@ type AvatarInfo struct {
|
|||
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
|
||||
}
|
||||
|
||||
func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(u.Username) > 0 {
|
||||
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
|
||||
match := re.MatchString(u.Username)
|
||||
if !match {
|
||||
func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(req.Username) > 0 {
|
||||
if checker.IsInvalidUsername(req.Username) {
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: err.Error(),
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
req.BioHTML = converter.Markdown2HTML(req.Bio)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -121,33 +121,14 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if objInfo.ObjectType == constant.QuestionObjectType {
|
||||
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
|
||||
objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, comment.OriginalText)
|
||||
} else if objInfo.ObjectType == constant.AnswerObjectType {
|
||||
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
|
||||
objInfo.ObjectCreatorUserID, comment.ID, req.UserID, comment.OriginalText)
|
||||
}
|
||||
if len(req.MentionUsernameList) > 0 {
|
||||
cs.notificationMention(ctx, req.MentionUsernameList, comment.ID, req.UserID)
|
||||
}
|
||||
|
||||
resp = &schema.GetCommentResp{}
|
||||
resp.SetFromComment(comment)
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete)
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
|
||||
time.Now(), req.CanEdit, req.CanDelete)
|
||||
|
||||
// get reply user info
|
||||
if len(resp.ReplyUserID) > 0 {
|
||||
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
resp.ReplyUsername = replyUser.Username
|
||||
resp.ReplyUserDisplayName = replyUser.DisplayName
|
||||
resp.ReplyUserStatus = replyUser.Status
|
||||
}
|
||||
cs.notificationCommentReply(ctx, replyUser.ID, objInfo.QuestionID, req.UserID)
|
||||
commentResp, err := cs.addCommentNotification(ctx, req, resp, comment, objInfo)
|
||||
if err != nil {
|
||||
return commentResp, err
|
||||
}
|
||||
|
||||
// get user info
|
||||
|
@ -178,6 +159,50 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
func (cs *CommentService) addCommentNotification(
|
||||
ctx context.Context, req *schema.AddCommentReq, resp *schema.GetCommentResp,
|
||||
comment *entity.Comment, objInfo *schema.SimpleObjectInfo) (*schema.GetCommentResp, error) {
|
||||
// The priority of the notification
|
||||
// 1. reply to user
|
||||
// 2. comment mention to user
|
||||
// 3. answer or question was commented
|
||||
alreadyNotifiedUserID := make(map[string]bool)
|
||||
|
||||
// get reply user info
|
||||
if len(resp.ReplyUserID) > 0 && resp.ReplyUserID != req.UserID {
|
||||
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
resp.ReplyUsername = replyUser.Username
|
||||
resp.ReplyUserDisplayName = replyUser.DisplayName
|
||||
resp.ReplyUserStatus = replyUser.Status
|
||||
}
|
||||
cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID)
|
||||
alreadyNotifiedUserID[replyUser.ID] = true
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(req.MentionUsernameList) > 0 {
|
||||
alreadyNotifiedUserIDs := cs.notificationMention(
|
||||
ctx, req.MentionUsernameList, comment.ID, req.UserID, alreadyNotifiedUserID)
|
||||
for _, userID := range alreadyNotifiedUserIDs {
|
||||
alreadyNotifiedUserID[userID] = true
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
|
||||
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
|
||||
objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, comment.OriginalText)
|
||||
} else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
|
||||
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
|
||||
objInfo.ObjectCreatorUserID, comment.ID, req.UserID, comment.OriginalText)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// RemoveComment delete comment
|
||||
func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) {
|
||||
return cs.commentRepo.RemoveComment(ctx, req.CommentID)
|
||||
|
@ -185,6 +210,19 @@ func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveC
|
|||
|
||||
// UpdateComment update comment
|
||||
func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (err error) {
|
||||
old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.CommentNotFound)
|
||||
}
|
||||
|
||||
// user can edit the comment that was posted by himself before deadline.
|
||||
if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) {
|
||||
return errors.BadRequest(reason.CommentCannotEditAfterDeadline)
|
||||
}
|
||||
|
||||
comment := &entity.Comment{}
|
||||
_ = copier.Copy(comment, req)
|
||||
comment.ID = req.CommentID
|
||||
|
@ -198,7 +236,7 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
|
|||
return
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UnknownError)
|
||||
return nil, errors.BadRequest(reason.CommentNotFound)
|
||||
}
|
||||
|
||||
resp = &schema.GetCommentResp{
|
||||
|
@ -243,7 +281,8 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
|
|||
// check if current user vote this comment
|
||||
resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID)
|
||||
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete)
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
|
||||
comment.CreatedAt, req.CanEdit, req.CanDelete)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
@ -339,7 +378,7 @@ func (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *sc
|
|||
commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID)
|
||||
|
||||
commentResp.MemberActions = permission.GetCommentPermission(ctx,
|
||||
req.UserID, commentResp.UserID, req.CanEdit, req.CanDelete)
|
||||
req.UserID, commentResp.UserID, comment.CreatedAt, req.CanEdit, req.CanDelete)
|
||||
return commentResp, nil
|
||||
}
|
||||
|
||||
|
@ -521,14 +560,16 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
|
|||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
func (cs *CommentService) notificationMention(ctx context.Context, mentionUsernameList []string, commentID, commentUserID string) {
|
||||
func (cs *CommentService) notificationMention(
|
||||
ctx context.Context, mentionUsernameList []string, commentID, commentUserID string,
|
||||
alreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) {
|
||||
for _, username := range mentionUsernameList {
|
||||
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
if exist {
|
||||
if exist && !alreadyNotifiedUserID[userInfo.ID] {
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: userInfo.ID,
|
||||
TriggerUserID: commentUserID,
|
||||
|
@ -538,6 +579,8 @@ func (cs *CommentService) notificationMention(ctx context.Context, mentionUserna
|
|||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.MentionYou
|
||||
notice_queue.AddNotification(msg)
|
||||
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
|
||||
}
|
||||
}
|
||||
return alreadyNotifiedUserIDs
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
|
|||
continue
|
||||
}
|
||||
lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language)
|
||||
item.NotificationAction = translator.GlobalTrans.Tr(lang, item.NotificationAction)
|
||||
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
|
||||
item.ID = notificationInfo.ID
|
||||
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
|
||||
if notificationInfo.IsRead == schema.NotificationRead {
|
||||
|
|
|
@ -73,7 +73,7 @@ func (ns *NotificationCommon) HandleNotification() {
|
|||
|
||||
// AddNotification
|
||||
// need set
|
||||
// UserID
|
||||
// LoginUserID
|
||||
// Type 1 inbox 2 achievement
|
||||
// [inbox] Activity
|
||||
// [achievement] Rank
|
||||
|
|
|
@ -2,13 +2,15 @@ package permission
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
)
|
||||
|
||||
// GetCommentPermission get comment permission
|
||||
func GetCommentPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) (
|
||||
actions []*schema.PermissionMemberAction) {
|
||||
func GetCommentPermission(ctx context.Context, userID string, creatorUserID string,
|
||||
createdAt time.Time, canEdit, canDelete bool) (actions []*schema.PermissionMemberAction) {
|
||||
actions = make([]*schema.PermissionMemberAction, 0)
|
||||
if len(userID) > 0 {
|
||||
actions = append(actions, &schema.PermissionMemberAction{
|
||||
|
@ -17,7 +19,8 @@ func GetCommentPermission(ctx context.Context, userID string, creatorUserID stri
|
|||
Type: "reason",
|
||||
})
|
||||
}
|
||||
if canEdit || userID == creatorUserID {
|
||||
deadline := createdAt.Add(constant.CommentEditDeadline)
|
||||
if canEdit || (userID == creatorUserID && time.Now().Before(deadline)) {
|
||||
actions = append(actions, &schema.PermissionMemberAction{
|
||||
Action: "edit",
|
||||
Name: "Edit",
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/internal/service/meta"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
||||
|
@ -30,8 +31,8 @@ type QuestionRepo interface {
|
|||
UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error)
|
||||
GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error)
|
||||
GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error)
|
||||
GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questions []*entity.Question, total int64, err error)
|
||||
SearchList(ctx context.Context, search *schema.QuestionSearch) ([]*entity.QuestionTag, int64, error)
|
||||
GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
|
||||
questionList []*entity.Question, total int64, err error)
|
||||
UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error)
|
||||
SearchByTitleLike(ctx context.Context, title string) (questionList []*entity.Question, err error)
|
||||
UpdatePvCount(ctx context.Context, questionID string) (err error)
|
||||
|
@ -126,21 +127,15 @@ func (qs *QuestionCommon) UpdataPostSetTime(ctx context.Context, questionID stri
|
|||
|
||||
func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfo, error) {
|
||||
list := make(map[string]*schema.QuestionInfo)
|
||||
listAddTag := make([]*entity.QuestionTag, 0)
|
||||
questionList, err := qs.questionRepo.FindByID(ctx, questionIDs)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
for _, item := range questionList {
|
||||
itemAddTag := &entity.QuestionTag{}
|
||||
itemAddTag.Question = *item
|
||||
listAddTag = append(listAddTag, itemAddTag)
|
||||
}
|
||||
QuestionInfo, err := qs.ListFormat(ctx, listAddTag, loginUserID)
|
||||
questions, err := qs.FormatQuestions(ctx, questionList, loginUserID)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
for _, item := range QuestionInfo {
|
||||
for _, item := range questions {
|
||||
list[item.ID] = item
|
||||
}
|
||||
return list, nil
|
||||
|
@ -193,9 +188,15 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
|
|||
showinfo.Tags = tagmap
|
||||
|
||||
userIds := make([]string, 0)
|
||||
userIds = append(userIds, dbinfo.UserID)
|
||||
userIds = append(userIds, dbinfo.LastEditUserID)
|
||||
userIds = append(userIds, showinfo.LastAnsweredUserID)
|
||||
if checker.IsNotZeroString(dbinfo.UserID) {
|
||||
userIds = append(userIds, dbinfo.UserID)
|
||||
}
|
||||
if checker.IsNotZeroString(dbinfo.LastEditUserID) {
|
||||
userIds = append(userIds, dbinfo.LastEditUserID)
|
||||
}
|
||||
if checker.IsNotZeroString(showinfo.LastAnsweredUserID) {
|
||||
userIds = append(userIds, showinfo.LastAnsweredUserID)
|
||||
}
|
||||
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
|
||||
if err != nil {
|
||||
return showinfo, err
|
||||
|
@ -244,13 +245,109 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
|
|||
return showinfo, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) ListFormat(ctx context.Context, questionList []*entity.QuestionTag, loginUserID string) ([]*schema.QuestionInfo, error) {
|
||||
func (qs *QuestionCommon) FormatQuestionsPage(
|
||||
ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) (
|
||||
formattedQuestions []*schema.QuestionPageResp, err error) {
|
||||
formattedQuestions = make([]*schema.QuestionPageResp, 0)
|
||||
questionIDs := make([]string, 0)
|
||||
userIDs := make([]string, 0)
|
||||
for _, questionInfo := range questionList {
|
||||
t := &schema.QuestionPageResp{
|
||||
ID: questionInfo.ID,
|
||||
CreatedAt: questionInfo.CreatedAt.Unix(),
|
||||
Title: questionInfo.Title,
|
||||
UrlTitle: htmltext.UrlTitle(questionInfo.Title),
|
||||
Description: htmltext.FetchExcerpt(questionInfo.ParsedText, "...", 240),
|
||||
Status: questionInfo.Status,
|
||||
ViewCount: questionInfo.ViewCount,
|
||||
UniqueViewCount: questionInfo.UniqueViewCount,
|
||||
VoteCount: questionInfo.VoteCount,
|
||||
AnswerCount: questionInfo.AnswerCount,
|
||||
CollectionCount: questionInfo.CollectionCount,
|
||||
FollowCount: questionInfo.FollowCount,
|
||||
AcceptedAnswerID: questionInfo.AcceptedAnswerID,
|
||||
LastAnswerID: questionInfo.LastAnswerID,
|
||||
}
|
||||
|
||||
questionIDs = append(questionIDs, questionInfo.ID)
|
||||
userIDs = append(userIDs, questionInfo.UserID)
|
||||
haveEdited, haveAnswered := false, false
|
||||
if checker.IsNotZeroString(questionInfo.LastEditUserID) {
|
||||
haveEdited = true
|
||||
userIDs = append(userIDs, questionInfo.LastEditUserID)
|
||||
}
|
||||
if checker.IsNotZeroString(questionInfo.LastAnswerID) {
|
||||
haveAnswered = true
|
||||
|
||||
answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, questionInfo.LastAnswerID)
|
||||
if err == nil && exist {
|
||||
if answerInfo.LastEditUserID != "0" {
|
||||
t.LastAnsweredUserID = answerInfo.LastEditUserID
|
||||
} else {
|
||||
t.LastAnsweredUserID = answerInfo.UserID
|
||||
}
|
||||
t.LastAnsweredAt = answerInfo.CreatedAt
|
||||
userIDs = append(userIDs, t.LastAnsweredUserID)
|
||||
}
|
||||
}
|
||||
|
||||
// if order condition is newest or nobody edited or nobody answered, only show question author
|
||||
if orderCond == schema.QuestionOrderCondNewest || (!haveEdited && !haveAnswered) {
|
||||
t.OperationType = schema.QuestionPageRespOperationTypeAsked
|
||||
t.OperatedAt = questionInfo.CreatedAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID}
|
||||
} else {
|
||||
// if no one
|
||||
if haveEdited {
|
||||
t.OperationType = schema.QuestionPageRespOperationTypeModified
|
||||
t.OperatedAt = questionInfo.UpdatedAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID}
|
||||
}
|
||||
|
||||
if haveAnswered {
|
||||
if t.LastAnsweredAt.Unix() > t.OperatedAt {
|
||||
t.OperationType = schema.QuestionPageRespOperationTypeAnswered
|
||||
t.OperatedAt = t.LastAnsweredAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID}
|
||||
}
|
||||
}
|
||||
}
|
||||
formattedQuestions = append(formattedQuestions, t)
|
||||
}
|
||||
|
||||
tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, questionIDs)
|
||||
if err != nil {
|
||||
return formattedQuestions, err
|
||||
}
|
||||
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
|
||||
if err != nil {
|
||||
return formattedQuestions, err
|
||||
}
|
||||
|
||||
for _, item := range formattedQuestions {
|
||||
tags, ok := tagsMap[item.ID]
|
||||
if ok {
|
||||
item.Tags = tags
|
||||
} else {
|
||||
item.Tags = make([]*schema.TagResp, 0)
|
||||
}
|
||||
userInfo := userInfoMap[item.Operator.ID]
|
||||
if userInfo != nil {
|
||||
item.Operator.DisplayName = userInfo.DisplayName
|
||||
item.Operator.Username = userInfo.Username
|
||||
item.Operator.Rank = userInfo.Rank
|
||||
}
|
||||
}
|
||||
return formattedQuestions, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) FormatQuestions(ctx context.Context, questionList []*entity.Question, loginUserID string) ([]*schema.QuestionInfo, error) {
|
||||
list := make([]*schema.QuestionInfo, 0)
|
||||
objectIds := make([]string, 0)
|
||||
userIds := make([]string, 0)
|
||||
|
||||
for _, questionInfo := range questionList {
|
||||
item := qs.ShowListFormat(ctx, questionInfo)
|
||||
item := qs.ShowFormat(ctx, questionInfo)
|
||||
list = append(list, item)
|
||||
objectIds = append(objectIds, item.ID)
|
||||
userIds = append(userIds, item.UserID)
|
||||
|
@ -387,8 +484,8 @@ func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err erro
|
|||
return as.answerRepo.RemoveAnswer(ctx, id)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.QuestionTag) *schema.QuestionInfo {
|
||||
return qs.ShowFormat(ctx, &data.Question)
|
||||
func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfo {
|
||||
return qs.ShowFormat(ctx, data)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfo {
|
||||
|
|
|
@ -140,8 +140,8 @@ func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language)
|
|||
return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
for _, t := range resp {
|
||||
t.Name = translator.GlobalTrans.Tr(lang, t.Name)
|
||||
t.Description = translator.GlobalTrans.Tr(lang, t.Description)
|
||||
t.Name = translator.Tr(lang, t.Name)
|
||||
t.Description = translator.Tr(lang, t.Description)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.Que
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
@ -176,7 +176,7 @@ func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.Que
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
@ -213,7 +213,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
@ -226,7 +226,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
@ -476,11 +476,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
|||
question.UpdatedAt = now
|
||||
question.PostUpdateTime = now
|
||||
question.UserID = dbinfo.UserID
|
||||
|
||||
question.LastEditUserID = "0"
|
||||
if dbinfo.UserID != req.UserID {
|
||||
question.LastEditUserID = req.UserID
|
||||
}
|
||||
question.LastEditUserID = req.UserID
|
||||
|
||||
oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)
|
||||
if tagerr != nil {
|
||||
|
@ -543,7 +539,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
|||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
return errorlist, err
|
||||
|
@ -655,12 +651,13 @@ func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order s
|
|||
if !Exist {
|
||||
return userlist, 0, nil
|
||||
}
|
||||
search := &schema.QuestionSearch{}
|
||||
search.Order = order
|
||||
search := &schema.QuestionPageReq{}
|
||||
search.OrderCond = order
|
||||
search.Page = page
|
||||
search.PageSize = pageSize
|
||||
search.UserID = userinfo.ID
|
||||
questionlist, count, err := qs.SearchList(ctx, search, loginUserID)
|
||||
search.UserIDBeSearched = userinfo.ID
|
||||
search.LoginUserID = loginUserID
|
||||
questionlist, count, err := qs.GetQuestionPage(ctx, search)
|
||||
if err != nil {
|
||||
return userlist, 0, err
|
||||
}
|
||||
|
@ -778,12 +775,13 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin
|
|||
if !Exist {
|
||||
return userQuestionlist, userAnswerlist, nil
|
||||
}
|
||||
search := &schema.QuestionSearch{}
|
||||
search.Order = "score"
|
||||
search := &schema.QuestionPageReq{}
|
||||
search.OrderCond = "score"
|
||||
search.Page = 0
|
||||
search.PageSize = 5
|
||||
search.UserID = userinfo.ID
|
||||
questionlist, _, err := qs.SearchList(ctx, search, loginUserID)
|
||||
search.UserIDBeSearched = userinfo.ID
|
||||
search.LoginUserID = loginUserID
|
||||
questionlist, _, err := qs.GetQuestionPage(ctx, search)
|
||||
if err != nil {
|
||||
return userQuestionlist, userAnswerlist, err
|
||||
}
|
||||
|
@ -858,57 +856,64 @@ func (qs *QuestionService) SearchByTitleLike(ctx context.Context, title string,
|
|||
}
|
||||
|
||||
// SimilarQuestion
|
||||
func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
|
||||
list := make([]*schema.QuestionInfo, 0)
|
||||
func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionPageResp, int64, error) {
|
||||
question, err := qs.questioncommon.Info(ctx, questionID, loginUserID)
|
||||
if err != nil {
|
||||
return list, 0, nil
|
||||
return nil, 0, nil
|
||||
}
|
||||
tagNames := make([]string, 0, len(question.Tags))
|
||||
for _, tag := range question.Tags {
|
||||
tagNames = append(tagNames, tag.SlugName)
|
||||
}
|
||||
search := &schema.QuestionSearch{}
|
||||
search.Order = "frequent"
|
||||
search := &schema.QuestionPageReq{}
|
||||
search.OrderCond = "frequent"
|
||||
search.Page = 0
|
||||
search.PageSize = 6
|
||||
if len(tagNames) > 0 {
|
||||
search.Tag = tagNames[0]
|
||||
}
|
||||
return qs.SearchList(ctx, search, loginUserID)
|
||||
search.LoginUserID = loginUserID
|
||||
return qs.GetQuestionPage(ctx, search)
|
||||
}
|
||||
|
||||
// SearchList
|
||||
func (qs *QuestionService) SearchList(ctx context.Context, req *schema.QuestionSearch, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
|
||||
// GetQuestionPage query questions page
|
||||
func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.QuestionPageReq) (
|
||||
questions []*schema.QuestionPageResp, total int64, err error) {
|
||||
questions = make([]*schema.QuestionPageResp, 0)
|
||||
|
||||
// query by tag condition
|
||||
if len(req.Tag) > 0 {
|
||||
tagInfo, has, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))
|
||||
tagInfo, exist, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))
|
||||
if err != nil {
|
||||
log.Error("tagCommon.GetTagListByNames error", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
if has {
|
||||
req.TagIDs = append(req.TagIDs, tagInfo.ID)
|
||||
if exist {
|
||||
req.TagID = tagInfo.ID
|
||||
}
|
||||
}
|
||||
list := make([]*schema.QuestionInfo, 0)
|
||||
if req.UserName != "" {
|
||||
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.UserName)
|
||||
|
||||
// query by user condition
|
||||
if req.Username != "" {
|
||||
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
|
||||
if err != nil {
|
||||
return list, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
if !exist {
|
||||
return list, 0, err
|
||||
return questions, 0, nil
|
||||
}
|
||||
req.UserID = userinfo.ID
|
||||
req.UserIDBeSearched = userinfo.ID
|
||||
}
|
||||
questionList, count, err := qs.questionRepo.SearchList(ctx, req)
|
||||
|
||||
questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize,
|
||||
req.UserIDBeSearched, req.TagID, req.OrderCond)
|
||||
if err != nil {
|
||||
return list, count, err
|
||||
return nil, 0, err
|
||||
}
|
||||
list, err = qs.questioncommon.ListFormat(ctx, questionList, loginUserID)
|
||||
questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond)
|
||||
if err != nil {
|
||||
return list, count, err
|
||||
return nil, 0, err
|
||||
}
|
||||
return list, count, nil
|
||||
return questions, total, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionID string, setStatusStr string) error {
|
||||
|
|
|
@ -74,8 +74,8 @@ func (rs *ReportService) GetReportTypeList(ctx context.Context, lang i18n.Langua
|
|||
err = errors.BadRequest(reason.UnknownError)
|
||||
}
|
||||
for _, t := range resp {
|
||||
t.Name = translator.GlobalTrans.Tr(lang, t.Name)
|
||||
t.Description = translator.GlobalTrans.Tr(lang, t.Description)
|
||||
t.Name = translator.Tr(lang, t.Name)
|
||||
t.Description = translator.Tr(lang, t.Description)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
|
|
@ -72,13 +72,13 @@ func (rs *RoleService) GetRoleMapping(ctx context.Context) (roleMapping map[int]
|
|||
func (rs *RoleService) translateRole(ctx context.Context, role *entity.Role) {
|
||||
switch role.Name {
|
||||
case roleUserName:
|
||||
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameUser)
|
||||
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser)
|
||||
role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameUser)
|
||||
role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser)
|
||||
case roleAdminName:
|
||||
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin)
|
||||
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin)
|
||||
role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin)
|
||||
role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin)
|
||||
case roleModeratorName:
|
||||
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator)
|
||||
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator)
|
||||
role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator)
|
||||
role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -496,7 +496,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
|
|||
thisObjTagNameList := make([]string, 0)
|
||||
thisObjTagIDList := make([]string, 0)
|
||||
for _, t := range objectTagData.Tags {
|
||||
t.SlugName = strings.ToLower(t.SlugName)
|
||||
// t.SlugName = strings.ToLower(t.SlugName)
|
||||
thisObjTagNameList = append(thisObjTagNameList, t.SlugName)
|
||||
}
|
||||
|
||||
|
@ -508,13 +508,13 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
|
|||
|
||||
tagInDbMapping := make(map[string]*entity.Tag)
|
||||
for _, tag := range tagListInDb {
|
||||
tagInDbMapping[tag.SlugName] = tag
|
||||
tagInDbMapping[strings.ToLower(tag.SlugName)] = tag
|
||||
thisObjTagIDList = append(thisObjTagIDList, tag.ID)
|
||||
}
|
||||
|
||||
addTagList := make([]*entity.Tag, 0)
|
||||
for _, tag := range objectTagData.Tags {
|
||||
_, ok := tagInDbMapping[tag.SlugName]
|
||||
_, ok := tagInDbMapping[strings.ToLower(tag.SlugName)]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/answerdev/answer/internal/service/auth"
|
||||
"github.com/answerdev/answer/internal/service/role"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
|
@ -38,6 +39,7 @@ type UserAdminService struct {
|
|||
userRoleRelService *role.UserRoleRelService
|
||||
authService *auth.AuthService
|
||||
userCommonService *usercommon.UserCommon
|
||||
userActivity activity.UserActiveActivityRepo
|
||||
}
|
||||
|
||||
// NewUserAdminService new user admin service
|
||||
|
@ -46,12 +48,14 @@ func NewUserAdminService(
|
|||
userRoleRelService *role.UserRoleRelService,
|
||||
authService *auth.AuthService,
|
||||
userCommonService *usercommon.UserCommon,
|
||||
userActivity activity.UserActiveActivityRepo,
|
||||
) *UserAdminService {
|
||||
return &UserAdminService{
|
||||
userRepo: userRepo,
|
||||
userRoleRelService: userRoleRelService,
|
||||
authService: authService,
|
||||
userCommonService: userCommonService,
|
||||
userActivity: userActivity,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +87,17 @@ func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.Up
|
|||
userInfo.Status = entity.UserStatusAvailable
|
||||
userInfo.MailStatus = entity.EmailStatusAvailable
|
||||
}
|
||||
return us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail)
|
||||
|
||||
err = us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if user reputation is zero means this user is inactive, so try to activate this user.
|
||||
if req.IsNormal() && userInfo.Rank == 0 {
|
||||
return us.userActivity.UserActive(ctx, userInfo.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserRole update user role
|
||||
|
|
|
@ -2,7 +2,6 @@ package usercommon
|
|||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Chain-Zhang/pinyin"
|
||||
|
@ -130,9 +129,7 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use
|
|||
username = strings.ToLower(username)
|
||||
suffix := ""
|
||||
|
||||
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
|
||||
match := re.MatchString(username)
|
||||
if !match {
|
||||
if checker.IsInvalidUsername(username) {
|
||||
return "", errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/internal/service/user_external_login"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/google/uuid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -246,20 +247,31 @@ func (us *UserService) UserModifyPassword(ctx context.Context, request *schema.U
|
|||
}
|
||||
|
||||
// UpdateInfo update user info
|
||||
func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (err error) {
|
||||
func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (
|
||||
errFields []*validator.FormErrorField, err error) {
|
||||
if len(req.Username) > 0 {
|
||||
userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if exist && userInfo.ID != req.UserID {
|
||||
return errors.BadRequest(reason.UsernameDuplicate)
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameDuplicate,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameDuplicate)
|
||||
}
|
||||
if checker.IsReservedUsername(req.Username) {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
avatar, err := json.Marshal(req.Avatar)
|
||||
if err != nil {
|
||||
err = errors.BadRequest(reason.UserSetAvatar).WithError(err).WithStack()
|
||||
return err
|
||||
return nil, errors.BadRequest(reason.UserSetAvatar).WithError(err).WithStack()
|
||||
}
|
||||
userInfo := entity.User{}
|
||||
userInfo.ID = req.UserID
|
||||
|
@ -270,10 +282,8 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq
|
|||
userInfo.Location = req.Location
|
||||
userInfo.Website = req.Website
|
||||
userInfo.Username = req.Username
|
||||
if err := us.userRepo.UpdateInfo(ctx, &userInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
err = us.userRepo.UpdateInfo(ctx, &userInfo)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, error) {
|
||||
|
@ -505,7 +515,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
|
|||
if exist {
|
||||
resp = append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "e_mail",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate),
|
||||
})
|
||||
return resp, errors.BadRequest(reason.EmailDuplicate)
|
||||
}
|
||||
|
|
|
@ -2,15 +2,29 @@ package checker
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/answerdev/answer/configs"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
)
|
||||
|
||||
var (
|
||||
reservedUsernameMapping = make(map[string]bool)
|
||||
reservedUsernameInit sync.Once
|
||||
)
|
||||
|
||||
func init() {
|
||||
func initReservedUsername() {
|
||||
reservedUsernamesJsonFilePath := filepath.Join(cli.ConfigFileDir, cli.DefaultReservedUsernamesConfigFileName)
|
||||
if dir.CheckFileExist(reservedUsernamesJsonFilePath) {
|
||||
// if reserved username file exists, read it and replace configuration
|
||||
reservedUsernamesJsonFile, err := os.ReadFile(reservedUsernamesJsonFilePath)
|
||||
if err == nil {
|
||||
configs.ReservedUsernames = reservedUsernamesJsonFile
|
||||
}
|
||||
}
|
||||
var usernames []string
|
||||
_ = json.Unmarshal(configs.ReservedUsernames, &usernames)
|
||||
for _, username := range usernames {
|
||||
|
@ -20,5 +34,6 @@ func init() {
|
|||
|
||||
// IsReservedUsername checks whether the username is reserved
|
||||
func IsReservedUsername(username string) bool {
|
||||
reservedUsernameInit.Do(initReservedUsername)
|
||||
return reservedUsernameMapping[username]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package checker
|
||||
|
||||
import "regexp"
|
||||
|
||||
var (
|
||||
usernameReg = regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
|
||||
)
|
||||
|
||||
func IsInvalidUsername(username string) bool {
|
||||
return !usernameReg.MatchString(username)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package checker
|
||||
|
||||
// IsNotZeroString check s is not empty string and is not "0"
|
||||
func IsNotZeroString(s string) bool {
|
||||
return len(s) > 0 && s != "0"
|
||||
}
|
|
@ -3,22 +3,28 @@ package converter
|
|||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
goldmarkHTML "github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// Markdown2HTML convert markdown to html
|
||||
func Markdown2HTML(source string) string {
|
||||
mdConverter := goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM),
|
||||
goldmark.WithExtensions(&DangerousHTMLFilterExtension{}, extension.GFM),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithHardWraps(),
|
||||
goldmarkHTML.WithHardWraps(),
|
||||
),
|
||||
)
|
||||
var buf bytes.Buffer
|
||||
|
@ -28,3 +34,84 @@ func Markdown2HTML(source string) string {
|
|||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type DangerousHTMLFilterExtension struct {
|
||||
}
|
||||
|
||||
func (e *DangerousHTMLFilterExtension) Extend(m goldmark.Markdown) {
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(&DangerousHTMLRenderer{
|
||||
Config: goldmarkHTML.NewConfig(),
|
||||
Filter: bluemonday.UGCPolicy(),
|
||||
}, 1),
|
||||
))
|
||||
}
|
||||
|
||||
type DangerousHTMLRenderer struct {
|
||||
goldmarkHTML.Config
|
||||
Filter *bluemonday.Policy
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
|
||||
reg.Register(ast.KindRawHTML, r.renderRawHTML)
|
||||
reg.Register(ast.KindLink, r.renderLink)
|
||||
}
|
||||
|
||||
func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
n := node.(*ast.RawHTML)
|
||||
l := n.Segments.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
segment := n.Segments.At(i)
|
||||
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.HTMLBlock)
|
||||
if entering {
|
||||
l := n.Lines().Len()
|
||||
for i := 0; i < l; i++ {
|
||||
line := n.Lines().At(i)
|
||||
r.Writer.SecureWrite(w, r.Filter.SanitizeBytes(line.Value(source)))
|
||||
}
|
||||
} else {
|
||||
if n.HasClosure() {
|
||||
closure := n.ClosureLine
|
||||
r.Writer.SecureWrite(w, closure.Value(source))
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Link)
|
||||
if entering && r.renderLinkIsUrl(string(n.Destination)) {
|
||||
_, _ = w.WriteString("<a href=\"")
|
||||
if r.Unsafe || !html.IsDangerousURL(n.Destination) {
|
||||
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
|
||||
}
|
||||
_ = w.WriteByte('"')
|
||||
if n.Title != nil {
|
||||
_, _ = w.WriteString(` title="`)
|
||||
r.Writer.Write(w, n.Title)
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, html.LinkAttributeFilter)
|
||||
}
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("</a>")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyUrl string) bool {
|
||||
return govalidator.IsURL(verifyUrl)
|
||||
}
|
||||
|
|
|
@ -75,11 +75,10 @@ func FetchExcerpt(html, trimMarker string, limit int) (text string) {
|
|||
runeText := []rune(text)
|
||||
if len(runeText) <= limit {
|
||||
text = string(runeText)
|
||||
} else {
|
||||
text = string(runeText[0:limit])
|
||||
return
|
||||
}
|
||||
|
||||
text += trimMarker
|
||||
text = string(runeText[0:limit]) + trimMarker
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,10 @@ func TestFetchExcerpt(t *testing.T) {
|
|||
expected = "hello你好😂..."
|
||||
text = FetchExcerpt("<p>hello你好😂world</p>", "...", 8)
|
||||
assert.Equal(t, expected, text)
|
||||
|
||||
expected = "hello你好"
|
||||
text = FetchExcerpt("<p>hello你好</p>", "...", 8)
|
||||
assert.Equal(t, expected, text)
|
||||
}
|
||||
|
||||
func TestUrlTitle(t *testing.T) {
|
||||
|
|
|
@ -9,7 +9,12 @@
|
|||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
|
||||
/build/*/*/*
|
||||
/build/*.json
|
||||
/build/*.ico
|
||||
/build/*.txt
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
|
|
@ -71,7 +71,7 @@ If you would like to help us with the i18n translation, please visit [Answer@Cro
|
|||
├── common (project information/data defined here)
|
||||
├── components (all components of the project)
|
||||
├── hooks (all hooks of the project)
|
||||
├── i18n (Initialize the front-end i18n)
|
||||
├── i18n (Used only to initialize the front-end i18n tool)
|
||||
├── pages (all pages of the project)
|
||||
├── router (Project routing definition)
|
||||
├── services (all data api of the project)
|
||||
|
|
|
@ -1 +1 @@
|
|||
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.2dcebe98.js"></script><link href="/static/css/main.401dc3ca.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask"><noscript><style>#spin-mask{display:none!important}</style></noscript><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>
|
||||
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.cb9bf782.js"></script><link href="/static/css/main.b8d8739f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask"><noscript><style>#spin-mask{display:none!important}</style></noscript><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
# Build Static
|
1131
ui/pnpm-lock.yaml
1131
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,5 @@
|
|||
$link-hover-decoration: none;
|
||||
$enable-negative-margins: true;
|
||||
$blue: #0033FF !default;
|
||||
$placeholder-opacity-max: .2;
|
||||
$placeholder-opacity-min: .1;
|
||||
|
|
|
@ -102,6 +102,7 @@ export interface ModifyUserReq {
|
|||
}
|
||||
|
||||
export interface UserInfoBase {
|
||||
id?: string;
|
||||
avatar: any;
|
||||
username: string;
|
||||
display_name: string;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { marked } from 'marked';
|
|||
import * as Types from '@/common/interface';
|
||||
import { Modal } from '@/components';
|
||||
import { usePageUsers, useReportModal } from '@/hooks';
|
||||
import { matchedUsers, parseUserInfo, scrollTop } from '@/utils';
|
||||
import { matchedUsers, parseUserInfo, scrollTop, bgFadeOut } from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import {
|
||||
useQueryComments,
|
||||
|
@ -27,7 +27,6 @@ import './index.scss';
|
|||
const Comment = ({ objectId, mode, commentId }) => {
|
||||
const pageUsers = usePageUsers();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [comments, setComments] = useState<any>([]);
|
||||
const [visibleComment, setVisibleComment] = useState(false);
|
||||
const pageSize = pageIndex === 0 ? 3 : 15;
|
||||
const { data, mutate } = useQueryComments({
|
||||
|
@ -36,6 +35,7 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
page: pageIndex,
|
||||
page_size: pageSize,
|
||||
});
|
||||
const [comments, setComments] = useState<any>([]);
|
||||
|
||||
const reportModal = useReportModal();
|
||||
|
||||
|
@ -44,6 +44,7 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
if (pageIndex === 0 && co.comment_id === commentId) {
|
||||
setTimeout(() => {
|
||||
scrollTop(el);
|
||||
bgFadeOut(el);
|
||||
}, 100);
|
||||
}
|
||||
}, []);
|
||||
|
@ -74,6 +75,9 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
}, [data]);
|
||||
|
||||
const handleReply = (id) => {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
setComments(
|
||||
comments.map((item) => {
|
||||
if (item.comment_id === id) {
|
||||
|
@ -99,6 +103,9 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
const users = matchedUsers(item.value);
|
||||
const userNames = unionBy(users.map((user) => user.userName));
|
||||
const html = marked.parse(parseUserInfo(item.value));
|
||||
if (!item.value || !html) {
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
object_id: objectId,
|
||||
original_text: item.value,
|
||||
|
@ -163,9 +170,8 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
deleteComment(id).then(() => {
|
||||
if (pageIndex === 0) {
|
||||
mutate();
|
||||
} else {
|
||||
setComments(comments.filter((item) => item.comment_id !== id));
|
||||
}
|
||||
setComments(comments.filter((item) => item.comment_id !== id));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -258,7 +264,7 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
)}
|
||||
|
||||
<div
|
||||
className="fmt fs-14"
|
||||
className="fmt fs-14 text-break text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: item.parsed_text }}
|
||||
/>
|
||||
</div>
|
||||
|
@ -302,7 +308,9 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
variant="link"
|
||||
className="p-0 fs-14 btn-no-border"
|
||||
onClick={() => {
|
||||
setVisibleComment(!visibleComment);
|
||||
if (tryNormalLogged(true)) {
|
||||
setVisibleComment(!visibleComment);
|
||||
}
|
||||
}}>
|
||||
{t('btn_add_comment')}
|
||||
</Button>
|
||||
|
|
|
@ -93,16 +93,18 @@ const Index: FC<Props> = ({
|
|||
className={classnames(
|
||||
'fs-14 font-monospace',
|
||||
newData.original_text && 'mb-4',
|
||||
)}>
|
||||
{`/tags/${
|
||||
newData?.main_tag_slug_name
|
||||
? diffText(
|
||||
newData.main_tag_slug_name,
|
||||
oldData?.main_tag_slug_name,
|
||||
)
|
||||
: diffText(newData.slug_name, oldData?.slug_name)
|
||||
}`}
|
||||
</div>
|
||||
)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `/tags/${
|
||||
newData?.main_tag_slug_name
|
||||
? diffText(
|
||||
newData.main_tag_slug_name,
|
||||
oldData?.main_tag_slug_name,
|
||||
)
|
||||
: diffText(newData.slug_name, oldData?.slug_name)
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
|
|
|
@ -251,7 +251,7 @@ const Image: FC<IEditorContext> = ({ editor }) => {
|
|||
|
||||
<Form.Group controlId="editor.imgDescription" className="mb-3">
|
||||
<Form.Label>
|
||||
{t('image.form_image.fields.description.label')}
|
||||
{t('image.form_image.fields.desc.label')}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
|
|
|
@ -7,28 +7,31 @@ import {
|
|||
useImperativeHandle,
|
||||
} from 'react';
|
||||
|
||||
import { marked } from 'marked';
|
||||
import { markdownToHtml } from '@/services';
|
||||
|
||||
import { htmlRender } from './utils';
|
||||
|
||||
let scrollTop = 0;
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
});
|
||||
let renderTimer;
|
||||
|
||||
const Index = ({ value }, ref) => {
|
||||
const [html, setHtml] = useState('');
|
||||
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const renderMarkdown = (markdown) => {
|
||||
clearTimeout(renderTimer);
|
||||
const timeout = renderTimer ? 1000 : 0;
|
||||
renderTimer = setTimeout(() => {
|
||||
markdownToHtml(markdown).then((resp) => {
|
||||
scrollTop = previewRef.current?.scrollTop || 0;
|
||||
setHtml(resp);
|
||||
});
|
||||
}, timeout);
|
||||
};
|
||||
useEffect(() => {
|
||||
const previewHtml = marked(value).replace(
|
||||
/<img/gi,
|
||||
'<img referrerpolicy="no-referrer"',
|
||||
);
|
||||
scrollTop = previewRef.current?.scrollTop || 0;
|
||||
setHtml(previewHtml);
|
||||
renderMarkdown(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!html) {
|
||||
return;
|
||||
|
|
|
@ -56,6 +56,7 @@ const ToolItem: FC<IProps> = (props) => {
|
|||
disable ? 'disabled' : ''
|
||||
} `}
|
||||
disabled={disable}
|
||||
tabIndex={-1}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (typeof onClick === 'function') {
|
||||
|
|
|
@ -113,4 +113,20 @@ export function htmlRender(el: HTMLElement | null) {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
el.querySelectorAll('table').forEach((table) => {
|
||||
if (
|
||||
(table.parentNode as HTMLDivElement)?.classList.contains(
|
||||
'table-responsive',
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
table.classList.add('table', 'table-bordered');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'table-responsive';
|
||||
table.parentNode?.replaceChild(div, table);
|
||||
div.appendChild(table);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,12 +5,10 @@ import { Trans } from 'react-i18next';
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||
|
||||
const Index = () => {
|
||||
const fullYear = dayjs().format('YYYY');
|
||||
const siteName =
|
||||
siteInfoStore((state) => state.siteInfo.name) || DEFAULT_SITE_NAME;
|
||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
const cc = `${fullYear} ${siteName}`;
|
||||
return (
|
||||
<footer className="bg-light py-3">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { Nav, Dropdown } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { Avatar, Icon } from '@/components';
|
||||
|
@ -14,6 +14,13 @@ interface Props {
|
|||
|
||||
const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const handleLinkClick = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { href } = evt.currentTarget;
|
||||
const { pathname } = new URL(href);
|
||||
navigate(pathname);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Nav className="flex-row">
|
||||
|
@ -26,7 +33,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
</Nav.Link>
|
||||
|
||||
<Nav.Link
|
||||
as={Link}
|
||||
as={NavLink}
|
||||
to="/users/notifications/achievement"
|
||||
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative">
|
||||
<Icon name="trophy-fill" className="fs-4" />
|
||||
|
@ -46,17 +53,26 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item href={`/users/${userInfo.username}`}>
|
||||
<Dropdown.Item
|
||||
href={`/users/${userInfo.username}`}
|
||||
onClick={handleLinkClick}>
|
||||
{t('header.nav.profile')}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href="/users/settings/profile">
|
||||
<Dropdown.Item
|
||||
href="/users/settings/profile"
|
||||
onClick={handleLinkClick}>
|
||||
{t('header.nav.setting')}
|
||||
</Dropdown.Item>
|
||||
{userInfo?.is_admin ? (
|
||||
<Dropdown.Item href="/admin">{t('header.nav.admin')}</Dropdown.Item>
|
||||
<Dropdown.Item href="/admin" onClick={handleLinkClick}>
|
||||
{t('header.nav.admin')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
{redDot?.can_revision ? (
|
||||
<Dropdown.Item href="/review" className="position-relative">
|
||||
<Dropdown.Item
|
||||
href="/review"
|
||||
className="position-relative"
|
||||
onClick={handleLinkClick}>
|
||||
{t('header.nav.review')}
|
||||
{redDot?.revision > 0 && (
|
||||
<span className="position-absolute top-50 translate-middle-y end-0 me-3 p-2 bg-danger border border-light rounded-circle">
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
siteInfoStore,
|
||||
|
@ -27,8 +28,6 @@ import {
|
|||
themeSettingStore,
|
||||
} from '@/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@/services';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||
|
||||
import NavItems from './components/NavItems';
|
||||
|
||||
|
@ -36,7 +35,7 @@ import './index.scss';
|
|||
|
||||
const Header: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, clear } = loggedUserInfoStore();
|
||||
const { user, clear: clearUserStore } = loggedUserInfoStore();
|
||||
const { t } = useTranslation();
|
||||
const [urlSearch] = useSearchParams();
|
||||
const q = urlSearch.get('q');
|
||||
|
@ -49,11 +48,25 @@ const Header: FC = () => {
|
|||
const handleInput = (val) => {
|
||||
setSearch(val);
|
||||
};
|
||||
const handleSearch = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (!searchStr) {
|
||||
return;
|
||||
}
|
||||
const searchUrl = `/search?q=${encodeURIComponent(searchStr)}`;
|
||||
navigate(searchUrl);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
clear();
|
||||
navigate(RouteAlias.home);
|
||||
clearUserStore();
|
||||
window.location.replace(window.location.href);
|
||||
};
|
||||
const onLoginClick = (evt) => {
|
||||
evt.preventDefault();
|
||||
floppyNavigation.navigateToLogin((loginPath) => {
|
||||
navigate(loginPath, { replace: true });
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -108,7 +121,7 @@ const Header: FC = () => {
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<span>{siteInfo.name || DEFAULT_SITE_NAME}</span>
|
||||
<span>{siteInfo.name}</span>
|
||||
)}
|
||||
</Navbar.Brand>
|
||||
|
||||
|
@ -118,7 +131,14 @@ const Header: FC = () => {
|
|||
<NavItems redDot={redDot} userInfo={user} logOut={handleLogout} />
|
||||
) : (
|
||||
<>
|
||||
<Button variant="link" className="me-2" href="/users/login">
|
||||
<Button
|
||||
variant="link"
|
||||
className={classnames('me-2', {
|
||||
'link-light': navbarStyle === 'theme-colored',
|
||||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href="/users/login">
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
{loginSetting.allow_new_registrations && (
|
||||
|
@ -153,7 +173,10 @@ const Header: FC = () => {
|
|||
<hr className="hr lg-none mt-2" />
|
||||
|
||||
<Col lg={4} className="d-flex justify-content-center">
|
||||
<Form action="/search" className="w-75 px-0 px-lg-2">
|
||||
<Form
|
||||
action="/search"
|
||||
className="w-75 px-0 px-lg-2"
|
||||
onSubmit={handleSearch}>
|
||||
<FormControl
|
||||
placeholder={t('header.search.placeholder')}
|
||||
className="placeholder-search"
|
||||
|
@ -202,6 +225,7 @@ const Header: FC = () => {
|
|||
'link-light': navbarStyle === 'theme-colored',
|
||||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href="/users/login">
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { loginToContinueStore, siteInfoStore } from '@/stores';
|
||||
|
||||
interface IProps {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const Index: React.FC<IProps> = ({ visible = false }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
const { update: updateStore } = loginToContinueStore();
|
||||
const { siteInfo } = siteInfoStore((_) => _);
|
||||
const closeModal = () => {
|
||||
updateStore({ show: false });
|
||||
};
|
||||
const linkClick = (evt) => {
|
||||
evt.stopPropagation();
|
||||
closeModal();
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
title="LoginToContinue"
|
||||
show={visible}
|
||||
onHide={closeModal}
|
||||
centered
|
||||
fullscreen="sm-down">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title as="h5">{t('login_to_continue')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="p-5">
|
||||
<div className="d-flex flex-column align-items-center text-center text-body">
|
||||
<h3>{t('page_title', { site_name: siteInfo.name })}</h3>
|
||||
<p>{siteInfo.description}</p>
|
||||
</div>
|
||||
<div className="d-grid gap-2">
|
||||
<Link
|
||||
to="/users/login"
|
||||
className="btn btn-primary"
|
||||
onClick={linkClick}>
|
||||
{t('login', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
<Link
|
||||
to="/users/register"
|
||||
className="btn btn-link"
|
||||
onClick={linkClick}>
|
||||
{t('signup', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default Index;
|
|
@ -1,6 +1,7 @@
|
|||
import DefaultModal from './Modal';
|
||||
import confirm, { Config } from './Confirm';
|
||||
import PicAuthCodeModal from './PicAuthCodeModal';
|
||||
import LoginToContinueModal from './LoginToContinueModal';
|
||||
|
||||
type ModalType = typeof DefaultModal & {
|
||||
confirm: (config: Config) => void;
|
||||
|
@ -14,3 +15,4 @@ Modal.confirm = function (props: Config) {
|
|||
export default Modal;
|
||||
|
||||
export { PicAuthCodeModal };
|
||||
export { LoginToContinueModal };
|
||||
|
|
|
@ -41,10 +41,10 @@ const Index: FC<IProps> = ({
|
|||
const navigate = useNavigate();
|
||||
const reportModal = useReportModal();
|
||||
|
||||
const refershQuestion = () => {
|
||||
const refreshQuestion = () => {
|
||||
callback?.('default');
|
||||
};
|
||||
const closeModal = useReportModal(refershQuestion);
|
||||
const closeModal = useReportModal(refreshQuestion);
|
||||
const editUrl =
|
||||
type === 'answer' ? `/posts/${qid}/${aid}/edit` : `/posts/${qid}/edit`;
|
||||
|
||||
|
@ -97,7 +97,7 @@ const Index: FC<IProps> = ({
|
|||
deleteAnswer({
|
||||
id: aid,
|
||||
}).then(() => {
|
||||
// refersh page
|
||||
// refresh page
|
||||
toast.onShow({
|
||||
msg: t('tip_answer_deleted'),
|
||||
variant: 'success',
|
||||
|
@ -132,7 +132,7 @@ const Index: FC<IProps> = ({
|
|||
msg: t('success', { keyPrefix: 'question_detail.reopen' }),
|
||||
variant: 'success',
|
||||
});
|
||||
refershQuestion();
|
||||
refreshQuestion();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
Empty,
|
||||
BaseUserCard,
|
||||
QueryGroup,
|
||||
QuestionListLoader,
|
||||
} from '@/components';
|
||||
import { useQuestionList } from '@/services';
|
||||
|
||||
|
@ -28,60 +29,6 @@ interface Props {
|
|||
source: 'questions' | 'tag';
|
||||
}
|
||||
|
||||
const QuestionLastUpdate = ({ q }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
if (q.update_time > q.edit_time) {
|
||||
// question answered
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<BaseUserCard
|
||||
data={q.last_answered_user_info}
|
||||
showAvatar={false}
|
||||
className="me-1"
|
||||
/>
|
||||
•
|
||||
<FormatTime
|
||||
time={q.update_time}
|
||||
className="text-secondary ms-1"
|
||||
preFix={t('answered')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (q.edit_time > q.update_time) {
|
||||
// question modified
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<BaseUserCard
|
||||
data={q.update_user_info}
|
||||
showAvatar={false}
|
||||
className="me-1"
|
||||
/>
|
||||
•
|
||||
<FormatTime
|
||||
time={q.edit_time}
|
||||
className="text-secondary ms-1"
|
||||
preFix={t('modified')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// default: asked
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<BaseUserCard data={q.user_info} showAvatar={false} className="me-1" />
|
||||
•
|
||||
<FormatTime
|
||||
time={q.create_time}
|
||||
preFix={t('asked')}
|
||||
className="text-secondary ms-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const QuestionList: FC<Props> = ({ source }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
const { tagName = '' } = useParams();
|
||||
|
@ -118,57 +65,73 @@ const QuestionList: FC<Props> = ({ source }) => {
|
|||
/>
|
||||
</div>
|
||||
<ListGroup className="rounded-0">
|
||||
{listData?.list?.map((li) => {
|
||||
return (
|
||||
<ListGroup.Item
|
||||
key={li.id}
|
||||
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
|
||||
<h5 className="text-wrap text-break">
|
||||
<NavLink
|
||||
to={pathFactory.questionLanding(li.id, li.url_title)}
|
||||
className="link-dark">
|
||||
{li.title}
|
||||
{li.status === 2 ? ` [${t('closed')}]` : ''}
|
||||
</NavLink>
|
||||
</h5>
|
||||
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 mb-2 text-secondary">
|
||||
<QuestionLastUpdate q={li} />
|
||||
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
|
||||
<span>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
<em className="fst-normal ms-1">{li.vote_count}</em>
|
||||
</span>
|
||||
<span
|
||||
className={`ms-3 ${
|
||||
li.accepted_answer_id >= 1 ? 'text-success' : ''
|
||||
}`}>
|
||||
<Icon
|
||||
name={
|
||||
li.accepted_answer_id >= 1
|
||||
? 'check-circle-fill'
|
||||
: 'chat-square-text-fill'
|
||||
}
|
||||
{isLoading ? (
|
||||
<QuestionListLoader />
|
||||
) : (
|
||||
listData?.list?.map((li) => {
|
||||
return (
|
||||
<ListGroup.Item
|
||||
key={li.id}
|
||||
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
|
||||
<h5 className="text-wrap text-break">
|
||||
<NavLink
|
||||
to={pathFactory.questionLanding(li.id, li.url_title)}
|
||||
className="link-dark">
|
||||
{li.title}
|
||||
{li.status === 2 ? ` [${t('closed')}]` : ''}
|
||||
</NavLink>
|
||||
</h5>
|
||||
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 mb-2 text-secondary">
|
||||
<div className="d-flex">
|
||||
<BaseUserCard
|
||||
data={li.operator}
|
||||
showAvatar={false}
|
||||
className="me-1"
|
||||
/>
|
||||
<em className="fst-normal ms-1">{li.answer_count}</em>
|
||||
</span>
|
||||
<span className="summary-stat ms-3">
|
||||
<Icon name="eye-fill" />
|
||||
<em className="fst-normal ms-1">{li.view_count}</em>
|
||||
</span>
|
||||
•
|
||||
<FormatTime
|
||||
time={li.operated_at}
|
||||
className="text-secondary ms-1"
|
||||
preFix={t(li.operation_type)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
|
||||
<span>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
<em className="fst-normal ms-1">{li.vote_count}</em>
|
||||
</span>
|
||||
<span
|
||||
className={`ms-3 ${
|
||||
li.accepted_answer_id >= 1 ? 'text-success' : ''
|
||||
}`}>
|
||||
<Icon
|
||||
name={
|
||||
li.accepted_answer_id >= 1
|
||||
? 'check-circle-fill'
|
||||
: 'chat-square-text-fill'
|
||||
}
|
||||
/>
|
||||
<em className="fst-normal ms-1">{li.answer_count}</em>
|
||||
</span>
|
||||
<span className="summary-stat ms-3">
|
||||
<Icon name="eye-fill" />
|
||||
<em className="fst-normal ms-1">{li.view_count}</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="question-tags m-n1">
|
||||
{Array.isArray(li.tags)
|
||||
? li.tags.map((tag) => {
|
||||
return (
|
||||
<Tag key={tag.slug_name} className="m-1" data={tag} />
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
);
|
||||
})}
|
||||
<div className="question-tags m-n1">
|
||||
{Array.isArray(li.tags)
|
||||
? li.tags.map((tag) => {
|
||||
return (
|
||||
<Tag key={tag.slug_name} className="m-1" data={tag} />
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ListGroup>
|
||||
{count <= 0 && !isLoading && <Empty />}
|
||||
<div className="mt-4 mb-2 d-flex justify-content-center">
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { ListGroupItem } from 'react-bootstrap';
|
||||
|
||||
interface Props {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ count = 10 }) => {
|
||||
const list = new Array(count).fill(0).map((v, i) => v + i);
|
||||
return (
|
||||
<>
|
||||
{list.map((v) => (
|
||||
<ListGroupItem
|
||||
className="bg-transparent py-3 px-0 border-start-0 border-end-0 placeholder-glow"
|
||||
key={v}>
|
||||
<div
|
||||
className="placeholder w-100 h5 align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="placeholder w-75 d-block align-top mb-2"
|
||||
style={{ height: '21px' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="placeholder w-50 align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
</ListGroupItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -1,4 +1,5 @@
|
|||
import React, { memo, FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -21,8 +22,8 @@ const Index: FC<IProps> = ({
|
|||
href ||= pathFactory.tagLanding(data?.slug_name);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
<Link
|
||||
to={href}
|
||||
className={classNames(
|
||||
'badge-tag rounded-1',
|
||||
data.reserved && 'badge-tag-reserved',
|
||||
|
@ -30,7 +31,7 @@ const Index: FC<IProps> = ({
|
|||
className,
|
||||
)}>
|
||||
<span className={textClassName}>{data.slug_name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
.tag-selector-wrap {
|
||||
.btn {
|
||||
&.warning {
|
||||
-webkit-animation: tag-input-warning 2s;
|
||||
animation: tag-input-warning 2s;
|
||||
}
|
||||
}
|
||||
.dropdown-menu {
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
@ -18,23 +12,5 @@
|
|||
color: #212529;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
@-webkit-keyframes tag-input-warning {
|
||||
0% {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tag-input-warning {
|
||||
0% {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue