mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/ui-1.1.0' into feature-plugin
# Conflicts: # internal/base/reason/reason.go # internal/schema/user_schema.go # internal/service/user_service.go
This commit is contained in:
commit
9bd9b475f4
2
Makefile
2
Makefile
|
@ -1,6 +1,6 @@
|
|||
.PHONY: build clean ui
|
||||
|
||||
VERSION=1.0.6
|
||||
VERSION=1.0.7
|
||||
BIN=answer
|
||||
DIR_SRC=./cmd/answer
|
||||
DOCKER_CMD=docker
|
||||
|
|
|
@ -134,7 +134,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
|
||||
questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo)
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo)
|
||||
tagRelRepo := tag.NewTagRelRepo(dataData)
|
||||
tagRelRepo := tag.NewTagRelRepo(dataData, uniqueIDRepo)
|
||||
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
|
||||
revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo)
|
||||
revisionService := revision_common.NewRevisionService(revisionRepo, userRepo)
|
||||
|
@ -172,7 +172,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
|
||||
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData)
|
||||
questionController := controller.NewQuestionController(questionService, rankService)
|
||||
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService)
|
||||
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService)
|
||||
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
|
||||
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
|
||||
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
|
||||
|
|
117
docs/docs.go
117
docs/docs.go
|
@ -4276,6 +4276,38 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "add tag",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Tag"
|
||||
],
|
||||
"summary": "add tag",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "tag",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.AddTagReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "delete tag",
|
||||
"consumes": [
|
||||
|
@ -5889,6 +5921,31 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.AddTagReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"display_name",
|
||||
"original_text",
|
||||
"slug_name"
|
||||
],
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"description": "display_name",
|
||||
"type": "string",
|
||||
"maxLength": 35
|
||||
},
|
||||
"original_text": {
|
||||
"description": "original text",
|
||||
"type": "string",
|
||||
"maxLength": 65536
|
||||
},
|
||||
"slug_name": {
|
||||
"description": "slug_name",
|
||||
"type": "string",
|
||||
"maxLength": 35
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.AddUserReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -6223,6 +6280,10 @@ const docTemplate = `{
|
|||
"title": {
|
||||
"description": "title",
|
||||
"type": "string"
|
||||
},
|
||||
"url_title": {
|
||||
"description": "url title",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -6382,14 +6443,6 @@ const docTemplate = `{
|
|||
"description": "user id",
|
||||
"type": "string"
|
||||
},
|
||||
"ip_info": {
|
||||
"description": "ip info",
|
||||
"type": "string"
|
||||
},
|
||||
"is_admin": {
|
||||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"last_login_date": {
|
||||
"description": "last login date",
|
||||
"type": "integer"
|
||||
|
@ -6525,6 +6578,10 @@ const docTemplate = `{
|
|||
"title": {
|
||||
"description": "title",
|
||||
"type": "string"
|
||||
},
|
||||
"url_title": {
|
||||
"description": "url title",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -6662,6 +6719,10 @@ const docTemplate = `{
|
|||
"description": "created time",
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"description": "description",
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"description": "display_name",
|
||||
"type": "string"
|
||||
|
@ -6917,10 +6978,6 @@ const docTemplate = `{
|
|||
"description": "ip info",
|
||||
"type": "string"
|
||||
},
|
||||
"is_admin": {
|
||||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string"
|
||||
|
@ -6953,6 +7010,10 @@ const docTemplate = `{
|
|||
"description": "rank",
|
||||
"type": "integer"
|
||||
},
|
||||
"role_id": {
|
||||
"description": "role id",
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"description": "user status",
|
||||
"type": "string"
|
||||
|
@ -7017,10 +7078,6 @@ const docTemplate = `{
|
|||
"description": "ip info",
|
||||
"type": "string"
|
||||
},
|
||||
"is_admin": {
|
||||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string"
|
||||
|
@ -7053,6 +7110,10 @@ const docTemplate = `{
|
|||
"description": "rank",
|
||||
"type": "integer"
|
||||
},
|
||||
"role_id": {
|
||||
"description": "role id",
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"description": "user status",
|
||||
"type": "string"
|
||||
|
@ -7104,6 +7165,10 @@ const docTemplate = `{
|
|||
"description": "title",
|
||||
"type": "string"
|
||||
},
|
||||
"url_title": {
|
||||
"description": "url title",
|
||||
"type": "string"
|
||||
},
|
||||
"vote_type": {
|
||||
"description": "vote type",
|
||||
"type": "string"
|
||||
|
@ -7671,10 +7736,18 @@ const docTemplate = `{
|
|||
"schema.SiteInterfaceReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"default_avatar",
|
||||
"language",
|
||||
"time_zone"
|
||||
],
|
||||
"properties": {
|
||||
"default_avatar": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"system",
|
||||
"gravatar"
|
||||
]
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
|
@ -7688,10 +7761,18 @@ const docTemplate = `{
|
|||
"schema.SiteInterfaceResp": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"default_avatar",
|
||||
"language",
|
||||
"time_zone"
|
||||
],
|
||||
"properties": {
|
||||
"default_avatar": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"system",
|
||||
"gravatar"
|
||||
]
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
|
@ -7767,7 +7848,7 @@ const docTemplate = `{
|
|||
"properties": {
|
||||
"permalink": {
|
||||
"type": "integer",
|
||||
"maximum": 3,
|
||||
"maximum": 4,
|
||||
"minimum": 0
|
||||
},
|
||||
"robots": {
|
||||
|
@ -7784,7 +7865,7 @@ const docTemplate = `{
|
|||
"properties": {
|
||||
"permalink": {
|
||||
"type": "integer",
|
||||
"maximum": 3,
|
||||
"maximum": 4,
|
||||
"minimum": 0
|
||||
},
|
||||
"robots": {
|
||||
|
|
|
@ -4264,6 +4264,38 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "add tag",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Tag"
|
||||
],
|
||||
"summary": "add tag",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "tag",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.AddTagReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "delete tag",
|
||||
"consumes": [
|
||||
|
@ -5877,6 +5909,31 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.AddTagReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"display_name",
|
||||
"original_text",
|
||||
"slug_name"
|
||||
],
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"description": "display_name",
|
||||
"type": "string",
|
||||
"maxLength": 35
|
||||
},
|
||||
"original_text": {
|
||||
"description": "original text",
|
||||
"type": "string",
|
||||
"maxLength": 65536
|
||||
},
|
||||
"slug_name": {
|
||||
"description": "slug_name",
|
||||
"type": "string",
|
||||
"maxLength": 35
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.AddUserReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -6211,6 +6268,10 @@
|
|||
"title": {
|
||||
"description": "title",
|
||||
"type": "string"
|
||||
},
|
||||
"url_title": {
|
||||
"description": "url title",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -6370,14 +6431,6 @@
|
|||
"description": "user id",
|
||||
"type": "string"
|
||||
},
|
||||
"ip_info": {
|
||||
"description": "ip info",
|
||||
"type": "string"
|
||||
},
|
||||
"is_admin": {
|
||||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"last_login_date": {
|
||||
"description": "last login date",
|
||||
"type": "integer"
|
||||
|
@ -6513,6 +6566,10 @@
|
|||
"title": {
|
||||
"description": "title",
|
||||
"type": "string"
|
||||
},
|
||||
"url_title": {
|
||||
"description": "url title",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -6650,6 +6707,10 @@
|
|||
"description": "created time",
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"description": "description",
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"description": "display_name",
|
||||
"type": "string"
|
||||
|
@ -6905,10 +6966,6 @@
|
|||
"description": "ip info",
|
||||
"type": "string"
|
||||
},
|
||||
"is_admin": {
|
||||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string"
|
||||
|
@ -6941,6 +6998,10 @@
|
|||
"description": "rank",
|
||||
"type": "integer"
|
||||
},
|
||||
"role_id": {
|
||||
"description": "role id",
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"description": "user status",
|
||||
"type": "string"
|
||||
|
@ -7005,10 +7066,6 @@
|
|||
"description": "ip info",
|
||||
"type": "string"
|
||||
},
|
||||
"is_admin": {
|
||||
"description": "is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"language": {
|
||||
"description": "language",
|
||||
"type": "string"
|
||||
|
@ -7041,6 +7098,10 @@
|
|||
"description": "rank",
|
||||
"type": "integer"
|
||||
},
|
||||
"role_id": {
|
||||
"description": "role id",
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"description": "user status",
|
||||
"type": "string"
|
||||
|
@ -7092,6 +7153,10 @@
|
|||
"description": "title",
|
||||
"type": "string"
|
||||
},
|
||||
"url_title": {
|
||||
"description": "url title",
|
||||
"type": "string"
|
||||
},
|
||||
"vote_type": {
|
||||
"description": "vote type",
|
||||
"type": "string"
|
||||
|
@ -7659,10 +7724,18 @@
|
|||
"schema.SiteInterfaceReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"default_avatar",
|
||||
"language",
|
||||
"time_zone"
|
||||
],
|
||||
"properties": {
|
||||
"default_avatar": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"system",
|
||||
"gravatar"
|
||||
]
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
|
@ -7676,10 +7749,18 @@
|
|||
"schema.SiteInterfaceResp": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"default_avatar",
|
||||
"language",
|
||||
"time_zone"
|
||||
],
|
||||
"properties": {
|
||||
"default_avatar": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"system",
|
||||
"gravatar"
|
||||
]
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
|
@ -7755,7 +7836,7 @@
|
|||
"properties": {
|
||||
"permalink": {
|
||||
"type": "integer",
|
||||
"maximum": 3,
|
||||
"maximum": 4,
|
||||
"minimum": 0
|
||||
},
|
||||
"robots": {
|
||||
|
@ -7772,7 +7853,7 @@
|
|||
"properties": {
|
||||
"permalink": {
|
||||
"type": "integer",
|
||||
"maximum": 3,
|
||||
"maximum": 4,
|
||||
"minimum": 0
|
||||
},
|
||||
"robots": {
|
||||
|
|
|
@ -174,6 +174,25 @@ definitions:
|
|||
- object_id
|
||||
- report_type
|
||||
type: object
|
||||
schema.AddTagReq:
|
||||
properties:
|
||||
display_name:
|
||||
description: display_name
|
||||
maxLength: 35
|
||||
type: string
|
||||
original_text:
|
||||
description: original text
|
||||
maxLength: 65536
|
||||
type: string
|
||||
slug_name:
|
||||
description: slug_name
|
||||
maxLength: 35
|
||||
type: string
|
||||
required:
|
||||
- display_name
|
||||
- original_text
|
||||
- slug_name
|
||||
type: object
|
||||
schema.AddUserReq:
|
||||
properties:
|
||||
display_name:
|
||||
|
@ -406,6 +425,9 @@ definitions:
|
|||
title:
|
||||
description: title
|
||||
type: string
|
||||
url_title:
|
||||
description: url title
|
||||
type: string
|
||||
type: object
|
||||
schema.GetCommentResp:
|
||||
properties:
|
||||
|
@ -523,12 +545,6 @@ definitions:
|
|||
id:
|
||||
description: user id
|
||||
type: string
|
||||
ip_info:
|
||||
description: ip info
|
||||
type: string
|
||||
is_admin:
|
||||
description: is admin
|
||||
type: boolean
|
||||
last_login_date:
|
||||
description: last login date
|
||||
type: integer
|
||||
|
@ -625,6 +641,9 @@ definitions:
|
|||
title:
|
||||
description: title
|
||||
type: string
|
||||
url_title:
|
||||
description: url title
|
||||
type: string
|
||||
type: object
|
||||
schema.GetReportTypeResp:
|
||||
properties:
|
||||
|
@ -718,6 +737,9 @@ definitions:
|
|||
created_at:
|
||||
description: created time
|
||||
type: integer
|
||||
description:
|
||||
description: description
|
||||
type: string
|
||||
display_name:
|
||||
description: display_name
|
||||
type: string
|
||||
|
@ -904,9 +926,6 @@ definitions:
|
|||
ip_info:
|
||||
description: ip info
|
||||
type: string
|
||||
is_admin:
|
||||
description: is admin
|
||||
type: boolean
|
||||
language:
|
||||
description: language
|
||||
type: string
|
||||
|
@ -931,6 +950,9 @@ definitions:
|
|||
rank:
|
||||
description: rank
|
||||
type: integer
|
||||
role_id:
|
||||
description: role id
|
||||
type: integer
|
||||
status:
|
||||
description: user status
|
||||
type: string
|
||||
|
@ -978,9 +1000,6 @@ definitions:
|
|||
ip_info:
|
||||
description: ip info
|
||||
type: string
|
||||
is_admin:
|
||||
description: is admin
|
||||
type: boolean
|
||||
language:
|
||||
description: language
|
||||
type: string
|
||||
|
@ -1005,6 +1024,9 @@ definitions:
|
|||
rank:
|
||||
description: rank
|
||||
type: integer
|
||||
role_id:
|
||||
description: role id
|
||||
type: integer
|
||||
status:
|
||||
description: user status
|
||||
type: string
|
||||
|
@ -1043,6 +1065,9 @@ definitions:
|
|||
title:
|
||||
description: title
|
||||
type: string
|
||||
url_title:
|
||||
description: url title
|
||||
type: string
|
||||
vote_type:
|
||||
description: vote type
|
||||
type: string
|
||||
|
@ -1438,6 +1463,11 @@ definitions:
|
|||
type: object
|
||||
schema.SiteInterfaceReq:
|
||||
properties:
|
||||
default_avatar:
|
||||
enum:
|
||||
- system
|
||||
- gravatar
|
||||
type: string
|
||||
language:
|
||||
maxLength: 128
|
||||
type: string
|
||||
|
@ -1445,11 +1475,17 @@ definitions:
|
|||
maxLength: 128
|
||||
type: string
|
||||
required:
|
||||
- default_avatar
|
||||
- language
|
||||
- time_zone
|
||||
type: object
|
||||
schema.SiteInterfaceResp:
|
||||
properties:
|
||||
default_avatar:
|
||||
enum:
|
||||
- system
|
||||
- gravatar
|
||||
type: string
|
||||
language:
|
||||
maxLength: 128
|
||||
type: string
|
||||
|
@ -1457,6 +1493,7 @@ definitions:
|
|||
maxLength: 128
|
||||
type: string
|
||||
required:
|
||||
- default_avatar
|
||||
- language
|
||||
- time_zone
|
||||
type: object
|
||||
|
@ -1499,7 +1536,7 @@ definitions:
|
|||
schema.SiteSeoReq:
|
||||
properties:
|
||||
permalink:
|
||||
maximum: 3
|
||||
maximum: 4
|
||||
minimum: 0
|
||||
type: integer
|
||||
robots:
|
||||
|
@ -1511,7 +1548,7 @@ definitions:
|
|||
schema.SiteSeoResp:
|
||||
properties:
|
||||
permalink:
|
||||
maximum: 3
|
||||
maximum: 4
|
||||
minimum: 0
|
||||
type: integer
|
||||
robots:
|
||||
|
@ -4614,6 +4651,27 @@ paths:
|
|||
summary: get tag one
|
||||
tags:
|
||||
- Tag
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: add tag
|
||||
parameters:
|
||||
- description: tag
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.AddTagReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
summary: add tag
|
||||
tags:
|
||||
- Tag
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
|
|
2
go.mod
2
go.mod
|
@ -28,7 +28,7 @@ require (
|
|||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/ory/dockertest/v3 v3.9.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/segmentfault/pacman v1.0.2
|
||||
github.com/segmentfault/pacman v1.0.3
|
||||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221219081300-f734f4a16aa0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -609,8 +609,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0
|
|||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||
github.com/segmentfault/pacman v1.0.2 h1:tXWkEzePiSVQXYwFH3tOuxC1/DJ5ISi35F93lKNGs3o=
|
||||
github.com/segmentfault/pacman v1.0.2/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs=
|
||||
github.com/segmentfault/pacman v1.0.3 h1:/K8LJHQMiCaCIvC/e8GQITpYTEG6RH4KTLTZjPTghl4=
|
||||
github.com/segmentfault/pacman v1.0.3/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs=
|
||||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0 h1:4x0qG7H2M3qH7Yo2BhGrVlji1iTmRAWgINY/JyENeHs=
|
||||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk=
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: E-Mail und Password stimmen nicht überein.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: E-Mail und Password stimmen nicht überein.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: Das neue Passwort ist das gleiche wie das vorherige Passwort.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Frage nicht gefunden.
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -48,6 +48,8 @@ backend:
|
|||
other: No permission to delete.
|
||||
cannot_update:
|
||||
other: No permission to update.
|
||||
question_closed_cannot_add:
|
||||
other: Questions are closed and cannot be added.
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: Comment are not allowed to edit.
|
||||
|
@ -104,6 +106,8 @@ backend:
|
|||
not_found:
|
||||
other: Report not found.
|
||||
tag:
|
||||
already_exist:
|
||||
other: Tag already exists.
|
||||
not_found:
|
||||
other: Tag not found.
|
||||
recommend_tag_not_found:
|
||||
|
@ -278,6 +282,7 @@ ui:
|
|||
tag: Tag
|
||||
tags: Tags
|
||||
tag_wiki: tag wiki
|
||||
create_tag: Create Tag
|
||||
edit_tag: Edit Tag
|
||||
ask_a_question: Add Question
|
||||
edit_question: Edit Question
|
||||
|
@ -299,6 +304,8 @@ ui:
|
|||
maintenance: Website Maintenance
|
||||
users: Users
|
||||
oauth_callback: Processing
|
||||
http_404: HTTP Error 404
|
||||
http_50X: HTTP Error 500
|
||||
notifications:
|
||||
title: Notifications
|
||||
inbox: Inbox
|
||||
|
@ -442,7 +449,7 @@ ui:
|
|||
range: Display name up to 35 characters.
|
||||
slug_name:
|
||||
label: URL Slug
|
||||
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
|
||||
desc: URL slug up to 35 characters.
|
||||
msg:
|
||||
empty: URL slug cannot be empty.
|
||||
range: URL slug up to 35 characters.
|
||||
|
@ -451,6 +458,7 @@ ui:
|
|||
label: Description
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
btn_post: Post new tag
|
||||
tag_info:
|
||||
created_at: Created
|
||||
edited_at: Edited
|
||||
|
@ -1272,6 +1280,9 @@ ui:
|
|||
label: Timezone
|
||||
msg: Timezone cannot be empty.
|
||||
text: Choose a city in the same timezone as you.
|
||||
avatar:
|
||||
label: Default Avatar
|
||||
text: For users without a custom avatar of their own.
|
||||
smtp:
|
||||
page_title: SMTP
|
||||
from_email:
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: Contraseña o correo incorrecto.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: Contraseña o correo incorrecto.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: La nueva contraseña es igual a la anterior.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Pregunta no encontrada.
|
||||
cannot_deleted:
|
||||
|
@ -807,6 +813,7 @@ ui:
|
|||
approve: Aprobar
|
||||
reject: Rechazar
|
||||
skip: Omitir
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Resultados de la búsqueda
|
||||
keywords: Palabras claves
|
||||
|
@ -996,9 +1003,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1385,6 +1394,10 @@ ui:
|
|||
reputation: reputación
|
||||
votes: votos
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: L'email et le mot de passe ne correspondent pas.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: L'email et le mot de passe ne correspondent pas.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: Le nouveau mot de passe est le même que le précédent.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Question non trouvée.
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approuver
|
||||
reject: Rejeter
|
||||
skip: Ignorer
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Résultats de la recherche
|
||||
keywords: Mots-clés
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: réponses
|
||||
accepted: Accepté
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Nous sommes désolés, mais cette page n’existe pas."
|
||||
back_home: Retour à la page d'accueil
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: Le serveur a rencontré une erreur et n'a pas pu répondre à votre requête.
|
||||
back_home: Retour à la page d'accueil
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: réputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Voulez-vous vraiment quitter la page ?"
|
||||
changes_not_save: "Impossible d'enregistrer vos modifications."
|
||||
leave_page: Voulez-vous vraiment quitter la page ?
|
||||
changes_not_save: Impossible d'enregistrer vos modifications.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: Email dan kata sandi tidak cocok.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: Email dan kata sandi tidak cocok.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: Password baru sama dengan password yang sebelumnya.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Pertanyaan tidak ditemukan.
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Sayangnya, halaman ini tidak ada."
|
||||
back_home: Kembali ke beranda
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: Server mengalami kesalahan internal dan tidak dapat menyelesaikan permintaan Anda.
|
||||
back_home: Kembali ke beranda
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: Email o password errati
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: Email o password errati
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: La nuova password è identica alla precedente
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: domanda non trovata
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: Email and password do not match.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: Email and password do not match.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: The new password is the same as the previous one.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Question not found.
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: Email and password do not match.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: Email and password do not match.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: The new password is the same as the previous one.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Question not found.
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: O e-mail e a palavra-passe não coincidem.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: O e-mail e a palavra-passe não coincidem.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: A nova senha é a mesma que a anterior.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Pergunta não encontrada.
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: Неверное имя пользователя или пароль.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: Неверное имя пользователя или пароль.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: Пароль не может быть таким же как прежний.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Вопрос не найден.
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: E-Posta ve parola eşleşmiyor.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: E-Posta ve parola eşleşmiyor.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: Yeni parola bir önceki parolanızın aynısı.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Soru bulunamadı.
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -34,6 +34,10 @@ backend:
|
|||
other: Email and password do not match.
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: Email and password do not match.
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: The new password is the same as the previous one.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Question not found.
|
||||
cannot_deleted:
|
||||
|
@ -782,6 +788,7 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -971,9 +978,11 @@ ui:
|
|||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
|
@ -1360,6 +1369,10 @@ ui:
|
|||
reputation: reputation
|
||||
votes: votes
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#The following fields are used for back-end
|
||||
|
||||
backend:
|
||||
base:
|
||||
success:
|
||||
|
@ -35,6 +34,10 @@ backend:
|
|||
other: 邮箱和密码不匹配。
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: 邮箱和密码不匹配。
|
||||
answer:
|
||||
|
@ -44,6 +47,8 @@ backend:
|
|||
other: 没有删除权限。
|
||||
cannot_update:
|
||||
other: 没有更新权限。
|
||||
question_closed_cannot_add:
|
||||
other: 问题已关闭不可以新增回答
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: 不允许编辑评论。
|
||||
|
@ -82,7 +87,7 @@ backend:
|
|||
other: 新密码与之前的设置相同
|
||||
question:
|
||||
already_deleted:
|
||||
other: 该内容已被删除
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: 问题未找到
|
||||
cannot_deleted:
|
||||
|
@ -785,6 +790,7 @@ ui:
|
|||
approve: 批准
|
||||
reject: 拒绝
|
||||
skip: 略过
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: 搜索结果
|
||||
keywords: 关键词
|
||||
|
@ -974,9 +980,11 @@ ui:
|
|||
answers: 个回答
|
||||
accepted: 已被采纳
|
||||
page_404:
|
||||
http_error: HTTP Error 404
|
||||
desc: "很抱歉,此页面不存在。"
|
||||
back_home: 回到主页
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: 服务器遇到了一个错误,无法完成你的请求。
|
||||
back_home: 回到主页
|
||||
page_maintenance:
|
||||
|
@ -1387,6 +1395,10 @@ ui:
|
|||
reputation: 声望值
|
||||
votes: 投票
|
||||
prompt:
|
||||
leave_page: "确定要离开此页面?"
|
||||
changes_not_save: "您的更改尚未保存"
|
||||
leave_page: 确定要离开此页面?
|
||||
changes_not_save: 您的更改尚未保存
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
180
i18n/zh_TW.yaml
180
i18n/zh_TW.yaml
|
@ -34,6 +34,10 @@ backend:
|
|||
other: 電郵和密碼不匹配。
|
||||
error:
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: 電郵和密碼不匹配。
|
||||
answer:
|
||||
|
@ -80,6 +84,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: 新密碼與先前的一樣。
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: 找不到問題。
|
||||
cannot_deleted:
|
||||
|
@ -487,8 +493,10 @@ ui:
|
|||
btn_save_edits: 保存
|
||||
btn_cancel: 取消
|
||||
show_more: 顯示更多評論
|
||||
tip_question: 通过評論询问更多问题或提出改進建議。避免在評論中回答問題。
|
||||
tip_answer: 使用評論回復其他用戶或通知他們进行更改。如果你要添加新的信息,請編輯你的帖子,而不是發表評論。
|
||||
tip_question: >-
|
||||
通过評論询问更多问题或提出改進建議。避免在評論中回答問題。
|
||||
tip_answer: >-
|
||||
使用評論回復其他用戶或通知他們进行更改。如果你要添加新的信息,請編輯你的帖子,而不是發表評論。
|
||||
edit_answer:
|
||||
title: 編輯回答
|
||||
default_reason: 編輯回答
|
||||
|
@ -502,7 +510,8 @@ ui:
|
|||
characters: 內容必須至少6個字元長度。
|
||||
edit_summary:
|
||||
label: 編輯概要
|
||||
placeholder: 簡單描述更改原因 (錯別字、文字表達、格式等等)
|
||||
placeholder: >-
|
||||
簡單描述更改原因 (錯別字、文字表達、格式等等)
|
||||
btn_save_edits: 儲存更改
|
||||
btn_cancel: 取消
|
||||
tags:
|
||||
|
@ -546,7 +555,8 @@ ui:
|
|||
empty: 回答內容不能為空
|
||||
edit_summary:
|
||||
label: 編輯概要
|
||||
placeholder: 簡單描述更改原因 (錯別字、文字表達、格式等等)
|
||||
placeholder: >-
|
||||
簡單描述更改原因 (錯別字、文字表達、格式等等)
|
||||
btn_post_question: 提出問題
|
||||
btn_save_edits: 儲存更改
|
||||
answer_question: 回答您自己的問題
|
||||
|
@ -555,7 +565,7 @@ ui:
|
|||
add_btn: 建立標籤
|
||||
create_btn: 建立新標籤
|
||||
search_tag: 搜尋標籤
|
||||
hint: 请描述您的問題,至少需要一個標籤。
|
||||
hint: "请描述您的問題,至少需要一個標籤。"
|
||||
no_result: 沒有匹配的標籤
|
||||
tag_required_text: 必填標籤 (至少一個)
|
||||
header:
|
||||
|
@ -582,9 +592,11 @@ ui:
|
|||
msg:
|
||||
empty: 验证码不能為空
|
||||
inactive:
|
||||
first: 就差一步!我們寄送了一封啟用電子郵件到 <bold>{{mail}}</bold>。請按照郵件中的說明啟用您的帳戶。
|
||||
info: 如果沒有收到,請檢查您的垃圾郵件文件夾。
|
||||
another: 我們向您發送了另一封啟用電子郵件,地址為 <bold>{{mail}}</bold>。它可能需要幾分鐘才能到達;請務必檢查您的垃圾郵件文件夾。
|
||||
first: >-
|
||||
就差一步!我們寄送了一封啟用電子郵件到 <bold>{{mail}}</bold>。請按照郵件中的說明啟用您的帳戶。
|
||||
info: "如果沒有收到,請檢查您的垃圾郵件文件夾。"
|
||||
another: >-
|
||||
我們向您發送了另一封啟用電子郵件,地址為 <bold>{{mail}}</bold>。它可能需要幾分鐘才能到達;請務必檢查您的垃圾郵件文件夾。
|
||||
btn_name: 重新發送啟用郵件
|
||||
change_btn_name: 更改郵箱
|
||||
msg:
|
||||
|
@ -613,7 +625,8 @@ ui:
|
|||
account_forgot:
|
||||
page_title: 忘記密碼
|
||||
btn_name: 向我發送恢復郵件
|
||||
send_success: 如果帳號與<strong>{{mail}}</strong>相符,您應該很快就會收到一封電子郵件,說明如何重置您的密碼。
|
||||
send_success: >-
|
||||
如果帳號與<strong>{{mail}}</strong>相符,您應該很快就會收到一封電子郵件,說明如何重置您的密碼。
|
||||
email:
|
||||
label: 郵箱
|
||||
msg:
|
||||
|
@ -622,7 +635,8 @@ ui:
|
|||
page_title: 歡迎來到 {{site_name}}
|
||||
btn_cancel: 取消
|
||||
btn_update: 更新電子郵件地址
|
||||
send_success: 如果帳號與<strong>{{mail}}</strong>相符,您應該很快就會收到一封電子郵件,說明如何重置您的密碼。
|
||||
send_success: >-
|
||||
如果帳號與<strong>{{mail}}</strong>相符,您應該很快就會收到一封電子郵件,說明如何重置您的密碼。
|
||||
email:
|
||||
label: 新電子郵件
|
||||
msg:
|
||||
|
@ -630,8 +644,10 @@ ui:
|
|||
password_reset:
|
||||
page_title: 密碼重置
|
||||
btn_name: 重置我的密碼
|
||||
reset_success: 你已經成功更改密碼,將返回登入頁面
|
||||
link_invalid: 抱歉,此密碼重置連結已失效。也許是你已經重置過密碼了?
|
||||
reset_success: >-
|
||||
你已經成功更改密碼,將返回登入頁面
|
||||
link_invalid: >-
|
||||
抱歉,此密碼重置連結已失效。也許是你已經重置過密碼了?
|
||||
to_login: 前往登入頁面
|
||||
password:
|
||||
label: 密碼
|
||||
|
@ -660,7 +676,7 @@ ui:
|
|||
caption: 用戶之間可以通過 "@用戶名" 進行交互。
|
||||
msg: 用戶名不能為空
|
||||
msg_range: 使用者名稱最長 30 個字元。
|
||||
character: 必須由 "a-z", "0-9", " - . _" 組成
|
||||
character: '必須由 "a-z", "0-9", " - . _" 組成'
|
||||
avatar:
|
||||
label: 頭像
|
||||
gravatar: 頭像
|
||||
|
@ -674,21 +690,22 @@ ui:
|
|||
label: 關於我
|
||||
website:
|
||||
label: 網站
|
||||
placeholder: https://example.com
|
||||
placeholder: "https://example.com"
|
||||
msg: 網站格式不正確
|
||||
location:
|
||||
label: 位置
|
||||
placeholder: 城市, 國家
|
||||
placeholder: "城市, 國家"
|
||||
notification:
|
||||
heading: 通知
|
||||
email:
|
||||
label: 郵件通知
|
||||
radio: 回答你的問題,評論,以及更多
|
||||
radio: "回答你的問題,評論,以及更多"
|
||||
account:
|
||||
heading: 帳號
|
||||
change_email_btn: 更改郵箱
|
||||
change_pass_btn: 更改密碼
|
||||
change_email_info: 我們已經寄出一封郵件至此電子郵件地址,請遵照說明進行確認。
|
||||
change_email_info: >-
|
||||
我們已經寄出一封郵件至此電子郵件地址,請遵照說明進行確認。
|
||||
email:
|
||||
label: 郵箱
|
||||
msg: 郵箱不能為空
|
||||
|
@ -741,7 +758,8 @@ ui:
|
|||
add_another_answer: 添加另一個答案
|
||||
confirm_title: 繼續回答
|
||||
continue: 繼續
|
||||
confirm_info: <p>您確定要添加一個新的回答嗎?</p><p>您可以使用编辑链接来完善和改进您现有的答案。</p>
|
||||
confirm_info: >-
|
||||
<p>您確定要添加一個新的回答嗎?</p><p>您可以使用编辑链接来完善和改进您现有的答案。</p>
|
||||
empty: 回答內容不能為空。
|
||||
characters: 內容必須至少6個字元長度。
|
||||
reopen:
|
||||
|
@ -750,8 +768,10 @@ ui:
|
|||
success: 這個貼文已被重新打開
|
||||
delete:
|
||||
title: 刪除此貼
|
||||
question: 我們不建議<strong>刪除有回答的貼文</strong>。因為這樣做會使得後來的讀者無法從該問題中獲得幫助。</p><p>如果刪除過多有回答的貼文,你的帳號將會被禁止提問。你確定要刪除嗎?
|
||||
answer_accepted: <p>我們不建議<strong>刪除被採納的回答</strong>。因為這樣做會使得後來的讀者無法從該回答中獲得幫助。</p>如果刪除過多被採納的貼文,你的帳號將會被禁止回答任何提問。你確定要刪除嗎?
|
||||
question: >-
|
||||
我們不建議<strong>刪除有回答的貼文</strong>。因為這樣做會使得後來的讀者無法從該問題中獲得幫助。</p><p>如果刪除過多有回答的貼文,你的帳號將會被禁止提問。你確定要刪除嗎?
|
||||
answer_accepted: >-
|
||||
<p>我們不建議<strong>刪除被採納的回答</strong>。因為這樣做會使得後來的讀者無法從該回答中獲得幫助。</p>如果刪除過多被採納的貼文,你的帳號將會被禁止回答任何提問。你確定要刪除嗎?
|
||||
other: 你確定要刪除?
|
||||
tip_question_deleted: 該帖子已被刪除。
|
||||
tip_answer_deleted: 此回答已被刪除
|
||||
|
@ -768,6 +788,7 @@ ui:
|
|||
approve: 核准
|
||||
reject: 拒絕
|
||||
skip: 略過
|
||||
discard_draft: Discard draft
|
||||
search:
|
||||
title: 搜尋結果
|
||||
keywords: 關鍵詞
|
||||
|
@ -784,12 +805,12 @@ ui:
|
|||
more: 更多
|
||||
tips:
|
||||
title: 高級搜尋提示
|
||||
tag: <1>[tag]</1> 在指定標籤中搜尋
|
||||
user: <1>user:username</1> 根據作者搜尋
|
||||
answer: <1>answers:0</1> 搜尋未回答的問題
|
||||
score: <1>score:3</1> 得分為 3+ 的帖子
|
||||
question: <1>is:question</1> 只搜尋問題
|
||||
is_answer: <1>is:answer</1> 只搜尋回答
|
||||
tag: "<1>[tag]</1> 在指定標籤中搜尋"
|
||||
user: "<1>user:username</1> 根據作者搜尋"
|
||||
answer: "<1>answers:0</1> 搜尋未回答的問題"
|
||||
score: "<1>score:3</1> 得分為 3+ 的帖子"
|
||||
question: "<1>is:question</1> 只搜尋問題"
|
||||
is_answer: "<1>is:answer</1> 只搜尋回答"
|
||||
empty: 找不到任何相關的內容。<br /> 請嘗試其他關鍵字,或者減少查找內容的長度。
|
||||
share:
|
||||
name: 分享
|
||||
|
@ -805,9 +826,11 @@ ui:
|
|||
page_title: 歡迎來到 {{site_name}}
|
||||
success: 你的帳號已通過驗證,即將返回首頁。
|
||||
link: 繼續訪問主頁
|
||||
invalid: 抱歉,此驗證連結已失效。也許是你的帳號已經通過驗證了?
|
||||
invalid: >-
|
||||
抱歉,此驗證連結已失效。也許是你的帳號已經通過驗證了?
|
||||
confirm_new_email: 你的電子郵箱已更新
|
||||
confirm_new_email_invalid: 抱歉,此驗證連結已失效。也許是你的郵箱已經成功更改了?
|
||||
confirm_new_email_invalid: >-
|
||||
抱歉,此驗證連結已失效。也許是你的郵箱已經成功更改了?
|
||||
unsubscribe:
|
||||
page_title: 退訂
|
||||
success_title: 取消訂閱成功
|
||||
|
@ -848,12 +871,12 @@ ui:
|
|||
newest: 最新
|
||||
score: 評分
|
||||
edit_profile: 編輯個人資料
|
||||
visited_x_days: 已造訪 {{ count }} 天
|
||||
visited_x_days: "已造訪 {{ count }} 天"
|
||||
viewed: 閱讀次數
|
||||
joined: 加入於
|
||||
last_login: 出現時間
|
||||
about_me: 關於我
|
||||
about_me_empty: // 你好, 世界 !
|
||||
about_me_empty: "// 你好, 世界 !"
|
||||
top_answers: 熱門回答
|
||||
top_questions: 熱門問題
|
||||
stats: 狀態
|
||||
|
@ -887,7 +910,7 @@ ui:
|
|||
placeholder: root
|
||||
msg: 密碼不能為空
|
||||
db_host:
|
||||
label: "數據庫伺服器"
|
||||
label: 資料庫主機位址
|
||||
placeholder: "db: 3306"
|
||||
msg: 資料庫主機位址不能為空
|
||||
db_name:
|
||||
|
@ -901,7 +924,8 @@ ui:
|
|||
config_yaml:
|
||||
title: 創建 config.yaml
|
||||
label: 已創建 config.yaml 文件。
|
||||
desc: 您可以手動在 <1>/var/wwww/xxx/</1> 目錄中創建<1>config.yaml</1> 文件並粘貼以下文本。
|
||||
desc: >-
|
||||
您可以手動在 <1>/var/wwww/xxx/</1> 目錄中創建<1>config.yaml</1> 文件並粘貼以下文本。
|
||||
info: 完成後點擊"下一步"按鈕。
|
||||
site_information: 網站資訊
|
||||
admin_account: 管理員帳戶
|
||||
|
@ -925,7 +949,8 @@ ui:
|
|||
msg: 暱稱不能為空。
|
||||
admin_password:
|
||||
label: 密碼
|
||||
text: 您需要此密碼才能登入。請將其儲存在一個安全的位置。
|
||||
text: >-
|
||||
您需要此密碼才能登入。請將其儲存在一個安全的位置。
|
||||
msg: 密碼不能為空。
|
||||
admin_email:
|
||||
label: 郵箱
|
||||
|
@ -934,28 +959,34 @@ ui:
|
|||
empty: 郵箱不能為空。
|
||||
incorrect: 郵箱格式不正確。
|
||||
ready_title: 你的Answer已經準備好了!
|
||||
ready_desc: 如果你想改變更多的設定,請瀏覽<1>管理員部分</1>;在網站選單中找到它。
|
||||
good_luck: 玩得愉快,祝您好運!
|
||||
ready_desc: >-
|
||||
如果你想改變更多的設定,請瀏覽<1>管理員部分</1>;在網站選單中找到它。
|
||||
good_luck: "玩得愉快,祝您好運!"
|
||||
warn_title: 警告
|
||||
warn_desc: 檔案<1>config.yaml</1>已存在。如果您需要重置此文件中的任何配置項,請先刪除它。
|
||||
warn_desc: >-
|
||||
檔案<1>config.yaml</1>已存在。如果您需要重置此文件中的任何配置項,請先刪除它。
|
||||
install_now: 您可以嘗試<1>現在安裝</1>。
|
||||
installed: 已安裝
|
||||
installed_desc: 您似乎已經安裝過了。要重新安裝,請先清除舊的資料庫表。
|
||||
installed_desc: >-
|
||||
您似乎已經安裝過了。要重新安裝,請先清除舊的資料庫表。
|
||||
db_failed: 資料連接異常!
|
||||
db_failed_desc: 這要么意味著你<1>config.yaml</1>文件中的數據庫信息不正確,要么意味著無法與數據庫服務器建立聯繫。這可能意味著你的主機的數據庫服務器已經停機。
|
||||
db_failed_desc: >-
|
||||
這要么意味著你<1>config.yaml</1>文件中的數據庫信息不正確,要么意味著無法與數據庫服務器建立聯繫。這可能意味著你的主機的數據庫服務器已經停機。
|
||||
counts:
|
||||
views: 觀看
|
||||
votes: 得票
|
||||
answers: 回答
|
||||
accepted: 已採納
|
||||
page_404:
|
||||
desc: 很抱歉,此頁面不存在。
|
||||
http_error: HTTP Error 404
|
||||
desc: "很抱歉,此頁面不存在。"
|
||||
back_home: 回到首頁
|
||||
page_50X:
|
||||
http_error: HTTP Error 500
|
||||
desc: 伺服器遇到了一個錯誤,無法完成你的請求。
|
||||
back_home: 回到首頁
|
||||
page_maintenance:
|
||||
desc: 我們正在維護中,很快就會回來。
|
||||
desc: "我們正在維護中,很快就會回來。"
|
||||
nav_menus:
|
||||
dashboard: 後台管理
|
||||
contents: 內容
|
||||
|
@ -990,15 +1021,15 @@ ui:
|
|||
votes: "投票:"
|
||||
active_users: "活躍用戶:"
|
||||
flags: "檢舉:"
|
||||
site_health_status: "健康狀態:"
|
||||
version: 版本
|
||||
site_health_status: '健康狀態:'
|
||||
version: "版本"
|
||||
https: "HTTPS:"
|
||||
uploading_files: "上傳文件:"
|
||||
smtp: "SMTP:"
|
||||
timezone: 時區:
|
||||
timezone: "時區:"
|
||||
system_info: 系統資訊
|
||||
storage_used: 已用儲存空間:
|
||||
uptime: 運行時間:
|
||||
storage_used: "已用儲存空間:"
|
||||
uptime: "運行時間:"
|
||||
answer_links: 回答連結
|
||||
documents: 文件
|
||||
feedback: 用戶反饋
|
||||
|
@ -1008,8 +1039,8 @@ ui:
|
|||
update_to: 更新到
|
||||
latest: 最新版本
|
||||
check_failed: 校驗失敗
|
||||
yes: 是
|
||||
no: 否
|
||||
"yes": "是"
|
||||
"no": "否"
|
||||
not_allowed: 不允許
|
||||
allowed: 允許
|
||||
enabled: 已啟用
|
||||
|
@ -1031,7 +1062,7 @@ ui:
|
|||
suspended_name: 停權
|
||||
suspended_desc: 被停權的使用者將無法登入。
|
||||
deleted_name: 刪除
|
||||
deleted_desc: 刪除個人資料和身份驗證關聯。
|
||||
deleted_desc: "刪除個人資料和身份驗證關聯。"
|
||||
inactive_name: 不活躍
|
||||
inactive_desc: 不活躍的用戶必須重新驗證郵箱。
|
||||
confirm_title: 刪除此用戶
|
||||
|
@ -1040,11 +1071,11 @@ ui:
|
|||
msg:
|
||||
empty: 請選擇一個原因
|
||||
status_modal:
|
||||
title: 更改 {{ type }} 狀態為...
|
||||
title: "更改 {{ type }} 狀態為..."
|
||||
normal_name: 正常
|
||||
normal_desc: 所有使用者都可以瀏覽的普通貼文。
|
||||
closed_name: 關閉
|
||||
closed_desc: 關閉的問題不能回答,但仍然可以編輯、投票和評論。
|
||||
closed_desc: "關閉的問題不能回答,但仍然可以編輯、投票和評論。"
|
||||
deleted_name: 刪除
|
||||
deleted_desc: 獲得和失去的所有信譽將被恢復。
|
||||
btn_cancel: 取消
|
||||
|
@ -1076,7 +1107,7 @@ ui:
|
|||
Admin: 管理員
|
||||
User: 用戶
|
||||
filter:
|
||||
placeholder: 按名稱篩選,用戶:id
|
||||
placeholder: "按名稱篩選,用戶:id"
|
||||
set_new_password: 設置新密碼
|
||||
change_status: 更改狀態
|
||||
change_role: 更改角色
|
||||
|
@ -1120,7 +1151,7 @@ ui:
|
|||
action: 操作
|
||||
change: 更改
|
||||
filter:
|
||||
placeholder: 按標題過濾,問題:id
|
||||
placeholder: "按標題過濾,問題:id"
|
||||
answers:
|
||||
page_title: 回答
|
||||
normal: 正常
|
||||
|
@ -1132,13 +1163,13 @@ ui:
|
|||
action: 操作
|
||||
change: 更改
|
||||
filter:
|
||||
placeholder: 按名稱篩選,answer:id
|
||||
placeholder: "按名稱篩選,answer:id"
|
||||
general:
|
||||
page_title: 一般
|
||||
name:
|
||||
label: 網站名稱
|
||||
msg: 不能為空
|
||||
text: 網站的名稱,如標題標籤中所用。
|
||||
text: "網站的名稱,如標題標籤中所用。"
|
||||
site_url:
|
||||
label: 網站網址
|
||||
msg: 網站網址不能為空。
|
||||
|
@ -1147,11 +1178,11 @@ ui:
|
|||
short_desc:
|
||||
label: 網站簡短描述
|
||||
msg: 網站簡短描述不能為空。
|
||||
text: 簡短的描述,如主頁上的標題標籤所使用的那样。
|
||||
text: "簡短的描述,如主頁上的標題標籤所使用的那样。"
|
||||
desc:
|
||||
label: 網站描述
|
||||
msg: 網站描述不能為空。
|
||||
text: 使用一句話描述本站,作為網站的描述(Html 的 meta 標籤)。
|
||||
text: "使用一句話描述本站,作為網站的描述(Html 的 meta 標籤)。"
|
||||
contact_email:
|
||||
label: 聯絡人信箱
|
||||
msg: 聯絡人信箱不能為空。
|
||||
|
@ -1205,8 +1236,8 @@ ui:
|
|||
label: 啟用身份驗證
|
||||
title: SMTP身份驗證
|
||||
msg: SMTP 身份驗證不能為空。
|
||||
yes: 是
|
||||
no: 否
|
||||
"yes": "是"
|
||||
"no": "否"
|
||||
branding:
|
||||
page_title: 品牌
|
||||
logo:
|
||||
|
@ -1227,22 +1258,22 @@ ui:
|
|||
page_title: 法律條款
|
||||
terms_of_service:
|
||||
label: 服務條款
|
||||
text: 您可以在此加入服務內容的條款。如果您已經在別處托管了文檔,請在這裡提供完整的URL。
|
||||
text: "您可以在此加入服務內容的條款。如果您已經在別處托管了文檔,請在這裡提供完整的URL。"
|
||||
privacy_policy:
|
||||
label: 隱私條款
|
||||
text: 您可以在此加入隱私政策內容。如果您已經在別處托管了文檔,請在這裡提供完整的URL。
|
||||
text: "您可以在此加入隱私政策內容。如果您已經在別處托管了文檔,請在這裡提供完整的URL。"
|
||||
write:
|
||||
page_title: 編輯
|
||||
recommend_tags:
|
||||
label: 推薦標籤
|
||||
text: 請在上方輸入標籤固定連結,每行一個標籤。
|
||||
text: "請在上方輸入標籤固定連結,每行一個標籤。"
|
||||
required_tag:
|
||||
title: 必需的標籤
|
||||
label: 根據需要設置推薦標籤
|
||||
text: 每個新問題必須至少有一個推薦標籤。
|
||||
text: "每個新問題必須至少有一個推薦標籤。"
|
||||
reserved_tags:
|
||||
label: 保留標籤
|
||||
text: 保留的標籤只能由版主加入到一個貼文中。
|
||||
text: "保留的標籤只能由版主加入到一個貼文中。"
|
||||
seo:
|
||||
page_title: 搜尋引擎優化
|
||||
permalink:
|
||||
|
@ -1291,13 +1322,13 @@ ui:
|
|||
empty: 不能為空
|
||||
invalid: 是無效的
|
||||
btn_submit: 儲存
|
||||
not_found_props: 所需屬性 {{ key }} 未找到。
|
||||
not_found_props: "所需屬性 {{ key }} 未找到。"
|
||||
page_review:
|
||||
review: 審核
|
||||
proposed: 提案
|
||||
question_edit: 問題編輯
|
||||
answer_edit: 回答編輯
|
||||
tag_edit: "標籤管理: 編輯標籤"
|
||||
tag_edit: '標籤管理: 編輯標籤'
|
||||
edit_summary: 編輯摘要
|
||||
edit_question: 編輯問題
|
||||
edit_answer: 編輯回答
|
||||
|
@ -1310,7 +1341,7 @@ ui:
|
|||
upvote: 贊同
|
||||
accept: 採納
|
||||
cancelled: 已取消
|
||||
commented: "評論:"
|
||||
commented: '評論:'
|
||||
rollback: 回滾
|
||||
edited: 最後編輯於
|
||||
answered: 回答於
|
||||
|
@ -1318,18 +1349,18 @@ ui:
|
|||
closed: 關閉
|
||||
reopened: 重新開啟
|
||||
created: 創建於
|
||||
title: 歷史記錄
|
||||
tag_title: 時間線
|
||||
show_votes: 顯示投票
|
||||
title: "歷史記錄"
|
||||
tag_title: "時間線"
|
||||
show_votes: "顯示投票"
|
||||
n_or_a: N/A
|
||||
title_for_question: 時間線
|
||||
title_for_question: "時間線"
|
||||
title_for_answer: "{{ title }} 的 {{ author }} 回答時間線"
|
||||
title_for_tag: 標籤的時間線
|
||||
title_for_tag: "標籤的時間線"
|
||||
datetime: 日期時間
|
||||
type: 類型
|
||||
by: 由
|
||||
comment: 評論
|
||||
no_data: 我們找不到任何東西。
|
||||
no_data: "我們找不到任何東西。"
|
||||
users:
|
||||
title: 用戶
|
||||
users_with_the_most_reputation: 信譽積分最高的用戶
|
||||
|
@ -1340,3 +1371,8 @@ ui:
|
|||
prompt:
|
||||
leave_page: 你確定要離開此頁面?
|
||||
changes_not_save: 你所做的變更可能不會儲存。
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package constant
|
||||
|
||||
var (
|
||||
DefaultAvatar = "system"
|
||||
)
|
|
@ -4,6 +4,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/role"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
|
@ -154,7 +155,7 @@ func GetIsAdminFromContext(ctx *gin.Context) (isAdmin bool) {
|
|||
if userInfo == nil {
|
||||
return false
|
||||
}
|
||||
return userInfo.IsAdmin
|
||||
return userInfo.RoleID == role.RoleAdminID
|
||||
}
|
||||
|
||||
// GetUserInfoFromContext get user info from context
|
||||
|
|
|
@ -25,6 +25,7 @@ const (
|
|||
AnswerNotFound = "error.answer.not_found"
|
||||
AnswerCannotDeleted = "error.answer.cannot_deleted"
|
||||
AnswerCannotUpdate = "error.answer.cannot_update"
|
||||
AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add"
|
||||
CommentEditWithoutPermission = "error.comment.edit_without_permission"
|
||||
DisallowVote = "error.object.disallow_vote"
|
||||
DisallowFollow = "error.object.disallow_follow"
|
||||
|
@ -45,6 +46,7 @@ const (
|
|||
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
|
||||
TagCannotUpdate = "error.tag.cannot_update"
|
||||
TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete"
|
||||
TagAlreadyExist = "error.tag.already_exist"
|
||||
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
|
||||
ThemeNotFound = "error.theme.not_found"
|
||||
LangNotFound = "error.lang.not_found"
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/answerdev/answer/internal/service/role"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
@ -37,10 +39,11 @@ func (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
if userInfo := middleware.GetUserInfoFromContext(ctx); userInfo != nil {
|
||||
req.IsAdmin = userInfo.IsAdmin
|
||||
req.IsAdmin = userInfo.RoleID == role.RoleAdminID
|
||||
}
|
||||
|
||||
resp, err := ac.activityService.GetObjectTimeline(ctx, req)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -49,15 +50,18 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
|
||||
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerDelete, req.ID)
|
||||
objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
|
||||
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
req.CanDelete = canList[0] || objectOwner
|
||||
if !req.CanDelete {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
@ -109,6 +113,7 @@ func (ac *AnswerController) Add(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAdd, "")
|
||||
|
@ -136,6 +141,24 @@ func (ac *AnswerController) Add(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerEdit,
|
||||
permission.AnswerDelete,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, info.ID)
|
||||
req.CanEdit = canList[0] || objectOwner
|
||||
req.CanDelete = canList[1] || objectOwner
|
||||
if !can {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
info.MemberActions = permission.GetAnswerPermission(ctx, req.UserID, info.UserID, req.CanEdit, req.CanDelete)
|
||||
handler.HandleResponse(ctx, nil, gin.H{
|
||||
"info": info,
|
||||
"question": questionInfo,
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
@ -37,6 +38,7 @@ func (cc *CollectionController) CollectionSwitch(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
|
||||
dto := &schema.CollectionSwitchDTO{}
|
||||
_ = copier.Copy(dto, req)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/comment"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -40,7 +41,7 @@ func (cc *CommentController) AddComment(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.CommentAdd,
|
||||
|
@ -154,7 +155,7 @@ func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.CommentEdit,
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/follow"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
)
|
||||
|
@ -34,7 +35,7 @@ func (fc *FollowController) Follow(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
dto := &schema.FollowDTO{}
|
||||
_ = copier.Copy(dto, req)
|
||||
dto.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/permission"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -42,6 +43,7 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
|
||||
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionDelete, req.ID)
|
||||
|
@ -73,6 +75,7 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionClose, "")
|
||||
if err != nil {
|
||||
|
@ -103,6 +106,7 @@ func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionReopen, "")
|
||||
if err != nil {
|
||||
|
@ -130,6 +134,7 @@ func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) {
|
|||
// @Router /answer/api/v1/question/info [get]
|
||||
func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
||||
id := ctx.Query("id")
|
||||
id = uid.DeShortID(id)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
req := schema.QuestionPermission{}
|
||||
canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{
|
||||
|
@ -168,6 +173,7 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
|||
// @Router /answer/api/v1/question/similar/tag [get]
|
||||
func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) {
|
||||
questionID := ctx.Query("question_id")
|
||||
questionID = uid.DeShortID(questionID)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
list, count, err := qc.questionService.SimilarQuestion(ctx, questionID, userID)
|
||||
if err != nil {
|
||||
|
@ -290,6 +296,7 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
|
|||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
|
@ -507,6 +514,7 @@ func (qc *QuestionController) AdminSearchAnswerList(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
questionList, count, err := qc.questionService.AdminSearchAnswerList(ctx, req, userID)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
|
@ -530,6 +538,7 @@ func (qc *QuestionController) AdminSetQuestionStatus(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
err := qc.questionService.AdminSetQuestionStatus(ctx, req.QuestionID, req.StatusStr)
|
||||
handler.HandleResponse(ctx, err, gin.H{})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/permission"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/internal/service/report"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -39,7 +40,7 @@ func (rc *ReportController) AddReport(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, permission.ReportAdd, "")
|
||||
if err != nil {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/permission"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/pkg/obj"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -45,7 +46,7 @@ func (rc *RevisionController) GetRevisionList(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
|
||||
objectID = uid.DeShortID(objectID)
|
||||
req := &schema.GetRevisionListReq{
|
||||
ObjectID: objectID,
|
||||
}
|
||||
|
@ -137,6 +138,7 @@ func (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) {
|
|||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
action := ""
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
objectTypeStr, _ := obj.GetObjectTypeStrByObjectID(req.ID)
|
||||
switch objectTypeStr {
|
||||
case constant.QuestionObjectType:
|
||||
|
|
|
@ -98,6 +98,38 @@ func (tc *TagController) RemoveTag(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// AddTag add tag
|
||||
// @Summary add tag
|
||||
// @Description add tag
|
||||
// @Tags Tag
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.AddTagReq true "tag"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/tag [post]
|
||||
func (tc *TagController) AddTag(ctx *gin.Context) {
|
||||
req := &schema.AddTagReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.TagAdd,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !canList[0] {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := tc.tagCommonService.AddTag(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdateTag update tag
|
||||
// @Summary update tag
|
||||
// @Description update tag
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/answerdev/answer/pkg/obj"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/answerdev/answer/ui"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -159,8 +160,21 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s
|
|||
id := ctx.Param("id")
|
||||
title := ctx.Param("title")
|
||||
titleIsAnswerID := false
|
||||
NeedChangeShortID := false
|
||||
isShortID := uid.IsShortID(id)
|
||||
if uid.ShortIDSwitch {
|
||||
if !isShortID {
|
||||
id = uid.EnShortID(id)
|
||||
NeedChangeShortID = true
|
||||
}
|
||||
} else {
|
||||
if isShortID {
|
||||
NeedChangeShortID = true
|
||||
id = uid.DeShortID(id)
|
||||
}
|
||||
}
|
||||
|
||||
objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(title)
|
||||
objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(uid.DeShortID(title))
|
||||
if objectTypeerr == nil {
|
||||
if objectType == constant.AnswerObjectType {
|
||||
titleIsAnswerID = true
|
||||
|
@ -169,16 +183,20 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s
|
|||
|
||||
url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
|
||||
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionID {
|
||||
if len(ctx.Request.URL.Query()) > 0 {
|
||||
url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery)
|
||||
}
|
||||
if NeedChangeShortID {
|
||||
return true, url
|
||||
}
|
||||
//not have title
|
||||
if titleIsAnswerID || len(title) == 0 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, url
|
||||
} else {
|
||||
//have title
|
||||
if len(title) > 0 && !titleIsAnswerID && correctTitle {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
detail, err := tc.templateRenderController.QuestionDetail(ctx, id)
|
||||
if err != nil {
|
||||
tc.Page404(ctx)
|
||||
|
@ -188,6 +206,17 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s
|
|||
if titleIsAnswerID {
|
||||
url = fmt.Sprintf("%s/%s", url, title)
|
||||
}
|
||||
|
||||
if len(ctx.Request.URL.Query()) > 0 {
|
||||
url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery)
|
||||
}
|
||||
//have title
|
||||
if len(title) > 0 && !titleIsAnswerID && correctTitle {
|
||||
if NeedChangeShortID {
|
||||
return true, url
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
return true, url
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
@ -38,8 +39,9 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true)
|
||||
can, _, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
|
@ -74,9 +76,9 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
can, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false)
|
||||
can, _, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
|
|
|
@ -5,5 +5,5 @@ type UserCacheInfo struct {
|
|||
UserID string `json:"user_id"`
|
||||
UserStatus int `json:"user_status"`
|
||||
EmailStatus int `json:"email_status"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
RoleID int `json:"role_id"`
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
answercommon "github.com/answerdev/answer/internal/service/answer_common"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/internal/service/unique"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
|
@ -46,6 +47,7 @@ func NewAnswerRepo(
|
|||
|
||||
// AddAnswer add answer
|
||||
func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err error) {
|
||||
answer.QuestionID = uid.DeShortID(answer.QuestionID)
|
||||
ID, err := ar.uniqueIDRepo.GenUniqueIDStr(ctx, answer.TableName())
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
@ -56,11 +58,14 @@ func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err
|
|||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
answer.ID = uid.EnShortID(answer.ID)
|
||||
answer.QuestionID = uid.EnShortID(answer.QuestionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAnswer delete answer
|
||||
func (ar *answerRepo) RemoveAnswer(ctx context.Context, id string) (err error) {
|
||||
id = uid.DeShortID(id)
|
||||
answer := &entity.Answer{
|
||||
ID: id,
|
||||
Status: entity.AnswerStatusDeleted,
|
||||
|
@ -74,6 +79,8 @@ func (ar *answerRepo) RemoveAnswer(ctx context.Context, id string) (err error) {
|
|||
|
||||
// UpdateAnswer update answer
|
||||
func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, Colar []string) (err error) {
|
||||
answer.ID = uid.DeShortID(answer.ID)
|
||||
answer.QuestionID = uid.DeShortID(answer.QuestionID)
|
||||
_, err = ar.data.DB.ID(answer.ID).Cols(Colar...).Update(answer)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
@ -83,6 +90,7 @@ func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, C
|
|||
|
||||
func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error) {
|
||||
now := time.Now()
|
||||
answer.ID = uid.DeShortID(answer.ID)
|
||||
answer.UpdatedAt = now
|
||||
_, err = ar.data.DB.Where("id =?", answer.ID).Cols("status", "updated_at").Update(answer)
|
||||
if err != nil {
|
||||
|
@ -95,11 +103,15 @@ func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answer *entity.Ans
|
|||
func (ar *answerRepo) GetAnswer(ctx context.Context, id string) (
|
||||
answer *entity.Answer, exist bool, err error,
|
||||
) {
|
||||
id = uid.DeShortID(id)
|
||||
answer = &entity.Answer{}
|
||||
exist, err = ar.data.DB.ID(id).Get(answer)
|
||||
if err != nil {
|
||||
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
answer.ID = uid.EnShortID(answer.ID)
|
||||
answer.QuestionID = uid.EnShortID(answer.QuestionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -116,20 +128,32 @@ func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err erro
|
|||
// GetAnswerList get answer list all
|
||||
func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) {
|
||||
answerList = make([]*entity.Answer, 0)
|
||||
answer.ID = uid.DeShortID(answer.ID)
|
||||
answer.QuestionID = uid.DeShortID(answer.QuestionID)
|
||||
err = ar.data.DB.Find(answerList, answer)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range answerList {
|
||||
item.ID = uid.EnShortID(item.ID)
|
||||
item.QuestionID = uid.EnShortID(item.QuestionID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAnswerPage get answer page
|
||||
func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) {
|
||||
answer.ID = uid.DeShortID(answer.ID)
|
||||
answer.QuestionID = uid.DeShortID(answer.QuestionID)
|
||||
answerList = make([]*entity.Answer, 0)
|
||||
total, err = pager.Help(page, pageSize, answerList, answer, ar.data.DB.NewSession())
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range answerList {
|
||||
item.ID = uid.EnShortID(item.ID)
|
||||
item.QuestionID = uid.EnShortID(item.QuestionID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -139,6 +163,8 @@ func (ar *answerRepo) UpdateAccepted(ctx context.Context, id string, questionID
|
|||
if questionID == "" {
|
||||
return nil
|
||||
}
|
||||
id = uid.DeShortID(id)
|
||||
questionID = uid.DeShortID(questionID)
|
||||
var data entity.Answer
|
||||
data.ID = id
|
||||
|
||||
|
@ -160,24 +186,34 @@ func (ar *answerRepo) UpdateAccepted(ctx context.Context, id string, questionID
|
|||
// GetByID
|
||||
func (ar *answerRepo) GetByID(ctx context.Context, id string) (*entity.Answer, bool, error) {
|
||||
var resp entity.Answer
|
||||
id = uid.DeShortID(id)
|
||||
has, err := ar.data.DB.Where("id =? ", id).Get(&resp)
|
||||
if err != nil {
|
||||
return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
resp.ID = uid.EnShortID(resp.ID)
|
||||
resp.QuestionID = uid.EnShortID(resp.QuestionID)
|
||||
return &resp, has, nil
|
||||
}
|
||||
|
||||
func (ar *answerRepo) GetByUserIDQuestionID(ctx context.Context, userID string, questionID string) (*entity.Answer, bool, error) {
|
||||
questionID = uid.DeShortID(questionID)
|
||||
var resp entity.Answer
|
||||
has, err := ar.data.DB.Where("question_id =? and user_id = ?", questionID, userID).Get(&resp)
|
||||
if err != nil {
|
||||
return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
resp.ID = uid.EnShortID(resp.ID)
|
||||
resp.QuestionID = uid.EnShortID(resp.QuestionID)
|
||||
return &resp, has, nil
|
||||
}
|
||||
|
||||
// SearchList
|
||||
func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) {
|
||||
if search.QuestionID != "" {
|
||||
search.QuestionID = uid.DeShortID(search.QuestionID)
|
||||
}
|
||||
search.ID = uid.DeShortID(search.ID)
|
||||
var count int64
|
||||
var err error
|
||||
rows := make([]*entity.Answer, 0)
|
||||
|
@ -215,6 +251,10 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
|
|||
if err != nil {
|
||||
return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range rows {
|
||||
item.ID = uid.EnShortID(item.ID)
|
||||
item.QuestionID = uid.EnShortID(item.QuestionID)
|
||||
}
|
||||
return rows, count, nil
|
||||
}
|
||||
|
||||
|
@ -224,6 +264,9 @@ func (ar *answerRepo) AdminSearchList(ctx context.Context, search *entity.AdminA
|
|||
err error
|
||||
session = ar.data.DB.Table([]string{entity.Answer{}.TableName(), "a"}).Select("a.*")
|
||||
)
|
||||
if search.QuestionID != "" {
|
||||
search.QuestionID = uid.DeShortID(search.QuestionID)
|
||||
}
|
||||
|
||||
session.Where(builder.Eq{
|
||||
"a.status": search.Status,
|
||||
|
@ -250,6 +293,7 @@ func (ar *answerRepo) AdminSearchList(ctx context.Context, search *entity.AdminA
|
|||
if strings.Contains(search.Query, "answer:") {
|
||||
idSearch = true
|
||||
id = strings.TrimSpace(strings.TrimPrefix(search.Query, "answer:"))
|
||||
id = uid.DeShortID(id)
|
||||
for _, r := range id {
|
||||
if !unicode.IsDigit(r) {
|
||||
idSearch = false
|
||||
|
@ -285,5 +329,9 @@ func (ar *answerRepo) AdminSearchList(ctx context.Context, search *entity.AdminA
|
|||
if err != nil {
|
||||
return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range rows {
|
||||
item.ID = uid.EnShortID(item.ID)
|
||||
item.QuestionID = uid.EnShortID(item.QuestionID)
|
||||
}
|
||||
return rows, count, nil
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ func NewCollectionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) collec
|
|||
|
||||
// AddCollection add collection
|
||||
func (cr *collectionRepo) AddCollection(ctx context.Context, collection *entity.Collection) (err error) {
|
||||
needAdd := false
|
||||
_, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
|
||||
var has bool
|
||||
dbcollection := &entity.Collection{}
|
||||
|
@ -41,16 +42,23 @@ func (cr *collectionRepo) AddCollection(ctx context.Context, collection *entity.
|
|||
if has {
|
||||
return
|
||||
}
|
||||
needAdd = true
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if needAdd {
|
||||
id, err := cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName())
|
||||
if err == nil {
|
||||
collection.ID = id
|
||||
_, err = session.Insert(collection)
|
||||
_, err = cr.data.DB.Insert(collection)
|
||||
if err != nil {
|
||||
return result, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
|
@ -27,6 +28,7 @@ func NewNotificationRepo(data *data.Data) notficationcommon.NotificationRepo {
|
|||
|
||||
// AddNotification add notification
|
||||
func (nr *notificationRepo) AddNotification(ctx context.Context, notification *entity.Notification) (err error) {
|
||||
notification.ObjectID = uid.DeShortID(notification.ObjectID)
|
||||
_, err = nr.data.DB.Insert(notification)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
@ -37,6 +39,7 @@ func (nr *notificationRepo) AddNotification(ctx context.Context, notification *e
|
|||
func (nr *notificationRepo) UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error) {
|
||||
now := time.Now()
|
||||
notification.UpdatedAt = now
|
||||
notification.ObjectID = uid.DeShortID(notification.ObjectID)
|
||||
_, err = nr.data.DB.Where("id =?", notification.ID).Cols("content", "updated_at").Update(notification)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
questioncommon "github.com/answerdev/answer/internal/service/question_common"
|
||||
"github.com/answerdev/answer/internal/service/unique"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -49,11 +50,13 @@ func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Questi
|
|||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
question.ID = uid.EnShortID(question.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveQuestion delete question
|
||||
func (qr *questionRepo) RemoveQuestion(ctx context.Context, id string) (err error) {
|
||||
id = uid.DeShortID(id)
|
||||
_, err = qr.data.DB.Where("id =?", id).Delete(&entity.Question{})
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
@ -63,14 +66,17 @@ func (qr *questionRepo) RemoveQuestion(ctx context.Context, id string) (err erro
|
|||
|
||||
// UpdateQuestion update question
|
||||
func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error) {
|
||||
question.ID = uid.DeShortID(question.ID)
|
||||
_, err = qr.data.DB.Where("id =?", question.ID).Cols(Cols...).Update(question)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
question.ID = uid.EnShortID(question.ID)
|
||||
return
|
||||
}
|
||||
|
||||
func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (err error) {
|
||||
questionID = uid.DeShortID(questionID)
|
||||
question := &entity.Question{}
|
||||
_, err = qr.data.DB.Where("id =?", questionID).Incr("view_count", 1).Update(question)
|
||||
if err != nil {
|
||||
|
@ -80,6 +86,7 @@ func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (e
|
|||
}
|
||||
|
||||
func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error) {
|
||||
questionID = uid.DeShortID(questionID)
|
||||
question := &entity.Question{}
|
||||
_, err = qr.data.DB.Where("id =?", questionID).Incr("answer_count", num).Update(question)
|
||||
if err != nil {
|
||||
|
@ -89,6 +96,7 @@ func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string
|
|||
}
|
||||
|
||||
func (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionID string, num int) (err error) {
|
||||
questionID = uid.DeShortID(questionID)
|
||||
question := &entity.Question{}
|
||||
_, err = qr.data.DB.Where("id =?", questionID).Incr("collection_count", num).Update(question)
|
||||
if err != nil {
|
||||
|
@ -98,6 +106,7 @@ func (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionID st
|
|||
}
|
||||
|
||||
func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error) {
|
||||
question.ID = uid.DeShortID(question.ID)
|
||||
now := time.Now()
|
||||
question.UpdatedAt = now
|
||||
_, err = qr.data.DB.Where("id =?", question.ID).Cols("status", "updated_at").Update(question)
|
||||
|
@ -108,6 +117,7 @@ func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, question *enti
|
|||
}
|
||||
|
||||
func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) {
|
||||
question.ID = uid.DeShortID(question.ID)
|
||||
_, err = qr.data.DB.Where("id =?", question.ID).Cols("status").Update(question)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
@ -116,6 +126,7 @@ func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Contex
|
|||
}
|
||||
|
||||
func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) {
|
||||
question.ID = uid.DeShortID(question.ID)
|
||||
_, err = qr.data.DB.Where("id =?", question.ID).Cols("accepted_answer_id").Update(question)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
@ -124,6 +135,7 @@ func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Que
|
|||
}
|
||||
|
||||
func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) {
|
||||
question.ID = uid.DeShortID(question.ID)
|
||||
_, err = qr.data.DB.Where("id =?", question.ID).Cols("last_answer_id").Update(question)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
@ -135,12 +147,14 @@ func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Q
|
|||
func (qr *questionRepo) GetQuestion(ctx context.Context, id string) (
|
||||
question *entity.Question, exist bool, err error,
|
||||
) {
|
||||
id = uid.DeShortID(id)
|
||||
question = &entity.Question{}
|
||||
question.ID = id
|
||||
exist, err = qr.data.DB.Where("id = ?", id).Get(question)
|
||||
if err != nil {
|
||||
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
question.ID = uid.EnShortID(question.ID)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -151,25 +165,38 @@ func (qr *questionRepo) SearchByTitleLike(ctx context.Context, title string) (qu
|
|||
if err != nil {
|
||||
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range questionList {
|
||||
item.ID = uid.EnShortID(item.ID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (qr *questionRepo) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) {
|
||||
for key, itemID := range id {
|
||||
id[key] = uid.DeShortID(itemID)
|
||||
}
|
||||
questionList = make([]*entity.Question, 0)
|
||||
err = qr.data.DB.Table("question").In("id", id).Find(&questionList)
|
||||
if err != nil {
|
||||
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range questionList {
|
||||
item.ID = uid.EnShortID(item.ID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetQuestionList get question list all
|
||||
func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Question) (questionList []*entity.Question, err error) {
|
||||
question.ID = uid.DeShortID(question.ID)
|
||||
questionList = make([]*entity.Question, 0)
|
||||
err = qr.data.DB.Find(questionList, question)
|
||||
if err != nil {
|
||||
return questionList, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range questionList {
|
||||
item.ID = uid.DeShortID(item.ID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -199,15 +226,19 @@ func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize i
|
|||
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
|
||||
session = session.Limit(pageSize, offset)
|
||||
session = session.OrderBy("question.created_at asc")
|
||||
err = session.Select("id,title,post_update_time").Find(&rows)
|
||||
err = session.Select("id,title,created_at,post_update_time").Find(&rows)
|
||||
if err != nil {
|
||||
return questionIDList, err
|
||||
}
|
||||
for _, question := range rows {
|
||||
item := &schema.SiteMapQuestionInfo{}
|
||||
item.ID = question.ID
|
||||
item.ID = uid.EnShortID(question.ID)
|
||||
item.Title = htmltext.UrlTitle(question.Title)
|
||||
item.UpdateTime = fmt.Sprintf("%v", question.PostUpdateTime.Format(time.RFC3339))
|
||||
updateTime := fmt.Sprintf("%v", question.PostUpdateTime.Format(time.RFC3339))
|
||||
if question.PostUpdateTime.Unix() < 1 {
|
||||
updateTime = fmt.Sprintf("%v", question.CreatedAt.Format(time.RFC3339))
|
||||
}
|
||||
item.UpdateTime = updateTime
|
||||
questionIDList = append(questionIDList, item)
|
||||
}
|
||||
return questionIDList, nil
|
||||
|
@ -246,6 +277,9 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
|
|||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range questionList {
|
||||
item.ID = uid.EnShortID(item.ID)
|
||||
}
|
||||
return questionList, total, err
|
||||
}
|
||||
|
||||
|
@ -277,9 +311,11 @@ func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.Admi
|
|||
idSearch = false
|
||||
id = ""
|
||||
)
|
||||
|
||||
if strings.Contains(search.Query, "question:") {
|
||||
idSearch = true
|
||||
id = strings.TrimSpace(strings.TrimPrefix(search.Query, "question:"))
|
||||
id = uid.DeShortID(id)
|
||||
for _, r := range id {
|
||||
if !unicode.IsDigit(r) {
|
||||
idSearch = false
|
||||
|
@ -308,5 +344,8 @@ func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.Admi
|
|||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return rows, count, err
|
||||
}
|
||||
for _, item := range rows {
|
||||
item.ID = uid.EnShortID(item.ID)
|
||||
}
|
||||
return rows, count, nil
|
||||
}
|
||||
|
|
|
@ -7,32 +7,44 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/answerdev/answer/internal/service/unique"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// tagRelRepo tag rel repository
|
||||
type tagRelRepo struct {
|
||||
data *data.Data
|
||||
data *data.Data
|
||||
uniqueIDRepo unique.UniqueIDRepo
|
||||
}
|
||||
|
||||
// NewTagRelRepo new repository
|
||||
func NewTagRelRepo(data *data.Data) tagcommon.TagRelRepo {
|
||||
func NewTagRelRepo(data *data.Data,
|
||||
uniqueIDRepo unique.UniqueIDRepo) tagcommon.TagRelRepo {
|
||||
return &tagRelRepo{
|
||||
data: data,
|
||||
data: data,
|
||||
uniqueIDRepo: uniqueIDRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// AddTagRelList add tag list
|
||||
func (tr *tagRelRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) {
|
||||
for _, item := range tagList {
|
||||
item.ObjectID = uid.DeShortID(item.ObjectID)
|
||||
}
|
||||
_, err = tr.data.DB.Insert(tagList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range tagList {
|
||||
item.ObjectID = uid.EnShortID(item.ObjectID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveTagRelListByObjectID delete tag list
|
||||
func (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
|
||||
objectID = uid.DeShortID(objectID)
|
||||
_, err = tr.data.DB.Where("object_id = ?", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted})
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
@ -53,12 +65,14 @@ func (tr *tagRelRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (e
|
|||
func (tr *tagRelRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectID, tagID string) (
|
||||
tagRel *entity.TagRel, exist bool, err error,
|
||||
) {
|
||||
objectID = uid.DeShortID(objectID)
|
||||
tagRel = &entity.TagRel{}
|
||||
session := tr.data.DB.Where("object_id = ?", objectID).And("tag_id = ?", tagID)
|
||||
exist, err = session.Get(tagRel)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
tagRel.ObjectID = uid.EnShortID(tagRel.ObjectID)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -73,6 +87,7 @@ func (tr *tagRelRepo) EnableTagRelByIDs(ctx context.Context, ids []int64) (err e
|
|||
|
||||
// GetObjectTagRelList get object tag relation list all
|
||||
func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string) (tagListList []*entity.TagRel, err error) {
|
||||
objectID = uid.DeShortID(objectID)
|
||||
tagListList = make([]*entity.TagRel, 0)
|
||||
session := tr.data.DB.Where("object_id = ?", objectID)
|
||||
session.Where("status = ?", entity.TagRelStatusAvailable)
|
||||
|
@ -80,11 +95,17 @@ func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string)
|
|||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range tagListList {
|
||||
item.ObjectID = uid.EnShortID(item.ObjectID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// BatchGetObjectTagRelList get object tag relation list all
|
||||
func (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) {
|
||||
for num, item := range objectIds {
|
||||
objectIds[num] = uid.DeShortID(item)
|
||||
}
|
||||
tagListList = make([]*entity.TagRel, 0)
|
||||
session := tr.data.DB.In("object_id", objectIds)
|
||||
session.Where("status = ?", entity.TagRelStatusAvailable)
|
||||
|
@ -92,6 +113,9 @@ func (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []
|
|||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
for _, item := range tagListList {
|
||||
item.ObjectID = uid.EnShortID(item.ObjectID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -178,6 +178,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
// tag
|
||||
r.GET("/question/tags", a.tagController.SearchTagLike)
|
||||
r.POST("/tag", a.tagController.AddTag)
|
||||
r.PUT("/tag", a.tagController.UpdateTag)
|
||||
r.DELETE("/tag", a.tagController.RemoveTag)
|
||||
r.PUT("/tag/synonym", a.tagController.UpdateTagSynonym)
|
||||
|
|
|
@ -10,8 +10,9 @@ type RemoveAnswerReq struct {
|
|||
// answer id
|
||||
ID string `validate:"required" json:"id"`
|
||||
// user id
|
||||
UserID string `json:"-"`
|
||||
IsAdmin bool `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
// whether user can delete it
|
||||
CanDelete bool `json:"-"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -24,6 +25,8 @@ type AnswerAddReq struct {
|
|||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
|
||||
HTML string `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
CanEdit bool `json:"-"`
|
||||
CanDelete bool `json:"-"`
|
||||
}
|
||||
|
||||
func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
|
|
|
@ -198,6 +198,8 @@ type GetCommentPersonalWithPageResp struct {
|
|||
ObjectType string `json:"object_type" enums:"question,answer,tag,comment"`
|
||||
// title
|
||||
Title string `json:"title"`
|
||||
// url title
|
||||
UrlTitle string `json:"url_title"`
|
||||
// content
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
|
|
@ -215,14 +215,16 @@ type UserAnswerInfo struct {
|
|||
CreateTime int `json:"create_time"`
|
||||
UpdateTime int `json:"update_time"`
|
||||
QuestionInfo struct {
|
||||
Title string `json:"title"`
|
||||
Tags []interface{} `json:"tags"`
|
||||
Title string `json:"title"`
|
||||
UrlTitle string `json:"url_title"`
|
||||
Tags []interface{} `json:"tags"`
|
||||
} `json:"question_info"`
|
||||
}
|
||||
|
||||
type UserQuestionInfo struct {
|
||||
ID string `json:"question_id"`
|
||||
Title string `json:"title"`
|
||||
UrlTitle string `json:"url_title"`
|
||||
VoteCount int `json:"vote_count"`
|
||||
Tags []interface{} `json:"tags"`
|
||||
ViewCount int `json:"view_count"`
|
||||
|
|
|
@ -26,6 +26,8 @@ type GetRankPersonalWithPageResp struct {
|
|||
ObjectType string `json:"object_type" enums:"question,answer,tag,comment"`
|
||||
// title
|
||||
Title string `json:"title"`
|
||||
// url title
|
||||
UrlTitle string `json:"url_title"`
|
||||
// content
|
||||
Content string `json:"content"`
|
||||
// reputation
|
||||
|
|
|
@ -13,8 +13,10 @@ import (
|
|||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
const PermaLinkQuestionIDAndTitle = 1
|
||||
const PermaLinkQuestionID = 2
|
||||
const PermaLinkQuestionIDAndTitle = 1 // /questions/10010000000000001/post-title
|
||||
const PermaLinkQuestionID = 2 // /questions/10010000000000001
|
||||
const PermaLinkQuestionIDAndTitleByShortID = 3 // /questions/11/post-title
|
||||
const PermaLinkQuestionIDByShortID = 4 // /questions/11
|
||||
|
||||
// SiteGeneralReq site general request
|
||||
type SiteGeneralReq struct {
|
||||
|
@ -26,7 +28,7 @@ type SiteGeneralReq struct {
|
|||
}
|
||||
|
||||
type SiteSeoReq struct {
|
||||
PermaLink int `validate:"required,lte=3,gte=0" form:"permalink" json:"permalink"`
|
||||
PermaLink int `validate:"required,lte=4,gte=0" form:"permalink" json:"permalink"`
|
||||
Robots string `validate:"required" form:"robots" json:"robots"`
|
||||
}
|
||||
|
||||
|
@ -40,8 +42,9 @@ func (r *SiteGeneralReq) FormatSiteUrl() {
|
|||
|
||||
// SiteInterfaceReq site interface request
|
||||
type SiteInterfaceReq struct {
|
||||
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
|
||||
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
|
||||
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
|
||||
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
|
||||
DefaultAvatar string `validate:"required,oneof=system gravatar" form:"default_avatar" json:"default_avatar"`
|
||||
}
|
||||
|
||||
// SiteBrandingReq site branding request
|
||||
|
|
|
@ -108,6 +108,8 @@ type GetTagPageResp struct {
|
|||
DisplayName string `json:"display_name"`
|
||||
// excerpt
|
||||
Excerpt string `json:"excerpt"`
|
||||
//description
|
||||
Description string `json:"description"`
|
||||
// original text
|
||||
OriginalText string `json:"original_text"`
|
||||
// parsed_text
|
||||
|
@ -127,7 +129,7 @@ type GetTagPageResp struct {
|
|||
}
|
||||
|
||||
func (tr *GetTagPageResp) GetExcerpt() {
|
||||
excerpt := strings.TrimSpace(tr.OriginalText)
|
||||
excerpt := strings.TrimSpace(tr.ParsedText)
|
||||
idx := strings.Index(excerpt, "\n")
|
||||
if idx >= 0 {
|
||||
excerpt = excerpt[0:idx]
|
||||
|
@ -161,6 +163,31 @@ type RemoveTagReq struct {
|
|||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
// AddTagReq add tag request
|
||||
type AddTagReq struct {
|
||||
// slug_name
|
||||
SlugName string `validate:"required,gt=0,lte=35" json:"slug_name"`
|
||||
// display_name
|
||||
DisplayName string `validate:"required,gt=0,lte=35" json:"display_name"`
|
||||
// original text
|
||||
OriginalText string `validate:"required,gt=0,lte=65536" json:"original_text"`
|
||||
// parsed text
|
||||
ParsedText string `json:"-"`
|
||||
// user id
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
func (req *AddTagReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
req.ParsedText = converter.Markdown2HTML(req.OriginalText)
|
||||
req.SlugName = strings.ToLower(req.SlugName)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// AddTagResp add tag response
|
||||
type AddTagResp struct {
|
||||
SlugName string `json:"slug_name"`
|
||||
}
|
||||
|
||||
// UpdateTagReq update tag request
|
||||
type UpdateTagReq struct {
|
||||
// tag_id
|
||||
|
@ -269,7 +296,8 @@ type GetFollowingTagsResp struct {
|
|||
}
|
||||
|
||||
type SearchTagLikeResp struct {
|
||||
SlugName string `json:"slug_name"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Reserved bool `json:"reserved"`
|
||||
SlugName string `json:"slug_name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Reserved bool `json:"reserved"`
|
||||
}
|
||||
|
|
|
@ -3,11 +3,13 @@ package schema
|
|||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"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/answerdev/answer/pkg/gravatar"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -66,8 +68,8 @@ type GetUserResp struct {
|
|||
Language string `json:"language"`
|
||||
// access token
|
||||
AccessToken string `json:"access_token"`
|
||||
// is admin
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
// role id
|
||||
RoleID int `json:"role_id"`
|
||||
// user status
|
||||
Status string `json:"status"`
|
||||
// user have password
|
||||
|
@ -76,7 +78,7 @@ type GetUserResp struct {
|
|||
|
||||
func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) {
|
||||
_ = copier.Copy(r, userInfo)
|
||||
r.Avatar = FormatAvatarInfo(userInfo.Avatar)
|
||||
r.Avatar = FormatAvatarInfo(userInfo.Avatar, userInfo.EMail)
|
||||
r.CreatedAt = userInfo.CreatedAt.Unix()
|
||||
r.LastLoginDate = userInfo.LastLoginDate.Unix()
|
||||
statusShow, ok := UserStatusShow[userInfo.Status]
|
||||
|
@ -102,6 +104,10 @@ func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) {
|
|||
}
|
||||
avatarInfo := &AvatarInfo{}
|
||||
_ = json.Unmarshal([]byte(userInfo.Avatar), avatarInfo)
|
||||
if constant.DefaultAvatar == "gravatar" && avatarInfo.Type == "" {
|
||||
avatarInfo.Type = "gravatar"
|
||||
avatarInfo.Gravatar = gravatar.GetAvatarURL(userInfo.EMail)
|
||||
}
|
||||
// if json.Unmarshal Error avatarInfo.Type is Empty
|
||||
r.Avatar = avatarInfo
|
||||
}
|
||||
|
@ -112,7 +118,13 @@ const (
|
|||
AvatarTypeCustom = "custom"
|
||||
)
|
||||
|
||||
func FormatAvatarInfo(avatarJson string) string {
|
||||
func FormatAvatarInfo(avatarJson, email string) (res string) {
|
||||
defer func() {
|
||||
if constant.DefaultAvatar == "gravatar" && len(res) == 0 {
|
||||
res = gravatar.GetAvatarURL(email)
|
||||
}
|
||||
}()
|
||||
|
||||
if avatarJson == "" {
|
||||
return ""
|
||||
}
|
||||
|
@ -169,18 +181,14 @@ type GetOtherUserInfoByUsernameResp struct {
|
|||
// website
|
||||
Website string `json:"website"`
|
||||
// location
|
||||
Location string `json:"location"`
|
||||
// ip info
|
||||
IPInfo string `json:"ip_info"`
|
||||
// is admin
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Location string `json:"location"`
|
||||
Status string `json:"status"`
|
||||
StatusMsg string `json:"status_msg,omitempty"`
|
||||
}
|
||||
|
||||
func (r *GetOtherUserInfoByUsernameResp) GetFromUserEntity(userInfo *entity.User) {
|
||||
_ = copier.Copy(r, userInfo)
|
||||
Avatar := FormatAvatarInfo(userInfo.Avatar)
|
||||
Avatar := FormatAvatarInfo(userInfo.Avatar, userInfo.EMail)
|
||||
r.Avatar = Avatar
|
||||
|
||||
r.CreatedAt = userInfo.CreatedAt.Unix()
|
||||
|
|
|
@ -60,6 +60,8 @@ type GetVoteWithPageResp struct {
|
|||
ObjectType string `json:"object_type" enums:"question,answer,tag,comment"`
|
||||
// title
|
||||
Title string `json:"title"`
|
||||
// url title
|
||||
UrlTitle string `json:"url_title"`
|
||||
// content
|
||||
Content string `json:"content"`
|
||||
// vote type
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
@ -60,8 +61,8 @@ func (ac *ActivityCommon) HandleActivity() {
|
|||
act := &entity.Activity{
|
||||
UserID: msg.UserID,
|
||||
TriggerUserID: msg.TriggerUserID,
|
||||
ObjectID: msg.ObjectID,
|
||||
OriginalObjectID: msg.OriginalObjectID,
|
||||
ObjectID: uid.DeShortID(msg.ObjectID),
|
||||
OriginalObjectID: uid.DeShortID(msg.OriginalObjectID),
|
||||
ActivityType: activityType,
|
||||
Cancelled: entity.ActivityAvailable,
|
||||
}
|
||||
|
|
|
@ -20,8 +20,10 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/permission"
|
||||
questioncommon "github.com/answerdev/answer/internal/service/question_common"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/internal/service/role"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/encryption"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
@ -39,6 +41,7 @@ type AnswerService struct {
|
|||
AnswerCommon *answercommon.AnswerCommon
|
||||
voteRepo activity_common.VoteRepo
|
||||
emailService *export.EmailService
|
||||
roleService *role.UserRoleRelService
|
||||
}
|
||||
|
||||
func NewAnswerService(
|
||||
|
@ -53,6 +56,7 @@ func NewAnswerService(
|
|||
answerCommon *answercommon.AnswerCommon,
|
||||
voteRepo activity_common.VoteRepo,
|
||||
emailService *export.EmailService,
|
||||
roleService *role.UserRoleRelService,
|
||||
) *AnswerService {
|
||||
return &AnswerService{
|
||||
answerRepo: answerRepo,
|
||||
|
@ -66,6 +70,7 @@ func NewAnswerService(
|
|||
AnswerCommon: answerCommon,
|
||||
voteRepo: voteRepo,
|
||||
emailService: emailService,
|
||||
roleService: roleService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +87,11 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
|
|||
if answerInfo.Status == entity.AnswerStatusDeleted {
|
||||
return nil
|
||||
}
|
||||
if !req.IsAdmin {
|
||||
roleID, err := as.roleService.GetUserRole(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if roleID != role.RoleAdminID && roleID != role.RoleModeratorID {
|
||||
if answerInfo.UserID != req.UserID {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
|
@ -92,19 +101,14 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
|
|||
if answerInfo.Accepted == schema.AnswerAcceptedEnable {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
|
||||
_, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
|
||||
if err != nil {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
if questionInfo.AnswerCount > 1 {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
if questionInfo.AcceptedAnswerID != "" {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// user add question count
|
||||
|
@ -143,6 +147,10 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
|
|||
if !exist {
|
||||
return "", errors.BadRequest(reason.QuestionNotFound)
|
||||
}
|
||||
if questionInfo.Status == entity.QuestionStatusClosed || questionInfo.Status == entity.QuestionStatusDeleted {
|
||||
err = errors.BadRequest(reason.AnswerCannotAddByClosedQuestion)
|
||||
return "", err
|
||||
}
|
||||
insertData := new(entity.Answer)
|
||||
insertData.UserID = req.UserID
|
||||
insertData.OriginalText = req.Content
|
||||
|
@ -160,7 +168,7 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
|
|||
if err != nil {
|
||||
log.Error("IncreaseAnswerCount error", err.Error())
|
||||
}
|
||||
err = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionID, insertData.ID)
|
||||
err = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionID, uid.DeShortID(insertData.ID))
|
||||
if err != nil {
|
||||
log.Error("UpdateLastAnswer error", err.Error())
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
|
|||
log.Debugf("user status updated: %+v", cacheInfo)
|
||||
userCacheInfo.UserStatus = cacheInfo.UserStatus
|
||||
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
|
||||
userCacheInfo.IsAdmin = cacheInfo.IsAdmin
|
||||
userCacheInfo.RoleID = cacheInfo.RoleID
|
||||
// update current user cache info
|
||||
err := as.authRepo.SetUserCacheInfo(ctx, accessToken, userCacheInfo)
|
||||
if err != nil {
|
||||
|
|
|
@ -18,6 +18,9 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/permission"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/encryption"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -97,6 +100,9 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objInfo.ObjectID = uid.DeShortID(objInfo.ObjectID)
|
||||
objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)
|
||||
objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)
|
||||
if objInfo.ObjectType == constant.QuestionObjectType || objInfo.ObjectType == constant.AnswerObjectType {
|
||||
comment.QuestionID = objInfo.QuestionID
|
||||
}
|
||||
|
@ -442,8 +448,10 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s
|
|||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
spew.Dump("==", objInfo)
|
||||
commentResp.ObjectType = objInfo.ObjectType
|
||||
commentResp.Title = objInfo.Title
|
||||
commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)
|
||||
commentResp.QuestionID = objInfo.QuestionID
|
||||
commentResp.AnswerID = objInfo.AnswerID
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/schema"
|
||||
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -140,6 +141,20 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
|
|||
if notificationInfo.IsRead == schema.NotificationRead {
|
||||
item.IsRead = true
|
||||
}
|
||||
answerID, ok := item.ObjectInfo.ObjectMap["answer"]
|
||||
if ok {
|
||||
if item.ObjectInfo.ObjectID == answerID {
|
||||
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
|
||||
}
|
||||
item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
|
||||
}
|
||||
questionID, ok := item.ObjectInfo.ObjectMap["question"]
|
||||
if ok {
|
||||
if item.ObjectInfo.ObjectID == questionID {
|
||||
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
|
||||
}
|
||||
item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
|
||||
}
|
||||
resp = append(resp, item)
|
||||
}
|
||||
return pager.NewPageModel(total, resp), nil
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/notice_queue"
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
@ -81,12 +82,13 @@ func (ns *NotificationCommon) HandleNotification() {
|
|||
// ObjectInfo.ObjectID
|
||||
// ObjectInfo.ObjectType
|
||||
func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) error {
|
||||
|
||||
req := &schema.NotificationContent{
|
||||
TriggerUserID: msg.TriggerUserID,
|
||||
ReceiverUserID: msg.ReceiverUserID,
|
||||
ObjectInfo: schema.ObjectInfo{
|
||||
Title: msg.Title,
|
||||
ObjectID: msg.ObjectID,
|
||||
ObjectID: uid.DeShortID(msg.ObjectID),
|
||||
ObjectType: msg.ObjectType,
|
||||
},
|
||||
NotificationAction: msg.NotificationAction,
|
||||
|
@ -100,8 +102,8 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N
|
|||
req.ObjectInfo.Title = objInfo.Title
|
||||
questionID = objInfo.QuestionID
|
||||
objectMap := make(map[string]string)
|
||||
objectMap["question"] = objInfo.QuestionID
|
||||
objectMap["answer"] = objInfo.AnswerID
|
||||
objectMap["question"] = uid.DeShortID(objInfo.QuestionID)
|
||||
objectMap["answer"] = uid.DeShortID(objInfo.AnswerID)
|
||||
objectMap["comment"] = objInfo.CommentID
|
||||
req.ObjectInfo.ObjectMap = objectMap
|
||||
}
|
||||
|
@ -196,7 +198,7 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context,
|
|||
}
|
||||
condObjectID := msg.ObjectID
|
||||
if len(questionID) > 0 {
|
||||
condObjectID = questionID
|
||||
condObjectID = uid.DeShortID(questionID)
|
||||
}
|
||||
userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, condObjectID)
|
||||
if err != nil {
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/meta"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
|
@ -147,6 +148,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
|
|||
if err != nil {
|
||||
return showinfo, err
|
||||
}
|
||||
dbinfo.ID = uid.DeShortID(dbinfo.ID)
|
||||
if !has {
|
||||
return showinfo, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
|
@ -234,6 +235,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
|||
|
||||
tagNameList := make([]string, 0)
|
||||
for _, tag := range req.Tags {
|
||||
tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-")
|
||||
tagNameList = append(tagNameList, tag.SlugName)
|
||||
}
|
||||
Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
|
||||
|
@ -374,7 +376,7 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
|
|||
log.Errorf("user DeleteQuestion rank rollback error %s", err.Error())
|
||||
}
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: questionInfo.UserID,
|
||||
UserID: req.UserID,
|
||||
ObjectID: questionInfo.ID,
|
||||
OriginalObjectID: questionInfo.ID,
|
||||
ActivityTypeKey: constant.ActQuestionDeleted,
|
||||
|
@ -480,7 +482,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
|||
question.Title = req.Title
|
||||
question.OriginalText = req.Content
|
||||
question.ParsedText = req.HTML
|
||||
question.ID = req.ID
|
||||
question.ID = uid.DeShortID(req.ID)
|
||||
question.UpdatedAt = now
|
||||
question.PostUpdateTime = now
|
||||
question.UserID = dbinfo.UserID
|
||||
|
@ -494,6 +496,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
|||
tagNameList := make([]string, 0)
|
||||
oldtagNameList := make([]string, 0)
|
||||
for _, tag := range req.Tags {
|
||||
tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-")
|
||||
tagNameList = append(tagNameList, tag.SlugName)
|
||||
}
|
||||
for _, tag := range oldTags {
|
||||
|
@ -719,12 +722,13 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o
|
|||
for _, item := range answerList {
|
||||
answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item)
|
||||
answerlist = append(answerlist, answerinfo)
|
||||
questionIDs = append(questionIDs, item.QuestionID)
|
||||
questionIDs = append(questionIDs, uid.DeShortID(item.QuestionID))
|
||||
}
|
||||
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
|
||||
if err != nil {
|
||||
return userAnswerlist, count, err
|
||||
}
|
||||
|
||||
for _, item := range answerlist {
|
||||
_, ok := questionMaps[item.QuestionID]
|
||||
if ok {
|
||||
|
@ -768,13 +772,13 @@ func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, p
|
|||
return list, count, err
|
||||
}
|
||||
for _, id := range questionIDs {
|
||||
_, ok := questionMaps[id]
|
||||
_, ok := questionMaps[uid.EnShortID(id)]
|
||||
if ok {
|
||||
questionMaps[id].LastAnsweredUserInfo = nil
|
||||
questionMaps[id].UpdateUserInfo = nil
|
||||
questionMaps[id].Content = ""
|
||||
questionMaps[id].HTML = ""
|
||||
list = append(list, questionMaps[id])
|
||||
questionMaps[uid.EnShortID(id)].LastAnsweredUserInfo = nil
|
||||
questionMaps[uid.EnShortID(id)].UpdateUserInfo = nil
|
||||
questionMaps[uid.EnShortID(id)].Content = ""
|
||||
questionMaps[uid.EnShortID(id)].HTML = ""
|
||||
list = append(list, questionMaps[uid.EnShortID(id)])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -832,6 +836,7 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin
|
|||
for _, item := range questionlist {
|
||||
info := &schema.UserQuestionInfo{}
|
||||
_ = copier.Copy(info, item)
|
||||
info.UrlTitle = htmltext.UrlTitle(info.Title)
|
||||
userQuestionlist = append(userQuestionlist, info)
|
||||
}
|
||||
|
||||
|
@ -840,6 +845,7 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin
|
|||
_ = copier.Copy(info, item)
|
||||
info.AnswerID = item.ID
|
||||
info.QuestionID = item.QuestionID
|
||||
info.QuestionInfo.UrlTitle = htmltext.UrlTitle(info.QuestionInfo.Title)
|
||||
userAnswerlist = append(userAnswerlist, info)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/permission"
|
||||
"github.com/answerdev/answer/internal/service/role"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
|
@ -88,7 +90,7 @@ func (rs *RankService) CheckOperationPermission(ctx context.Context, userID stri
|
|||
}
|
||||
}
|
||||
|
||||
can = rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
|
||||
can, _ = rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
|
||||
return can, nil
|
||||
}
|
||||
|
||||
|
@ -115,7 +117,7 @@ func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID str
|
|||
can[idx] = true
|
||||
continue
|
||||
}
|
||||
meetRank := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
|
||||
meetRank, _ := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
|
||||
can[idx] = meetRank
|
||||
}
|
||||
return can, nil
|
||||
|
@ -123,6 +125,7 @@ func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID str
|
|||
|
||||
// CheckOperationObjectOwner check operation object owner
|
||||
func (rs *RankService) CheckOperationObjectOwner(ctx context.Context, userID, objectID string) bool {
|
||||
objectID = uid.DeShortID(objectID)
|
||||
objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -138,22 +141,22 @@ func (rs *RankService) CheckOperationObjectOwner(ctx context.Context, userID, ob
|
|||
|
||||
// CheckVotePermission verify that the user has vote permission
|
||||
func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID string, voteUp bool) (
|
||||
can bool, err error) {
|
||||
can bool, rank int, err error) {
|
||||
if len(userID) == 0 || len(objectID) == 0 {
|
||||
return false, nil
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
// get the rank of the current user
|
||||
userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID)
|
||||
if err != nil {
|
||||
return can, err
|
||||
return can, 0, err
|
||||
}
|
||||
if !exist {
|
||||
return can, nil
|
||||
return can, 0, nil
|
||||
}
|
||||
objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
|
||||
if err != nil {
|
||||
return can, err
|
||||
return can, 0, err
|
||||
}
|
||||
action := ""
|
||||
switch objectInfo.ObjectType {
|
||||
|
@ -176,13 +179,13 @@ func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID
|
|||
action = permission.CommentVoteDown
|
||||
}
|
||||
}
|
||||
meetRank, rank := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
|
||||
powerMapping := rs.getUserPowerMapping(ctx, userID)
|
||||
if powerMapping[action] {
|
||||
return true, nil
|
||||
return true, rank, nil
|
||||
}
|
||||
|
||||
meetRank := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
|
||||
return meetRank, nil
|
||||
return meetRank, rank, nil
|
||||
}
|
||||
|
||||
// getUserPowerMapping get user power mapping
|
||||
|
@ -207,19 +210,19 @@ func (rs *RankService) getUserPowerMapping(ctx context.Context, userID string) (
|
|||
|
||||
// CheckRankPermission verify that the user meets the prestige criteria
|
||||
func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRank int, action string) (
|
||||
can bool) {
|
||||
can bool, rank int) {
|
||||
// get the amount of rank required for the current operation
|
||||
requireRank, err := rs.configRepo.GetInt(action)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return false
|
||||
return false, requireRank
|
||||
}
|
||||
if userRank < requireRank || requireRank < 0 {
|
||||
log.Debugf("user %s want to do action %s, but rank %d < %d",
|
||||
userID, action, userRank, requireRank)
|
||||
return false
|
||||
return false, requireRank
|
||||
}
|
||||
return true
|
||||
return true, requireRank
|
||||
}
|
||||
|
||||
// GetRankPersonalWithPage get personal comment list page
|
||||
|
@ -260,6 +263,7 @@ func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.
|
|||
commentResp.RankType = activity_type.Format(userRankInfo.ActivityType)
|
||||
commentResp.ObjectType = objInfo.ObjectType
|
||||
commentResp.Title = objInfo.Title
|
||||
commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)
|
||||
commentResp.Content = objInfo.Content
|
||||
commentResp.QuestionID = objInfo.QuestionID
|
||||
commentResp.AnswerID = objInfo.AnswerID
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/service/revision"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
||||
|
@ -20,7 +21,9 @@ type RevisionService struct {
|
|||
userRepo usercommon.UserRepo
|
||||
}
|
||||
|
||||
func NewRevisionService(revisionRepo revision.RevisionRepo, userRepo usercommon.UserRepo) *RevisionService {
|
||||
func NewRevisionService(revisionRepo revision.RevisionRepo,
|
||||
userRepo usercommon.UserRepo,
|
||||
) *RevisionService {
|
||||
return &RevisionService{
|
||||
revisionRepo: revisionRepo,
|
||||
userRepo: userRepo,
|
||||
|
@ -42,6 +45,7 @@ func (rs *RevisionService) GetUnreviewedRevisionCount(ctx context.Context, req *
|
|||
// example: user can edit the object, but need audit, the revision_id will be updated when admin approved
|
||||
func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) (
|
||||
revisionID string, err error) {
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
rev := &entity.Revision{}
|
||||
_ = copier.Copy(rev, req)
|
||||
err = rs.revisionRepo.AddRevision(ctx, rev, autoUpdateRevisionID)
|
||||
|
@ -67,6 +71,7 @@ func (rs *RevisionService) GetRevision(ctx context.Context, revisionID string) (
|
|||
|
||||
// ExistUnreviewedByObjectID
|
||||
func (rs *RevisionService) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) {
|
||||
objectID = uid.DeShortID(objectID)
|
||||
revision, exist, err = rs.revisionRepo.ExistUnreviewedByObjectID(ctx, objectID)
|
||||
return revision, exist, err
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -29,6 +30,14 @@ func NewSiteInfoService(
|
|||
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
|
||||
emailService *export.EmailService,
|
||||
tagCommonService *tagcommon.TagCommonService) *SiteInfoService {
|
||||
|
||||
resp, err := siteInfoCommonService.GetSiteInterface(context.Background())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
constant.DefaultAvatar = resp.DefaultAvatar
|
||||
}
|
||||
|
||||
return &SiteInfoService{
|
||||
siteInfoRepo: siteInfoRepo,
|
||||
siteInfoCommonService: siteInfoCommonService,
|
||||
|
@ -132,6 +141,9 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site
|
|||
}
|
||||
|
||||
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
|
||||
if err == nil {
|
||||
constant.DefaultAvatar = req.DefaultAvatar
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -280,5 +292,13 @@ func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (e
|
|||
}
|
||||
|
||||
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if req.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID || req.PermaLink == schema.PermaLinkQuestionIDByShortID {
|
||||
uid.ShortIDSwitch = true
|
||||
} else {
|
||||
uid.ShortIDSwitch = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
//go:generate mockgen -source=./siteinfo_service.go -destination=../mock/siteinfo_repo_mock.go -package=mock
|
||||
|
@ -22,9 +24,20 @@ type SiteInfoCommonService struct {
|
|||
|
||||
// NewSiteInfoCommonService new site info common service
|
||||
func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService {
|
||||
return &SiteInfoCommonService{
|
||||
siteInfo := &SiteInfoCommonService{
|
||||
siteInfoRepo: siteInfoRepo,
|
||||
}
|
||||
seoinfo, err := siteInfo.GetSiteSeo(context.Background())
|
||||
if err != nil {
|
||||
log.Error("seoinfo error", err)
|
||||
}
|
||||
if seoinfo.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID || seoinfo.PermaLink == schema.PermaLinkQuestionIDByShortID {
|
||||
uid.ShortIDSwitch = true
|
||||
} else {
|
||||
uid.ShortIDSwitch = false
|
||||
}
|
||||
|
||||
return siteInfo
|
||||
}
|
||||
|
||||
// GetSiteGeneral get site info general
|
||||
|
|
|
@ -353,10 +353,10 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
|
|||
|
||||
resp := make([]*schema.GetTagPageResp, 0)
|
||||
for _, tag := range tags {
|
||||
//excerpt := htmltext.FetchExcerpt(tag.ParsedText, "...", 240)
|
||||
resp = append(resp, &schema.GetTagPageResp{
|
||||
item := &schema.GetTagPageResp{
|
||||
TagID: tag.ID,
|
||||
SlugName: tag.SlugName,
|
||||
Description: htmltext.FetchExcerpt(tag.ParsedText, "...", 240),
|
||||
DisplayName: tag.DisplayName,
|
||||
OriginalText: tag.OriginalText,
|
||||
ParsedText: tag.ParsedText,
|
||||
|
@ -367,7 +367,10 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
|
|||
UpdatedAt: tag.UpdatedAt.Unix(),
|
||||
Recommend: tag.Recommend,
|
||||
Reserved: tag.Reserved,
|
||||
})
|
||||
}
|
||||
item.GetExcerpt()
|
||||
resp = append(resp, item)
|
||||
|
||||
}
|
||||
return pager.NewPageModel(total, resp), nil
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.Searc
|
|||
for _, tag := range tags {
|
||||
item := schema.SearchTagLikeResp{}
|
||||
item.SlugName = tag.SlugName
|
||||
item.DisplayName = tag.DisplayName
|
||||
item.Recommend = tag.Recommend
|
||||
item.Reserved = tag.Reserved
|
||||
resp = append(resp, item)
|
||||
|
@ -199,6 +200,7 @@ func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.T
|
|||
}
|
||||
tagNames := make([]string, 0)
|
||||
for _, item := range tags {
|
||||
item.SlugName = strings.ReplaceAll(item.SlugName, " ", "-")
|
||||
tagNames = append(tagNames, item.SlugName)
|
||||
}
|
||||
list, err := ts.GetTagListByNames(ctx, tagNames)
|
||||
|
@ -222,6 +224,42 @@ func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (
|
|||
return ts.TagFormat(ctx, tagsInfoList)
|
||||
}
|
||||
|
||||
// AddTag get object tag
|
||||
func (ts *TagCommonService) AddTag(ctx context.Context, req *schema.AddTagReq) (resp *schema.AddTagResp, err error) {
|
||||
_, exist, err := ts.GetTagBySlugName(ctx, req.SlugName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
return nil, errors.BadRequest(reason.TagAlreadyExist)
|
||||
}
|
||||
tagInfo := &entity.Tag{
|
||||
SlugName: strings.ReplaceAll(req.SlugName, " ", "-"),
|
||||
DisplayName: req.DisplayName,
|
||||
OriginalText: req.OriginalText,
|
||||
ParsedText: req.ParsedText,
|
||||
Status: entity.TagStatusAvailable,
|
||||
UserID: req.UserID,
|
||||
}
|
||||
tagList := []*entity.Tag{tagInfo}
|
||||
err = ts.tagCommonRepo.AddTagList(ctx, tagList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
revisionDTO := &schema.AddRevisionDTO{
|
||||
UserID: req.UserID,
|
||||
ObjectID: tagInfo.ID,
|
||||
Title: tagInfo.SlugName,
|
||||
}
|
||||
tagInfoJson, _ := json.Marshal(tagInfo)
|
||||
revisionDTO.Content = string(tagInfoJson)
|
||||
_, err = ts.revisionService.AddRevision(ctx, revisionDTO, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &schema.AddTagResp{SlugName: tagInfo.SlugName}, nil
|
||||
}
|
||||
|
||||
// AddTagList get object tag
|
||||
func (ts *TagCommonService) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
|
||||
return ts.tagCommonRepo.AddTagList(ctx, tagList)
|
||||
|
@ -520,7 +558,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
|
|||
continue
|
||||
}
|
||||
item := &entity.Tag{}
|
||||
item.SlugName = tag.SlugName
|
||||
item.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-")
|
||||
item.DisplayName = tag.DisplayName
|
||||
item.OriginalText = tag.OriginalText
|
||||
item.ParsedText = tag.ParsedText
|
||||
|
|
|
@ -241,7 +241,7 @@ func (us *UserAdminService) GetUserPage(ctx context.Context, req *schema.GetUser
|
|||
|
||||
resp := make([]*schema.GetUserPageResp, 0)
|
||||
for _, u := range users {
|
||||
avatar := schema.FormatAvatarInfo(u.Avatar)
|
||||
avatar := schema.FormatAvatarInfo(u.Avatar, u.EMail)
|
||||
t := &schema.GetUserPageResp{
|
||||
UserID: u.ID,
|
||||
CreatedAt: u.CreatedAt.Unix(),
|
||||
|
|
|
@ -100,7 +100,7 @@ func (us *UserCommon) FormatUserBasicInfo(ctx context.Context, userInfo *entity.
|
|||
userBasicInfo.Username = userInfo.Username
|
||||
userBasicInfo.Rank = userInfo.Rank
|
||||
userBasicInfo.DisplayName = userInfo.DisplayName
|
||||
userBasicInfo.Avatar = schema.FormatAvatarInfo(userInfo.Avatar)
|
||||
userBasicInfo.Avatar = schema.FormatAvatarInfo(userInfo.Avatar, userInfo.EMail)
|
||||
userBasicInfo.Website = userInfo.Website
|
||||
userBasicInfo.Location = userInfo.Location
|
||||
userBasicInfo.IPInfo = userInfo.IPInfo
|
||||
|
|
|
@ -86,7 +86,7 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
|
|||
resp = &schema.GetUserToSetShowResp{}
|
||||
resp.GetFromUserEntity(userInfo)
|
||||
resp.AccessToken = token
|
||||
resp.IsAdmin = roleID == role.RoleAdminID
|
||||
resp.RoleID = roleID
|
||||
resp.HavePassword = len(userInfo.Pass) > 0
|
||||
return resp, nil
|
||||
}
|
||||
|
@ -135,14 +135,14 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
|
|||
UserID: userInfo.ID,
|
||||
EmailStatus: userInfo.MailStatus,
|
||||
UserStatus: userInfo.Status,
|
||||
IsAdmin: roleID == role.RoleAdminID,
|
||||
RoleID: roleID,
|
||||
}
|
||||
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.IsAdmin = userCacheInfo.IsAdmin
|
||||
if resp.IsAdmin {
|
||||
resp.RoleID = userCacheInfo.RoleID
|
||||
if resp.RoleID == role.RoleAdminID {
|
||||
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, userCacheInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -369,14 +369,14 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
|
|||
UserID: userInfo.ID,
|
||||
EmailStatus: userInfo.MailStatus,
|
||||
UserStatus: userInfo.Status,
|
||||
IsAdmin: roleID == role.RoleAdminID,
|
||||
RoleID: roleID,
|
||||
}
|
||||
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
resp.IsAdmin = userCacheInfo.IsAdmin
|
||||
if resp.IsAdmin {
|
||||
resp.RoleID = userCacheInfo.RoleID
|
||||
if resp.RoleID == role.RoleAdminID {
|
||||
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -468,12 +468,27 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
|
|||
|
||||
resp = &schema.GetUserResp{}
|
||||
resp.GetFromUserEntity(userInfo)
|
||||
resp.IsAdmin = userCacheInfo.IsAdmin
|
||||
resp.AccessToken = accessToken
|
||||
userCacheInfo := &entity.UserCacheInfo{
|
||||
UserID: userInfo.ID,
|
||||
EmailStatus: userInfo.MailStatus,
|
||||
UserStatus: userInfo.Status,
|
||||
RoleID: roleID,
|
||||
}
|
||||
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// User verified email will update user email status. So user status cache should be updated.
|
||||
if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.RoleID = userCacheInfo.RoleID
|
||||
if resp.RoleID == role.RoleAdminID {
|
||||
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
@ -703,7 +718,7 @@ func (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string)
|
|||
return nil, err
|
||||
}
|
||||
for _, user := range userInfoList {
|
||||
user.Avatar = schema.FormatAvatarInfo(user.Avatar)
|
||||
user.Avatar = schema.FormatAvatarInfo(user.Avatar, user.EMail)
|
||||
userInfoMapping[user.ID] = user
|
||||
}
|
||||
return userInfoMapping, nil
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/comment_common"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/answerdev/answer/pkg/obj"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
||||
|
@ -190,6 +191,7 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
|
|||
AnswerID: objInfo.AnswerID,
|
||||
ObjectType: objInfo.ObjectType,
|
||||
Title: objInfo.Title,
|
||||
UrlTitle: htmltext.UrlTitle(objInfo.Title),
|
||||
Content: objInfo.Content,
|
||||
VoteType: activity_type.Format(voteInfo.ActivityType),
|
||||
}
|
||||
|
|
|
@ -32,7 +32,10 @@ func Markdown2HTML(source string) string {
|
|||
log.Error(err)
|
||||
return source
|
||||
}
|
||||
return buf.String()
|
||||
html := buf.String()
|
||||
filter := bluemonday.UGCPolicy()
|
||||
html = filter.Sanitize(html)
|
||||
return html
|
||||
}
|
||||
|
||||
// Markdown2BasicHTML convert markdown to html ,Only basic syntax can be used
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package uid
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/segmentfault/pacman/utils"
|
||||
)
|
||||
|
||||
const salt = int64(100)
|
||||
|
||||
var ShortIDSwitch = false
|
||||
|
||||
// NumToString num to string
|
||||
func NumToShortID(id int64) string {
|
||||
sid := strconv.FormatInt(id, 10)
|
||||
if len(sid) < 17 {
|
||||
return ""
|
||||
}
|
||||
sTypeCode := sid[1:4]
|
||||
sid = sid[4:int32(len(sid))]
|
||||
id, err := strconv.ParseInt(sid, 10, 64)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
typeCode, err := strconv.ParseInt(sTypeCode, 10, 64)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
code := utils.EnShortID(id, salt)
|
||||
tcode := utils.EnShortID(typeCode, salt)
|
||||
return string(tcode) + string(code)
|
||||
}
|
||||
|
||||
// StringToNum string to num
|
||||
func ShortIDToNum(code string) int64 {
|
||||
if len(code) < 2 {
|
||||
return 0
|
||||
}
|
||||
scodeType := code[0:2]
|
||||
code = code[2:int32(len(code))]
|
||||
|
||||
id := utils.DeShortID(code, salt)
|
||||
codeType := utils.DeShortID(scodeType, salt)
|
||||
return 10000000000000000 + codeType*10000000000000 + id
|
||||
}
|
||||
|
||||
func EnShortID(id string) string {
|
||||
if ShortIDSwitch {
|
||||
num, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return NumToShortID(num)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func DeShortID(sid string) string {
|
||||
num, err := strconv.ParseInt(sid, 10, 64)
|
||||
if err != nil {
|
||||
return strconv.FormatInt(ShortIDToNum(sid), 10)
|
||||
}
|
||||
if num < 10000000000000000 {
|
||||
return strconv.FormatInt(ShortIDToNum(sid), 10)
|
||||
}
|
||||
return sid
|
||||
}
|
||||
|
||||
func IsShortID(id string) bool {
|
||||
num, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if num < 10000000000000000 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package uid
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_ShortID(t *testing.T) {
|
||||
nums := []int64{
|
||||
10010000000000001, 10010000000000002, 10010000000000003, 10010000000000004,
|
||||
10030000000000689, 10010000000000676, 10020000000000658, 10020000000000654,
|
||||
10030000009000689, 10010000009000676, 10020000009000658, 10020000009999999,
|
||||
10030000090000689, 10010000090000676, 10020000090000658, 10020000099999999,
|
||||
10030000900000689, 10010000900000676, 10020000900000658, 10020000999999999,
|
||||
10030009000000689, 10010009000000676, 10020009000000658, 10020009999999999,
|
||||
10030090000000689, 10010090000000676, 10020090000000658, 10020099999999999,
|
||||
10030900000000689, 10010900000000676, 10020900000000658, 10020999999999999,
|
||||
10039000000000689, 10019000000000676, 10029000000000658, 10029999999999999,
|
||||
10610000000000689, 10610000000000676, 10610000000000658, 10610000000000654,
|
||||
19990000000000689, 19990000000000676, 19990000000000658, 19990000000000654,
|
||||
}
|
||||
for _, num := range nums {
|
||||
code := NumToShortID(num)
|
||||
denum := ShortIDToNum(code)
|
||||
fmt.Println(num, code, denum)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_EnDeShortID(t *testing.T) {
|
||||
nums := []string{"0", "1", "10", "100", "1000", "10000", "100000", "1234567", "10000000000000000", "10010000000001316", "19930000000001316"}
|
||||
ShortIDSwitch = true
|
||||
for _, num := range nums {
|
||||
code := EnShortID(num)
|
||||
denum := DeShortID(code)
|
||||
fmt.Println(num, code, denum)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Demo(t *testing.T) {
|
||||
nums := []int64{0, 1, 10, 100, 1000, 10000, 100000, 1000000001316, 9000000001316, 10000000000000000, 10010000000001316, 10030000000001316, 99999999999999999, 999999999999999999, 1999999999999999999}
|
||||
for _, num := range nums {
|
||||
code := strconv.FormatInt(num, 36) //10 yo 16
|
||||
fmt.Println(num, code)
|
||||
}
|
||||
}
|
|
@ -601,3 +601,14 @@ export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
|
|||
'reopened',
|
||||
'closed',
|
||||
];
|
||||
|
||||
export const SYSTEM_AVATAR_OPTIONS = [
|
||||
{
|
||||
label: 'System',
|
||||
value: 'system',
|
||||
},
|
||||
{
|
||||
label: 'Gravatar',
|
||||
value: 'gravatar',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -26,13 +26,13 @@ export interface ReportParams {
|
|||
export interface TagBase {
|
||||
display_name: string;
|
||||
slug_name: string;
|
||||
recommend: boolean;
|
||||
reserved: boolean;
|
||||
original_text?: string;
|
||||
recommend?: boolean;
|
||||
reserved?: boolean;
|
||||
}
|
||||
|
||||
export interface Tag extends TagBase {
|
||||
main_tag_slug_name?: string;
|
||||
original_text?: string;
|
||||
parsed_text?: string;
|
||||
}
|
||||
|
||||
|
@ -103,6 +103,11 @@ export interface ModifyUserReq {
|
|||
website: string;
|
||||
}
|
||||
|
||||
enum RoleId {
|
||||
User = 1,
|
||||
Admin = 2,
|
||||
Moderator = 3,
|
||||
}
|
||||
export interface UserInfoBase {
|
||||
id?: string;
|
||||
avatar: any;
|
||||
|
@ -116,7 +121,7 @@ export interface UserInfoBase {
|
|||
*/
|
||||
status?: string;
|
||||
/** roles */
|
||||
is_admin?: boolean;
|
||||
role_id: RoleId;
|
||||
}
|
||||
|
||||
export interface UserInfoRes extends UserInfoBase {
|
||||
|
@ -129,7 +134,6 @@ export interface UserInfoRes extends UserInfoBase {
|
|||
*/
|
||||
mail_status: number;
|
||||
language: string;
|
||||
is_admin: boolean;
|
||||
e_mail?: string;
|
||||
have_password: boolean;
|
||||
[prop: string]: any;
|
||||
|
@ -305,6 +309,7 @@ export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {
|
|||
export interface AdminSettingsInterface {
|
||||
language: string;
|
||||
time_zone?: string;
|
||||
default_avatar?: string;
|
||||
}
|
||||
|
||||
export interface AdminSettingsSmtp {
|
||||
|
|
|
@ -4,7 +4,6 @@ const pattern = {
|
|||
emoji: emojiRegex(),
|
||||
email:
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/,
|
||||
isAnswerId: /^1002\d{13}$/,
|
||||
};
|
||||
|
||||
export default pattern;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { Icon } from '@/components';
|
||||
import './index.css';
|
||||
|
||||
|
@ -103,10 +104,12 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
|||
|
||||
const [openKey, setOpenKey] = useState(getOpenKey());
|
||||
const menuClick = (evt, menu, href, isLeaf) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
if (isLeaf) {
|
||||
navigate(href);
|
||||
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
evt.preventDefault();
|
||||
navigate(href);
|
||||
}
|
||||
} else {
|
||||
setOpenKey(openKey === menu.name ? '' : menu.name);
|
||||
}
|
||||
|
|
|
@ -107,7 +107,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
onClick={() => handleVote('up')}>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
</Button>
|
||||
<Button variant="outline-dark text-body" disabled>
|
||||
<Button variant="outline-secondary" className="opacity-100" disabled>
|
||||
{votes}
|
||||
</Button>
|
||||
<Button
|
||||
|
|
|
@ -5,6 +5,7 @@ import { NavLink, useNavigate } from 'react-router-dom';
|
|||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { Avatar, Icon } from '@/components';
|
||||
import { floppyNavigation } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
redDot: Type.NotificationStatus | undefined;
|
||||
|
@ -16,10 +17,12 @@ 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);
|
||||
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
evt.preventDefault();
|
||||
const { href } = evt.currentTarget;
|
||||
const { pathname } = new URL(href);
|
||||
navigate(pathname);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
@ -63,7 +66,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
onClick={handleLinkClick}>
|
||||
{t('header.nav.setting')}
|
||||
</Dropdown.Item>
|
||||
{userInfo?.is_admin ? (
|
||||
{userInfo?.role_id === 2 ? (
|
||||
<Dropdown.Item href="/admin" onClick={handleLinkClick}>
|
||||
{t('header.nav.admin')}
|
||||
</Dropdown.Item>
|
||||
|
|
|
@ -9,9 +9,6 @@
|
|||
}
|
||||
|
||||
.nav-link {
|
||||
&.active {
|
||||
font-weight: 700;
|
||||
}
|
||||
&.icon-link {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
|
|
@ -70,14 +70,17 @@ const Header: FC = () => {
|
|||
window.location.replace(window.location.href);
|
||||
};
|
||||
const onLoginClick = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (location.pathname === '/users/login') {
|
||||
evt.preventDefault();
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
floppyNavigation.navigateToLogin((loginPath) => {
|
||||
navigate(loginPath, { replace: true });
|
||||
});
|
||||
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
evt.preventDefault();
|
||||
floppyNavigation.navigateToLogin((loginPath) => {
|
||||
navigate(loginPath, { replace: true });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
reopenQuestion,
|
||||
} from '@/services';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import { floppyNavigation } from '@/utils';
|
||||
|
||||
interface IProps {
|
||||
type: 'answer' | 'question';
|
||||
|
@ -109,6 +110,9 @@ const Index: FC<IProps> = ({
|
|||
}
|
||||
};
|
||||
const handleEdit = (evt, targetUrl) => {
|
||||
if (!floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
return;
|
||||
}
|
||||
evt.preventDefault();
|
||||
let checkObjectId = qid;
|
||||
if (type === 'answer') {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FC, useEffect } from 'react';
|
||||
import { FC, useEffect, useLayoutEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
|
||||
import { brandingStore, pageTagStore, siteInfoStore } from '@/stores';
|
||||
|
@ -23,10 +23,21 @@ const Index: FC = () => {
|
|||
);
|
||||
}
|
||||
};
|
||||
const setDocTitle = () => {
|
||||
try {
|
||||
if (pageTitle) {
|
||||
document.title = pageTitle;
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (ex) {}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAppGenerator();
|
||||
}, [appVersion]);
|
||||
useLayoutEffect(() => {
|
||||
setDocTitle();
|
||||
}, [pageTitle]);
|
||||
return (
|
||||
<Helmet>
|
||||
<link
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Pagination } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { scrollToDocTop } from '@/utils';
|
||||
import { scrollToDocTop, floppyNavigation } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
currentPage: number;
|
||||
|
@ -48,10 +48,12 @@ const PageItem = ({ page, currentPage, path }: PageItemProps) => {
|
|||
active={currentPage === page}
|
||||
href={path}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigate(path);
|
||||
scrollToDocTop();
|
||||
if (floppyNavigation.shouldProcessLinkClick(e)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigate(path);
|
||||
scrollToDocTop();
|
||||
}
|
||||
}}>
|
||||
{page}
|
||||
</Pagination.Item>
|
||||
|
@ -91,9 +93,11 @@ const Index: FC<Props> = ({
|
|||
<Pagination.Prev
|
||||
href={handleParams(currentPage - 1)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(handleParams(currentPage - 1));
|
||||
scrollToDocTop();
|
||||
if (floppyNavigation.shouldProcessLinkClick(e)) {
|
||||
e.preventDefault();
|
||||
navigate(handleParams(currentPage - 1));
|
||||
scrollToDocTop();
|
||||
}
|
||||
}}>
|
||||
{t('prev')}
|
||||
</Pagination.Prev>
|
||||
|
@ -186,9 +190,11 @@ const Index: FC<Props> = ({
|
|||
disabled={currentPage === totalPage}
|
||||
href={handleParams(currentPage + 1)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(handleParams(currentPage + 1));
|
||||
scrollToDocTop();
|
||||
if (floppyNavigation.shouldProcessLinkClick(e)) {
|
||||
e.preventDefault();
|
||||
navigate(handleParams(currentPage + 1));
|
||||
scrollToDocTop();
|
||||
}
|
||||
}}>
|
||||
{t('next')}
|
||||
</Pagination.Next>
|
||||
|
|
|
@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
data;
|
||||
i18nKeyPrefix: string;
|
||||
|
@ -39,12 +41,14 @@ const Index: FC<Props> = ({
|
|||
};
|
||||
|
||||
const handleClick = (e, type) => {
|
||||
e.preventDefault();
|
||||
const str = handleParams(type);
|
||||
if (pathname) {
|
||||
navigate(`${pathname}${str}`);
|
||||
} else {
|
||||
setUrlSearchParams(str);
|
||||
if (floppyNavigation.shouldProcessLinkClick(e)) {
|
||||
e.preventDefault();
|
||||
if (pathname) {
|
||||
navigate(`${pathname}${str}`);
|
||||
} else {
|
||||
setUrlSearchParams(str);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FC } from 'react';
|
||||
import { ListGroup } from 'react-bootstrap';
|
||||
import { NavLink, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { NavLink, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
@ -15,7 +15,6 @@ import {
|
|||
QuestionListLoader,
|
||||
Counts,
|
||||
} from '@/components';
|
||||
import { useQuestionList } from '@/services';
|
||||
|
||||
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
|
||||
'newest',
|
||||
|
@ -27,28 +26,17 @@ const QuestionOrderKeys: Type.QuestionOrderBy[] = [
|
|||
|
||||
interface Props {
|
||||
source: 'questions' | 'tag';
|
||||
data;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const QuestionList: FC<Props> = ({ source }) => {
|
||||
const QuestionList: FC<Props> = ({ source, data, isLoading = false }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
const { tagName = '' } = useParams();
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const curOrder = urlSearchParams.get('order') || QuestionOrderKeys[0];
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
const pageSize = 20;
|
||||
const reqParams: Type.QueryQuestionsReq = {
|
||||
page_size: pageSize,
|
||||
page: curPage,
|
||||
order: curOrder as Type.QuestionOrderBy,
|
||||
tag: tagName,
|
||||
};
|
||||
|
||||
if (source === 'questions') {
|
||||
delete reqParams.tag;
|
||||
}
|
||||
const { data: listData, isLoading } = useQuestionList(reqParams);
|
||||
const count = listData?.count || 0;
|
||||
|
||||
const count = data?.count || 0;
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 d-flex flex-wrap justify-content-between">
|
||||
|
@ -68,7 +56,7 @@ const QuestionList: FC<Props> = ({ source }) => {
|
|||
{isLoading ? (
|
||||
<QuestionListLoader />
|
||||
) : (
|
||||
listData?.list?.map((li) => {
|
||||
data?.list?.map((li) => {
|
||||
return (
|
||||
<ListGroup.Item
|
||||
key={li.id}
|
||||
|
|
|
@ -30,7 +30,7 @@ const Index: FC<IProps> = ({
|
|||
data.recommend && 'badge-tag-required',
|
||||
className,
|
||||
)}>
|
||||
<span className={textClassName}>{data.slug_name}</span>
|
||||
<span className={textClassName}>{data.display_name}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -202,7 +202,7 @@ const TagSelector: FC<IProps> = ({
|
|||
item.reserved ? 'danger' : item.recommend ? 'dark' : 'secondary'
|
||||
}`}
|
||||
size="sm">
|
||||
{item.slug_name}
|
||||
{item.display_name}
|
||||
<span className="ms-1" onMouseUp={() => handleRemove(item)}>
|
||||
×
|
||||
</span>
|
||||
|
@ -247,7 +247,7 @@ const TagSelector: FC<IProps> = ({
|
|||
eventKey={index}
|
||||
active={index === currentIndex}
|
||||
onClick={() => handleClick(item)}>
|
||||
{item.slug_name}
|
||||
{item.display_name}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -34,6 +34,8 @@ import QuestionListLoader from './QuestionListLoader';
|
|||
import TagsLoader from './TagsLoader';
|
||||
import WelcomeTitle from './WelcomeTitle';
|
||||
import Counts from './Counts';
|
||||
import QuestionList from './QuestionList';
|
||||
import HotQuestions from './HotQuestions';
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
|
@ -74,5 +76,7 @@ export {
|
|||
TagsLoader,
|
||||
WelcomeTitle,
|
||||
Counts,
|
||||
QuestionList,
|
||||
HotQuestions,
|
||||
};
|
||||
export type { EditorRef, JSONSchema, UISchema };
|
||||
|
|
|
@ -294,6 +294,12 @@ img:not(a img, img.broken) {
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.reset-p {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bg-fade-out {
|
||||
0%,
|
||||
25% {
|
||||
|
|
|
@ -3,6 +3,9 @@ import { Container, Button } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_404' });
|
||||
useEffect(() => {
|
||||
|
@ -14,6 +17,10 @@ const Index = () => {
|
|||
pageWrap.style.display = 'block';
|
||||
};
|
||||
}, []);
|
||||
|
||||
usePageTags({
|
||||
title: t('http_404', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
return (
|
||||
<Container
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
|
|
|
@ -3,6 +3,9 @@ import { Container, Button } from 'react-bootstrap';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_50X' });
|
||||
useEffect(() => {
|
||||
|
@ -14,8 +17,14 @@ const Index = () => {
|
|||
pageWrap.style.display = 'block';
|
||||
};
|
||||
}, []);
|
||||
|
||||
usePageTags({
|
||||
title: t('http_50X', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
return (
|
||||
<Container className="d-flex flex-column justify-content-center align-items-center page-wrap">
|
||||
<Container
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
style={{ flex: 1 }}>
|
||||
<div
|
||||
className="mb-4 text-secondary"
|
||||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
|
|
|
@ -7,10 +7,14 @@ import {
|
|||
FormDataType,
|
||||
AdminSettingsInterface,
|
||||
} from '@/common/interface';
|
||||
import { interfaceStore } from '@/stores';
|
||||
import { JSONSchema, SchemaForm, UISchema, initFormData } from '@/components';
|
||||
import { DEFAULT_TIMEZONE } from '@/common/constants';
|
||||
import { updateInterfaceSetting, useInterfaceSetting } from '@/services';
|
||||
import { interfaceStore, loggedUserInfoStore } from '@/stores';
|
||||
import { JSONSchema, SchemaForm, UISchema } from '@/components';
|
||||
import { DEFAULT_TIMEZONE, SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
|
||||
import {
|
||||
updateInterfaceSetting,
|
||||
useInterfaceSetting,
|
||||
getLoggedUserInfo,
|
||||
} from '@/services';
|
||||
import {
|
||||
setupAppLanguage,
|
||||
loadLanguageOptions,
|
||||
|
@ -44,10 +48,33 @@ const Interface: FC = () => {
|
|||
description: t('time_zone.text'),
|
||||
default: setting?.time_zone || DEFAULT_TIMEZONE,
|
||||
},
|
||||
default_avatar: {
|
||||
type: 'string',
|
||||
title: t('avatar.label'),
|
||||
description: t('avatar.text'),
|
||||
enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value),
|
||||
enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
language: {
|
||||
value: setting?.language || storeInterface.language,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
time_zone: {
|
||||
value: setting?.time_zone || DEFAULT_TIMEZONE,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
default_avatar: {
|
||||
value: setting?.default_avatar || 'system',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
|
||||
const uiSchema: UISchema = {
|
||||
language: {
|
||||
|
@ -56,6 +83,9 @@ const Interface: FC = () => {
|
|||
time_zone: {
|
||||
'ui:widget': 'timezone',
|
||||
},
|
||||
default_avatar: {
|
||||
'ui:widget': 'select',
|
||||
},
|
||||
};
|
||||
const getLangs = async () => {
|
||||
const res: LangsType[] = await loadLanguageOptions(true);
|
||||
|
@ -88,6 +118,7 @@ const Interface: FC = () => {
|
|||
const reqParams: AdminSettingsInterface = {
|
||||
language: formData.language.value,
|
||||
time_zone: formData.time_zone.value,
|
||||
default_avatar: formData.default_avatar.value,
|
||||
};
|
||||
|
||||
updateInterfaceSetting(reqParams)
|
||||
|
@ -95,6 +126,9 @@ const Interface: FC = () => {
|
|||
interfaceStore.getState().update(reqParams);
|
||||
setupAppLanguage();
|
||||
setupAppTimeZone();
|
||||
getLoggedUserInfo().then((info) => {
|
||||
loggedUserInfoStore.getState().update(info);
|
||||
});
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
|
@ -112,12 +146,10 @@ const Interface: FC = () => {
|
|||
if (setting) {
|
||||
const formMeta = {};
|
||||
Object.keys(setting).forEach((k) => {
|
||||
// In this form, all form items are `Select` and must have a default value
|
||||
let fieldVal = setting[k];
|
||||
if (!fieldVal && formData[k] && formData[k].value) {
|
||||
fieldVal = formData[k].value;
|
||||
formMeta[k] = { ...formData[k], value: setting[k] };
|
||||
if (k === 'default_avatar') {
|
||||
formMeta[k].value = setting[k] || 'system';
|
||||
}
|
||||
formMeta[k] = { ...formData[k], value: fieldVal };
|
||||
});
|
||||
setFormData({ ...formData, ...formMeta });
|
||||
}
|
||||
|
|
|
@ -19,8 +19,13 @@ const Index: FC = () => {
|
|||
type: 'number',
|
||||
title: t('permalink.label'),
|
||||
description: t('permalink.text'),
|
||||
enum: [1, 2],
|
||||
enumNames: ['/questions/123/post-title', '/questions/123'],
|
||||
enum: [1, 2, 3, 4],
|
||||
enumNames: [
|
||||
'/questions/10010000000000001/post-title',
|
||||
'/questions/10010000000000001',
|
||||
'/questions/D1D1/post-title',
|
||||
'/questions/D1D1',
|
||||
],
|
||||
default: 1,
|
||||
},
|
||||
robots: {
|
||||
|
@ -74,7 +79,7 @@ const Index: FC = () => {
|
|||
const formMeta = { ...formData };
|
||||
formMeta.robots.value = setting.robots;
|
||||
formMeta.permalink.value = setting.permalink;
|
||||
if (formMeta.permalink.value !== 1 && formMeta.permalink.value !== 2) {
|
||||
if (!/[1234]/.test(formMeta.permalink.value)) {
|
||||
formMeta.permalink.value = 1;
|
||||
}
|
||||
setFormData(formMeta);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { HelmetProvider } from 'react-helmet-async';
|
|||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { toastStore, loginToContinueStore, notFoundStore } from '@/stores';
|
||||
import { toastStore, loginToContinueStore, errorCode } from '@/stores';
|
||||
import {
|
||||
Header,
|
||||
Footer,
|
||||
|
@ -16,6 +16,7 @@ import {
|
|||
import { LoginToContinueModal } from '@/components/Modal';
|
||||
import { useImgViewer } from '@/hooks';
|
||||
import Component404 from '@/pages/404';
|
||||
import Component50X from '@/pages/50X';
|
||||
|
||||
const Layout: FC = () => {
|
||||
const location = useLocation();
|
||||
|
@ -23,13 +24,13 @@ const Layout: FC = () => {
|
|||
const closeToast = () => {
|
||||
toastClear();
|
||||
};
|
||||
const { visible: show404, hide: notFoundHide } = notFoundStore();
|
||||
const { code: httpStatusCode, reset: httpStatusReset } = errorCode();
|
||||
|
||||
const imgViewer = useImgViewer();
|
||||
const { show: showLoginToContinueModal } = loginToContinueStore();
|
||||
|
||||
useEffect(() => {
|
||||
notFoundHide();
|
||||
httpStatusReset();
|
||||
}, [location]);
|
||||
return (
|
||||
<HelmetProvider>
|
||||
|
@ -44,7 +45,13 @@ const Layout: FC = () => {
|
|||
<div
|
||||
className="position-relative page-wrap"
|
||||
onClick={imgViewer.checkClickForImgView}>
|
||||
{show404 ? <Component404 /> : <Outlet />}
|
||||
{httpStatusCode === '404' ? (
|
||||
<Component404 />
|
||||
) : httpStatusCode === '50X' ? (
|
||||
<Component50X />
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
</div>
|
||||
<Toast msg={toastMsg} variant={variant} onClose={closeToast} />
|
||||
<Footer />
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
} from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Pattern from '@/common/pattern';
|
||||
import { Pagination } from '@/components';
|
||||
import { loggedUserInfoStore, toastStore } from '@/stores';
|
||||
import { scrollToElementTop } from '@/utils';
|
||||
|
@ -40,7 +39,7 @@ const Index = () => {
|
|||
* Note: Compatible with Permalink
|
||||
*/
|
||||
let { aid = '' } = useParams();
|
||||
if (!aid && Pattern.isAnswerId.test(slugPermalink)) {
|
||||
if (!aid && slugPermalink) {
|
||||
aid = slugPermalink;
|
||||
}
|
||||
|
||||
|
@ -56,7 +55,7 @@ const Index = () => {
|
|||
const { setUsers } = usePageUsers();
|
||||
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||
const isAuthor = userInfo?.username === question?.user_info?.username;
|
||||
const isAdmin = userInfo?.is_admin;
|
||||
const isAdmin = userInfo?.role_id === 2;
|
||||
const isLogged = Boolean(userInfo?.access_token);
|
||||
const { state: locationState } = useLocation();
|
||||
|
||||
|
@ -76,9 +75,10 @@ const Index = () => {
|
|||
page: 1,
|
||||
page_size: 999,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
res.list = res.list?.filter((v) => {
|
||||
// delete answers pnly show to author and admin and has searchparams aid
|
||||
// delete answers only show to author and admin and has search params aid
|
||||
if (v.status === 10) {
|
||||
if (
|
||||
(v?.user_info.username === userInfo?.username || isAdmin) &&
|
||||
|
@ -240,15 +240,17 @@ const Index = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !question?.operation?.operation_type && (
|
||||
<WriteAnswer
|
||||
data={{
|
||||
qid,
|
||||
answered: question?.answered,
|
||||
}}
|
||||
callback={writeAnswerCallback}
|
||||
/>
|
||||
)}
|
||||
{!isLoading &&
|
||||
Number(question?.status) !== 2 &&
|
||||
!question?.operation?.type && (
|
||||
<WriteAnswer
|
||||
data={{
|
||||
qid,
|
||||
answered: question?.answered,
|
||||
}}
|
||||
callback={writeAnswerCallback}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<RelatedQuestions id={question?.id || ''} />
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
import { FC } from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { useMatch, Link } from 'react-router-dom';
|
||||
import { useMatch, Link, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { FollowingTags } from '@/components';
|
||||
import QuestionList from '@/components/QuestionList';
|
||||
import HotQuestions from '@/components/HotQuestions';
|
||||
import { FollowingTags, QuestionList, HotQuestions } from '@/components';
|
||||
import { siteInfoStore, loggedUserInfoStore } from '@/stores';
|
||||
import { useQuestionList } from '@/services';
|
||||
import * as Type from '@/common/interface';
|
||||
|
||||
const Questions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
const { t: t2 } = useTranslation('translation');
|
||||
const { user: loggedUser } = loggedUserInfoStore((_) => _);
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
const curOrder = urlSearchParams.get('order') || 'newest';
|
||||
const reqParams: Type.QueryQuestionsReq = {
|
||||
page_size: 20,
|
||||
page: curPage,
|
||||
order: curOrder as Type.QuestionOrderBy,
|
||||
};
|
||||
const { data: listData, isLoading: listLoading } = useQuestionList(reqParams);
|
||||
const isIndexPage = useMatch('/');
|
||||
let pageTitle = t('questions', { keyPrefix: 'page_title' });
|
||||
let slogan = '';
|
||||
|
@ -27,7 +36,11 @@ const Questions: FC = () => {
|
|||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col xxl={7} lg={8} sm={12}>
|
||||
<QuestionList source="questions" />
|
||||
<QuestionList
|
||||
source="questions"
|
||||
data={listData}
|
||||
isLoading={listLoading}
|
||||
/>
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
{!loggedUser.access_token && (
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { usePageTags, usePromptWithUnload } from '@/hooks';
|
||||
import { Editor, EditorRef } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { createTag } from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
interface FormDataItem {
|
||||
displayName: Type.FormValue<string>;
|
||||
slugName: Type.FormValue<string>;
|
||||
description: Type.FormValue<string>;
|
||||
}
|
||||
const initFormData = {
|
||||
displayName: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
slugName: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
description: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
};
|
||||
|
||||
const Index = () => {
|
||||
const { role_id = 1 } = loggedUserInfoStore((state) => state.user);
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'tag_modal' });
|
||||
const [focusType, setForceType] = useState('');
|
||||
|
||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||
const [immData] = useState(initFormData);
|
||||
const [contentChanged, setContentChanged] = useState(false);
|
||||
|
||||
const editorRef = useRef<EditorRef>({
|
||||
getHtml: () => '',
|
||||
});
|
||||
|
||||
usePromptWithUnload({
|
||||
when: contentChanged,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { displayName, slugName, description } = formData;
|
||||
const {
|
||||
displayName: display_name,
|
||||
slugName: slug_name,
|
||||
description: original_text,
|
||||
} = immData;
|
||||
if (!display_name || !slug_name || !original_text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
display_name.value !== displayName.value ||
|
||||
slug_name.value !== slugName.value ||
|
||||
original_text.value !== description.value
|
||||
) {
|
||||
setContentChanged(true);
|
||||
} else {
|
||||
setContentChanged(false);
|
||||
}
|
||||
}, [
|
||||
formData.displayName.value,
|
||||
formData.slugName.value,
|
||||
formData.description.value,
|
||||
]);
|
||||
|
||||
const handleDescriptionChange = (value: string) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
description: { ...formData.description, value },
|
||||
});
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setContentChanged(false);
|
||||
const params = {
|
||||
display_name: formData.displayName.value,
|
||||
slug_name: formData.slugName.value,
|
||||
original_text: formData.description.value,
|
||||
};
|
||||
createTag(params)
|
||||
.then((res) => {
|
||||
navigate(`/tags/${res.slug_name}/info`, {
|
||||
replace: true,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData, [
|
||||
{ from: 'display_name', to: 'displayName' },
|
||||
{ from: 'slug_name', to: 'slugName' },
|
||||
{ from: 'original_text', to: 'description' },
|
||||
]);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
displayName: { ...formData.displayName, value: e.currentTarget.value },
|
||||
});
|
||||
};
|
||||
|
||||
const handleSlugNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
slugName: { ...formData.slugName, value: e.currentTarget.value },
|
||||
});
|
||||
};
|
||||
|
||||
usePageTags({
|
||||
title: t('create_tag', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
return (
|
||||
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
|
||||
<Row className="justify-content-center">
|
||||
<Col xxl={10} md={12}>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="justify-content-center">
|
||||
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="display_name" className="mb-3">
|
||||
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
value={formData.displayName.value}
|
||||
isInvalid={formData.displayName.isInvalid}
|
||||
disabled={role_id !== 2 && role_id !== 3}
|
||||
onChange={handleDisplayNameChange}
|
||||
/>
|
||||
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.displayName.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="slug_name" className="mb-3">
|
||||
<Form.Label>{t('form.fields.slug_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
value={formData.slugName.value}
|
||||
isInvalid={formData.slugName.isInvalid}
|
||||
disabled={role_id !== 2 && role_id !== 3}
|
||||
onChange={handleSlugNameChange}
|
||||
/>
|
||||
<Form.Text as="div">{t('form.fields.slug_name.desc')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.slugName.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="description" className="mt-4">
|
||||
<Form.Label>{t('form.fields.desc.label')}</Form.Label>
|
||||
<Editor
|
||||
value={formData.description.value}
|
||||
onChange={handleDescriptionChange}
|
||||
className={classNames(
|
||||
'form-control p-0',
|
||||
focusType === 'description' && 'focus',
|
||||
)}
|
||||
onFocus={() => {
|
||||
setForceType('description');
|
||||
}}
|
||||
onBlur={() => {
|
||||
setForceType('');
|
||||
}}
|
||||
ref={editorRef}
|
||||
/>
|
||||
<Form.Control
|
||||
value={formData.description.value}
|
||||
type="text"
|
||||
isInvalid={formData.description.isInvalid}
|
||||
readOnly
|
||||
hidden
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.description.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<div className="mt-3">
|
||||
<Button type="submit">{t('btn_post')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<Card>
|
||||
<Card.Header>
|
||||
{t('title', { keyPrefix: 'how_to_format' })}
|
||||
</Card.Header>
|
||||
<Card.Body
|
||||
className="fmt small"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('desc', { keyPrefix: 'how_to_format' }),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -1,12 +1,22 @@
|
|||
import { FC, useEffect, useState } from 'react';
|
||||
import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
useParams,
|
||||
Link,
|
||||
useNavigate,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import * as Type from '@/common/interface';
|
||||
import { FollowingTags } from '@/components';
|
||||
import { useTagInfo, useFollow, useQuerySynonymsTags } from '@/services';
|
||||
import {
|
||||
useTagInfo,
|
||||
useFollow,
|
||||
useQuerySynonymsTags,
|
||||
useQuestionList,
|
||||
} from '@/services';
|
||||
import QuestionList from '@/components/QuestionList';
|
||||
import HotQuestions from '@/components/HotQuestions';
|
||||
import { escapeRemove, guard } from '@/utils';
|
||||
|
@ -17,9 +27,19 @@ const Questions: FC = () => {
|
|||
const navigate = useNavigate();
|
||||
const routeParams = useParams();
|
||||
const curTagName = routeParams.tagName || '';
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const curOrder = urlSearchParams.get('order') || 'newest';
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
const reqParams: Type.QueryQuestionsReq = {
|
||||
page_size: 20,
|
||||
page: curPage,
|
||||
order: curOrder as Type.QuestionOrderBy,
|
||||
tag: routeParams.tagName,
|
||||
};
|
||||
const [tagInfo, setTagInfo] = useState<any>({});
|
||||
const [tagFollow, setTagFollow] = useState<Type.FollowParams>();
|
||||
const { data: tagResp, isLoading } = useTagInfo({ name: curTagName });
|
||||
const { data: listData, isLoading: listLoading } = useQuestionList(reqParams);
|
||||
const { data: followResp } = useFollow(tagFollow);
|
||||
const { data: synonymsRes } = useQuerySynonymsTags(tagInfo?.tag_id);
|
||||
const toggleFollow = () => {
|
||||
|
@ -31,6 +51,13 @@ const Questions: FC = () => {
|
|||
object_id: tagInfo.tag_id,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!listLoading) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}, [listLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tagResp) {
|
||||
const info = { ...tagResp };
|
||||
|
@ -76,7 +103,7 @@ const Questions: FC = () => {
|
|||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col xxl={7} lg={8} sm={12}>
|
||||
{isLoading ? (
|
||||
{isLoading || listLoading ? (
|
||||
<div className="tag-box mb-5 placeholder-glow">
|
||||
<div className="mb-3 h3 placeholder" style={{ width: '120px' }} />
|
||||
<p
|
||||
|
@ -122,7 +149,7 @@ const Questions: FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<QuestionList source="tag" />
|
||||
<QuestionList source="tag" data={listData} isLoading={listLoading} />
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<FollowingTags />
|
||||
|
|
|
@ -42,7 +42,7 @@ const initFormData = {
|
|||
};
|
||||
|
||||
const Index = () => {
|
||||
const { is_admin = false } = loggedUserInfoStore((state) => state.user);
|
||||
const { role_id = 1 } = loggedUserInfoStore((state) => state.user);
|
||||
|
||||
const { tagId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
@ -219,7 +219,7 @@ const Index = () => {
|
|||
<Form.Control
|
||||
value={formData.displayName.value}
|
||||
isInvalid={formData.displayName.isInvalid}
|
||||
disabled={!is_admin}
|
||||
disabled={role_id !== 2 && role_id !== 3}
|
||||
onChange={handleDisplayNameChange}
|
||||
/>
|
||||
|
||||
|
@ -232,7 +232,7 @@ const Index = () => {
|
|||
<Form.Control
|
||||
value={formData.slugName.value}
|
||||
isInvalid={formData.slugName.isInvalid}
|
||||
disabled={!is_admin}
|
||||
disabled={role_id !== 2 && role_id !== 3}
|
||||
onChange={handleSlugNameChange}
|
||||
/>
|
||||
<Form.Text as="div">{t('form.fields.slug_name.info')}</Form.Text>
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import { useState } from 'react';
|
||||
import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Container,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Button,
|
||||
Form,
|
||||
Stack,
|
||||
} from 'react-bootstrap';
|
||||
import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { Tag, Pagination, QueryGroup, TagsLoader } from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
import { formatCount, escapeRemove } from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import { useQueryTags, following } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
const sortBtns = ['popular', 'name', 'newest'];
|
||||
|
||||
|
@ -15,6 +24,7 @@ const Tags = () => {
|
|||
const [urlSearch] = useSearchParams();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'tags' });
|
||||
const [searchTag, setSearchTag] = useState('');
|
||||
const { role_id } = loggedUserInfoStore((_) => _.user);
|
||||
|
||||
const page = Number(urlSearch.get('page')) || 1;
|
||||
const sort = urlSearch.get('sort');
|
||||
|
@ -55,17 +65,26 @@ const Tags = () => {
|
|||
<Col xxl={10} sm={12}>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<div className="d-flex justify-content-between align-items-center flex-wrap">
|
||||
<Form>
|
||||
<Form.Group controlId="formBasicEmail">
|
||||
<Form.Control
|
||||
value={searchTag}
|
||||
placeholder={t('search_placeholder')}
|
||||
type="text"
|
||||
onChange={handleChange}
|
||||
size="sm"
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
<Stack direction="horizontal" gap={3}>
|
||||
<Form>
|
||||
<Form.Group controlId="formBasicEmail">
|
||||
<Form.Control
|
||||
value={searchTag}
|
||||
placeholder={t('search_placeholder')}
|
||||
type="text"
|
||||
onChange={handleChange}
|
||||
size="sm"
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
{role_id === 2 || role_id === 3 ? (
|
||||
<Link
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
to="/tags/create">
|
||||
{t('title', { keyPrefix: 'tag_modal' })}
|
||||
</Link>
|
||||
) : null}
|
||||
</Stack>
|
||||
<QueryGroup
|
||||
data={sortBtns}
|
||||
currentSort={sort || 'popular'}
|
||||
|
@ -92,9 +111,9 @@ const Tags = () => {
|
|||
<Card.Body className="d-flex flex-column align-items-start">
|
||||
<Tag className="mb-3" data={tag} />
|
||||
|
||||
<p className="fs-14 flex-fill text-break text-wrap text-truncate-3">
|
||||
{tag.original_text}
|
||||
</p>
|
||||
<div className="fs-14 flex-fill text-break text-wrap text-truncate-3 reset-p mb-3">
|
||||
{escapeRemove(tag.excerpt)}
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
<Button
|
||||
className={`me-2 ${tag.is_follower ? 'active' : ''}`}
|
||||
|
|
|
@ -15,7 +15,7 @@ import HistoryItem from './components/Item';
|
|||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
||||
const { qid = '', aid = '', tid = '' } = useParams();
|
||||
const { is_admin } = loggedUserInfoStore((state) => state.user);
|
||||
const { role_id } = loggedUserInfoStore((state) => state.user);
|
||||
const [showVotes, setShowVotes] = useState(false);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [timelineData, setTimelineData] = useState<Type.TimelineRes>();
|
||||
|
@ -114,7 +114,7 @@ const Index: FC = () => {
|
|||
data={item}
|
||||
objectInfo={timelineData?.object_info}
|
||||
key={item.activity_id}
|
||||
isAdmin={is_admin}
|
||||
isAdmin={role_id === 2}
|
||||
revisionList={revisionList}
|
||||
/>
|
||||
);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue