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:
LinkinStars 2023-03-21 11:45:50 +08:00
commit 9bd9b475f4
118 changed files with 1866 additions and 439 deletions

View File

@ -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

View File

@ -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)

View File

@ -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": {

View File

@ -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": {

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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 nexiste 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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1,5 @@
package constant
var (
DefaultAvatar = "system"
)

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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{})
}

View File

@ -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 {

View File

@ -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:

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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) {

View File

@ -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"`
}

View File

@ -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"`

View File

@ -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

View File

@ -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

View File

@ -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"`
}

View File

@ -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()

View File

@ -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

View File

@ -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,
}

View File

@ -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())
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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

View File

@ -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),
}

View File

@ -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

78
pkg/uid/sid.go Normal file
View File

@ -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
}

46
pkg/uid/sid_test.go Normal file
View File

@ -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)
}
}

View File

@ -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',
},
];

View File

@ -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 {

View File

@ -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;

View File

@ -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);
}

View File

@ -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

View File

@ -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>

View File

@ -9,9 +9,6 @@
}
.nav-link {
&.active {
font-weight: 700;
}
&.icon-link {
width: 36px;
height: 36px;

View File

@ -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(() => {

View File

@ -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') {

View File

@ -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

View File

@ -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>

View File

@ -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);
}
}
};

View File

@ -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}

View File

@ -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>
);
};

View File

@ -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>
);
})}

View File

@ -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 };

View File

@ -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% {

View File

@ -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"

View File

@ -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 }}>

View File

@ -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 });
}

View File

@ -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);

View File

@ -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 />

View File

@ -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 || ''} />

View File

@ -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 && (

View File

@ -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;

View File

@ -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 />

View File

@ -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>

View File

@ -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' : ''}`}

View File

@ -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